Author SHA1 Message Date
bbldrizzy 5152e6e0a6 make fabric entry points wired through an adapter 2026-11-03 11:46:46 +00:00
bbldrizzy 0abefe7a04 Merge branch 'main' of https://codeberg.org/SkidSolutions/OpenPrestige 2026-11-03 11:42:12 +00:00
bbldrizzy 036b24f2ae 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.
2026-11-03 11:40:43 +00:00
bbldrizzy 41d3e4a65e Update README.md 2026-10-28 11:27:50 +01:00
19 changed files with 236 additions and 80 deletions

No files matched your search

+1
View File
@@ -19,3 +19,4 @@ openvape.zip
/dist/
/injector/target/
/tools
/examples
-2
View File
@@ -5,8 +5,6 @@ A semi deobfuscated, IntelliJ-ready, runnable open sourced version of the Presti
---
- we will work on removing the param obf and also renaming vars in the coming days. stay tuned!
- if vikas wants to pay me to remove this, make ticket in discord and we can negotiate.
@ 1500 discord members, we will release injector
<p align="center">
<img src="docs/img/deobf-bird.png" width="260" alt="Deobf?">
+17 -2
View File
@@ -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 <pid> 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 <pid> [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
+10 -5
View File
@@ -221,14 +221,12 @@ fn pick_process() -> Option<u32> {
let mut selected = 0usize;
let mut processes = platform::java_processes();
let mut last_refresh = Instant::now();
loop {
if selected >= processes.len() {
selected = processes.len().saturating_sub(1);
}
draw(&processes, selected);
loop {
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<u32> {
}
platform::Key::Escape => return None,
}
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();
}
}
@@ -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,
-3
View File
@@ -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
}
+25 -10
View File
@@ -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<PathBuf, String> {
}
/// 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<JObject, String> {
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<JObject, Strin
if loader.is_null() {
return Err("Knot target class loader is null".into());
}
Ok(jni.new_global_ref(loader))
Ok(loader)
}
const FABRIC_BASE: &str = "net.fabricmc.loader.impl.launch.FabricLauncherBase";
/// Finds the class loader that can see fabric-loader. The system class loader is tried first (official launcher, most
/// others); launchers that run the game from their own URLClassLoader (Prism/MultiMC, many modpack launchers) keep
/// fabric-loader out of it, so every thread's context class loader is probed too. Returns the FabricLauncherBase class
/// and the loader it was found through (null = FindClass / system loader).
/// Prefer the render thread's context loader when a launcher supplies its own loader hierarchy.
/// Fall back to the system loader and then the other thread context loaders.
unsafe fn locate_fabric(jni: &Jni) -> 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<Vec<JObject>, String> {
unsafe fn thread_loaders(jni: &Jni) -> Result<(Option<JObject>, Vec<JObject>), 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<Vec<JObject>, String> {
let threads = jni.call_object(jni_non_null(set)?, to_array, &[]);
jni.check("thread list")?;
let mut loaders: Vec<JObject> = 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<Vec<JObject>, 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<JObject, String> {
@@ -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() {
}
@@ -34,11 +34,10 @@ import dev.zprestige.prestige.util.SimulatedClickManager;
import dev.zprestige.prestige.util.FrameDeltaTimer;
import dev.zprestige.prestige.util.NotificationManager;
import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.api.ModInitializer;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.ChatScreen;
public class Prestige implements ModInitializer {
public class Prestige {
private static final AtomicBoolean initialized = new AtomicBoolean(false);
private static final java.util.concurrent.atomic.AtomicInteger renderErrorCount = new java.util.concurrent.atomic.AtomicInteger();
public static EventBus eventBus;
@@ -160,8 +159,4 @@ public class Prestige implements ModInitializer {
}
}
@Override
public void onInitialize() {
this.init();
}
}
@@ -0,0 +1,21 @@
package dev.zprestige.prestige.fabric;
import dev.zprestige.prestige.client.Prestige;
import dev.zprestige.prestige.ui.mc.Entry;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
public final class FabricEntrypoints implements ModInitializer, ClientModInitializer {
@Override
public void onInitialize() {
new Prestige().init();
}
@Override
public void onInitializeClient() {
Entry entry = new Entry();
entry.initialize();
ClientTickEvents.END_CLIENT_TICK.register(entry::tick);
}
}
@@ -1,19 +1,26 @@
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.fabric.FabricEntrypoints;
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 +48,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 +75,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 +155,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 +195,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 +245,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);
@@ -244,8 +296,9 @@ public final class Bootstrap {
private static void initClient() {
try {
new Prestige().onInitialize();
new Entry().onInitializeClient();
FabricEntrypoints entrypoints = new FabricEntrypoints();
entrypoints.onInitialize();
entrypoints.onInitializeClient();
log("client initialised");
} catch (Throwable t) {
log("client init failed: " + 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();
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;
@@ -89,7 +89,7 @@ public class RenderCore {
Render2D.currentBuffers = BUFFER_SOURCE;
worldContext = context;
try {
((RenderCallback)RenderEvents.WORLD_RENDER.invoker()).onRender(BUFFER_SOURCE, context);
RenderEvents.WORLD_RENDER.invoker().onRender(BUFFER_SOURCE, context);
BUFFER_SOURCE.flush(context);
} catch (Throwable throwable) {
LOGGER.error("World render failed", throwable);
@@ -102,9 +102,9 @@ public class RenderCore {
Render2D.currentContext = context;
Render2D.currentBuffers = BUFFER_SOURCE;
try {
((RenderCallback)RenderEvents.HUD_RENDER.invoker()).onRender(BUFFER_SOURCE, context);
RenderEvents.HUD_RENDER.invoker().onRender(BUFFER_SOURCE, context);
BUFFER_SOURCE.flush(context);
((RenderCallback)RenderEvents.AFTER_HUD_RENDER.invoker()).onRender(BUFFER_SOURCE, context);
RenderEvents.AFTER_HUD_RENDER.invoker().onRender(BUFFER_SOURCE, context);
BUFFER_SOURCE.flush(context);
} catch (Throwable throwable) {
LOGGER.error("HUD render failed", throwable);
@@ -1,27 +1,34 @@
package dev.zprestige.prestige.render;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public interface RenderEvents {
Logger LOGGER = LoggerFactory.getLogger(RenderEvents.class);
Event HUD_RENDER = createEvent();
Event WORLD_RENDER = createEvent();
Event AFTER_HUD_RENDER = createEvent();
RenderEvent HUD_RENDER = new RenderEvent();
RenderEvent WORLD_RENDER = new RenderEvent();
RenderEvent AFTER_HUD_RENDER = new RenderEvent();
@NotNull
private static Event createEvent() {
return EventFactory.createArrayBacked(RenderCallback.class, listeners -> (buffers, context) -> {
for (RenderCallback listener : (listeners)) {
final class RenderEvent {
private final CopyOnWriteArrayList<RenderCallback> listeners = new CopyOnWriteArrayList<>();
private final RenderCallback invoker = (buffers, context) -> {
for (RenderCallback listener : listeners) {
try {
listener.onRender(buffers, context);
} catch (Throwable throwable) {
LOGGER.error("Render listener failed", throwable);
}
}
});
};
public void register(RenderCallback listener) {
listeners.add(Objects.requireNonNull(listener));
}
public RenderCallback invoker() {
return invoker;
}
}
}
@@ -1,19 +1,16 @@
package dev.zprestige.prestige.ui.mc;
import dev.zprestige.prestige.ui.Input;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.MinecraftClient;
import org.lwjgl.glfw.GLFW;
/** Opens the ported ClickGUI with the configured menu key (Input.menuKey, default RIGHT SHIFT); closing is handled by Input via the key mixin. */
public final class Entry implements ClientModInitializer {
public final class Entry {
static boolean prev;
private static int cachedVk = -1, cachedGlfw;
@Override public void onInitializeClient() {
public void initialize() {
try { Skid.run(); } catch (Throwable t) { System.out.println("[prestige-ui] splash " + t); }
ClientTickEvents.END_CLIENT_TICK.register(this::tick);
System.out.println("[prestige-ui] loaded (menu key opens the menu)");
}
@@ -30,7 +27,7 @@ public final class Entry implements ClientModInitializer {
static boolean bridgeTried;
void tick(MinecraftClient mc) {
public void tick(MinecraftClient mc) {
if (dev.zprestige.prestige.client.Prestige.selfDestructed) return;
if (!bridgeTried) { bridgeTried = true; dev.zprestige.prestige.ui.bridge.Bridge.bind(); }
try {
+2 -2
View File
@@ -6,8 +6,8 @@
"description": "Prestige client with the imgui-java ClickGUI.",
"environment": "client",
"entrypoints": {
"main": ["dev.zprestige.prestige.client.Prestige"],
"client": ["dev.zprestige.prestige.ui.mc.Entry", "dev.zprestige.prestige.engine.EngineDev"]
"main": ["dev.zprestige.prestige.fabric.FabricEntrypoints"],
"client": ["dev.zprestige.prestige.fabric.FabricEntrypoints", "dev.zprestige.prestige.engine.EngineDev"]
},
"mixins": ["prestige-engine.mixins.json", "prestige.mixins.json"],
"accessWidener": "prestige-client.accesswidener",