From 036b24f2ae32e0db859ddebe9eda4f70b9858838 Mon Sep 17 00:00:00 2001 From: bbldrizzy Date: Tue, 3 Nov 2026 11:40:43 +0000 Subject: [PATCH] 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. --- .gitignore | 1 + injector/README.md | 19 +++++- injector/crates/injector/src/main.rs | 17 ++++-- injector/crates/injector/src/platform/mod.rs | 1 + injector/crates/loader/src/jni.rs | 3 - injector/crates/loader/src/worker.rs | 35 +++++++---- .../java/dev/zprestige/prestige/Module.java | 8 ++- .../zprestige/prestige/inject/Bootstrap.java | 59 ++++++++++++++++++- .../prestige/inject/InPlaceTransformer.java | 52 +++++++++++++--- .../mixin/InGameOverlayRendererMixin.java | 9 +-- .../prestige/mixin/ServerWorldMixin.java | 3 + .../prestige/module/misc/SelfDestruct.java | 27 +++++++-- 12 files changed, 186 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index db63b88..c3b5866 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ openvape.zip /dist/ /injector/target/ /tools +/examples diff --git a/injector/README.md b/injector/README.md index c85ba77..f0d7213 100644 --- a/injector/README.md +++ b/injector/README.md @@ -16,7 +16,7 @@ The injector and library must be built for the same architecture as the target J ## How it works -1. **Injector** lists the running Fabric client JVMs, then loads the library into the chosen process. +1. **Injector** lists the running Minecraft JVMs, then loads the library into the chosen process. - **Windows:** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen process through `CreateRemoteThread` and confirms the module is mapped. - **Linux / macOS:** uses the HotSpot attach protocol (the same mechanism as `jcmd JVMTI.agent_load`). The injector @@ -29,7 +29,8 @@ The injector and library must be built for the same architecture as the target J unix, which also hands over the `JavaVM*`). The worker: - waits for the JVM (Windows: `jvm.dll`, `JNI_GetCreatedJavaVMs`), attaches, acquires the JVMTI redefine/retransform capabilities; - writes the embedded payload jar (the remapped `prestige-*.jar`) to the temp directory under `PrestigeLoader`; - - `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the Knot class loader; + - looks for Fabric through the render thread's context class loader, then the system and other thread loaders; + - `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the launcher's game class loader; - registers `NativeBridge.classBytes` / `NativeBridge.redefine` and calls `Bootstrap.start()`. 3. **`Bootstrap`** (Java) waits for the client window, then for every ClassTransform hook target gets the current class bytes (JVMTI retransform capture), runs the hooks through `InPlaceTransformer` and redefines the class. The bundled ClassTransform @@ -71,6 +72,20 @@ PrestigeInjector [lib path] # inject into a specific pid, optionally w On Windows this is `PrestigeInjector.exe`. By default the library is looked up beside the injector. +### Dawn and Lunar Client 1.21.11 + +Use a 1.21.11 Fabric profile in Dawn or enable Lunar Client's Fabric add-on for 1.21.11. The target +profile needs Fabric Loader 0.19.x and Fabric API. Wait for the title screen before selecting the game +in the injector. The default Prestige menu key is Right Shift, which may overlap with a launcher keybind. + +For Lunar, the native log should identify `GenesisClassLoader`. Check the game log for `hooks installed` +and `client initialised`. Hook counts refer to classes with changed bytecode. A skipped or failed hook +can leave its feature unavailable even if the menu opens. + +An offline JVM replay accepted all 42 changed classes from Lunar's installed 1.21.11 cache after +recalculating stack and local limits on the cached classes. Live injection and Dawn's in-game profile +still need smoke testing. + ## Install as a mod instead The payload jar is also a normal Fabric mod. Put `prestige-*.jar` (from `dist/` or the CI `payload-jar` artifact) in the diff --git a/injector/crates/injector/src/main.rs b/injector/crates/injector/src/main.rs index 8d0805f..96949e2 100644 --- a/injector/crates/injector/src/main.rs +++ b/injector/crates/injector/src/main.rs @@ -221,14 +221,12 @@ fn pick_process() -> Option { let mut selected = 0usize; let mut processes = platform::java_processes(); let mut last_refresh = Instant::now(); + draw(&processes, selected); loop { - if selected >= processes.len() { - selected = processes.len().saturating_sub(1); - } - draw(&processes, selected); let deadline = Instant::now() + Duration::from_millis(50); while Instant::now() < deadline { if let Some(key) = platform::poll_key() { + let previous = selected; match key { platform::Key::Up => selected = selected.saturating_sub(1), platform::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)), @@ -239,12 +237,19 @@ fn pick_process() -> Option { } platform::Key::Escape => return None, } - draw(&processes, selected); + if selected != previous { + draw(&processes, selected); + } } std::thread::sleep(Duration::from_millis(10)); } if last_refresh.elapsed() >= Duration::from_millis(750) { - processes = platform::java_processes(); + let refreshed = platform::java_processes(); + if refreshed != processes { + processes = refreshed; + selected = selected.min(processes.len().saturating_sub(1)); + draw(&processes, selected); + } last_refresh = Instant::now(); } } diff --git a/injector/crates/injector/src/platform/mod.rs b/injector/crates/injector/src/platform/mod.rs index fb7bd2c..c64d4cb 100644 --- a/injector/crates/injector/src/platform/mod.rs +++ b/injector/crates/injector/src/platform/mod.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; /// A running Minecraft JVM shown in the picker. +#[derive(PartialEq)] pub struct JavaProcess { pub pid: u32, pub title: String, diff --git a/injector/crates/loader/src/jni.rs b/injector/crates/loader/src/jni.rs index a2c4051..4f7a5a1 100644 --- a/injector/crates/loader/src/jni.rs +++ b/injector/crates/loader/src/jni.rs @@ -56,9 +56,6 @@ impl Jni { pub unsafe fn exception_clear(&self) { f!(self, 17, unsafe extern "system" fn(EnvPtr))(self.env) } - pub unsafe fn new_global_ref(&self, o: JObject) -> JObject { - f!(self, 21, unsafe extern "system" fn(EnvPtr, JObject) -> JObject)(self.env, o) - } pub unsafe fn is_same_object(&self, a: JObject, b: JObject) -> bool { f!(self, 24, unsafe extern "system" fn(EnvPtr, JObject, JObject) -> JBool)(self.env, a, b) != 0 } diff --git a/injector/crates/loader/src/worker.rs b/injector/crates/loader/src/worker.rs index f50aa7b..551a645 100644 --- a/injector/crates/loader/src/worker.rs +++ b/injector/crates/loader/src/worker.rs @@ -48,7 +48,7 @@ unsafe fn install() -> Result<(), String> { log!("payload written to {}", jar.display()); let loader = add_to_knot(&jni, &jar)?; - log!("payload appended to the Fabric Knot class loader"); + log!("payload appended to the game class loader: {}", describe(&jni, loader)); register_natives(&jni, loader)?; log!("natives registered"); @@ -111,7 +111,7 @@ fn write_payload() -> Result { } /// FabricLauncherBase.getLauncher().addToClassPath(path) so the payload and the game classes share the Knot class loader. -/// Returns a global reference to the Knot target class loader. +/// Returns a local reference to the Fabric launcher's target class loader. unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result { jni.ensure_local_capacity(4096); let (base, fabric_loader) = locate_fabric(jni)?; @@ -149,23 +149,30 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result Result<(JClass, JObject), String> { + let (render, loaders) = thread_loaders(jni)?; + if let Some(loader) = render { + if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) { + log!("fabric-loader found through the render thread: {}", describe(jni, loader)); + return Ok((class, loader)); + } + } let base = jni.find_class(&FABRIC_BASE.replace('.', "/")); if jni.take_exception().is_none() && !base.is_null() { return Ok((base, std::ptr::null_mut())); } log!("FabricLauncherBase is not visible to the system class loader, probing thread context class loaders"); - let loaders = thread_loaders(jni)?; for &loader in &loaders { + if render.is_some_and(|r| jni.is_same_object(r, loader)) { + continue; + } if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) { log!("fabric-loader found through {}", describe(jni, loader)); return Ok((class, loader)); @@ -180,10 +187,11 @@ unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> { } /// Distinct context class loaders of all live threads (the game threads inherit the launcher's loader). -unsafe fn thread_loaders(jni: &Jni) -> Result, String> { +unsafe fn thread_loaders(jni: &Jni) -> Result<(Option, Vec), String> { let thread_class = jni.find_class("java/lang/Thread"); let get_all = jni.get_static_method_id(thread_class, "getAllStackTraces", "()Ljava/util/Map;"); let get_context = jni.get_method_id(thread_class, "getContextClassLoader", "()Ljava/lang/ClassLoader;"); + let get_name = jni.get_method_id(thread_class, "getName", "()Ljava/lang/String;"); let map_class = jni.find_class("java/util/Map"); let key_set = jni.get_method_id(map_class, "keySet", "()Ljava/util/Set;"); let set_class = jni.find_class("java/util/Set"); @@ -195,6 +203,7 @@ unsafe fn thread_loaders(jni: &Jni) -> Result, String> { let threads = jni.call_object(jni_non_null(set)?, to_array, &[]); jni.check("thread list")?; let mut loaders: Vec = Vec::new(); + let mut render = None; for i in 0..jni.get_array_length(jni_non_null(threads)?) { let thread = jni.get_object_array_element(threads, i); if thread.is_null() { @@ -204,11 +213,17 @@ unsafe fn thread_loaders(jni: &Jni) -> Result, String> { if jni.take_exception().is_some() || loader.is_null() { continue; } + let name = jni.call_object(thread, get_name, &[]); + if jni.take_exception().is_none() && !name.is_null() { + if matches!(jni.get_string_utf(name).as_str(), "Render thread" | "Client thread") { + render = Some(loader); + } + } if !loaders.iter().any(|&known| jni.is_same_object(known, loader)) { loaders.push(loader); } } - Ok(loaders) + Ok((render, loaders)) } unsafe fn jni_non_null(o: JObject) -> Result { diff --git a/src/main/java/dev/zprestige/prestige/Module.java b/src/main/java/dev/zprestige/prestige/Module.java index 0fb23c1..88d73f2 100644 --- a/src/main/java/dev/zprestige/prestige/Module.java +++ b/src/main/java/dev/zprestige/prestige/Module.java @@ -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() { } diff --git a/src/main/java/dev/zprestige/prestige/inject/Bootstrap.java b/src/main/java/dev/zprestige/prestige/inject/Bootstrap.java index a5f4940..b542e0f 100644 --- a/src/main/java/dev/zprestige/prestige/inject/Bootstrap.java +++ b/src/main/java/dev/zprestige/prestige/inject/Bootstrap.java @@ -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 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, ClassNode> declarations = new HashMap<>(); + for (Class type : types) { + if (type == null) continue; + try { + Set 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, ClassNode> declarations, Set 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); diff --git a/src/main/java/dev/zprestige/prestige/inject/InPlaceTransformer.java b/src/main/java/dev/zprestige/prestige/inject/InPlaceTransformer.java index e4bb816..3a2537d 100644 --- a/src/main/java/dev/zprestige/prestige/inject/InPlaceTransformer.java +++ b/src/main/java/dev/zprestige/prestige/inject/InPlaceTransformer.java @@ -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 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 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 originals) { + List 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 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) { diff --git a/src/main/java/dev/zprestige/prestige/mixin/InGameOverlayRendererMixin.java b/src/main/java/dev/zprestige/prestige/mixin/InGameOverlayRendererMixin.java index 173c738..34d6c5b 100644 --- a/src/main/java/dev/zprestige/prestige/mixin/InGameOverlayRendererMixin.java +++ b/src/main/java/dev/zprestige/prestige/mixin/InGameOverlayRendererMixin.java @@ -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(); diff --git a/src/main/java/dev/zprestige/prestige/mixin/ServerWorldMixin.java b/src/main/java/dev/zprestige/prestige/mixin/ServerWorldMixin.java index ded4ec3..994730f 100644 --- a/src/main/java/dev/zprestige/prestige/mixin/ServerWorldMixin.java +++ b/src/main/java/dev/zprestige/prestige/mixin/ServerWorldMixin.java @@ -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 blockParticles, RegistryEntry soundEvent, InjectionCallback ci ) { diff --git a/src/main/java/dev/zprestige/prestige/module/misc/SelfDestruct.java b/src/main/java/dev/zprestige/prestige/module/misc/SelfDestruct.java index 708df28..0e7c4ff 100644 --- a/src/main/java/dev/zprestige/prestige/module/misc/SelfDestruct.java +++ b/src/main/java/dev/zprestige/prestige/module/misc/SelfDestruct.java @@ -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 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 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;