Improve launcher injection and hook reliability
Find Fabric through the render thread's class loader and make bytecode transformations safer for launcher-provided class definitions. Improve hook reporting, process-picker refresh behavior, and self-destruct cleanup; document Dawn and Lunar 1.21.11 setup.
This commit is contained in:
1 parent
38ff6d387c
commit
036b24f2ae
12 files changed
+186
-48
No files matched your search
@@ -4,6 +4,7 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -11,8 +12,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.lenni0451.classtransform.TransformerManager;
|
||||
import net.lenni0451.classtransform.utils.ASMUtils;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
@@ -48,7 +51,7 @@ public final class InPlaceTransformer {
|
||||
|
||||
/** @param dottedName runtime name of the target class, @param current the bytes of the class as currently loaded */
|
||||
public Result transform(String dottedName, byte[] current) throws Exception {
|
||||
byte[] out = manager.transform(dottedName, current, true);
|
||||
byte[] out = manager.transform(dottedName, current, false);
|
||||
if (out == null) throw new IllegalStateException("ClassTransform failed for " + dottedName);
|
||||
ClassNode before = read(current);
|
||||
ClassNode after = read(out);
|
||||
@@ -63,9 +66,9 @@ public final class InPlaceTransformer {
|
||||
before.methods.forEach(m -> existing.put(m.name + m.desc, m));
|
||||
Map<String, MethodNode> moved = new LinkedHashMap<>();
|
||||
for (MethodNode m : after.methods) if (!existing.containsKey(m.name + m.desc)) moved.put(m.name + m.desc, m);
|
||||
if (moved.isEmpty()) return new Result(out, Map.of(), 0);
|
||||
if (after.interfaces.size() != before.interfaces.size())
|
||||
if (!after.interfaces.equals(before.interfaces))
|
||||
throw new IllegalStateException(dottedName + ": transformer changed the interface list");
|
||||
if (moved.isEmpty()) return new Result(writeTarget(current, before, after, existing), Map.of(), 0);
|
||||
|
||||
String helperName = HOOK_PACKAGE + "H" + (sequence.getAndIncrement()) + "_" + after.name.substring(after.name.lastIndexOf('/') + 1).replace('$', '_');
|
||||
ClassNode helper = new ClassNode();
|
||||
@@ -100,13 +103,44 @@ public final class InPlaceTransformer {
|
||||
accessors.finish();
|
||||
after.methods.removeIf(m -> moved.containsKey(m.name + m.desc));
|
||||
|
||||
ClassWriter targetWriter = new ClassWriter(0);
|
||||
after.accept(targetWriter);
|
||||
ClassWriter helperWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
helper.accept(helperWriter);
|
||||
Map<String, byte[]> helpers = new LinkedHashMap<>();
|
||||
helpers.put(helperName.replace('/', '.'), helperWriter.toByteArray());
|
||||
return new Result(targetWriter.toByteArray(), helpers, moved.size());
|
||||
helpers.put(helperName.replace('/', '.'), ASMUtils.toBytes(helper, manager.getClassTree(), manager.getClassProvider()));
|
||||
return new Result(writeTarget(current, before, after, existing), helpers, moved.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute frames only in changed methods. Launchers can transform unrelated methods and
|
||||
* supply valid frames for them even when their class resources are unavailable to ClassTransform.
|
||||
*/
|
||||
private byte[] writeTarget(byte[] current, ClassNode before, ClassNode after, Map<String, MethodNode> originals) {
|
||||
List<MethodNode> changed = new ArrayList<>();
|
||||
for (MethodNode method : after.methods) {
|
||||
MethodNode original = originals.get(method.name + method.desc);
|
||||
if (original == null) throw new IllegalStateException("Unexpected method " + method.name + method.desc);
|
||||
if (!Arrays.equals(methodBytes(original), methodBytes(method))) changed.add(method);
|
||||
}
|
||||
if (changed.isEmpty()) return current;
|
||||
after.methods = changed;
|
||||
ClassNode computed = read(ASMUtils.toBytes(after, manager.getClassTree(), manager.getClassProvider()));
|
||||
Map<String, MethodNode> replacements = new HashMap<>();
|
||||
computed.methods.forEach(m -> replacements.put(m.name + m.desc, m));
|
||||
before.methods.replaceAll(m -> replacements.getOrDefault(m.name + m.desc, m));
|
||||
ClassWriter writer = new ClassWriter(new ClassReader(current), 0);
|
||||
before.accept(writer);
|
||||
return writer.toByteArray();
|
||||
}
|
||||
|
||||
private static byte[] methodBytes(MethodNode method) {
|
||||
ClassWriter writer = new ClassWriter(0);
|
||||
writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, "MethodComparison", null, "java/lang/Object", null);
|
||||
MethodVisitor visitor = writer.visitMethod(method.access, method.name, method.desc, method.signature,
|
||||
method.exceptions.toArray(String[]::new));
|
||||
method.accept(new MethodVisitor(Opcodes.ASM9, visitor) {
|
||||
@Override public void visitFrame(int type, int locals, Object[] local, int stackSize, Object[] stack) {}
|
||||
@Override public void visitMaxs(int maxStack, int maxLocals) { super.visitMaxs(0, 0); }
|
||||
});
|
||||
writer.visitEnd();
|
||||
return writer.toByteArray();
|
||||
}
|
||||
|
||||
private static ClassNode read(byte[] bytes) {
|
||||
|
||||
Reference in new issue
Block a user