You've already forked AstralRinth
forked from didirus/AstralRinth
Allow direct joining servers on old instances (#4094)
* Implement direct server joining for 1.6.2 through 1.19.4 * Implement direct server joining for versions before 1.6.2 * Ignore methods with a $ in them * Run intl:extract * Improve code of MinecraftTransformer * Support showing last played time for profiles before 1.7 * Reorganize QuickPlayVersion a bit to prepare for singleplayer * Only inject quick play checking in versions where it's needed * Optimize agent some and fix error on NeoForge * Remove some code for quickplay singleplayer support before 1.20, as we can't reasonably support that with an agent * Invert the default hasServerQuickPlaySupport return value * Remove Play Anyway button * Fix "Server couldn't be contacted" on singleplayer worlds * Fix "Jump back in" section not working
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package com.modrinth.theseus.agent;
|
||||
|
||||
import java.util.ListIterator;
|
||||
import java.util.function.Predicate;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.FieldInsnNode;
|
||||
|
||||
public interface InsnPattern extends Predicate<AbstractInsnNode> {
|
||||
/**
|
||||
* Advances past the first match of all instructions in the pattern.
|
||||
* @return {@code true} if the pattern was found, {@code false} if not
|
||||
*/
|
||||
static boolean findAndSkip(ListIterator<AbstractInsnNode> iterator, InsnPattern... pattern) {
|
||||
if (pattern.length == 0) {
|
||||
return true;
|
||||
}
|
||||
int patternIndex = 0;
|
||||
while (iterator.hasNext()) {
|
||||
final AbstractInsnNode insn = iterator.next();
|
||||
if (insn.getOpcode() == -1) continue;
|
||||
if (pattern[patternIndex].test(insn) && ++patternIndex == pattern.length) {
|
||||
return true;
|
||||
} else {
|
||||
patternIndex = 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static InsnPattern opcode(int opcode) {
|
||||
return insn -> insn.getOpcode() == opcode;
|
||||
}
|
||||
|
||||
static InsnPattern field(int opcode, Type fieldType) {
|
||||
final String typeDescriptor = fieldType.getDescriptor();
|
||||
return insn -> {
|
||||
if (insn.getOpcode() != opcode || !(insn instanceof FieldInsnNode)) {
|
||||
return false;
|
||||
}
|
||||
final FieldInsnNode fieldInsn = (FieldInsnNode) insn;
|
||||
return typeDescriptor.equals(fieldInsn.desc);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.modrinth.theseus.agent;
|
||||
|
||||
// Must be kept up-to-date with quick_play_version.rs
|
||||
public enum QuickPlayServerVersion {
|
||||
BUILTIN,
|
||||
BUILTIN_LEGACY,
|
||||
INJECTED,
|
||||
UNSUPPORTED;
|
||||
|
||||
public static final QuickPlayServerVersion CURRENT =
|
||||
valueOf(System.getProperty("modrinth.internal.quickPlay.serverVersion"));
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.modrinth.theseus.agent;
|
||||
|
||||
import com.modrinth.theseus.agent.transformers.ClassTransformer;
|
||||
import com.modrinth.theseus.agent.transformers.MinecraftTransformer;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
|
||||
@SuppressWarnings({"NullableProblems", "CallToPrintStackTrace"})
|
||||
public final class TheseusAgent {
|
||||
private static final boolean DEBUG_AGENT = Boolean.getBoolean("modrinth.debugAgent");
|
||||
|
||||
public static void premain(String args, Instrumentation instrumentation) {
|
||||
final Path debugPath = Paths.get("ModrinthDebugTransformed");
|
||||
if (DEBUG_AGENT) {
|
||||
System.out.println(
|
||||
"===== Theseus agent debugging enabled. Dumping transformed classes to " + debugPath + " =====");
|
||||
if (Files.exists(debugPath)) {
|
||||
try {
|
||||
Files.walkFileTree(debugPath, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
new UncheckedIOException("Failed to delete " + debugPath, e).printStackTrace();
|
||||
}
|
||||
}
|
||||
System.out.println("===== Quick play server version: " + QuickPlayServerVersion.CURRENT + " =====");
|
||||
}
|
||||
|
||||
final Map<String, ClassTransformer> transformers = new HashMap<>();
|
||||
transformers.put("net/minecraft/client/Minecraft", new MinecraftTransformer());
|
||||
|
||||
instrumentation.addTransformer((loader, className, classBeingRedefined, protectionDomain, classData) -> {
|
||||
final ClassTransformer transformer = transformers.get(className);
|
||||
if (transformer == null) {
|
||||
return null;
|
||||
}
|
||||
final ClassReader reader = new ClassReader(classData);
|
||||
final ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
|
||||
try {
|
||||
if (!transformer.transform(reader, writer)) {
|
||||
if (DEBUG_AGENT) {
|
||||
System.out.println("Not writing " + className + " as its transformer returned false");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
new IllegalStateException("Failed to transform " + className, t).printStackTrace();
|
||||
return null;
|
||||
}
|
||||
final byte[] result = writer.toByteArray();
|
||||
if (DEBUG_AGENT) {
|
||||
try {
|
||||
final Path path = debugPath.resolve(className + ".class");
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.write(path, result);
|
||||
System.out.println("Dumped class to " + path.toAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
new UncheckedIOException("Failed to dump class " + className, e).printStackTrace();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.modrinth.theseus.agent.transformers;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
public abstract class ClassNodeTransformer extends ClassTransformer {
|
||||
protected abstract boolean transform(ClassNode classNode);
|
||||
|
||||
@Override
|
||||
public final boolean transform(ClassReader reader, ClassWriter writer) {
|
||||
final ClassNode classNode = new ClassNode();
|
||||
reader.accept(classNode, 0);
|
||||
if (!transform(classNode)) {
|
||||
return false;
|
||||
}
|
||||
classNode.accept(writer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.modrinth.theseus.agent.transformers;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
public abstract class ClassTransformer {
|
||||
public abstract boolean transform(ClassReader reader, ClassWriter writer);
|
||||
|
||||
protected static boolean needsStackMap(ClassNode classNode) {
|
||||
return (classNode.version & 0xffff) >= Opcodes.V1_6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.modrinth.theseus.agent.transformers;
|
||||
|
||||
import com.modrinth.theseus.agent.InsnPattern;
|
||||
import com.modrinth.theseus.agent.QuickPlayServerVersion;
|
||||
import java.util.ListIterator;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FrameNode;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.JumpInsnNode;
|
||||
import org.objectweb.asm.tree.LabelNode;
|
||||
import org.objectweb.asm.tree.LdcInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.VarInsnNode;
|
||||
|
||||
public final class MinecraftTransformer extends ClassNodeTransformer {
|
||||
private static final String SET_SERVER_NAME_DESC = "(Ljava/lang/String;I)V";
|
||||
private static final InsnPattern[] INITIALIZE_THIS_PATTERN = {InsnPattern.opcode(Opcodes.INVOKESPECIAL)};
|
||||
|
||||
@Override
|
||||
protected boolean transform(ClassNode classNode) {
|
||||
if (QuickPlayServerVersion.CURRENT == QuickPlayServerVersion.INJECTED) {
|
||||
return addServerJoinSupport(classNode);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean addServerJoinSupport(ClassNode classNode) {
|
||||
String setServerName = null;
|
||||
MethodNode constructor = null;
|
||||
for (final MethodNode method : classNode.methods) {
|
||||
if (constructor == null && method.name.equals("<init>")) {
|
||||
constructor = method;
|
||||
} else if (method.desc.equals(SET_SERVER_NAME_DESC) && method.name.indexOf('$') == -1) {
|
||||
// Check for $ is because Mixin-injected methods should have $ in it
|
||||
if (setServerName == null) {
|
||||
setServerName = method.name;
|
||||
} else {
|
||||
// Already found a setServer method, but we found another one? Since we can't
|
||||
// know which is real, just return so we don't call something we shouldn't.
|
||||
// Note this can't happen unless some other mod is adding a method with this
|
||||
// same descriptor.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (constructor == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ListIterator<AbstractInsnNode> it = constructor.instructions.iterator();
|
||||
if (!InsnPattern.findAndSkip(it, INITIALIZE_THIS_PATTERN)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final LabelNode noQuickPlayLabel = new LabelNode();
|
||||
final LabelNode doneQuickPlayLabel = new LabelNode();
|
||||
it.add(new LdcInsnNode("modrinth.internal.quickPlay.host"));
|
||||
// String
|
||||
it.add(new MethodInsnNode(
|
||||
Opcodes.INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;"));
|
||||
// String
|
||||
it.add(new InsnNode(Opcodes.DUP));
|
||||
// String String
|
||||
it.add(new JumpInsnNode(Opcodes.IFNULL, noQuickPlayLabel));
|
||||
// String
|
||||
it.add(new VarInsnNode(Opcodes.ALOAD, 0));
|
||||
// String Minecraft
|
||||
it.add(new InsnNode(Opcodes.SWAP));
|
||||
// Minecraft String
|
||||
it.add(new LdcInsnNode("modrinth.internal.quickPlay.port"));
|
||||
// Minecraft String String
|
||||
it.add(new MethodInsnNode(
|
||||
Opcodes.INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;"));
|
||||
// Minecraft String String
|
||||
it.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/Integer", "parseInt", "(Ljava/lang/String;)I"));
|
||||
// Minecraft String int
|
||||
it.add(new MethodInsnNode(
|
||||
Opcodes.INVOKEVIRTUAL, "net/minecraft/client/Minecraft", setServerName, SET_SERVER_NAME_DESC));
|
||||
//
|
||||
it.add(new JumpInsnNode(Opcodes.GOTO, doneQuickPlayLabel));
|
||||
it.add(noQuickPlayLabel);
|
||||
if (needsStackMap(classNode)) {
|
||||
it.add(new FrameNode(Opcodes.F_SAME, 0, null, 0, null));
|
||||
}
|
||||
// String
|
||||
it.add(new InsnNode(Opcodes.POP));
|
||||
//
|
||||
it.add(doneQuickPlayLabel);
|
||||
if (needsStackMap(classNode)) {
|
||||
it.add(new FrameNode(Opcodes.F_SAME, 0, null, 0, null));
|
||||
}
|
||||
//
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user