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
@@ -69,7 +69,7 @@ public class Module extends SettingHolder implements MinecraftAccess {
|
||||
this.setLastEnabled(this.isEnabled());
|
||||
if (this.isEnabled()) {
|
||||
Prestige.eventBus.register(this);
|
||||
if (mc.world != null && mc.player != null) {
|
||||
if (!requiresWorldOnEnable() || (mc.world != null && mc.player != null)) {
|
||||
this.onEnable();
|
||||
} else {
|
||||
Prestige.moduleManager.queueEnable(this);
|
||||
@@ -89,7 +89,7 @@ public class Module extends SettingHolder implements MinecraftAccess {
|
||||
public void applyState() {
|
||||
if (this.isEnabled()) {
|
||||
Prestige.eventBus.register(this);
|
||||
if (mc.world != null && mc.player != null) {
|
||||
if (!requiresWorldOnEnable() || (mc.world != null && mc.player != null)) {
|
||||
this.onEnable();
|
||||
} else {
|
||||
Prestige.moduleManager.queueEnable(this);
|
||||
@@ -123,6 +123,10 @@ public class Module extends SettingHolder implements MinecraftAccess {
|
||||
public void onEnable() {
|
||||
}
|
||||
|
||||
protected boolean requiresWorldOnEnable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,22 @@ 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.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToIntBiFunction;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.lenni0451.classtransform.TransformerManager;
|
||||
import net.lenni0451.classtransform.utils.mappings.SuperMappingFiller;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
/**
|
||||
* Entry point of the injected payload. prestige_native.dll appends this jar to the Fabric (Knot) class path, registers the
|
||||
@@ -41,11 +49,12 @@ public final class Bootstrap {
|
||||
log("waiting for the Minecraft client");
|
||||
waitForClient();
|
||||
Report report = install(NativeBridge::classBytes, NativeBridge::redefine, nativeBatch());
|
||||
log("hooks installed: " + report.redefined + " classes, " + report.moved + " handlers moved" + (report.failed.isEmpty() ? "" : ", FAILED " + report.failed));
|
||||
log("hooks installed: " + report.redefined + " classes, " + report.moved + " handlers moved"
|
||||
+ ", " + report.unchanged + " unchanged" + (report.failed.isEmpty() ? "" : ", FAILED " + report.failed));
|
||||
if (report.redefined == 0) throw new IllegalStateException("no hooks installed");
|
||||
// Some clients (Lunar) name a few classes and methods differently. Keep going with the hooks that did apply: a missing
|
||||
// hook only disables the features behind it, while stopping here would leave the client without Prestige at all.
|
||||
if (!report.failed.isEmpty()) {
|
||||
if (report.redefined == 0) throw new IllegalStateException("could not hook " + report.failed);
|
||||
log("continuing with " + report.redefined + " hooked classes; skipped " + report.failed);
|
||||
}
|
||||
log("initialising Prestige on the client thread");
|
||||
@@ -67,7 +76,7 @@ public final class Bootstrap {
|
||||
}
|
||||
|
||||
public static final class Report {
|
||||
public int redefined, moved;
|
||||
public int redefined, moved, unchanged;
|
||||
public final List<String> failed = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -147,6 +156,26 @@ public final class Bootstrap {
|
||||
}
|
||||
long captured = System.nanoTime();
|
||||
|
||||
// Genesis can expose different class resources from the definitions installed in its JVM.
|
||||
// Resolve inherited Yarn members using the actual superclass and interface declarations.
|
||||
var mappings = manager.getMapper().getRemapper();
|
||||
if (!mappings.isEmpty()) {
|
||||
Map<Class<?>, ClassNode> declarations = new HashMap<>();
|
||||
for (Class<?> type : types) {
|
||||
if (type == null) continue;
|
||||
try {
|
||||
Set<ClassNode> parents = new LinkedHashSet<>();
|
||||
collectParents(type.getSuperclass(), declarations, parents);
|
||||
for (Class<?> iface : type.getInterfaces()) collectParents(iface, declarations, parents);
|
||||
ClassNode target = new ClassNode();
|
||||
target.name = Type.getInternalName(type);
|
||||
SuperMappingFiller.fillSuperMembers(target, parents, mappings);
|
||||
} catch (LinkageError e) {
|
||||
log("could not inspect inherited mappings for " + type.getName() + ": " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ClassTransform, one class at a time (its mapper is not thread-safe: running it on several threads corrupts its state).
|
||||
// A result that is already in the disk cache skips the transformation, which is where nearly all of the time goes.
|
||||
InPlaceTransformer.Result[] results = new InPlaceTransformer.Result[total];
|
||||
@@ -167,6 +196,12 @@ public final class Bootstrap {
|
||||
continue;
|
||||
}
|
||||
results[i] = transformer.transform(target, originals[i]);
|
||||
if (results[i].target() == originals[i]) {
|
||||
log("no bytecode changes for " + target);
|
||||
report.unchanged++;
|
||||
results[i] = null;
|
||||
continue;
|
||||
}
|
||||
if (cache != null && results[i].helpers().isEmpty()) cache.put(target, originals[i], results[i].target());
|
||||
} catch (Throwable t) {
|
||||
fail(report, target, t);
|
||||
@@ -211,6 +246,24 @@ public final class Bootstrap {
|
||||
return report;
|
||||
}
|
||||
|
||||
private static void collectParents(Class<?> type, Map<Class<?>, ClassNode> declarations, Set<ClassNode> parents) {
|
||||
if (type == null || !type.getName().startsWith("net.minecraft.")) return;
|
||||
ClassNode declaration = declarations.computeIfAbsent(type, parent -> {
|
||||
ClassNode node = new ClassNode();
|
||||
node.name = Type.getInternalName(parent);
|
||||
for (var method : parent.getDeclaredMethods()) {
|
||||
node.methods.add(new MethodNode(method.getModifiers(), method.getName(), Type.getMethodDescriptor(method), null, null));
|
||||
}
|
||||
for (var field : parent.getDeclaredFields()) {
|
||||
node.fields.add(new FieldNode(field.getModifiers(), field.getName(), Type.getDescriptor(field.getType()), null, null));
|
||||
}
|
||||
return node;
|
||||
});
|
||||
if (!parents.add(declaration)) return;
|
||||
collectParents(type.getSuperclass(), declarations, parents);
|
||||
for (Class<?> iface : type.getInterfaces()) collectParents(iface, declarations, parents);
|
||||
}
|
||||
|
||||
private static void fail(Report report, String target, Throwable t) {
|
||||
report.failed.add(target);
|
||||
log("hook failed for " + target + ": " + t);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,20 +7,15 @@ import net.lenni0451.classtransform.annotations.CTarget;
|
||||
import net.lenni0451.classtransform.annotations.CTransformer;
|
||||
import net.lenni0451.classtransform.annotations.injection.CInject;
|
||||
import net.minecraft.client.gui.hud.InGameOverlayRenderer;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
|
||||
@CTransformer({InGameOverlayRenderer.class})
|
||||
public class InGameOverlayRendererMixin {
|
||||
@CInject(
|
||||
method = {"renderFireOverlay"},
|
||||
target = {@CTarget(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/client/util/math/MatrixStack;translate(FFF)V"
|
||||
)},
|
||||
target = {@CTarget("HEAD")},
|
||||
cancellable = true
|
||||
)
|
||||
private static void hideFireOverlayBeforeTranslate(MatrixStack matrices, VertexConsumerProvider vertexConsumers, InjectionCallback ci) {
|
||||
private static void hideFireOverlayAtHead(InjectionCallback ci) {
|
||||
FireOverlayEvent event = HookBridge.createFireOverlayEvent();
|
||||
if (HookBridge.postFireOverlayEvent(event)) {
|
||||
ci.cancel();
|
||||
|
||||
@@ -8,10 +8,12 @@ import net.lenni0451.classtransform.annotations.CTransformer;
|
||||
import net.lenni0451.classtransform.annotations.injection.CInject;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.damage.DamageSource;
|
||||
import net.minecraft.particle.BlockParticleEffect;
|
||||
import net.minecraft.particle.ParticleEffect;
|
||||
import net.minecraft.registry.entry.RegistryEntry;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.sound.SoundEvent;
|
||||
import net.minecraft.util.collection.Pool;
|
||||
import net.minecraft.world.World.ExplosionSourceType;
|
||||
import net.minecraft.world.explosion.ExplosionBehavior;
|
||||
|
||||
@@ -33,6 +35,7 @@ public class ServerWorldMixin {
|
||||
ExplosionSourceType explosionSourceType,
|
||||
ParticleEffect smallParticle,
|
||||
ParticleEffect largeParticle,
|
||||
Pool<BlockParticleEffect> blockParticles,
|
||||
RegistryEntry<SoundEvent> soundEvent,
|
||||
InjectionCallback ci
|
||||
) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package dev.zprestige.prestige.module.misc;
|
||||
|
||||
import dev.zprestige.prestige.Module;
|
||||
import dev.zprestige.prestige.client.Prestige;
|
||||
import dev.zprestige.prestige.engine.EngineState;
|
||||
import dev.zprestige.prestige.inject.Bootstrap;
|
||||
import dev.zprestige.prestige.inject.NativeBridge;
|
||||
import dev.zprestige.prestige.ui.mc.Overlay;
|
||||
@@ -22,9 +23,10 @@ public class SelfDestruct extends Module {
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (Prestige.selfDestructed) return;
|
||||
Prestige.selfDestructed = true;
|
||||
System.out.println("[Prestige] Self Destruct started");
|
||||
Overlay.shutdown();
|
||||
mc.setScreen(null);
|
||||
|
||||
List<Module> modules = new ArrayList<>(Prestige.moduleManager.getModules());
|
||||
for (Module module : modules) {
|
||||
@@ -41,10 +43,13 @@ public class SelfDestruct extends Module {
|
||||
}
|
||||
|
||||
// Disposing this module now would null its own "Enabled" value while Module.toggle still has to read it.
|
||||
mc.execute(() -> dispose(modules));
|
||||
EngineState.ready = false;
|
||||
mc.send(() -> dispose(modules));
|
||||
}
|
||||
|
||||
private void dispose(List<Module> modules) {
|
||||
Prestige.eventBus.unregister(this);
|
||||
Prestige.eventBus.unregister(Prestige.moduleManager);
|
||||
for (Module module : modules) {
|
||||
try {
|
||||
module.dispose();
|
||||
@@ -56,15 +61,20 @@ public class SelfDestruct extends Module {
|
||||
Prestige.moduleManager.setModules(new ArrayList<>());
|
||||
|
||||
if (Bootstrap.isInjected()) {
|
||||
// one more tick so no hook of the current tick is still being entered when the original classes come back
|
||||
mc.execute(() -> mc.execute(SelfDestruct::unhook));
|
||||
unhook();
|
||||
} else {
|
||||
System.out.println("[Prestige] Self Destruct finished");
|
||||
}
|
||||
}
|
||||
|
||||
private static void unhook() {
|
||||
try {
|
||||
Bootstrap.uninstall();
|
||||
NativeBridge.release();
|
||||
if (Bootstrap.uninstall() == 0) {
|
||||
NativeBridge.release();
|
||||
System.out.println("[Prestige] Self Destruct finished");
|
||||
} else {
|
||||
System.out.println("[Prestige] Self Destruct left hooks installed. Restart the game to clear them.");
|
||||
}
|
||||
} catch (UnsatisfiedLinkError ignored) {
|
||||
// loader already gone
|
||||
} catch (Throwable error) {
|
||||
@@ -73,6 +83,11 @@ public class SelfDestruct extends Module {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean requiresWorldOnEnable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPersistent() {
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user