add provides dependency type, add accesswidener processing, jij mod loading

This commit is contained in:
moehreag 2024-05-27 00:36:30 +02:00
parent 44378d47cc
commit dde054f6a9
9 changed files with 318 additions and 41 deletions

View file

@ -23,13 +23,15 @@ repositories {
} }
dependencies { dependencies {
implementation(libs.remapper) implementation(libs.remapper){
isTransitive = false
}
compileOnly("org.apache.logging.log4j:log4j-slf4j2-impl:3.0.0-beta2") compileOnly("org.apache.logging.log4j:log4j-slf4j2-impl:3.0.0-beta2")
compileOnly("org.apache.logging.log4j:log4j-api:3.0.0-beta2") compileOnly("org.apache.logging.log4j:log4j-api:3.0.0-beta2")
compileOnly("org.apache.logging.log4j:log4j-core:3.0.0-beta2") compileOnly("org.apache.logging.log4j:log4j-core:3.0.0-beta2")
api(libs.mixin) api(libs.mixin)
api(libs.nightconfig) implementation(libs.nightconfig)
api(libs.annotations) api(libs.annotations)
} }

View file

@ -21,5 +21,9 @@ minecraft("1.20.6")
dependencies { dependencies {
implementation(project(":")) implementation(project(":"))
annotationProcessor(libs.mixin) }
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
} }

View file

@ -1,11 +1,13 @@
package org.ecorous.esnesnon.nonsense.loader.api.extensions; package org.ecorous.esnesnon.nonsense.loader.api.extensions;
import org.ecorous.esnesnon.nonsense.loader.impl.mod.BuiltinExtensions;
/** /**
* The Pre-Launch Extension. * The Pre-Launch Extension.
* <p>This Extension is run right before the game is launched. (provided the used plugin supports it :) )</p> * <p>This Extension is run right before the game is launched. (provided the used plugin supports it :) )</p>
*/ */
public interface PreLaunchExtension { public interface PreLaunchExtension {
String ID = "pre_launch"; String ID = BuiltinExtensions.PRE_LAUNCH;
void onPreLaunch(); void onPreLaunch();
} }

View file

