Add Linux and macOS support, batch class transformations with a disk cache, and add cross-platform release packaging. Also improve runtime namespace handling, keybind editing, and startup behavior.
This commit is contained in:
bbldrizzy committed 2026-10-27 20:30:09 +00:00
1 parent 910a126f04
commit 38ff6d387c
29 files changed
+2234 -491

No files matched your search

@@ -5,6 +5,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -47,11 +48,29 @@ public final class EnginePlugin implements IMixinConfigPlugin {
private String namespace = "named";
private int expectedTargets;
/**
* The namespace the running classes are named in, which selects the target list and the mapping used by the transformers.
* Fabric reports it directly. Lunar's Genesis loader reports another namespace, but its classes carry intermediary names
* (its own stack traces show net.minecraft.class_310.method_55608), so intermediary is used there. Without this, the mapper
* resolved some names through the official namespace and others through the named one, which broke the hooks.
*/
public static String runtimeNamespace() {
String forced = System.getProperty("prestige.engine.namespace"); // test override ("named" / "intermediary")
if (forced != null) return forced;
String reported;
try {
reported = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace();
} catch (Throwable t) {
return "named";
}
if (reported.equals("named") || reported.equals("intermediary")) return reported;
System.out.println("[Engine] runtime reports namespace '" + reported + "'; the classes use intermediary names, so using intermediary");
return "intermediary";
}
@Override public void onLoad(String mixinPackage) {
try {
String forced = System.getProperty("prestige.engine.namespace"); // test override ("named" / "intermediary")
if (forced != null) namespace = forced;
else try { namespace = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace(); } catch (Throwable t) { namespace = "named"; }
namespace = runtimeNamespace();
List<String> transformers = lines("prestige-engine/transformers.list");
expectedTargets = lines(namespace.equals("named") ? "prestige-engine/targets.named.list" : "prestige-engine/targets.intermediary.list").size();
manager = createManager(namespace, transformers);
@@ -68,8 +87,10 @@ public final class EnginePlugin implements IMixinConfigPlugin {
public static TransformerManager createManager(String namespace, List<String> transformers) throws IOException {
ClassLoader loader = EnginePlugin.class.getClassLoader();
byte[] tiny = namespace.equals("named") ? null : resource("prestige-engine/mappings.tiny");
TransformerManager manager = new TransformerManager(new ResourceClassProvider(loader, new HashSet<>(transformers)), tiny == null ? null : new ByteArrayInputStream(tiny));
manager.setFailStrategy(FailStrategy.CANCEL);
TransformerManager manager = new TransformerManager(new ResourceClassProvider(loader, new HashSet<>(transformers), tiny), tiny == null ? null : new ByteArrayInputStream(tiny));
// A transformer whose target method does not exist in this runtime (Lunar's Genesis loader names some methods differently
// from the bundled mappings) is skipped on its own. CANCEL would drop every hook of the whole class and abort the injection.
manager.setFailStrategy(FailStrategy.CONTINUE);
for (String name : transformers) manager.addTransformer(name);
return manager;
}
@@ -78,14 +99,40 @@ public final class EnginePlugin implements IMixinConfigPlugin {
private static final class ResourceClassProvider implements IClassProvider {
private final ClassLoader loader;
private final Set<String> transformers;
ResourceClassProvider(ClassLoader loader, Set<String> transformers) { this.loader = loader; this.transformers = transformers; }
/** Named (Yarn) class name -> intermediary name, from the bundled mapping. Lunar's jar only has the intermediary files. */
private final Map<String, String> namedToIntermediary;
ResourceClassProvider(ClassLoader loader, Set<String> transformers, byte[] tiny) {
this.loader = loader;
this.transformers = transformers;
this.namedToIntermediary = classNames(tiny);
}
private static Map<String, String> classNames(byte[] tiny) {
Map<String, String> out = new HashMap<>();
if (tiny == null) return out;
for (String line : new String(tiny, StandardCharsets.UTF_8).split("\n")) {
String[] f = line.split("\t");
// header "tiny 2 0 intermediary named"; class lines "c <intermediary> <named>"
if (f.length >= 3 && f[0].equals("c")) out.put(f[2].trim(), f[1].trim());
}
return out;
}
@Override public byte[] getClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/') + ".class";
try (InputStream in = loader.getResourceAsStream(path)) {
if (in == null) throw new ClassNotFoundException(name + " (missing resource " + path + ")");
byte[] bytes = in.readAllBytes();
return transformers.contains(name.replace('/', '.')) ? gate(bytes) : bytes;
String slashed = name.replace('.', '/');
InputStream in = loader.getResourceAsStream(slashed + ".class");
if (in == null) {
// a named name the runtime does not have: load the intermediary class it maps to
String intermediary = namedToIntermediary.get(slashed);
if (intermediary != null) {
slashed = intermediary;
in = loader.getResourceAsStream(slashed + ".class");
}
}
if (in == null) throw new ClassNotFoundException(name + " (missing resource " + slashed + ".class)");
try (InputStream stream = in) {
byte[] bytes = stream.readAllBytes();
return transformers.contains(slashed.replace('/', '.')) ? gate(bytes) : bytes;
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
@@ -32,12 +32,22 @@ public final class Bootstrap {
public static synchronized void start() {
if (started) return;
started = true;
if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("prestige")) {
// the jar is also installed in mods/: Mixin already applied the hooks and Fabric ran the entrypoints
log("Prestige is already loaded as a Fabric mod, skipping injection");
return;
}
try {
log("waiting for the Minecraft client");
waitForClient();
Report report = install(NativeBridge::classBytes, NativeBridge::redefine);
Report report = install(NativeBridge::classBytes, NativeBridge::redefine, nativeBatch());
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);
// 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");
MinecraftClient.getInstance().execute(Bootstrap::initClient);
} catch (Throwable t) {
@@ -66,7 +76,39 @@ public final class Bootstrap {
* without the native library.
*/
public static Report install(Function<Class<?>, byte[]> classBytes, ToIntBiFunction<Class<?>, byte[]> redefine) throws Exception {
String namespace = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace();
return install(classBytes, redefine, null);
}
/** Many classes per native call: one RetransformClasses / RedefineClasses (one safepoint each) instead of one per class. */
public interface Batch {
byte[][] classBytes(Class<?>[] types);
int redefine(Class<?>[] types, byte[][] bytes);
}
/** The batch natives of {@link NativeBridge}, or null when the loaded prestige_native.dll predates them. */
private static Batch nativeBatch() {
try {
NativeBridge.classBytesBatch(new Class<?>[0]); // probes the registration; a missing native throws UnsatisfiedLinkError
} catch (UnsatisfiedLinkError e) {
return null;
} catch (Throwable ignored) {
// anything else means the native exists
}
return new Batch() {
@Override public byte[][] classBytes(Class<?>[] types) { return NativeBridge.classBytesBatch(types); }
@Override public int redefine(Class<?>[] types, byte[][] bytes) { return NativeBridge.redefineBatch(types, bytes); }
};
}
/**
* Same as {@link #install(Function, ToIntBiFunction)} with an optional batch path: all original bytes are captured with one
* call, the ClassTransform step reuses cached results where it can, and all hooked classes are redefined with one call. A
* failing batch step falls back to per-class calls, so one bad class only fails itself.
*/
public static Report install(Function<Class<?>, byte[]> classBytes, ToIntBiFunction<Class<?>, byte[]> redefine, Batch batch) throws Exception {
String namespace = EnginePlugin.runtimeNamespace();
log("runtime namespace " + namespace + " (the loader reports " + 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);
@@ -75,31 +117,106 @@ public final class Bootstrap {
EngineState.installed = true;
Report report = new Report();
int total = manager.getTransformedClasses().size(), done = 0;
List<String> targets = new ArrayList<>(manager.getTransformedClasses());
int total = targets.size();
log("transforming " + total + " hook targets");
for (String target : manager.getTransformedClasses()) {
if (done > 0 && done % 10 == 0) log("transformed " + done + "/" + total);
done++;
long start = System.nanoTime();
// 1. resolve the target classes
Class<?>[] types = new Class<?>[total];
for (int i = 0; i < total; i++) {
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);
ORIGINALS.put(type, current);
report.redefined++;
report.moved += result.movedMethods();
types[i] = Class.forName(targets.get(i), false, loader);
} catch (Throwable t) {
report.failed.add(target);
log("hook failed for " + target + ": " + t);
t.printStackTrace(System.out);
fail(report, targets.get(i), t);
}
}
// 2. the bytes each class currently has: one native call for all of them, per-class for any that did not come back
byte[][] originals = new byte[total][];
if (batch != null) {
Class<?>[] resolved = java.util.Arrays.stream(types).filter(java.util.Objects::nonNull).toArray(Class<?>[]::new);
byte[][] captured = resolved.length == 0 ? null : batch.classBytes(resolved);
if (captured != null && captured.length == resolved.length) {
int next = 0;
for (int i = 0; i < total; i++) if (types[i] != null) originals[i] = captured[next++];
}
}
for (int i = 0; i < total; i++) {
if (types[i] != null && originals[i] == null) originals[i] = classBytes.apply(types[i]);
}
long captured = System.nanoTime();
// 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];
TransformCache cache = TransformCache.open();
int cacheHits = 0;
for (int i = 0; i < total; i++) {
if (types[i] == null) continue;
String target = targets.get(i);
if (originals[i] == null) {
fail(report, target, new IllegalStateException("no class bytes"));
continue;
}
try {
byte[] hit = cache == null ? null : cache.get(target, originals[i]);
if (hit != null) {
results[i] = new InPlaceTransformer.Result(hit, Map.of(), 0);
cacheHits++;
continue;
}
results[i] = transformer.transform(target, originals[i]);
if (cache != null && results[i].helpers().isEmpty()) cache.put(target, originals[i], results[i].target());
} catch (Throwable t) {
fail(report, target, t);
}
}
long transformed = System.nanoTime();
// 4. helper classes first, then redefine everything in one call (all or nothing); on an error redefine one by one
List<Integer> ready = new ArrayList<>();
for (int i = 0; i < total; i++) {
if (results[i] == null) continue;
try {
for (byte[] helper : results[i].helpers().values()) Definer.define(helper);
ready.add(i);
} catch (Throwable t) {
fail(report, targets.get(i), t);
}
}
boolean redefinedAll = false;
if (batch != null && !ready.isEmpty()) {
Class<?>[] batchTypes = ready.stream().map(i -> types[i]).toArray(Class<?>[]::new);
byte[][] batchBytes = ready.stream().map(i -> results[i].target()).toArray(byte[][]::new);
redefinedAll = batch.redefine(batchTypes, batchBytes) == 0;
}
for (int i : ready) {
try {
if (!redefinedAll) {
int code = redefine.applyAsInt(types[i], results[i].target());
if (code != 0) throw new IllegalStateException("redefine returned JVMTI error " + code);
}
ORIGINALS.put(types[i], originals[i]);
report.redefined++;
report.moved += results[i].movedMethods();
} catch (Throwable t) {
fail(report, targets.get(i), t);
}
}
long end = System.nanoTime();
log(String.format("timing: bytes %d ms, transform %d ms (%d of %d from cache), redefine %d ms%s",
(captured - start) / 1_000_000, (transformed - captured) / 1_000_000, cacheHits, total, (end - transformed) / 1_000_000,
batch == null ? " [per-class natives]" : redefinedAll ? "" : " [batch redefine refused, per-class]"));
return report;
}
private static void fail(Report report, String target, Throwable t) {
report.failed.add(target);
log("hook failed for " + target + ": " + t);
t.printStackTrace(System.out);
}
/** True when this client was injected, i.e. hooks were applied through JVMTI and can be taken out again. */
public static boolean isInjected() {
return !ORIGINALS.isEmpty();
@@ -39,7 +39,7 @@ public final class InPlaceTransformer {
private final TransformerManager manager;
private final ClassLoader loader;
private int sequence;
private final java.util.concurrent.atomic.AtomicInteger sequence = new java.util.concurrent.atomic.AtomicInteger();
public InPlaceTransformer(TransformerManager manager, ClassLoader loader) {
this.manager = manager;
@@ -67,7 +67,7 @@ public final class InPlaceTransformer {
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('$', '_');
String helperName = HOOK_PACKAGE + "H" + (sequence.getAndIncrement()) + "_" + 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;
@@ -7,6 +7,12 @@ public final class 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);
/** classBytes for many classes with one RetransformClasses call; entries are null where nothing was captured, null on failure. */
public static native byte[][] classBytesBatch(Class<?>[] types);
/** RedefineClasses for many classes in one call, all or nothing; returns the JVMTI error code, 0 on success. */
public static native int redefineBatch(Class<?>[] types, byte[][] bytes);
/** Appends a line to prestige-native.log, which the injector tails to show progress. */
public static native void log(String message);
@@ -0,0 +1,94 @@
package dev.zprestige.prestige.inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.stream.Stream;
/**
* Disk cache of finished ClassTransform results. The ClassTransform step is by far the slowest part of an injection, and its
* output only depends on the payload jar and on the class bytes it is given, so a repeat injection into the same game setup can
* skip it. Entries are keyed by SHA-256(payload jar, target name, original class bytes); a rebuilt payload or a changed modpack
* simply misses. Only results that need no helper classes are stored.
*/
final class TransformCache {
private static final long MAX_AGE_MILLIS = 14L * 24 * 60 * 60 * 1000;
private final Path dir;
private final byte[] payloadId;
private TransformCache(Path dir, byte[] payloadId) {
this.dir = dir;
this.payloadId = payloadId;
}
/** The cache for the payload jar this class was loaded from, or null when it cannot be used. */
static TransformCache open() {
try {
Path jar = Path.of(Bootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI());
if (!Files.isRegularFile(jar)) return null;
Path dir = Path.of(System.getProperty("java.io.tmpdir"), "PrestigeLoader", "cache");
Files.createDirectories(dir);
prune(dir);
return new TransformCache(dir, MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(jar)));
} catch (Throwable t) {
return null;
}
}
/** Cached transformed bytes of {@code target}, or null. */
byte[] get(String target, byte[] original) {
try {
Path file = file(target, original);
if (!Files.isRegularFile(file)) return null;
byte[] bytes = Files.readAllBytes(file);
if (bytes.length < 4 || (bytes[0] & 0xFF) != 0xCA || (bytes[1] & 0xFF) != 0xFE || (bytes[2] & 0xFF) != 0xBA || (bytes[3] & 0xFF) != 0xBE) return null;
Files.setLastModifiedTime(file, FileTime.from(Instant.now()));
return bytes;
} catch (Throwable t) {
return null;
}
}
void put(String target, byte[] original, byte[] transformed) {
try {
Path file = file(target, original);
Path temp = file.resolveSibling(file.getFileName() + ".tmp");
Files.write(temp, transformed);
Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (Throwable ignored) {
// the cache is only an optimisation
}
}
private Path file(String target, byte[] original) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(payloadId);
digest.update(target.getBytes(java.nio.charset.StandardCharsets.UTF_8));
digest.update((byte) 0);
digest.update(original);
StringBuilder name = new StringBuilder();
for (byte b : digest.digest()) name.append(String.format("%02x", b));
return dir.resolve(name + ".class");
}
/** Drops entries that have not been used for two weeks (every rebuild of the payload starts a fresh set). */
private static void prune(Path dir) {
try (Stream<Path> files = Files.list(dir)) {
long limit = System.currentTimeMillis() - MAX_AGE_MILLIS;
files.forEach(file -> {
try {
if (Files.getLastModifiedTime(file).toMillis() < limit) Files.deleteIfExists(file);
} catch (IOException ignored) {
// leave it
}
});
} catch (Throwable ignored) {
// leave it
}
}
}
@@ -53,7 +53,7 @@ public class SelfDestruct extends Module {
error.printStackTrace();
}
}
Prestige.moduleManager.setModules(new ArrayList());
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
@@ -797,6 +797,7 @@ public final class Input {
if (!active) return;
syncMouse();
if (WidgetsPage.click(button)) return;
if (Ui.S.page == 10 && KeybindsPage.capturing && button >= 2 && button <= 4) { KeybindsPage.click(button); return; }
switch (button) {
case 0: lDown(); break;
case 1: rDown(); break;
@@ -2,12 +2,15 @@ package dev.zprestige.prestige.ui;
import static dev.zprestige.prestige.ui.Lib.*;
import dev.zprestige.prestige.ui.bridge.Bridge;
import java.util.ArrayList;
import java.util.List;
/**
* Keybinds page (page 10). Port of clean.c ~1273-1490 (draw), 180145320.c (click / key capture / wheel) and the key chip
* sub_18011F140. Key codes are Windows virtual-key codes like in the original (+216 of the "Bind" setting); 0 = None.
* sub_18011F140. Rows are modules whose Bind is set, read from the live client; edits are written back to that Bind.
* The page works in Windows virtual-key codes like the original (+216 of the module's bind); 0 = None. The client stores GLFW
* key codes (-1 = None) and 9995..9999 for mouse buttons (9999 - button), so values are converted on the way in and out.
*/
public final class KeybindsPage {
private KeybindsPage() {}
@@ -17,12 +20,49 @@ public final class KeybindsPage {
public String name, desc;
public int vk; // Windows VK code (or 9995..9999 for mouse buttons); 0 = none (hidden from the list)
public boolean toggle; // Hold / Toggle mode (not shown on this page, kept for the lead)
public Bound(String name, String desc, int vk, boolean toggle) { this.name = name; this.desc = desc; this.vk = vk; this.toggle = toggle; }
public Bridge.LiveSetting live; // the module's live Bind, written on every edit
public Bound(String name, String desc, int vk, boolean toggle, Bridge.LiveSetting live) {
this.name = name; this.desc = desc; this.vk = vk; this.toggle = toggle; this.live = live;
}
}
/** The lead fills this from the real modules (only entries with vk != 0 are listed). */
/** The rows, rebuilt from the live modules every frame the page is shown (only modules with a key set). */
public static final List<Bound> BOUND = new ArrayList<>();
/** Rebuilds {@link #BOUND} from the bridge. The original walks its module list and lists each module whose key is not None. */
static void refresh() {
BOUND.clear();
for (Bridge.LiveModule m : Bridge.modules()) {
Bridge.LiveSetting bind = m.keybind();
if (bind == null) continue;
int vk = liveToVk(bind.value());
if (vk == 0) continue;
BOUND.add(new Bound(m.name, m.desc, vk, bind.toggleMode(), bind));
}
}
/** Client key value (GLFW code, -1 = None, 9995..9999 = mouse) -> VK code used by this page (0 = None). */
static int liveToVk(Object value) {
int g = value instanceof Number n ? n.intValue() : -1;
if (g < 0) return 0;
if (g >= 9995) return g;
return Input.toVk(g);
}
/** VK code of this page -> client key value (-1 = None). */
static int vkToLive(int vk) {
if (vk == 0) return -1;
if (vk >= 9995) return vk;
for (int g = 32; g <= 348; g++) if (Input.toVk(g) == vk) return g;
return -1;
}
/** Sets a row's key on the page and on the module's live Bind. */
private static void setKey(Bound b, int vk) {
b.vk = vk;
if (b.live != null) b.live.set(vkToLive(vk));
}
/** dword_1804CCE5C: hovered row (-1 none). dword_1804CCE90 / byte_18070893F: row being captured / capture active. */
public static int hoverRow = -1, captureRow = -1;
public static boolean capturing;
@@ -118,6 +158,7 @@ public final class KeybindsPage {
public static void update(float dt) {
hoverRow = -1;
if (Ui.S.page != 10) return;
refresh();
int n = visible().size();
float v108 = v108(), w = width();
float y = 110f - scroll;
@@ -129,10 +170,16 @@ public final class KeybindsPage {
private static float maxScroll(int n) { return Math.max(0f, (n * 70f - 10f) - 575f); }
/** Left click: Edit starts capture, Delete clears the key (sets it to None). */
/** Click: while capturing, any mouse button becomes the bind (9999 - button, as the client encodes it). Otherwise Edit
* starts capture and Delete clears the key (sets it to None). */
public static boolean click(int button) {
if (button != 0) return false;
List<Bound> list = visible();
if (capturing) {
if (captureRow >= 0 && captureRow < list.size() && button >= 0 && button <= 4) setKey(list.get(captureRow), 9999 - button);
captureRow = -1; capturing = false;
return true;
}
if (button != 0) return false;
if (hoverRow < 0 || hoverRow >= list.size()) return false;
float v108 = v108(), w = width();
float rowY = hoverRow * 70f + (110f - scroll);
@@ -142,7 +189,7 @@ public final class KeybindsPage {
return true;
}
if (Input.mx >= del && Input.mx <= del + 80f && Input.my >= rowY + 15f && Input.my <= rowY + 45f) {
list.get(hoverRow).vk = 0;
setKey(list.get(hoverRow), 0);
captureRow = -1; capturing = false;
return true;
}
@@ -162,8 +209,8 @@ public final class KeybindsPage {
List<Bound> list = visible();
if (captureRow < list.size()) {
Bound b = list.get(captureRow);
if (glfwKey == 256 || glfwKey == 261) b.vk = 0; // VK 27 / 46
else { int vk = glfwToVk(glfwKey); if (vk != 0) b.vk = vk; else return true; }
if (glfwKey == 256 || glfwKey == 261) setKey(b, 0); // Esc / Delete clear the bind (VK 27 / 46)
else { int vk = Input.toVk(glfwKey); if (vk != 0) setKey(b, vk); else return true; }
}
capturing = false; captureRow = -1;
return true;
@@ -2,7 +2,6 @@ package dev.zprestige.prestige.ui.mc;
import javax.swing.JWindow;
import javax.swing.Timer;
import javax.swing.JOptionPane;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
@@ -24,13 +23,12 @@ import java.io.FileWriter;
import java.io.BufferedReader;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Skid extends JWindow implements ActionListener, MouseListener, MouseMotionListener {
private static final int WIDTH = 440;
private static final int HEIGHT = 270;
private static final int DURATION = 4000;
private static final String DISCORD_URL = "https://discord.gg/5FB5hqJacr";
private static final String DISCORD_URL = "https://discord.gg/x2FJyGrDNg";
private final CountDownLatch latch;
private final Timer timer;
@@ -161,27 +159,15 @@ public class Skid extends JWindow implements ActionListener, MouseListener, Mous
return;
}
final CountDownLatch latch = new CountDownLatch(1);
Skid ui = new Skid(latch);
ui.startSplash();
try {
latch.await(DURATION + 3000, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {}
// Never block the caller: run() is called on the game's render thread, and a modal Swing dialog there freezes
// Minecraft (the dialog opens behind the game window). The splash runs on the AWT event thread on its own.
if ("unset".equals(pref)) {
int res = JOptionPane.showConfirmDialog(
null,
"Do you want to see this splash screen again?",
"Skid Solutions",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE
);
if (res == JOptionPane.YES_OPTION) {
savePref("yes");
} else {
savePref("no");
}
savePref("yes");
}
java.awt.EventQueue.invokeLater(() -> {
try {
new Skid(new CountDownLatch(1)).startSplash();
} catch (Throwable ignored) {}
});
}
}