Files
Landtrain/tools/java/BaseVehicleConstraintPatch.java
2026-02-07 18:21:03 -05:00

184 lines
7.1 KiB
Java

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.Opcodes;
/**
* Patches zombie.vehicles.BaseVehicle so addPointConstraint() no longer force-breaks
* both vehicles before creating a new constraint.
*/
public final class BaseVehicleConstraintPatch {
private static final String TARGET_NAME = "addPointConstraint";
private static final String CLINIT_NAME = "<clinit>";
private static final String VOID_NOARG_DESC = "()V";
private static final String PATCH_LOG_LINE = "[Landtrain][BaseVehiclePatch] BaseVehicle override enabled";
private static final String BREAK_DESC_OBJECT_BOOL = "(ZLjava/lang/Boolean;)V";
private static final String BREAK_DESC_PRIMITIVE_BOOL = "(ZZ)V";
private static final String BASE_VEHICLE_OWNER = "zombie/vehicles/BaseVehicle";
private BaseVehicleConstraintPatch() {
}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: BaseVehicleConstraintPatch <input BaseVehicle.class> <output BaseVehicle.class>");
System.exit(2);
}
Path input = Paths.get(args[0]);
Path output = Paths.get(args[1]);
byte[] original = Files.readAllBytes(input);
ClassNode classNode = new ClassNode();
new ClassReader(original).accept(classNode, 0);
int removedCalls = 0;
int inspectedAddPointMethods = 0;
for (MethodNode method : classNode.methods) {
if (!TARGET_NAME.equals(method.name) || !isTargetAddPointConstraint(method.desc)) {
continue;
}
inspectedAddPointMethods++;
removedCalls += patchAddPointConstraint(method);
}
if (removedCalls < 2) {
throw new IllegalStateException(
"Expected to remove 2 breakConstraint calls, removed "
+ removedCalls
+ " (inspected addPoint methods: "
+ inspectedAddPointMethods
+ ")");
}
if (!ensureClassInitLog(classNode)) {
throw new IllegalStateException("Failed to inject BaseVehicle class-init debug log");
}
ClassWriter writer = new ClassWriter(0);
classNode.accept(writer);
Files.createDirectories(output.getParent());
Files.write(output, writer.toByteArray());
System.out.println(
"Patched BaseVehicle.class; removed breakConstraint calls: "
+ removedCalls
+ ", class-init debug log: enabled");
}
private static boolean isTargetAddPointConstraint(String methodDesc) {
// We only want the 5-arg overload:
// (IsoPlayer, BaseVehicle, String, String, boolean|Boolean) -> void
return "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Z)V"
.equals(methodDesc)
|| "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V"
.equals(methodDesc);
}
private static int patchAddPointConstraint(MethodNode method) {
int patched = 0;
InsnList insns = method.instructions;
for (AbstractInsnNode node = insns.getFirst(); node != null; ) {
AbstractInsnNode next = node.getNext();
if (node instanceof MethodInsnNode call
&& BASE_VEHICLE_OWNER.equals(call.owner)
&& "breakConstraint".equals(call.name)) {
if (!(BREAK_DESC_OBJECT_BOOL.equals(call.desc)
|| BREAK_DESC_PRIMITIVE_BOOL.equals(call.desc))) {
node = next;
continue;
}
// Keep stack-map frames valid by preserving stack effect:
// breakConstraint(...) consumes objectref + 2 args and returns void.
// Replace invoke with POP2 + POP (consume 3 category-1 stack slots).
InsnList replacement = new InsnList();
replacement.add(new InsnNode(Opcodes.POP2));
replacement.add(new InsnNode(Opcodes.POP));
insns.insert(node, replacement);
insns.remove(node);
patched++;
}
node = next;
}
return patched;
}
private static boolean ensureClassInitLog(ClassNode classNode) {
MethodNode clinit = null;
for (MethodNode method : classNode.methods) {
if (CLINIT_NAME.equals(method.name) && VOID_NOARG_DESC.equals(method.desc)) {
clinit = method;
break;
}
}
if (clinit == null) {
clinit = new MethodNode(Opcodes.ACC_STATIC, CLINIT_NAME, VOID_NOARG_DESC, null, null);
clinit.instructions = new InsnList();
clinit.instructions.add(createPatchLogInstructions());
clinit.instructions.add(new InsnNode(Opcodes.RETURN));
clinit.maxStack = 2;
clinit.maxLocals = 0;
classNode.methods.add(clinit);
return true;
}
if (hasPatchLog(clinit)) {
return true;
}
boolean inserted = false;
for (AbstractInsnNode node = clinit.instructions.getFirst(); node != null; ) {
AbstractInsnNode next = node.getNext();
if (node.getOpcode() == Opcodes.RETURN) {
clinit.instructions.insertBefore(node, createPatchLogInstructions());
inserted = true;
}
node = next;
}
if (inserted) {
clinit.maxStack = Math.max(clinit.maxStack, 2);
}
return inserted;
}
private static boolean hasPatchLog(MethodNode method) {
for (AbstractInsnNode node = method.instructions.getFirst(); node != null; node = node.getNext()) {
if (node instanceof LdcInsnNode ldc
&& ldc.cst instanceof String text
&& PATCH_LOG_LINE.equals(text)) {
return true;
}
}
return false;
}
private static InsnList createPatchLogInstructions() {
InsnList insns = new InsnList();
insns.add(new org.objectweb.asm.tree.FieldInsnNode(
Opcodes.GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"));
insns.add(new LdcInsnNode(PATCH_LOG_LINE));
insns.add(new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false));
return insns;
}
}