fixes
This commit is contained in:
1 parent
83aea53e57
commit
aa4f6b6ddc
842 files changed
+47284
-91563
No files matched your search
@@ -0,0 +1,60 @@
|
||||
package dev.zprestige.prestige.inject;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import org.objectweb.asm.Type;
|
||||
|
||||
/**
|
||||
* Resolves the MethodHandles behind generated accessors. A hook handler that was moved out of its target class into a helper class
|
||||
* can no longer touch the target's private members directly; the transformer routes those accesses through handles made here.
|
||||
* All handles take the receiver as Object so the generated call sites can use one erased signature.
|
||||
*/
|
||||
public final class Access {
|
||||
public static final int GET_FIELD = 0, PUT_FIELD = 1, GET_STATIC = 2, PUT_STATIC = 3, VIRTUAL = 4, STATIC = 5;
|
||||
|
||||
private Access() {}
|
||||
|
||||
public static MethodHandle resolve(String owner, String name, String desc, int kind) throws ReflectiveOperationException {
|
||||
Class<?> ref = Class.forName(owner, false, Access.class.getClassLoader());
|
||||
Class<?> decl = declaring(ref, name, desc, kind);
|
||||
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(decl, MethodHandles.lookup());
|
||||
switch (kind) {
|
||||
case GET_FIELD: {
|
||||
Class<?> t = fieldType(decl, name);
|
||||
return lookup.findGetter(decl, name, t).asType(MethodType.methodType(t, Object.class));
|
||||
}
|
||||
case PUT_FIELD: {
|
||||
Class<?> t = fieldType(decl, name);
|
||||
return lookup.findSetter(decl, name, t).asType(MethodType.methodType(void.class, Object.class, t));
|
||||
}
|
||||
case GET_STATIC:
|
||||
return lookup.findStaticGetter(decl, name, fieldType(decl, name));
|
||||
case PUT_STATIC:
|
||||
return lookup.findStaticSetter(decl, name, fieldType(decl, name));
|
||||
case VIRTUAL: {
|
||||
MethodType type = MethodType.fromMethodDescriptorString(desc, Access.class.getClassLoader());
|
||||
return lookup.findVirtual(decl, name, type).asType(type.insertParameterTypes(0, Object.class));
|
||||
}
|
||||
default:
|
||||
return lookup.findStatic(decl, name, MethodType.fromMethodDescriptorString(desc, Access.class.getClassLoader()));
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> fieldType(Class<?> decl, String name) throws NoSuchFieldException {
|
||||
return decl.getDeclaredField(name).getType();
|
||||
}
|
||||
|
||||
/** The class in the hierarchy of ref that actually declares the member (JVM resolution walks the superclasses). */
|
||||
private static Class<?> declaring(Class<?> ref, String name, String desc, int kind) throws ReflectiveOperationException {
|
||||
boolean field = kind <= PUT_STATIC;
|
||||
for (Class<?> c = ref; c != null; c = c.getSuperclass()) {
|
||||
if (field) {
|
||||
for (var f : c.getDeclaredFields()) if (f.getName().equals(name)) return c;
|
||||
} else {
|
||||
for (var m : c.getDeclaredMethods()) if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) return c;
|
||||
}
|
||||
}
|
||||
throw field ? new NoSuchFieldException(ref.getName() + "." + name) : new NoSuchMethodException(ref.getName() + "." + name + desc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package dev.zprestige.prestige.inject;
|
||||
|
||||
import dev.zprestige.prestige.client.Prestige;
|
||||
import dev.zprestige.prestige.engine.EnginePlugin;
|
||||
import dev.zprestige.prestige.engine.EngineState;
|
||||
import dev.zprestige.prestige.hooks.Definer;
|
||||
import dev.zprestige.prestige.ui.mc.Entry;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToIntBiFunction;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.lenni0451.classtransform.TransformerManager;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
|
||||
/**
|
||||
* Entry point of the injected payload. prestige_native.dll appends this jar to the Fabric (Knot) class path, registers the
|
||||
* {@link NativeBridge} natives and calls {@link #start()}. Everything Fabric would normally do at launch for the Prestige mod
|
||||
* (Mixin anchors, entrypoints) is done here instead: the ClassTransform hooks are applied to the loaded classes with
|
||||
* {@link InPlaceTransformer} + JVMTI redefinition, then the mod entrypoints run on the client thread.
|
||||
*/
|
||||
public final class Bootstrap {
|
||||
private static boolean started;
|
||||
|
||||
private Bootstrap() {}
|
||||
|
||||
/** Called by the native loader on its worker thread once Minecraft is up. */
|
||||
public static synchronized void start() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
try {
|
||||
log("waiting for the Minecraft client");
|
||||
waitForClient();
|
||||
Report report = install(NativeBridge::classBytes, NativeBridge::redefine);
|
||||
log("hooks installed: " + report.redefined + " classes, " + report.moved + " handlers moved" + (report.failed.isEmpty() ? "" : ", FAILED " + report.failed));
|
||||
if (!report.failed.isEmpty()) throw new IllegalStateException("could not hook " + report.failed);
|
||||
log("initialising Prestige on the client thread");
|
||||
MinecraftClient.getInstance().execute(Bootstrap::initClient);
|
||||
} catch (Throwable t) {
|
||||
log("start failed: " + t);
|
||||
t.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
|
||||
/** The loader may inject while the game is still starting: wait until the client and its window exist. */
|
||||
private static void waitForClient() throws InterruptedException {
|
||||
for (int i = 0; i < 6000; i++) {
|
||||
MinecraftClient client = MinecraftClient.getInstance();
|
||||
if (client != null && client.getWindow() != null) return;
|
||||
Thread.sleep(50);
|
||||
}
|
||||
throw new IllegalStateException("Minecraft client did not start");
|
||||
}
|
||||
|
||||
public static final class Report {
|
||||
public int redefined, moved;
|
||||
public final List<String> failed = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all injection-mode hooks. The byte source and the redefiner are parameters so the transformation can be exercised
|
||||
* without the native library.
|
||||
*/
|
||||
public static Report install(Function<Class<?>, byte[]> classBytes, ToIntBiFunction<Class<?>, byte[]> redefine) throws Exception {
|
||||
String namespace = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace();
|
||||
List<String> transformers = new ArrayList<>(EnginePlugin.lines("prestige-engine/transformers.list"));
|
||||
transformers.addAll(EnginePlugin.lines("prestige-engine/inject-transformers.list"));
|
||||
TransformerManager manager = EnginePlugin.createManager(namespace, transformers);
|
||||
ClassLoader loader = Bootstrap.class.getClassLoader();
|
||||
InPlaceTransformer transformer = new InPlaceTransformer(manager, loader);
|
||||
EngineState.installed = true;
|
||||
|
||||
Report report = new Report();
|
||||
int total = manager.getTransformedClasses().size(), done = 0;
|
||||
log("transforming " + total + " hook targets");
|
||||
for (String target : manager.getTransformedClasses()) {
|
||||
if (done > 0 && done % 10 == 0) log("transformed " + done + "/" + total);
|
||||
done++;
|
||||
try {
|
||||
Class<?> type = Class.forName(target, false, loader);
|
||||
byte[] current = classBytes.apply(type);
|
||||
if (current == null) throw new IllegalStateException("no class bytes");
|
||||
InPlaceTransformer.Result result = transformer.transform(target, current);
|
||||
for (byte[] helper : result.helpers().values()) Definer.define(helper);
|
||||
int code = redefine.applyAsInt(type, result.target());
|
||||
if (code != 0) throw new IllegalStateException("redefine returned JVMTI error " + code);
|
||||
report.redefined++;
|
||||
report.moved += result.movedMethods();
|
||||
} catch (Throwable t) {
|
||||
report.failed.add(target);
|
||||
log("hook failed for " + target + ": " + t);
|
||||
t.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
private static void initClient() {
|
||||
try {
|
||||
new Prestige().onInitialize();
|
||||
new Entry().onInitializeClient();
|
||||
log("client initialised");
|
||||
} catch (Throwable t) {
|
||||
log("client init failed: " + t);
|
||||
t.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
|
||||
private static void log(String message) {
|
||||
System.out.println("[Prestige-Inject] " + message);
|
||||
try {
|
||||
NativeBridge.log(message);
|
||||
} catch (UnsatisfiedLinkError ignored) {
|
||||
// not injected (dev run): the console line above is enough
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package dev.zprestige.prestige.inject;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.lenni0451.classtransform.TransformerManager;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
/**
|
||||
* Runs the ClassTransform hooks on already loaded classes without changing their shape.
|
||||
*
|
||||
* ClassTransform copies every hook handler into the target class. A running JVM refuses to redefine a class with added methods,
|
||||
* so the handlers are moved into a generated helper class and the call sites in the target become invokestatic calls to it
|
||||
* (an instance handler receives the target instance as its first argument, which keeps the local variable layout identical).
|
||||
* Accesses from a moved handler to members it can no longer reach directly (private fields and methods of the target) go through
|
||||
* generated accessors backed by {@link Access}.
|
||||
*/
|
||||
public final class InPlaceTransformer {
|
||||
public static final String HOOK_PACKAGE = "dev/zprestige/prestige/hooks/";
|
||||
private static final String MH = "java/lang/invoke/MethodHandle";
|
||||
|
||||
/** The class to redefine plus the helper classes that must be defined (by name, in order) before it runs. */
|
||||
public record Result(byte[] target, Map<String, byte[]> helpers, int movedMethods) {}
|
||||
|
||||
private final TransformerManager manager;
|
||||
private final ClassLoader loader;
|
||||
private int sequence;
|
||||
|
||||
public InPlaceTransformer(TransformerManager manager, ClassLoader loader) {
|
||||
this.manager = manager;
|
||||
this.loader = loader;
|
||||
}
|
||||
|
||||
/** @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);
|
||||
if (out == null) throw new IllegalStateException("ClassTransform failed for " + dottedName);
|
||||
ClassNode before = read(current);
|
||||
ClassNode after = read(out);
|
||||
|
||||
Set<String> beforeFields = new HashSet<>();
|
||||
before.fields.forEach(f -> beforeFields.add(f.name + ":" + f.desc));
|
||||
for (var f : after.fields)
|
||||
if (!beforeFields.contains(f.name + ":" + f.desc))
|
||||
throw new IllegalStateException(dottedName + ": transformer added field " + f.name + " (cannot be redefined in place)");
|
||||
|
||||
Map<String, MethodNode> existing = new HashMap<>();
|
||||
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())
|
||||
throw new IllegalStateException(dottedName + ": transformer changed the interface list");
|
||||
|
||||
String helperName = HOOK_PACKAGE + "H" + (sequence++) + "_" + after.name.substring(after.name.lastIndexOf('/') + 1).replace('$', '_');
|
||||
ClassNode helper = new ClassNode();
|
||||
helper.version = after.version;
|
||||
helper.access = Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER;
|
||||
helper.name = helperName;
|
||||
helper.superName = "java/lang/Object";
|
||||
|
||||
Map<String, Boolean> movedInstance = new HashMap<>();
|
||||
for (MethodNode m : moved.values()) movedInstance.put(m.name + m.desc, (m.access & Opcodes.ACC_STATIC) == 0);
|
||||
|
||||
for (MethodNode m : moved.values()) {
|
||||
boolean instance = movedInstance.get(m.name + m.desc);
|
||||
String desc = instance ? "(L" + after.name + ";" + m.desc.substring(1) : m.desc;
|
||||
MethodNode copy = new MethodNode(Opcodes.ASM9, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, m.name, desc, null, m.exceptions.toArray(new String[0]));
|
||||
m.accept(copy);
|
||||
copy.access = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC;
|
||||
copy.desc = desc;
|
||||
copy.visibleAnnotations = null;
|
||||
copy.invisibleAnnotations = null;
|
||||
copy.visibleParameterAnnotations = null;
|
||||
copy.invisibleParameterAnnotations = null;
|
||||
copy.visibleTypeAnnotations = null;
|
||||
copy.invisibleTypeAnnotations = null;
|
||||
helper.methods.add(copy);
|
||||
}
|
||||
|
||||
Accessors accessors = new Accessors(helper);
|
||||
for (MethodNode m : helper.methods) rewrite(m, after.name, helperName, movedInstance, accessors, true, dottedName);
|
||||
for (MethodNode m : after.methods)
|
||||
if (!moved.containsKey(m.name + m.desc)) rewrite(m, after.name, helperName, movedInstance, accessors, false, dottedName);
|
||||
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());
|
||||
}
|
||||
|
||||
private static ClassNode read(byte[] bytes) {
|
||||
ClassNode node = new ClassNode();
|
||||
new ClassReader(bytes).accept(node, 0);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void rewrite(MethodNode method, String targetName, String helperName, Map<String, Boolean> moved, Accessors accessors,
|
||||
boolean inHelper, String context) throws ReflectiveOperationException {
|
||||
for (AbstractInsnNode insn = method.instructions.getFirst(); insn != null; ) {
|
||||
AbstractInsnNode next = insn.getNext();
|
||||
if (insn instanceof MethodInsnNode call) {
|
||||
Boolean instance = call.owner.equals(targetName) ? moved.get(call.name + call.desc) : null;
|
||||
if (instance != null) {
|
||||
call.setOpcode(Opcodes.INVOKESTATIC);
|
||||
call.owner = helperName;
|
||||
call.itf = false;
|
||||
if (instance) call.desc = "(L" + targetName + ";" + call.desc.substring(1);
|
||||
} else if (inHelper && !call.name.equals("<init>")) {
|
||||
if (call.getOpcode() == Opcodes.INVOKESPECIAL && !call.owner.equals(targetName))
|
||||
throw new IllegalStateException(context + ": handler " + method.name + " calls super method " + call.owner + "." + call.name);
|
||||
if (!accessible(call.owner, call.name, call.desc, true))
|
||||
accessors.route(method, call, call.getOpcode() == Opcodes.INVOKESTATIC ? Access.STATIC : Access.VIRTUAL, helperName);
|
||||
}
|
||||
} else if (inHelper && insn instanceof FieldInsnNode field) {
|
||||
if (!accessible(field.owner, field.name, field.desc, false)) {
|
||||
int kind = switch (field.getOpcode()) {
|
||||
case Opcodes.GETFIELD -> Access.GET_FIELD;
|
||||
case Opcodes.PUTFIELD -> Access.PUT_FIELD;
|
||||
case Opcodes.GETSTATIC -> Access.GET_STATIC;
|
||||
default -> Access.PUT_STATIC;
|
||||
};
|
||||
accessors.route(method, field, kind, helperName);
|
||||
}
|
||||
}
|
||||
insn = next;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when code in another package can name the member directly: public member, public declaring class and public named owner. */
|
||||
private boolean accessible(String ownerInternal, String name, String desc, boolean method) throws ReflectiveOperationException {
|
||||
if (ownerInternal.startsWith("[")) return true;
|
||||
Class<?> owner;
|
||||
try {
|
||||
owner = Class.forName(ownerInternal.replace('/', '.'), false, loader);
|
||||
} catch (ClassNotFoundException e) {
|
||||
return true; // not a game class we can inspect; leave it for the verifier
|
||||
}
|
||||
if (!Modifier.isPublic(owner.getModifiers())) return false;
|
||||
for (Class<?> c = owner; c != null; c = c.getSuperclass()) {
|
||||
if (method) {
|
||||
for (Method m : c.getDeclaredMethods())
|
||||
if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc))
|
||||
return Modifier.isPublic(m.getModifiers()) && Modifier.isPublic(c.getModifiers());
|
||||
} else {
|
||||
for (Field f : c.getDeclaredFields())
|
||||
if (f.getName().equals(name))
|
||||
return Modifier.isPublic(f.getModifiers()) && Modifier.isPublic(c.getModifiers());
|
||||
}
|
||||
}
|
||||
if (method) {
|
||||
for (Class<?> i : owner.getInterfaces()) if (accessible(i.getName().replace('.', '/'), name, desc, true)) return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Collects the accessor methods and the MethodHandle fields they use, and builds the helper's static initialiser. */
|
||||
private static final class Accessors {
|
||||
private final ClassNode helper;
|
||||
private final Map<String, String> byKey = new HashMap<>();
|
||||
private final List<Object[]> entries = new ArrayList<>();
|
||||
|
||||
Accessors(ClassNode helper) { this.helper = helper; }
|
||||
|
||||
/** Replaces the member instruction by an invokestatic of a (new or reused) accessor. */
|
||||
void route(MethodNode in, AbstractInsnNode insn, int kind, String helperName) {
|
||||
String owner, name, desc;
|
||||
if (insn instanceof FieldInsnNode f) { owner = f.owner; name = f.name; desc = f.desc; }
|
||||
else { MethodInsnNode m = (MethodInsnNode) insn; owner = m.owner; name = m.name; desc = m.desc; }
|
||||
String key = kind + ":" + owner + "." + name + desc;
|
||||
String accessor = byKey.get(key);
|
||||
if (accessor == null) {
|
||||
accessor = "acc$" + byKey.size();
|
||||
byKey.put(key, accessor);
|
||||
String accessorDesc = accessorDescriptor(kind, desc);
|
||||
MethodNode node = new MethodNode(Opcodes.ASM9, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, accessor, accessorDesc, null, null);
|
||||
node.visitFieldInsn(Opcodes.GETSTATIC, helperName, "mh$" + (byKey.size() - 1), "L" + MH + ";");
|
||||
int slot = 0;
|
||||
for (Type arg : Type.getArgumentTypes(accessorDesc)) {
|
||||
node.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
|
||||
slot += arg.getSize();
|
||||
}
|
||||
node.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH, "invokeExact", accessorDesc, false);
|
||||
node.visitInsn(Type.getReturnType(accessorDesc).getOpcode(Opcodes.IRETURN));
|
||||
helper.methods.add(node);
|
||||
entries.add(new Object[]{byKey.size() - 1, owner.replace('/', '.'), name, desc, kind});
|
||||
}
|
||||
in.instructions.set(insn, new MethodInsnNode(Opcodes.INVOKESTATIC, helperName, accessor, accessorDescriptor(kind, desc), false));
|
||||
}
|
||||
|
||||
/** Descriptor of the accessor method as seen by callers (receiver stays typed Object). */
|
||||
private static String accessorDescriptor(int kind, String desc) {
|
||||
return switch (kind) {
|
||||
case Access.GET_FIELD -> "(Ljava/lang/Object;)" + desc;
|
||||
case Access.PUT_FIELD -> "(Ljava/lang/Object;" + desc + ")V";
|
||||
case Access.GET_STATIC -> "()" + desc;
|
||||
case Access.PUT_STATIC -> "(" + desc + ")V";
|
||||
case Access.VIRTUAL -> "(Ljava/lang/Object;" + desc.substring(1);
|
||||
default -> desc;
|
||||
};
|
||||
}
|
||||
|
||||
void finish() {
|
||||
if (entries.isEmpty()) return;
|
||||
MethodNode clinit = new MethodNode(Opcodes.ASM9, Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
|
||||
for (Object[] e : entries) {
|
||||
String field = "mh$" + e[0];
|
||||
helper.fields.add(new org.objectweb.asm.tree.FieldNode(Opcodes.ASM9, Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, field, "L" + MH + ";", null, null));
|
||||
clinit.visitLdcInsn(e[1]);
|
||||
clinit.visitLdcInsn(e[2]);
|
||||
clinit.visitLdcInsn(e[3]);
|
||||
clinit.visitIntInsn(Opcodes.BIPUSH, (Integer) e[4]);
|
||||
clinit.visitMethodInsn(Opcodes.INVOKESTATIC, "dev/zprestige/prestige/inject/Access", "resolve",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)L" + MH + ";", false);
|
||||
clinit.visitFieldInsn(Opcodes.PUTSTATIC, helper.name, field, "L" + MH + ";");
|
||||
}
|
||||
clinit.visitInsn(Opcodes.RETURN);
|
||||
helper.methods.add(clinit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.zprestige.prestige.inject;
|
||||
|
||||
/** Natives registered by the injected prestige_native.dll (JVMTI): current class bytes and class redefinition. */
|
||||
public final class NativeBridge {
|
||||
private NativeBridge() {}
|
||||
|
||||
/** Bytes of the loaded class as the JVM currently has it (including changes made by earlier transformers). */
|
||||
public static native byte[] classBytes(Class<?> type);
|
||||
|
||||
/** Appends a line to prestige-native.log, which the injector tails to show progress. */
|
||||
public static native void log(String message);
|
||||
|
||||
/** JVMTI RedefineClasses for one class; returns the JVMTI error code, 0 on success. */
|
||||
public static native int redefine(Class<?> type, byte[] bytes);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package dev.zprestige.prestige.inject;
|
||||
|
||||
import dev.zprestige.prestige.engine.EnginePlugin;
|
||||
import dev.zprestige.prestige.hooks.Definer;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.lenni0451.classtransform.TransformerManager;
|
||||
|
||||
/**
|
||||
* Dev check of the in-place transformer without the native library: transforms the pristine class files of every hook target,
|
||||
* defines the generated helper classes (verifies them and resolves their accessors) and links the transformed target in an
|
||||
* isolated class loader (verifies it). Run through the engine dev command "injectselftest".
|
||||
*/
|
||||
public final class SelfTest {
|
||||
private SelfTest() {}
|
||||
|
||||
public static String run() throws Exception {
|
||||
String namespace = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace();
|
||||
List<String> transformers = new ArrayList<>(EnginePlugin.lines("prestige-engine/transformers.list"));
|
||||
transformers.addAll(EnginePlugin.lines("prestige-engine/inject-transformers.list"));
|
||||
TransformerManager manager = EnginePlugin.createManager(namespace, transformers);
|
||||
ClassLoader loader = SelfTest.class.getClassLoader();
|
||||
InPlaceTransformer transformer = new InPlaceTransformer(manager, loader);
|
||||
|
||||
StringBuilder out = new StringBuilder("injectselftest namespace=" + namespace + " targets=" + manager.getTransformedClasses().size());
|
||||
int ok = 0, moved = 0;
|
||||
for (String target : manager.getTransformedClasses()) {
|
||||
try {
|
||||
byte[] original;
|
||||
try (InputStream in = loader.getResourceAsStream(target.replace('.', '/') + ".class")) {
|
||||
if (in == null) throw new IllegalStateException("no class resource");
|
||||
original = in.readAllBytes();
|
||||
}
|
||||
InPlaceTransformer.Result result = transformer.transform(target, original);
|
||||
byte[] raw = manager.transform(target, original, true);
|
||||
out.append("\n ").append(target).append(" original=").append(original.length).append(" ct=").append(raw == null ? -1 : raw.length)
|
||||
.append(" result=").append(result.target().length).append(" moved=").append(result.movedMethods());
|
||||
for (var helper : result.helpers().entrySet()) {
|
||||
Definer.define(helper.getValue());
|
||||
Class.forName(helper.getKey(), true, loader);
|
||||
}
|
||||
Class<?> linked = new Isolated(loader, target, result.target()).loadClass(target);
|
||||
for (Method ignored : linked.getDeclaredMethods()) { /* forces linking, i.e. bytecode verification */ }
|
||||
ok++;
|
||||
moved += result.movedMethods();
|
||||
} catch (Throwable t) {
|
||||
out.append("\n FAIL ").append(target).append(": ").append(t);
|
||||
t.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
return out.append("\n ok=").append(ok).append(" movedHandlers=").append(moved).toString();
|
||||
}
|
||||
|
||||
/** Defines the transformed target itself; everything else comes from the parent. */
|
||||
private static final class Isolated extends ClassLoader {
|
||||
private final String target;
|
||||
private final byte[] bytes;
|
||||
|
||||
Isolated(ClassLoader parent, String target, byte[] bytes) {
|
||||
super(parent);
|
||||
this.target = target;
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
@Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (!name.equals(target)) return super.loadClass(name, resolve);
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
Class<?> c = findLoadedClass(name);
|
||||
return c != null ? c : defineClass(name, bytes, 0, bytes.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package dev.zprestige.prestige.inject.hooks;
|
||||
|
||||
import dev.zprestige.prestige.ui.mc.Overlay;
|
||||
import net.lenni0451.classtransform.InjectionCallback;
|
||||
import net.lenni0451.classtransform.annotations.CTarget;
|
||||
import net.lenni0451.classtransform.annotations.CTransformer;
|
||||
import net.lenni0451.classtransform.annotations.injection.CInject;
|
||||
import net.minecraft.client.Keyboard;
|
||||
import net.minecraft.client.input.CharInput;
|
||||
import net.minecraft.client.input.KeyInput;
|
||||
|
||||
/** Injection-mode twin of ui.mixin.KeyboardHandlerMixin: swallows keyboard input while the overlay is visible. */
|
||||
@CTransformer({Keyboard.class})
|
||||
public class KeyboardHooks {
|
||||
@CInject(method = {"onKey"}, target = {@CTarget("HEAD")}, cancellable = true)
|
||||
private void key(long window, int action, KeyInput ev, InjectionCallback ci) {
|
||||
if (Overlay.onKey(ev.key(), action)) ci.cancel();
|
||||
}
|
||||
|
||||
@CInject(method = {"onChar"}, target = {@CTarget("HEAD")}, cancellable = true)
|
||||
private void character(long window, CharInput ev, InjectionCallback ci) {
|
||||
if (Overlay.onChar(ev.codepoint())) ci.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package dev.zprestige.prestige.inject.hooks;
|
||||
|
||||
import dev.zprestige.prestige.ui.mc.Overlay;
|
||||
import net.lenni0451.classtransform.InjectionCallback;
|
||||
import net.lenni0451.classtransform.annotations.CTarget;
|
||||
import net.lenni0451.classtransform.annotations.CTransformer;
|
||||
import net.lenni0451.classtransform.annotations.injection.CInject;
|
||||
import net.minecraft.client.Mouse;
|
||||
import net.minecraft.client.input.MouseInput;
|
||||
|
||||
/** Injection-mode twin of ui.mixin.MouseHandlerMixin: swallows mouse input while the overlay is visible. */
|
||||
@CTransformer({Mouse.class})
|
||||
public class MouseHooks {
|
||||
@CInject(method = {"onMouseButton"}, target = {@CTarget("HEAD")}, cancellable = true)
|
||||
private void button(long window, MouseInput info, int action, InjectionCallback ci) {
|
||||
if (Overlay.onMouseButton(info.button(), action)) ci.cancel();
|
||||
}
|
||||
|
||||
@CInject(method = {"onMouseScroll"}, target = {@CTarget("HEAD")}, cancellable = true)
|
||||
private void scroll(long window, double x, double y, InjectionCallback ci) {
|
||||
if (Overlay.onScroll(y)) ci.cancel();
|
||||
}
|
||||
|
||||
@CInject(method = {"onCursorPos"}, target = {@CTarget("HEAD")}, cancellable = true)
|
||||
private void move(long window, double x, double y, InjectionCallback ci) {
|
||||
if (Overlay.onMouseMove()) ci.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dev.zprestige.prestige.inject.hooks;
|
||||
|
||||
import dev.zprestige.prestige.ui.mc.Overlay;
|
||||
import net.lenni0451.classtransform.InjectionCallback;
|
||||
import net.lenni0451.classtransform.annotations.CTarget;
|
||||
import net.lenni0451.classtransform.annotations.CTransformer;
|
||||
import net.lenni0451.classtransform.annotations.injection.CInject;
|
||||
|
||||
/** Injection-mode twin of ui.mixin.RenderSystemMixin: draws the overlay at the head of RenderSystem.flipFrame (Mojang-named, never remapped). */
|
||||
@CTransformer(name = "com.mojang.blaze3d.systems.RenderSystem")
|
||||
public class RenderSystemHooks {
|
||||
@CInject(method = {"flipFrame"}, target = {@CTarget("HEAD")})
|
||||
private static void overlay(InjectionCallback ci) {
|
||||
Overlay.render();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user