@ -31,6 +31,8 @@ public final class License {
} catch (Exception e){ } catch (Exception e){
LoggerFactory.getLogger(License.class).warn("Failed to load license list!", e); LoggerFactory.getLogger(License.class).warn("Failed to load license list!", e);
} }
idToName.put("ARR", "All rights reserved");
idToName.put("", "Unknown License");
} }
public static License fromId(String id){ public static License fromId(String id){

View file

@ -6,10 +6,11 @@ public final class ModDependencies {
private final Map<Type, Collection<Entry>> entries = new HashMap<>(); private final Map<Type, Collection<Entry>> entries = new HashMap<>();
public ModDependencies(Collection<Entry> depends, Collection<Entry> breaks, Collection<Entry> suggests) { public ModDependencies(Collection<Entry> depends, Collection<Entry> breaks, Collection<Entry> suggests, Collection<Entry> provides) {
entries.put(Type.DEPEND, depends); entries.put(Type.DEPEND, depends);
entries.put(Type.BREAK, breaks); entries.put(Type.BREAK, breaks);
entries.put(Type.SUGGEST, suggests); entries.put(Type.SUGGEST, suggests);
entries.put(Type.PROVIDE, provides);
} }
public Collection<Entry> getForType(Type type) { public Collection<Entry> getForType(Type type) {
@ -51,6 +52,6 @@ public final class ModDependencies {
} }
public enum Type { public enum Type {
DEPEND, BREAK, SUGGEST DEPEND, BREAK, SUGGEST, PROVIDE
} }
} }

View file

@ -0,0 +1,11 @@
package org.ecorous.esnesnon.nonsense.loader.impl.mod;
import lombok.experimental.UtilityClass;
@UtilityClass
public class BuiltinExtensions {
public final String MIXIN_CONFIG = "mixin_config";
public final String INCLUDED_JARS = "included_jars";
public final String PRE_LAUNCH = "pre_launch";
public final String ACCESSWIDENER = "frog_aw";
}

View file

@ -61,6 +61,10 @@ public class ModPropertiesReader {
String version = config.get("frog.mod.version"); String version = config.get("frog.mod.version");
String license = config.get("frog.mod.license"); String license = config.get("frog.mod.license");
if (license == null){
license = "";
}
List<UnmodifiableConfig> creditsList = config.get("frog.mod.credits"); List<UnmodifiableConfig> creditsList = config.get("frog.mod.credits");
Map<String, Collection<String>> credits = new HashMap<>(); Map<String, Collection<String>> credits = new HashMap<>();
if (creditsList != null) { if (creditsList != null) {
@ -68,8 +72,21 @@ public class ModPropertiesReader {
} }
Collection<ModDependencies.Entry> depends = config.get("frog.dependencies.depends"); Collection<ModDependencies.Entry> depends = config.get("frog.dependencies.depends");
if (depends == null){
depends = Collections.emptySet();
}
Collection<ModDependencies.Entry> breaks = config.get("frog.dependencies.breaks"); Collection<ModDependencies.Entry> breaks = config.get("frog.dependencies.breaks");
if (breaks == null){
breaks = Collections.emptySet();
}
Collection<ModDependencies.Entry> suggests = config.get("frog.dependencies.suggests"); Collection<ModDependencies.Entry> suggests = config.get("frog.dependencies.suggests");
if (suggests == null){
suggests = Collections.emptySet();
}
Collection<ModDependencies.Entry> provides = config.get("frog.dependencies.provides");
if (provides == null){
provides = Collections.emptySet();
}
UnmodifiableConfig extensionsConfig = config.get("frog.extensions"); UnmodifiableConfig extensionsConfig = config.get("frog.extensions");
Map<String, Object> extensions = new HashMap<>(); Map<String, Object> extensions = new HashMap<>();
@ -77,7 +94,7 @@ public class ModPropertiesReader {
extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue())); extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue()));
} }
return new ModPropertiesImpl(id, name, SemVerImpl.parse(version), License.fromId(license), ModCredits.of(credits), new ModDependencies(depends, breaks, suggests), ModExtensions.of(extensions)); return new ModPropertiesImpl(id, name, SemVerImpl.parse(version), License.fromId(license), ModCredits.of(credits), new ModDependencies(depends, breaks, suggests, provides), ModExtensions.of(extensions));
}); });
private final String version; private final String version;

View file

