This commit is contained in:
nerjal committed 2026-10-19 13:06:17 +02:00
commit 1ca495a8df
10 files changed
+1326

No files matched your search

@@ -0,0 +1,73 @@
package com.nerjal.moddingutils.compat;
import org.objectweb.asm.tree.AnnotationNode;
import java.lang.annotation.*;
import java.util.Arrays;
import java.util.List;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Compat {
Mod[] mods();
@SuppressWarnings("ClassExplicitlyAnnotation")
class Impl implements Compat {
public static Impl ofNode(AnnotationNode node) {
if (node == null) {
return null;
}
if (!node.desc.equals(Compat.class.descriptorString())) {
throw new UnsupportedOperationException("Cannot create an implementation of "+Compat.class+" from a "+node.desc+" node");
}
List<Object> l = node.values;
Mod[] mods = null;
for (int i = 0; i < l.size(); i++) {
String key = (String) l.get(i++);
if (key.equals("mods")) {
//noinspection unchecked
List<AnnotationNode> modNodes = (List<AnnotationNode>) l.get(i);
mods = new Mod[modNodes.size()];
for (int j = 0; j < modNodes.size(); j++)
mods[j] = Mod.Impl.ofNode(modNodes.get(j));
break;
}
}
if (mods == null) {
throw new IllegalArgumentException("No 'mods' value provided");
}
return new Impl(clearDupes(mods));
}
static Mod[] clearDupes(Mod[] mods) {
Mod[] out = new Mod[mods.length];
int s = 0;
base: for (Mod m : mods) {
for (int x = 0; x < s; x++) {
if (m.modId().equals(out[x].modId())) {
continue base;
}
}
out[s++] = m;
}
return Arrays.copyOf(out, s);
}
private final Mod[] mods;
public Impl(Mod[] mods) {
this.mods = mods;
}
@Override
public Mod[] mods() {
return Arrays.copyOf(mods, mods.length);
}
@Override
public Class<? extends Annotation> annotationType() {
return Compat.class;
}
}
}
@@ -0,0 +1,107 @@
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 org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
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<>();
Set<String> skippedModCompats = new HashSet<>();
Logger logger;
@Override
public void onLoad(String mixinPackage) {
String[] split = mixinPackage.split("\\.");
int x = split.length;
String s = split[--x];
while (s.isBlank() && x > 0) s = split[--x];
logger = LoggerFactory.getLogger("["+s+"/MixinCompat]");
}
@Override
public String getRefMapperConfig() {
return null;
}
@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
try {
List<AnnotationNode> list = MixinService.getService().getBytecodeProvider().getClassNode(mixinClassName).visibleAnnotations;
if (list == null) {
return true;
}
for (AnnotationNode node : list) {
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<>();
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()) {
missingMods.add(modId);
} else {
modMap.put(m, opt.get());
}
}
if (!missingMods.isEmpty()) {
if (skippedModCompats.add(modIds)) {
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 (String v : e.getKey().versions()) {
if (v.isBlank()) continue;
Version version = VersionParser.parse(v, true);
if (version.compareTo(metadata.getVersion()) != 0) return false;
}
}
if (loadedCompatModIds.add(modIds)) {
logger.info("Loaded compat for mods {}", modIds);
}
return true;
}
return true;
} catch (Exception e) {
logger.error(e.getMessage(), e);
return true;
}
}
@Override
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
}
@Override
public List<String> getMixins() {
return null;
}
@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
}
@@ -0,0 +1,98 @@
package com.nerjal.moddingutils.compat;
import org.objectweb.asm.tree.AnnotationNode;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.List;
@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface Mod {
/**
* A required mod's ID for compatibility mixin application
*/
String modId();
/**
* A required mod's authors for compatibility mixin application
*/
String[] authors() default {};
/**
* A required mod's version ranges for compatibility mixin application.
* <p>
* This uses the same version system as the {@code fabric.mod.json} files, so
* it is not required to include every valid version.
*/
String[] versions() default {};
@SuppressWarnings("ClassExplicitlyAnnotation")
class Impl implements Mod {
public static Impl ofNode(AnnotationNode node) {
if (node == null) {
return null;
}
if (!node.desc.equals(Mod.class.descriptorString())) {
throw new UnsupportedOperationException("Cannot create an implementation of "+Mod.class+" from a "+node.desc+" node");
}
List<Object> l = node.values;
String modId = null;
String[] authors = new String[0];
String[] versions = new String[0];
for (int i = 0; i < l.size(); i++) {
String key = (String) l.get(i++);
Object v = l.get(i);
switch (key) {
case "modId":
modId = (String) v;
break;
case "authors":
authors = (String[]) v;
break;
case "versions":
versions = (String[]) v;
break;
}
}
if (modId == null) {
throw new IllegalArgumentException("No 'modId' annotation value provided");
}
return new Impl(modId, authors, versions);
}
private final String modId;
private final String[] authors;
private final String[] versions;
public Impl(String modId, String[] authors, String[] versions) {
this.modId = modId;
this.authors = authors;
this.versions = versions;
}
@Override
public String modId() {
return this.modId;
}
@Override
public String[] authors() {
return Arrays.copyOf(authors, authors.length);
}
@Override
public String[] versions() {
return Arrays.copyOf(versions, versions.length);
}
@Override
public Class<? extends Annotation> annotationType() {
return Mod.class;
}
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"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"
]
}
}
}