UPDATE
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:
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) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user