@ -0,0 +1,208 @@
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.minecraft;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.AllArgsConstructor;
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
import org.ecorous.esnesnon.nonsense.loader.impl.mod.BuiltinExtensions;
import org.objectweb.asm.*;
/**
* Minimal implementation of accesswideners.
* (Untested)
*/
class AWProcessor {
private static final String AW_EXTENSION_NAME = BuiltinExtensions.ACCESSWIDENER;
private static final Predicate<String> HEADER = Pattern.compile("accessWidener\\s+v[12]").asMatchPredicate();
private static final Predicate<String> COMMENT = Pattern.compile("^#.*").asMatchPredicate();
private static final Pattern SEPARATOR = Pattern.compile("[\\t ]+");
static void apply(Collection<ModProperties> mods, Path input, Path output) throws IOException {
List<Entry> entries = mods.parallelStream().map(ModProperties::extensions).map(e -> (String) e.get(AW_EXTENSION_NAME))
.filter(Objects::nonNull).map(AWProcessor.class::getResourceAsStream).filter(Objects::nonNull)
.map(InputStreamReader::new).map(BufferedReader::new).flatMap(BufferedReader::lines)
.filter(l -> !l.isBlank()).filter(l -> !COMMENT.test(l)).filter(l -> !HEADER.test(l)).distinct()
.map(l -> l.replace("transitive-", "")) // ignore all transitive declarations (just make them normal) as they're only relevant for dev envs
.map(SEPARATOR::matcher).filter(Matcher::matches).map(Entry::new).toList();
Map<String, Entry> classMap = new HashMap<>();
Map<String, Map<String, Entry>> methods = new HashMap<>();
Map<String, Map<String, Entry>> fields = new HashMap<>();
Map<String, Map<String, Entry>> mutations = new HashMap<>();
entries.forEach(e -> {
if ("class".equals(e.targetType)) {
if (e.type == AccessType.MUTABLE){
throw new IllegalArgumentException("aw format error: classes can not have a 'mutable' modifier (at: "+e+")");
}
if (!classMap.containsKey(e.className)) {
classMap.put(e.className, e);
} else {
var other = classMap.get(e.className);
if (e.isAccessGreaterThan(other)) {
classMap.put(e.className, e);
}
}
} else if ("method".equals(e.targetType)) {
if (e.type == AccessType.MUTABLE){
throw new IllegalArgumentException("aw format error: methods can not have a 'mutable' modifier (at: "+e+")");
}
var map = methods.computeIfAbsent(e.className, s -> new HashMap<>());
var id = e.name + e.descriptor;
if (!map.containsKey(id)) {
map.put(id, e);
} else {
var other = map.get(id);
if (e.isAccessGreaterThan(other)) {
classMap.put(id, e);
}
}
} else if ("field".equals(e.targetType)) {
if (e.type == AccessType.EXTENDABLE){
throw new IllegalArgumentException("aw format error: fields can not have a 'extendable' modifier (at: "+e+")");
}
var map = fields.computeIfAbsent(e.className, s -> new HashMap<>());
var id = e.name + e.descriptor;
if (e.type == AccessType.MUTABLE){
mutations.computeIfAbsent(e.className, s -> new HashMap<>()).putIfAbsent(id, e);
return;
}
if (!map.containsKey(id)) {
map.put(id, e);
} else {
var other = map.get(id);
if (e.isAccessGreaterThan(other)) {
classMap.put(id, e);
}
}
}
});
Files.deleteIfExists(output);
try (FileSystem in = FileSystems.newFileSystem(input.toAbsolutePath());
FileSystem out = FileSystems.newFileSystem(output, Map.of("create", "true"))) {
Files.walkFileTree(in.getPath("/"), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path output = out.getPath(file.toString());
Files.createDirectories(output.getParent());
if (file.getFileName().toString().endsWith(".class")) {
var className = file.toString().substring(0, file.toString().length() - 6);
ClassReader reader = new ClassReader(Files.newInputStream(file));
ClassWriter writer = new ClassWriter(0);
ClassVisitor mapper = new ClassVisitor(Opcodes.ASM9, writer) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
Entry e = classMap.get(className);
if (e != null) {
access = ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC);
access |= e.type.access;
} else if (fields.containsKey(className) || methods.containsKey(className) || mutations.containsKey(className)) { // make all classes with modifications public as well
access = ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC);
access |= Opcodes.ACC_PUBLIC;
}
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
Map<String, Entry> map = fields.get(className);
if (map != null) {
Entry e = map.get(name + descriptor);
if (e != null) {
access = ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC); // remove all access modifiers
access |= e.type.access; // re-add the new one
}
}
if ((map = mutations.get(className)) != null){
var e = map.get(name+descriptor);
if (e != null) {
access |= ~Opcodes.ACC_FINAL; // always AccessType.MUTABLE
}
}
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
Map<String, Entry> map = methods.get(className);
if (map != null) {
Entry e = map.get(name + descriptor);
if (e != null) {
access = ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC);
access |= e.type.access;
}
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
};
reader.accept(mapper, 0);
Files.write(output, writer.toByteArray());
} else {
Files.copy(file, output);
}
return FileVisitResult.CONTINUE;
}
});
}
}
private record Entry(AccessType type, String targetType, String className, String name, String descriptor) {
public Entry(Matcher line) {
this(AccessType.of(line.group(1)), line.group(2), line.group(3), line.group(4), line.group(5));
}
public boolean isAccessGreaterThan(Entry other) {
return Access.of(type().access).index < Access.of(other.type.access).index;
}
}
@AllArgsConstructor
private enum AccessType {
ACCESSIBLE("accessible", Opcodes.ACC_PUBLIC),
EXTENDABLE("extendable", Opcodes.ACC_PROTECTED),
MUTABLE("mutable", ~Opcodes.ACC_FINAL)
;
private final String id;
private final int access;
public static AccessType of(String name) {
return Arrays.stream(values()).filter(a -> a.id.equals(name)).findFirst().orElseThrow(() -> new IllegalStateException("Unknown access type: " + name));
}
}
@AllArgsConstructor
private enum Access {
PUBLIC(1), PROTECTED(2), PACKAGE_PRIVATE(3), PRIVATE(4);
private final int index;
public static Access of(int access) {
if ((access & Opcodes.ACC_PUBLIC) != 0) {
return PUBLIC;
}
if ((access & Opcodes.ACC_PROTECTED) != 0) {
return PROTECTED;
}
if ((access & Opcodes.ACC_PRIVATE) != 0) {
return PRIVATE;
}
return PACKAGE_PRIVATE;
}
}
}

