Now loader-agnostic

This commit is contained in:
nerjal committed 2026-10-20 12:42:59 +02:00
1 parent 9f5b16756a
commit 5192a5258b
19 files changed
+658 -76

No files matched your search

@@ -1,11 +1,7 @@
package com.nerjal.moddingutils.compat;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.api.Version;
import net.fabricmc.loader.api.metadata.ModMetadata;
import net.fabricmc.loader.api.metadata.Person;
import net.fabricmc.loader.impl.util.version.VersionParser;
import com.nerjal.moddingutils.mod.ModVersion;
import com.nerjal.moddingutils.platform.PlatformService;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.slf4j.Logger;
@@ -15,7 +11,6 @@ import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import org.spongepowered.asm.service.MixinService;
import java.util.*;
import java.util.stream.Collectors;
public class CompatMixinPlugin implements IMixinConfigPlugin {
Set<String> loadedCompatModIds = new HashSet<>();
@@ -47,16 +42,16 @@ public class CompatMixinPlugin implements IMixinConfigPlugin {
if (!node.desc.equals(Compat.class.descriptorString())) continue;
Compat c = Compat.Impl.ofNode(node);
List<Mod> mods = Arrays.stream(c.mods()).sorted(Comparator.comparing(Mod::modId)).toList();
Map<Mod, ModContainer> modMap = new HashMap<>();
Map<Mod, PlatformService.PlatformModContainer> modMap = new HashMap<>();
List<String> missingMods = new ArrayList<>();
String modIds = String.join(", ", mods.stream().map(Mod::modId).toList());
for (Mod m : mods) {
String modId = m.modId();
Optional<ModContainer> opt = FabricLoader.getInstance().getModContainer(modId);
if (opt.isEmpty()) {
Optional<PlatformService.PlatformModContainer> mod = PlatformService.getInstance().getMod(modId);
if (mod.isEmpty()) {
missingMods.add(modId);
} else {
modMap.put(m, opt.get());
modMap.put(m, mod.get());
}
}
if (!missingMods.isEmpty()) {
@@ -64,16 +59,14 @@ public class CompatMixinPlugin implements IMixinConfigPlugin {
logger.info("Skipped compatibility mixin with mod(s) [{}]: mod(s) {} are not present", modIds, missingMods);
}
}
for (Map.Entry<Mod, ModContainer> e : modMap.entrySet()) {
ModMetadata metadata = e.getValue().getMetadata();
Set<String> authors = metadata.getAuthors().stream().map(Person::getName).collect(Collectors.toSet());
if (!authors.containsAll(Arrays.asList(e.getKey().authors()))) {
return false;
for (Map.Entry<Mod, PlatformService.PlatformModContainer> e : modMap.entrySet()) {
for (String author : e.getKey().authors()) {
if (!e.getValue().hasAuthor(author)) return false;
}
for (String v : e.getKey().versions()) {
if (v.isBlank()) continue;
Version version = VersionParser.parse(v, true);
if (version.compareTo(metadata.getVersion()) != 0) return false;
ModVersion version = ModVersion.parse(v);
if (!e.getValue().matchesVersion(version)) return false;
}
}
if (loadedCompatModIds.add(modIds)) {
@@ -0,0 +1,9 @@
package com.nerjal.moddingutils.extra;
@SubjectiveOpinion({
"For the sake of leaving quirky comments in the code without affecting runtime",
"And it's funnier than just comments honestly"
})
public @interface SubjectiveOpinion {
String[] value();
}
@@ -0,0 +1,28 @@
package com.nerjal.moddingutils.mod;
import org.jetbrains.annotations.Contract;
/**
* This is straight-up copied from the Fabric loader api's versions,
* but since I did not find an easy way to only shadow the relevant
* classes, here we are.
*/
public interface ModVersion extends Comparable<ModVersion> {
String getFriendlyString();
@Contract("null -> fail")
static ModVersion parse(String string) throws VersionParsingException {
if (string == null || string.isEmpty()) {
throw new VersionParsingException("Version must be a non-empty string.");
}
ModVersion version;
try {
version = new SemanticModVersion(string);
} catch (VersionParsingException e) {
version = new StringModVersion(string);
}
return version;
}
}
@@ -0,0 +1,270 @@
package com.nerjal.moddingutils.mod;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
public class SemanticModVersion implements ModVersion {
private static final Pattern DOT_SEPARATED_ID = Pattern.compile("|[-0-9A-Za-z]+(\\.[-0-9A-Za-z]+)*");
private static final Pattern UNSIGNED_INTEGER = Pattern.compile("0|[1-9][0-9]*");
private static final int COMPONENT_WILDCARD = Integer.MIN_VALUE;
private final int[] components;
private final String prerelease;
private final String build;
private String friendlyName;
public SemanticModVersion(String version) throws VersionParsingException {
int buildDelimPos = version.indexOf('+');
if (buildDelimPos >= 0) {
build = version.substring(buildDelimPos + 1);
version = version.substring(0, buildDelimPos);
} else {
build = null;
}
int dashDelimPos = version.indexOf('-');
if (dashDelimPos >= 0) {
prerelease = version.substring(dashDelimPos + 1);
version = version.substring(0, dashDelimPos);
} else {
prerelease = null;
}
if (prerelease != null && !DOT_SEPARATED_ID.matcher(prerelease).matches()) {
throw new VersionParsingException("Invalid prerelease string '" + prerelease + "'!");
}
if (version.endsWith(".")) {
throw new VersionParsingException("Negative version number component found!");
} else if (version.startsWith(".")) {
throw new VersionParsingException("Missing version component!");
}
String[] componentStrings = version.split("\\.");
if (componentStrings.length < 1) {
throw new VersionParsingException("Did not provide version numbers!");
}
int[] components = new int[componentStrings.length];
int firstWildcardIdx = -1;
for (int i = 0; i < componentStrings.length; i++) {
String compStr = componentStrings[i];
if (compStr.equals("x") || compStr.equals("X") || compStr.equals("*")) {
if (prerelease != null) {
throw new VersionParsingException("Pre-release versions are not allowed to use X-ranges!");
}
components[i] = COMPONENT_WILDCARD;
if (firstWildcardIdx < 0) firstWildcardIdx = i;
continue;
} else if (i > 0 && components[i - 1] == COMPONENT_WILDCARD) {
throw new VersionParsingException("Interjacent wildcard (1.x.2) are disallowed!");
}
if (compStr.trim().isEmpty()) {
throw new VersionParsingException("Missing version number component!");
}
try {
components[i] = Integer.parseInt(compStr);
if (components[i] < 0) {
throw new VersionParsingException("Negative version number component '" + compStr + "'!");
}
} catch (NumberFormatException e) {
throw new VersionParsingException("Could not parse version number component '" + compStr + "'!", e);
}
}
if (components.length == 1 && components[0] == COMPONENT_WILDCARD) {
throw new VersionParsingException("Versions of form 'x' or 'X' not allowed!");
}
// strip extra wildcards (1.x.x -> 1.x)
if (firstWildcardIdx > 0 && components.length > firstWildcardIdx + 1) {
components = Arrays.copyOf(components, firstWildcardIdx + 1);
}
this.components = components;
buildFriendlyName();
}
public SemanticModVersion(int[] components, String prerelease, String build) {
if (components.length == 0 || components[0] == COMPONENT_WILDCARD) throw new IllegalArgumentException("Invalid components: "+Arrays.toString(components));
this.components = components;
this.prerelease = prerelease;
this.build = build;
buildFriendlyName();
}
private void buildFriendlyName() {
StringBuilder fnBuilder = new StringBuilder();
boolean first = true;
for (int i : components) {
if (first) {
first = false;
} else {
fnBuilder.append('.');
}
if (i == COMPONENT_WILDCARD) {
fnBuilder.append('x');
} else {
fnBuilder.append(i);
}
}
if (prerelease != null) {
fnBuilder.append('-').append(prerelease);
}
if (build != null) {
fnBuilder.append('+').append(build);
}
friendlyName = fnBuilder.toString();
}
public int getVersionComponentCount() {
return components.length;
}
public int getVersionComponent(int pos) {
if (pos < 0) {
throw new RuntimeException("Tried to access negative version number component!");
} else if (pos >= components.length) {
// Repeat "x" if x-range, otherwise repeat "0".
return components[components.length - 1] == COMPONENT_WILDCARD ? COMPONENT_WILDCARD : 0;
} else {
return components[pos];
}
}
public int[] getVersionComponents() {
return components.clone();
}
public Optional<String> getPrereleaseKey() {
return Optional.ofNullable(prerelease);
}
public Optional<String> getBuildKey() {
return Optional.ofNullable(build);
}
public String getFriendlyString() {
return friendlyName;
}
public boolean hasWildcard() {
for (int i : components) {
if (i < 0) {
return true;
}
}
return false;
}
public boolean equalsComponentsExactly(SemanticModVersion other) {
for (int i = 0; i < Math.max(getVersionComponentCount(), other.getVersionComponentCount()); i++) {
if (getVersionComponent(i) != other.getVersionComponent(i)) {
return false;
}
}
return true;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof SemanticModVersion other)) return false;
if (!equalsComponentsExactly(other)) {
return false;
}
return Objects.equals(prerelease, other.prerelease) && Objects.equals(build, other.build);
}
@Override
public int hashCode() {
return Arrays.hashCode(components) * 73 + (prerelease != null ? prerelease.hashCode() * 11 : 0) + (build != null ? build.hashCode() : 0);
}
@Override
public String toString() {
return getFriendlyString();
}
@Override
public int compareTo(ModVersion other) {
if (!(other instanceof SemanticModVersion o)) {
return getFriendlyString().compareTo(other.getFriendlyString());
}
for (int i = 0; i < Math.max(getVersionComponentCount(), o.getVersionComponentCount()); i++) {
int first = getVersionComponent(i);
int second = o.getVersionComponent(i);
if (first == COMPONENT_WILDCARD || second == COMPONENT_WILDCARD) {
continue;
}
int compare = Integer.compare(first, second);
if (compare != 0) return compare;
}
Optional<String> prereleaseA = getPrereleaseKey();
Optional<String> prereleaseB = o.getPrereleaseKey();
if (prereleaseA.isPresent() || prereleaseB.isPresent()) {
if (prereleaseA.isPresent() && prereleaseB.isPresent()) {
StringTokenizer prereleaseATokenizer = new StringTokenizer(prereleaseA.get(), ".");
StringTokenizer prereleaseBTokenizer = new StringTokenizer(prereleaseB.get(), ".");
while (prereleaseATokenizer.hasMoreElements()) {
if (prereleaseBTokenizer.hasMoreElements()) {
String partA = prereleaseATokenizer.nextToken();
String partB = prereleaseBTokenizer.nextToken();
if (UNSIGNED_INTEGER.matcher(partA).matches()) {
if (UNSIGNED_INTEGER.matcher(partB).matches()) {
int compare = Integer.compare(partA.length(), partB.length());
if (compare != 0) return compare;
} else {
return -1;
}
} else {
if (UNSIGNED_INTEGER.matcher(partB).matches()) {
return 1;
}
}
int compare = partA.compareTo(partB);
if (compare != 0) return compare;
} else {
return 1;
}
}
return prereleaseBTokenizer.hasMoreElements() ? -1 : 0;
} else if (prereleaseA.isPresent()) {
return o.hasWildcard() ? 0 : -1;
} else { // prereleaseB.isPresent()
return hasWildcard() ? 0 : 1;
}
} else {
return 0;
}
}
}
@@ -0,0 +1,32 @@
package com.nerjal.moddingutils.mod;
public class StringModVersion implements ModVersion {
private final String version;
public StringModVersion(String version) {
this.version = version;
}
@Override
public String getFriendlyString() {
return version;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof StringModVersion other)) {
return false;
}
return version.equals(other.version);
}
@Override
public int compareTo(ModVersion o) {
return getFriendlyString().compareTo(o.getFriendlyString());
}
@Override
public String toString() {
return version;
}
}
@@ -0,0 +1,19 @@
package com.nerjal.moddingutils.mod;
public class VersionParsingException extends Exception {
public VersionParsingException() {
super();
}
public VersionParsingException(Throwable t) {
super(t);
}
public VersionParsingException(String s) {
super(s);
}
public VersionParsingException(String s, Throwable t) {
super(s, t);
}
}
@@ -0,0 +1,40 @@
package com.nerjal.moddingutils.platform;
import com.nerjal.moddingutils.extra.SubjectiveOpinion;
public enum Loader {
FABRIC,
@Deprecated(since = "Since it has been led by a douche and a bunch of faggots")
@SubjectiveOpinion({
"The sole fact that this modloader is still alive baffles me.",
"Who still uses it? Who hasn't moved on? Why?"
})
FORGE,
@SubjectiveOpinion({
"I am not saying we should use forge instead, but I don't believe this loader is a good option.",
"Thank you for listening to my TED talk."
})
NEOFORGE,
@SubjectiveOpinion({
"I know you're gonna say I forgot one. I don't care. I will not add it.",
"I have no reason to endorse a dead project whose leaders have hurt my friends."
})
OTHER,
;
public static Loader parse(String s) {
String lower = s.toLowerCase();
return switch (s.toLowerCase()) {
case "modlauncher" -> {
if (Loader.class.getClassLoader().getResource("net/neoforged/fml/ModLoader.class") == null) {
yield FORGE;
} else {
yield NEOFORGE;
}
}
case "launchwrapper" -> FORGE;
case "knot/fabric", "launchwrapper/fabric" -> FABRIC;
default -> OTHER;
};
}
}
@@ -0,0 +1,90 @@
package com.nerjal.moddingutils.platform;
import com.nerjal.moddingutils.mod.ModVersion;
import com.nerjal.moddingutils.mod.VersionParsingException;
import org.jetbrains.annotations.Contract;
import org.spongepowered.asm.service.MixinService;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public abstract sealed class PlatformService permits PlatformServiceFabric, PlatformServiceForge, PlatformServiceNeoForge, PlatformServiceOther {
private static final Loader LOADER;
private static final Side SIDE;
private static PlatformService INSTANCE;
static {
String name = MixinService.getService().getName();
String side = MixinService.getService().getSideName();
LOADER = Loader.parse(name);
SIDE = Side.parse(side);
System.out.println(MixinService.getService().getName());
System.out.println(MixinService.getService().getSideName());
System.out.println(MixinService.getService().getClass());
}
@Contract(pure = true)
public static Loader getLoader() {
return LOADER;
}
@Contract(pure = true)
public static Side getSide() {
return SIDE;
}
public static PlatformService getInstance() {
if (INSTANCE == null) {
INSTANCE = switch (LOADER) {
case FABRIC -> new PlatformServiceFabric();
case NEOFORGE -> new PlatformServiceNeoForge();
case FORGE -> new PlatformServiceForge();
default -> new PlatformServiceOther();
};
}
return INSTANCE;
}
private final Map<String, Optional<PlatformModContainer>> modsMap = new HashMap<>();
public final Optional<PlatformModContainer> getMod(String modId) {
return modsMap.computeIfAbsent(modId, this::createModContainer);
}
@Contract(pure = true)
protected abstract Optional<PlatformModContainer> createModContainer(String modId);
public abstract static class PlatformModContainer {
private final String modId;
private final Set<String> authors;
private final ModVersion version;
protected PlatformModContainer(String modId, Set<String> authors, String versionString) {
this.modId = modId;
this.authors = authors;
try {
this.version = ModVersion.parse(versionString);
} catch (VersionParsingException e) {
throw new RuntimeException(e);
}
}
@Contract(pure = true)
public final boolean hasAuthor(String s) {
return authors.contains(s);
}
@Contract(pure = true)
public final boolean matchesVersion(ModVersion v) {
return version.compareTo(v) == 0;
}
@Contract(pure = true)
public final String getModId() {
return modId;
}
}
}
@@ -0,0 +1,27 @@
package com.nerjal.moddingutils.platform;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.api.metadata.ModMetadata;
import net.fabricmc.loader.api.metadata.Person;
import java.util.Optional;
import java.util.stream.Collectors;
final class PlatformServiceFabric extends PlatformService {
@Override
protected Optional<PlatformModContainer> createModContainer(String modId) {
return FabricLoader.getInstance().getModContainer(modId)
.map(ModContainer::getMetadata)
.map(FabricModContainer::new);
}
static final class FabricModContainer extends PlatformModContainer {
private FabricModContainer(ModMetadata modMetadata) {
super(modMetadata.getId(),
modMetadata.getAuthors().stream().map(Person::getName).collect(Collectors.toSet()),
modMetadata.getVersion().getFriendlyString());
}
}
}
@@ -0,0 +1,25 @@
package com.nerjal.moddingutils.platform;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.forgespi.language.IModFileInfo;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
final class PlatformServiceForge extends PlatformService {
@Override
protected Optional<PlatformModContainer> createModContainer(String modId) {
return Optional.ofNullable(ModList.get().getModFileById(modId)).map(i -> new ForgeModContainer(modId, i));
}
static final class ForgeModContainer extends PlatformModContainer {
ForgeModContainer(String modId, IModFileInfo modInfo) {
super(modId, Arrays.stream(modInfo.getConfig().getConfigElement("authors")
.map(Object::toString).orElse("").split("[\\w,+;]+"))
.collect(Collectors.toSet()),
modInfo.versionString());
}
}
}
@@ -0,0 +1,24 @@
package com.nerjal.moddingutils.platform;
import net.neoforged.fml.ModList;
import net.neoforged.neoforgespi.language.IModFileInfo;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
final class PlatformServiceNeoForge extends PlatformService {
@Override
protected Optional<PlatformModContainer> createModContainer(String modId) {
return Optional.ofNullable(ModList.get().getModFileById(modId)).map(i -> new NeoForgeModContainer(modId, i));
}
static final class NeoForgeModContainer extends PlatformModContainer {
NeoForgeModContainer(String modId, IModFileInfo modInfo) {
super(modId, Arrays.stream(modInfo.getConfig().getConfigElement("authors")
.map(Object::toString).orElse("").split("[\\w,+;]+"))
.collect(Collectors.toSet()),
modInfo.versionString());
}
}
}
@@ -0,0 +1,13 @@
package com.nerjal.moddingutils.platform;
import com.nerjal.moddingutils.extra.SubjectiveOpinion;
import java.util.Optional;
@SubjectiveOpinion("Unknown or voluntarily unsupported loaders")
final class PlatformServiceOther extends PlatformService {
@Override
protected Optional<PlatformModContainer> createModContainer(String modId) {
return Optional.empty();
}
}
@@ -0,0 +1,16 @@
package com.nerjal.moddingutils.platform;
public enum Side {
CLIENT,
SERVER,
UNKNOWN,
;
public static Side parse(String s) {
return switch (s.toLowerCase()) {
case "client" -> CLIENT;
case "server" -> SERVER;
default -> UNKNOWN;
};
}
}
+14
View File
@@ -0,0 +1,14 @@
module ModdingUtils {
requires net.fabricmc.loader;
requires org.jetbrains.annotations;
requires org.objectweb.asm.tree;
requires org.slf4j;
requires org.spongepowered.mixin;
requires fmlcore;
requires net.minecraftforge.forgespi;
requires net.neoforged.neoforgespi;
requires fml_core;
opens com.nerjal.moddingutils.compat;
opens com.nerjal.moddingutils.mod;
opens com.nerjal.moddingutils.platform;
}
-35
View File
@@ -1,35 +0,0 @@
{
"schemaVersion": 1,
"id": "moddingutils",
"version": "${version}",
"name": "ModdingUtils",
"description": "Various modding utilities to share and use freely",
"authors": [
{
"name": "Nerjal Nosk",
"contact": {
"homepage": "https://codeberg.org/NerjalNosk"
}
}
],
"contact": {
"homepage": "https://codeberg.org/NerjalNosk/ModdingUtils"
},
"license": "LGPL-3.0",
"environment": "*",
"entrypoints": {
},
"mixins": [
],
"depends": {
"fabricloader": "*",
"java": "${java_version}"
},
"custom": {
"modmenu": {
"badges": [
"library"
]
}
}
}