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.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 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 "); 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 + ")"); } 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); } 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; } }