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.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; /** * Patches zombie.vehicles.BaseVehicle for Landtrain chain support: * 1) remove forced breakConstraint() in addPointConstraint() * 2) route constraintChanged() driver lookups through helper that handles chain middle vehicles */ public final class BaseVehicleConstraintPatch { private static final String TARGET_NAME = "addPointConstraint"; private static final String CONSTRAINT_CHANGED_NAME = "constraintChanged"; private static final String IS_ENTER_BLOCKED_NAME = "isEnterBlocked"; private static final String IS_ENTER_BLOCKED2_NAME = "isEnterBlocked2"; private static final String CLINIT_NAME = ""; private static final String VOID_NOARG_DESC = "()V"; private static final String ENTER_BLOCKED_DESC = "(Lzombie/characters/IsoGameCharacter;I)Z"; private static final String EXIT_BLOCKED_DESC = "(Lzombie/characters/IsoGameCharacter;I)Z"; private static final String EXIT_BLOCKED2_DESC = "(I)Z"; 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 static final String GET_DRIVER_DESC = "()Lzombie/characters/IsoGameCharacter;"; private static final String HELPER_OWNER = "zombie/vehicles/LandtrainConstraintAuthHelper"; private static final String HELPER_METHOD = "resolveConstraintDriver"; private static final String HELPER_DESC = "(Lzombie/vehicles/BaseVehicle;)Lzombie/characters/IsoGameCharacter;"; private static final String HELPER_ENTER_BLOCKED = "isEnterBlockedLandtrain"; private static final String HELPER_ENTER_BLOCKED_DESC = "(Lzombie/vehicles/BaseVehicle;Lzombie/characters/IsoGameCharacter;I)Z"; private static final String HELPER_ENTER_BLOCKED2 = "isEnterBlocked2Landtrain"; private static final String HELPER_ENTER_BLOCKED2_DESC = "(Lzombie/vehicles/BaseVehicle;I)Z"; 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; int patchedConstraintDriverCalls = 0; int patchedEnterBlockedCalls = 0; int patchedEnterBlocked2Calls = 0; for (MethodNode method : classNode.methods) { if (TARGET_NAME.equals(method.name) && isTargetAddPointConstraint(method.desc)) { inspectedAddPointMethods++; removedCalls += patchAddPointConstraint(method); } else if (CONSTRAINT_CHANGED_NAME.equals(method.name) && VOID_NOARG_DESC.equals(method.desc)) { patchedConstraintDriverCalls += patchConstraintChangedDriverCalls(method); } else if (IS_ENTER_BLOCKED_NAME.equals(method.name) && ENTER_BLOCKED_DESC.equals(method.desc)) { patchedEnterBlockedCalls += patchEnterBlockedCall( method, "isExitBlocked", EXIT_BLOCKED_DESC, HELPER_ENTER_BLOCKED, HELPER_ENTER_BLOCKED_DESC); } else if (IS_ENTER_BLOCKED2_NAME.equals(method.name) && ENTER_BLOCKED_DESC.equals(method.desc)) { patchedEnterBlocked2Calls += patchEnterBlockedCall( method, "isExitBlocked2", EXIT_BLOCKED2_DESC, HELPER_ENTER_BLOCKED2, HELPER_ENTER_BLOCKED2_DESC); } } if (removedCalls < 2) { throw new IllegalStateException( "Expected to remove 2 breakConstraint calls, removed " + removedCalls + " (inspected addPoint methods: " + inspectedAddPointMethods + ")"); } if (patchedConstraintDriverCalls < 1) { throw new IllegalStateException( "Expected to patch at least 1 constraintChanged getDriver call, patched " + patchedConstraintDriverCalls); } if (patchedEnterBlockedCalls < 1) { throw new IllegalStateException( "Expected to patch isEnterBlocked call, patched " + patchedEnterBlockedCalls); } if (patchedEnterBlocked2Calls < 1) { throw new IllegalStateException( "Expected to patch isEnterBlocked2 call, patched " + patchedEnterBlocked2Calls); } 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 + ", constraint driver hooks: " + patchedConstraintDriverCalls + ", enter-block hooks: " + patchedEnterBlockedCalls + "/" + patchedEnterBlocked2Calls + ", class-init debug log: enabled"); } private static boolean isTargetAddPointConstraint(String methodDesc) { 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; } // breakConstraint(...) consumes objectref + 2 args and returns void. 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 int patchConstraintChangedDriverCalls(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)) { node = next; continue; } if (!BASE_VEHICLE_OWNER.equals(call.owner) || !"getDriver".equals(call.name) || !GET_DRIVER_DESC.equals(call.desc)) { node = next; continue; } MethodInsnNode replacement = new MethodInsnNode( Opcodes.INVOKESTATIC, HELPER_OWNER, HELPER_METHOD, HELPER_DESC, false); insns.set(call, replacement); patched++; node = next; } return patched; } private static int patchEnterBlockedCall( MethodNode method, String targetCallName, String targetCallDesc, String helperMethod, String helperDesc) { int patched = 0; InsnList insns = method.instructions; for (AbstractInsnNode node = insns.getFirst(); node != null; ) { AbstractInsnNode next = node.getNext(); if (!(node instanceof MethodInsnNode call)) { node = next; continue; } if (!BASE_VEHICLE_OWNER.equals(call.owner) || !targetCallName.equals(call.name) || !targetCallDesc.equals(call.desc)) { node = next; continue; } MethodInsnNode replacement = new MethodInsnNode( Opcodes.INVOKESTATIC, HELPER_OWNER, helperMethod, helperDesc, false); insns.set(call, replacement); 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; } }