304 lines
12 KiB
Java
304 lines
12 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.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 = "<clinit>";
|
|
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 <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;
|
|
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;
|
|
}
|
|
}
|