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.Type; 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; import org.objectweb.asm.tree.VarInsnNode; /** * 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 UPDATE_NAME = "update"; private static final String CONSTRAINT_CHANGED_NAME = "constraintChanged"; private static final String AUTHORIZATION_CHANGED_NAME = "authorizationChanged"; private static final String AUTHORIZATION_CLIENT_COLLIDE_NAME = "authorizationClientCollide"; private static final String AUTHORIZATION_SERVER_COLLIDE_NAME = "authorizationServerCollide"; private static final String AUTHORIZATION_SERVER_ON_SEAT_NAME = "authorizationServerOnSeat"; 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 AUTHORIZATION_CHANGED_DESC = "(Lzombie/characters/IsoGameCharacter;)V"; private static final String AUTHORIZATION_CLIENT_COLLIDE_DESC = "(Lzombie/characters/IsoPlayer;)V"; private static final String AUTHORIZATION_SERVER_COLLIDE_DESC = "(SZ)V"; private static final String AUTHORIZATION_SERVER_ON_SEAT_DESC = "(Lzombie/characters/IsoPlayer;Z)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 BULLET_OWNER = "zombie/core/physics/Bullet"; private static final String BULLET_ADD_ROPE = "addRopeConstraint"; private static final String BULLET_ADD_POINT = "addPointConstraint"; private static final String BULLET_ADD_ROPE_DESC = "(IIFFFFFFF)I"; private static final String BULLET_ADD_POINT_DESC = "(IIFFFFFF)I"; private static final String CAR_CONTROLLER_OWNER = "zombie/core/physics/CarController"; private static final String CAR_CONTROLLER_PARK = "park"; private static final String CAR_CONTROLLER_PARK_DESC = "()V"; 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 static final String HELPER_AUTHORIZATION_CHANGED = "authorizationChangedLandtrain"; private static final String HELPER_AUTHORIZATION_CHANGED_DESC = "(Lzombie/vehicles/BaseVehicle;Lzombie/characters/IsoGameCharacter;)V"; private static final String HELPER_AUTHORIZATION_CLIENT_COLLIDE = "authorizationClientCollideLandtrain"; private static final String HELPER_AUTHORIZATION_CLIENT_COLLIDE_DESC = "(Lzombie/vehicles/BaseVehicle;Lzombie/characters/IsoPlayer;)V"; private static final String HELPER_AUTHORIZATION_SERVER_COLLIDE = "authorizationServerCollideLandtrain"; private static final String HELPER_AUTHORIZATION_SERVER_COLLIDE_DESC = "(Lzombie/vehicles/BaseVehicle;SZ)V"; private static final String HELPER_AUTHORIZATION_SERVER_ON_SEAT = "authorizationServerOnSeatLandtrain"; private static final String HELPER_AUTHORIZATION_SERVER_ON_SEAT_DESC = "(Lzombie/vehicles/BaseVehicle;Lzombie/characters/IsoPlayer;Z)V"; private static final String HELPER_SAFE_PARK = "safeParkControllerLandtrain"; private static final String HELPER_SAFE_PARK_DESC = "(Lzombie/core/physics/CarController;)V"; private static final class AddPointPatchStats { int removedBreakCalls; int forcedRigidCalls; } 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 forcedRigidCalls = 0; int inspectedAddPointMethods = 0; int patchedConstraintDriverCalls = 0; int patchedEnterBlockedCalls = 0; int patchedEnterBlocked2Calls = 0; int patchedAuthorizationChangedMethods = 0; int patchedAuthorizationClientCollideMethods = 0; int patchedAuthorizationServerCollideMethods = 0; int patchedAuthorizationServerOnSeatMethods = 0; int patchedSafeParkCalls = 0; for (MethodNode method : classNode.methods) { if (TARGET_NAME.equals(method.name) && isTargetAddPointConstraint(method.desc)) { inspectedAddPointMethods++; AddPointPatchStats stats = patchAddPointConstraint(method); removedCalls += stats.removedBreakCalls; forcedRigidCalls += stats.forcedRigidCalls; } else if (UPDATE_NAME.equals(method.name) && VOID_NOARG_DESC.equals(method.desc)) { patchedSafeParkCalls += patchNullSafeParkCalls(method); } else if (CONSTRAINT_CHANGED_NAME.equals(method.name) && VOID_NOARG_DESC.equals(method.desc)) { patchedConstraintDriverCalls += patchConstraintChangedDriverCalls(method); } else if (AUTHORIZATION_CHANGED_NAME.equals(method.name) && AUTHORIZATION_CHANGED_DESC.equals(method.desc)) { patchedAuthorizationChangedMethods += patchMethodDelegateToHelper( method, HELPER_AUTHORIZATION_CHANGED, HELPER_AUTHORIZATION_CHANGED_DESC); } else if (AUTHORIZATION_CLIENT_COLLIDE_NAME.equals(method.name) && AUTHORIZATION_CLIENT_COLLIDE_DESC.equals(method.desc)) { patchedAuthorizationClientCollideMethods += patchMethodDelegateToHelper( method, HELPER_AUTHORIZATION_CLIENT_COLLIDE, HELPER_AUTHORIZATION_CLIENT_COLLIDE_DESC); } else if (AUTHORIZATION_SERVER_COLLIDE_NAME.equals(method.name) && AUTHORIZATION_SERVER_COLLIDE_DESC.equals(method.desc)) { patchedAuthorizationServerCollideMethods += patchMethodDelegateToHelper( method, HELPER_AUTHORIZATION_SERVER_COLLIDE, HELPER_AUTHORIZATION_SERVER_COLLIDE_DESC); } else if (AUTHORIZATION_SERVER_ON_SEAT_NAME.equals(method.name) && AUTHORIZATION_SERVER_ON_SEAT_DESC.equals(method.desc)) { patchedAuthorizationServerOnSeatMethods += patchMethodDelegateToHelper( method, HELPER_AUTHORIZATION_SERVER_ON_SEAT, HELPER_AUTHORIZATION_SERVER_ON_SEAT_DESC); } 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 (forcedRigidCalls < 1) { throw new IllegalStateException( "Expected to force at least 1 rope->point constraint call, patched " + forcedRigidCalls); } if (patchedConstraintDriverCalls < 1) { throw new IllegalStateException( "Expected to patch at least 1 constraintChanged getDriver call, patched " + patchedConstraintDriverCalls); } if (patchedAuthorizationChangedMethods < 1) { throw new IllegalStateException( "Expected to patch authorizationChanged, patched " + patchedAuthorizationChangedMethods); } if (patchedAuthorizationClientCollideMethods < 1) { throw new IllegalStateException( "Expected to patch authorizationClientCollide, patched " + patchedAuthorizationClientCollideMethods); } if (patchedAuthorizationServerCollideMethods < 1) { throw new IllegalStateException( "Expected to patch authorizationServerCollide, patched " + patchedAuthorizationServerCollideMethods); } if (patchedAuthorizationServerOnSeatMethods < 1) { throw new IllegalStateException( "Expected to patch authorizationServerOnSeat, patched " + patchedAuthorizationServerOnSeatMethods); } if (patchedSafeParkCalls < 1) { throw new IllegalStateException( "Expected to patch at least 1 CarController.park call, patched " + patchedSafeParkCalls); } 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 + ", forced rigid constraints: " + forcedRigidCalls + ", constraint driver hooks: " + patchedConstraintDriverCalls + ", auth hooks (changed/client/server/seat): " + patchedAuthorizationChangedMethods + "/" + patchedAuthorizationClientCollideMethods + "/" + patchedAuthorizationServerCollideMethods + "/" + patchedAuthorizationServerOnSeatMethods + ", safe park hooks: " + patchedSafeParkCalls + ", 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 AddPointPatchStats patchAddPointConstraint(MethodNode method) { AddPointPatchStats stats = new AddPointPatchStats(); 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); stats.removedBreakCalls++; } else if (node instanceof MethodInsnNode call && BULLET_OWNER.equals(call.owner) && BULLET_ADD_ROPE.equals(call.name) && BULLET_ADD_ROPE_DESC.equals(call.desc)) { // Drop the rope-length float and call Bullet.addPointConstraint(...) instead. insns.insertBefore(call, new InsnNode(Opcodes.POP)); MethodInsnNode replacement = new MethodInsnNode( Opcodes.INVOKESTATIC, BULLET_OWNER, BULLET_ADD_POINT, BULLET_ADD_POINT_DESC, false); insns.set(call, replacement); stats.forcedRigidCalls++; } node = next; } return stats; } 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 patchNullSafeParkCalls(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 (!CAR_CONTROLLER_OWNER.equals(call.owner) || !CAR_CONTROLLER_PARK.equals(call.name) || !CAR_CONTROLLER_PARK_DESC.equals(call.desc)) { node = next; continue; } MethodInsnNode replacement = new MethodInsnNode( Opcodes.INVOKESTATIC, HELPER_OWNER, HELPER_SAFE_PARK, HELPER_SAFE_PARK_DESC, false); insns.set(call, replacement); patched++; node = next; } return patched; } private static int patchMethodDelegateToHelper( MethodNode method, String helperMethod, String helperDesc) { InsnList insns = new InsnList(); int localIndex = 0; int maxStack = 0; if ((method.access & Opcodes.ACC_STATIC) == 0) { insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); localIndex = 1; maxStack = 1; } Type[] argumentTypes = Type.getArgumentTypes(method.desc); for (Type argumentType : argumentTypes) { insns.add(new VarInsnNode(argumentType.getOpcode(Opcodes.ILOAD), localIndex)); localIndex += argumentType.getSize(); maxStack += argumentType.getSize(); } insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, HELPER_OWNER, helperMethod, helperDesc, false)); Type returnType = Type.getReturnType(method.desc); if (returnType.getSort() == Type.VOID) { insns.add(new InsnNode(Opcodes.RETURN)); } else { insns.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); } method.instructions.clear(); method.instructions.add(insns); method.tryCatchBlocks.clear(); if (method.localVariables != null) { method.localVariables.clear(); } method.maxLocals = Math.max(method.maxLocals, localIndex); method.maxStack = Math.max(method.maxStack, maxStack); return 1; } 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; } }