View file

@ -1,18 +1,5 @@
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.minecraft; package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.minecraft;
import com.google.gson.JsonObject;
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.mod.ModPropertiesImpl;
import org.ecorous.esnesnon.nonsense.loader.impl.mod.ModPropertiesReader;
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
import org.ecorous.esnesnon.nonsense_remapper.NonsenseRemapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixins;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
@ -23,6 +10,20 @@ import java.net.URL;
import java.nio.file.*; import java.nio.file.*;
import java.util.*; import java.util.*;
import com.google.gson.JsonObject;
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.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.plugin.NonsensePlugin;
import org.ecorous.esnesnon.nonsense_remapper.NonsenseRemapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixins;
public class Minecraft implements NonsensePlugin { public class Minecraft implements NonsensePlugin {
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft"); private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
@ -45,9 +46,12 @@ public class Minecraft implements NonsensePlugin {
@Override @Override
public void init(LoaderImpl loader) throws Exception { public void init(LoaderImpl loader) throws Exception {
if (gamePath == null){
throw new IllegalStateException("Game not found yet!");
}
Path remappedGamePath = loader.getGameDir().resolve(".nonsense/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar"); Path remappedGamePath = loader.getGameDir().resolve(".nonsense/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar");
if (!Files.exists(remappedGamePath)){ if (!Files.exists(remappedGamePath.getParent())) {
try { try {
Files.createDirectories(remappedGamePath.getParent()); Files.createDirectories(remappedGamePath.getParent());
} catch (IOException e) { } catch (IOException e) {
@ -58,14 +62,19 @@ public class Minecraft implements NonsensePlugin {
modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft", modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft",
new MinecraftSemVerImpl(version), License.custom("MC-EULA", "Minecraft EULA"), new MinecraftSemVerImpl(version), License.custom("MC-EULA", "Minecraft EULA"),
ModCredits.of(Map.of("Mojang AB", Collections.singleton("Author"))), ModCredits.of(Map.of("Mojang AB", Collections.singleton("Author"))),
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet()), new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
ModExtensions.of(Collections.emptyMap()))); ModExtensions.of(Collections.emptyMap())));
Collection<Path> mods = Discovery.find(loader.getModsDir(), path -> Collection<Path> mods = Discovery.find(loader.getModsDir(), path ->
version.equals(path.getFileName().toString()), path -> version.equals(path.getFileName().toString()), path ->
path.getFileName().toString().endsWith(LoaderImpl.MOD_FILE_EXTENSION)); path.getFileName().toString().endsWith(LoaderImpl.MOD_FILE_EXTENSION));
// TODO add mods found on the classpath
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);
for (Path mod : new HashSet<>(mods)) {
findJiJMods(mod, mods);
}
mods.parallelStream().map(Path::toUri).map(uri -> { mods.parallelStream().map(Path::toUri).map(uri -> {
try { try {
return uri.toURL(); return uri.toURL();
@ -74,28 +83,49 @@ public class Minecraft implements NonsensePlugin {
} }
}).forEachOrdered(LoaderImpl.getInstance().getClassloader()::addURL); }).forEachOrdered(LoaderImpl.getInstance().getClassloader()::addURL);
classpathMods.parallelStream().map(ModPropertiesReader::readFile).forEachOrdered(modProperties::add); // TODO respect mod dependencies and display errors appropriately
mods.parallelStream().map(ModPropertiesReader::read).forEachOrdered(opt -> opt.ifPresent(modProperties::add));
modProperties.parallelStream().forEach(props -> { modProperties.parallelStream().forEach(props -> {
String name = props.extensions().get("mixin_config"); String name = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
if (name != null) { if (name != null) {
Mixins.addConfiguration(name); Mixins.addConfiguration(name);
} }
}); });
if (!Files.exists(remappedGamePath) && !loader.isDevelopment()){ if (!Files.exists(remappedGamePath)) {
try { if (!loader.isDevelopment()) {
NonsenseRemapper.run(version, gamePath, remappedGamePath, true, false); NonsenseRemapper.run(version, gamePath, remappedGamePath, true, false);
} catch (Throwable e) { } else {
throw new RuntimeException(e); Files.copy(gamePath, remappedGamePath);
} }
} }
Path runtimePath = remappedGamePath.resolveSibling("game-" + version + "-runtime.jar");
AWProcessor.apply(modProperties, remappedGamePath, runtimePath);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { try {
LoaderImpl.getInstance().getClassloader().addURL(remappedGamePath.toUri().toURL()); Files.deleteIfExists(runtimePath);
} catch (Throwable e) { } catch (IOException ignored) {
throw new RuntimeException(e); }
}));
LoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
}
private void findJiJMods(Path mod, Collection<Path> mods) throws IOException {
Optional<ModProperties> opt = ModPropertiesReader.read(mod);
if (opt.isPresent()) {
ModProperties p = opt.get();
modProperties.add(p);
List<String> jars = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
try (FileSystem fs = FileSystems.newFileSystem(mod)){
for (String jar : jars) {
Path path = fs.getPath(jar);
mods.add(path);
findJiJMods(path, mods);
}
}
} }
} }
@ -107,16 +137,16 @@ public class Minecraft implements NonsensePlugin {
private Path findGame() { private Path findGame() {
LOGGER.info("Locating game.."); LOGGER.info("Locating game..");
String jar = System.getProperty("nonsense.plugin.minecraft.gameJar"); String jar = System.getProperty("nonsense.plugin.minecraft.gameJar");
if (jar != null){ if (jar != null) {
Path p = Paths.get(jar); Path p = Paths.get(jar);
if (checkLocation(p)){ if (checkLocation(p)) {
return p; return p;
} }
} }
for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) { for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
Path p = Paths.get(s); Path p = Paths.get(s);
if (checkLocation(p)){ if (checkLocation(p)) {
return p; return p;
} }
} }
@ -124,15 +154,15 @@ public class Minecraft implements NonsensePlugin {
return null; return null;
} }
private boolean checkLocation(Path jar){ private boolean checkLocation(Path jar) {
if (!Files.exists(jar) || Files.isDirectory(jar)){ if (!Files.exists(jar) || Files.isDirectory(jar)) {
return false; return false;
} }
try (FileSystem fs = FileSystems.newFileSystem(jar)) { try (FileSystem fs = FileSystems.newFileSystem(jar)) {
for (String n : MINECRAFT_CLASSES) { for (String n : MINECRAFT_CLASSES) {
if (Files.exists(fs.getPath(n)) && n.contains(LoaderImpl.getInstance().getEnv().getIdentifier())){ if (Files.exists(fs.getPath(n)) && n.contains(LoaderImpl.getInstance().getEnv().getIdentifier())) {
LOGGER.info("Found game: {}", jar); LOGGER.info("Found game: {}", jar);
foundMainClass = n.substring(0, n.length()-6).replace("/", "."); foundMainClass = n.substring(0, n.length() - 6).replace("/", ".");
version = LoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString(); version = LoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString();
return true; return true;
} }