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
+657 -75

No files matched your search

+8 -7
View File
@@ -1,11 +1,12 @@
# Supported platforms
Fabric (required for platform mod resolution)
Java >= 17
# Features
## Platform detection
Automatic detection of the current modloader (between Fabric, Forge, Neoforge or Other)
and of the current execution side (client or server) using the mixin engine.
Access these via the `PlatformService` class.
## Modding compat
A mod compatibility mixin plugin is made available at
@@ -30,4 +31,4 @@ ModdingUtils.
## Latest version
[1.0](https://maven.nerjal.com/#/releases/com/nerjal/ModdingUtils/1.0)
[1.1](https://maven.nerjal.com/#/releases/com/nerjal/ModdingUtils/1.1)
+26 -4
View File
@@ -1,5 +1,5 @@
plugins {
id 'fabric-loom' version '1.17-SNAPSHOT'
id 'java'
id 'maven-publish'
id 'signing'
}
@@ -12,12 +12,34 @@ base {
}
repositories {
maven {
name = 'Fabric maven'
url = 'https://maven.fabricmc.net/'
}
maven {
name = 'NeoForge maven'
url = 'https://maven.neoforged.net/releases/'
}
maven {
name = "Forge maven"
url = "https://maven.minecraftforge.net/"
}
maven {
name = 'Minecraft maven'
url = 'https://libraries.minecraft.net/'
}
}
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings loom.officialMojangMappings()
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation("io.github.llamalad7:mixinextras-fabric:${project.mixinextras_version}")
compileOnly "net.fabricmc:fabric-loader:${project.fabric_version}"
//noinspection VulnerableLibrariesLocal
compileOnly "net.neoforged:neoforge:${project.neoforge_version}"
// istg why does forge need this many different dependencies to ensure getting all the needed classes,
// couldn't they use transitive dependencies like everyone?
compileOnly "net.minecraftforge:fmlloader:${project.forge_version}"
compileOnly "net.minecraftforge:forge:${project.forge_version}:universal"
compileOnly "net.minecraftforge:fmlcore:${project.forge_version}"
}
def targetJavaVersion = 17
+5 -7
View File
@@ -1,11 +1,9 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
# Fabric Properties
# check these on https://modmuss50.me/fabric.html
minecraft_version=1.20.1
loader_version=0.18.6
# Mod Properties
mod_version=1.0
mixinextras_version=0.4.1
fabric_version=0.18.6
neoforge_version=20.2.3-beta
forge_version=1.20.1-47.0.19
mod_version=1.1
maven_group=com.nerjal
maven_artifact=ModdingUtils
-4
View File
@@ -1,9 +1,5 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
gradlePluginPortal()
}
}
@@ -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"
]
}
}
}