/**
 * rscplus
 *
 * <p>This file is part of rscplus.
 *
 * <p>rscplus is free software: you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * <p>rscplus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * <p>You should have received a copy of the GNU General Public License along with rscplus. If not,
 * see <http://www.gnu.org/licenses/>.
 *
 * <p>Authors: see <https://github.com/RSCPlus/rscplus>
 */
package Client;

import Client.Settings.Dir;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Iterator;
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.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.IincInsnNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.objectweb.asm.util.Printer;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceMethodVisitor;

/** Singleton class which hooks variables and patches classes. */
public class JClassPatcher {

  // Singleton
  private static JClassPatcher instance = null;

  private Printer printer = new Textifier();
  private TraceMethodVisitor mp = new TraceMethodVisitor(printer);

  private JClassPatcher() {
    // Empty private constructor to prevent extra instances from being created.
  }

  public byte[] patch(byte[] data) {
    ClassReader reader = new ClassReader(data);
    ClassNode node = new ClassNode();
    reader.accept(node, ClassReader.SKIP_DEBUG);

    if (node.name.equals("ua")) patchRenderer(node);
    else if (node.name.equals("e")) patchApplet(node);
    else if (node.name.equals("qa")) patchMenu(node);
    else if (node.name.equals("m")) patchData(node);
    else if (node.name.equals("client")) patchClient(node);
    else if (node.name.equals("f")) patchRandom(node);
    else if (node.name.equals("da")) patchGameApplet(node);
    else if (node.name.equals("lb")) patchRendererHelper(node);

    // Patch applied to all classes
    patchGeneric(node);

    if (Settings.DISASSEMBLE.get(Settings.currentProfile)) {
      Settings.Dir.DUMP = Dir.JAR + "/" + Settings.DISASSEMBLE_DIRECTORY.get("custom");
      Util.makeDirectory(Dir.DUMP);
      Logger.Info("Disassembling file: " + node.name + ".class");
      dumpClass(node);
    }

    ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    node.accept(writer);
    return writer.toByteArray();
  }

  private void patchGeneric(ClassNode node) {
    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      // General byte patch
      Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
      while (insnNodeList.hasNext()) {
        AbstractInsnNode insnNode = insnNodeList.next();

        if (insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL) {
          MethodInsnNode call = (MethodInsnNode) insnNode;

          // Patch calls to System.out.println and route them to Logger.Game
          if (call.owner.equals("java/io/PrintStream") && call.name.equals("println")) {
            methodNode.instructions.insertBefore(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Client/Logger", "Game", "(Ljava/lang/String;)V"));
            methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.POP));
            methodNode.instructions.remove(insnNode);
          }
        }
      }

      hookClassVariable(
          methodNode,
          "ua",
          "fb",
          "Ljava/awt/image/ImageConsumer;",
          "Game/Renderer",
          "image_consumer",
          "Ljava/awt/image/ImageConsumer;",
          true,
          true);
      hookClassVariable(methodNode, "ua", "u", "I", "Game/Renderer", "width", "I", false, true);
      hookClassVariable(methodNode, "ua", "k", "I", "Game/Renderer", "height", "I", false, true);
      hookClassVariable(methodNode, "ua", "rb", "[I", "Game/Renderer", "pixels", "[I", true, true);

      hookClassVariable(
          methodNode,
          "e",
          "Ob",
          "Ljava/lang/String;",
          "Game/Client",
          "pm_enteredText",
          "Ljava/lang/String;",
          true,
          true);
      hookClassVariable(
          methodNode,
          "client",
          "Ob",
          "Ljava/lang/String;",
          "Game/Client",
          "pm_enteredText",
          "Ljava/lang/String;",
          true,
          true);
      hookClassVariable(
          methodNode,
          "e",
          "x",
          "Ljava/lang/String;",
          "Game/Client",
          "pm_text",
          "Ljava/lang/String;",
          true,
          true);
      hookClassVariable(
          methodNode,
          "client",
          "x",
          "Ljava/lang/String;",
          "Game/Client",
          "pm_text",
          "Ljava/lang/String;",
          true,
          true);

      hookClassVariable(
          methodNode,
          "client",
          "li",
          "Lba;",
          "Game/Renderer",
          "instance",
          "Ljava/lang/Object;",
          true,
          false);

      hookClassVariable(methodNode, "ba", "u", "I", "Game/Renderer", "width", "I", false, true);
      hookClassVariable(methodNode, "ba", "k", "I", "Game/Renderer", "height", "I", false, true);
      hookClassVariable(methodNode, "ba", "rb", "[I", "Game/Renderer", "pixels", "[I", true, true);

      hookStaticVariable(methodNode, "n", "g", "I", "Game/Client", "friends_count", "I");
      hookStaticVariable(
          methodNode,
          "ua",
          "h",
          "[Ljava/lang/String;",
          "Game/Client",
          "friends",
          "[Ljava/lang/String;");
      hookStaticVariable(
          methodNode,
          "ac",
          "z",
          "[Ljava/lang/String;",
          "Game/Client",
          "friends_world",
          "[Ljava/lang/String;");
      hookStaticVariable(
          methodNode,
          "cb",
          "c",
          "[Ljava/lang/String;",
          "Game/Client",
          "friends_formerly",
          "[Ljava/lang/String;");
      hookStaticVariable(methodNode, "client", "Fj", "[I", "Game/Client", "friends_online", "[I");

      hookStaticVariable(methodNode, "db", "g", "I", "Game/Client", "ignores_count", "I");
      hookStaticVariable(
          methodNode,
          "l",
          "c",
          "[Ljava/lang/String;",
          "Game/Client",
          "ignores",
          "[Ljava/lang/String;");
      hookStaticVariable(
          methodNode,
          "ia",
          "g",
          "[Ljava/lang/String;",
          "Game/Client",
          "ignores_formerly",
          "[Ljava/lang/String;");
      hookStaticVariable(
          methodNode,
          "ia",
          "a",
          "[Ljava/lang/String;",
          "Game/Client",
          "ignores_copy",
          "[Ljava/lang/String;");
      hookStaticVariable(
          methodNode,
          "ua",
          "wb",
          "[Ljava/lang/String;",
          "Game/Client",
          "ignores_formerly_copy",
          "[Ljava/lang/String;");

      hookClassVariable(
          methodNode, "client", "Wd", "I", "Game/Renderer", "width", "I", false, true);
      hookClassVariable(
          methodNode, "client", "Oi", "I", "Game/Renderer", "height_client", "I", false, true);

      hookClassVariable(methodNode, "e", "m", "I", "Game/Renderer", "width", "I", false, true);
      hookClassVariable(methodNode, "e", "a", "I", "Game/Renderer", "height", "I", false, true);
      hookClassVariable(
          methodNode, "e", "Ib", "I", "Game/Replay", "frame_time_slice", "I", true, true);
      hookClassVariable(
          methodNode, "client", "fc", "I", "Game/Replay", "connection_port", "I", true, true);

      hookClassVariable(methodNode, "lb", "pb", "[I", "Game/Renderer", "pixels", "[I", true, true);

      hookStaticVariable(
          methodNode,
          "client",
          "il",
          "[Ljava/lang/String;",
          "Game/Client",
          "strings",
          "[Ljava/lang/String;");

      hookStaticVariable(
          methodNode,
          "ac",
          "x",
          "[Ljava/lang/String;",
          "Game/Item",
          "item_name",
          "[Ljava/lang/String;");
      hookStaticVariable(
          methodNode,
          "lb",
          "ac",
          "[Ljava/lang/String;",
          "Game/Item",
          "item_commands",
          "[Ljava/lang/String;");

      hookConditionalClassVariable(
          methodNode,
          "lb",
          "Mb",
          "I",
          "Game/Camera",
          "distance1",
          "I",
          false,
          true,
          "VIEW_DISTANCE_BOOL");
      hookConditionalClassVariable(
          methodNode,
          "lb",
          "X",
          "I",
          "Game/Camera",
          "distance2",
          "I",
          false,
          true,
          "VIEW_DISTANCE_BOOL");
      hookConditionalClassVariable(
          methodNode,
          "lb",
          "P",
          "I",
          "Game/Camera",
          "distance3",
          "I",
          false,
          true,
          "VIEW_DISTANCE_BOOL");
      hookConditionalClassVariable(
          methodNode,
          "lb",
          "G",
          "I",
          "Game/Camera",
          "distance4",
          "I",
          false,
          true,
          "VIEW_DISTANCE_BOOL");

      hookClassVariable(
          methodNode, "client", "cl", "I", "Game/Client", "max_inventory", "I", true, false);
      hookClassVariable(
          methodNode, "client", "bk", "[Z", "Game/Client", "prayers_on", "[Z", true, false);
      hookClassVariable(
          methodNode,
          "client",
          "Fc",
          "[I",
          "Game/Client",
          "current_equipment_stats",
          "[I",
          true,
          false);
      hookClassVariable(
          methodNode, "client", "oh", "[I", "Game/Client", "current_level", "[I", true, false);
      hookClassVariable(
          methodNode, "client", "cg", "[I", "Game/Client", "base_level", "[I", true, false);
      hookClassVariable(
          methodNode,
          "client",
          "Vk",
          "[Ljava/lang/String;",
          "Game/Client",
          "skill_name",
          "[Ljava/lang/String;",
          true,
          false);
      hookClassVariable(methodNode, "client", "Ak", "[I", "Game/Client", "xp", "[I", true, false);
      hookClassVariable(
          methodNode, "client", "vg", "I", "Game/Client", "fatigue", "I", true, false);
      hookClassVariable(
          methodNode, "client", "Fg", "I", "Game/Client", "combat_style", "I", true, true);
      if (Settings.SAVE_LOGININFO.get(Settings.currentProfile))
        hookClassVariable(
            methodNode, "client", "Xd", "I", "Game/Client", "login_screen", "I", false, true);

      hookClassVariable(
          methodNode,
          "client",
          "Ek",
          "Llb;",
          "Game/Camera",
          "instance",
          "Ljava/lang/Object;",
          true,
          false);
      hookConditionalClassVariable(
          methodNode, "client", "qd", "I", "Game/Camera", "fov", "I", false, true, "FOV_BOOL");

      hookClassVariable(
          methodNode, "client", "ai", "I", "Game/Client", "combat_timer", "I", true, true);
      hookClassVariable(
          methodNode, "client", "Fe", "Z", "Game/Client", "show_bank", "Z", true, false);
      hookClassVariable(
          methodNode, "client", "dd", "Z", "Game/Client", "show_duel", "Z", true, false);
      hookClassVariable(
          methodNode, "client", "Pj", "Z", "Game/Client", "show_duelconfirm", "Z", true, false);
      hookClassVariable(
          methodNode, "client", "Bj", "I", "Game/Client", "show_friends", "I", true, true);
      hookClassVariable(
          methodNode, "client", "qc", "I", "Game/Client", "show_menu", "I", true, false);
      hookClassVariable(
          methodNode, "client", "Ph", "Z", "Game/Client", "show_questionmenu", "Z", true, false);
      hookClassVariable(
          methodNode, "client", "Vf", "I", "Game/Client", "show_report", "I", true, false);
      hookClassVariable(
          methodNode, "client", "uk", "Z", "Game/Client", "show_shop", "Z", true, false);
      hookClassVariable(
          methodNode, "client", "Qk", "Z", "Game/Client", "show_sleeping", "Z", true, false);
      hookClassVariable(
          methodNode, "client", "Hk", "Z", "Game/Client", "show_trade", "Z", true, false);
      hookClassVariable(
          methodNode, "client", "Xj", "Z", "Game/Client", "show_tradeconfirm", "Z", true, false);
      hookClassVariable(
          methodNode, "client", "Oh", "Z", "Game/Client", "show_welcome", "Z", true, true);

      hookClassVariable(
          methodNode,
          "client",
          "Qd",
          "Ljava/lang/String;",
          "Game/Client",
          "pm_username",
          "Ljava/lang/String;",
          true,
          true);

      hookClassVariable(
          methodNode,
          "client",
          "wh",
          "Ljava/lang/String;",
          "Game/Client",
          "username_login",
          "Ljava/lang/String;",
          true,
          true);
      hookClassVariable(
          methodNode, "client", "Vh", "I", "Game/Client", "autologin_timeout", "I", true, true);

      hookClassVariable(
          methodNode, "client", "lc", "I", "Game/Client", "inventory_count", "I", true, false);
      hookClassVariable(
          methodNode, "client", "vf", "[I", "Game/Client", "inventory_items", "[I", true, false);
      hookConditionalClassVariable(
          methodNode,
          "client",
          "kg",
          "I",
          "Game/Camera",
          "lookat_x",
          "I",
          false,
          true,
          "CAMERA_MOVABLE_BOOL");
      hookConditionalClassVariable(
          methodNode,
          "client",
          "Si",
          "I",
          "Game/Camera",
          "lookat_y",
          "I",
          false,
          true,
          "CAMERA_MOVABLE_BOOL");
      hookClassVariable(
          methodNode, "client", "Wc", "I", "Game/Camera", "auto_speed", "I", true, true);
      hookClassVariable(
          methodNode, "client", "Be", "I", "Game/Camera", "rotation_y", "I", true, true);
      hookClassVariable(methodNode, "client", "Kh", "Z", "Game/Camera", "auto", "Z", true, true);
      hookClassVariable(methodNode, "client", "si", "I", "Game/Camera", "angle", "I", true, true);

      hookConditionalClassVariable(
          methodNode,
          "client",
          "ug",
          "I",
          "Game/Camera",
          "rotation",
          "I",
          false,
          true,
          "CAMERA_ROTATABLE_BOOL");
      hookConditionalClassVariable(
          methodNode,
          "client",
          "ac",
          "I",
          "Game/Camera",
          "zoom",
          "I",
          false,
          true,
          "CAMERA_ZOOMABLE_BOOL");

      // Chat menu
      hookClassVariable(
          methodNode,
          "client",
          "yd",
          "Lqa;",
          "Game/Menu",
          "chat_menu",
          "Ljava/lang/Object;",
          true,
          false);
      hookClassVariable(
          methodNode, "client", "Fh", "I", "Game/Menu", "chat_type1", "I", true, false);
      hookClassVariable(
          methodNode, "client", "bh", "I", "Game/Menu", "chat_input", "I", true, false);
      hookClassVariable(
          methodNode, "client", "ud", "I", "Game/Menu", "chat_type2", "I", true, false);
      hookClassVariable(
          methodNode, "client", "mc", "I", "Game/Menu", "chat_type3", "I", true, false);

      // Quest menu
      hookClassVariable(
          methodNode,
          "client",
          "fe",
          "Lqa;",
          "Game/Menu",
          "quest_menu",
          "Ljava/lang/Object;",
          true,
          false);
      hookClassVariable(
          methodNode, "client", "lk", "I", "Game/Menu", "quest_handle", "I", true, false);

      // Friends menu
      hookClassVariable(
          methodNode,
          "client",
          "zk",
          "Lqa;",
          "Game/Menu",
          "friend_menu",
          "Ljava/lang/Object;",
          true,
          false);
      hookClassVariable(
          methodNode, "client", "Hi", "I", "Game/Menu", "friend_handle", "I", true, false);

      // Spell menu
      hookClassVariable(
          methodNode,
          "client",
          "Mc",
          "Lqa;",
          "Game/Menu",
          "spell_menu",
          "Ljava/lang/Object;",
          true,
          false);
      hookClassVariable(
          methodNode, "client", "Ud", "I", "Game/Menu", "spell_handle", "I", true, false);

      // Player name
      hookClassVariable(
          methodNode,
          "client",
          "wi",
          "Lta;",
          "Game/Client",
          "player_object",
          "Ljava/lang/Object;",
          true,
          false);
      // coordinates related
      hookClassVariable(
          methodNode, "client", "Qg", "I", "Game/Client", "regionX", "I", true, false);
      hookClassVariable(
          methodNode, "client", "zg", "I", "Game/Client", "regionY", "I", true, false);
      hookClassVariable(
          methodNode, "client", "Lf", "I", "Game/Client", "localRegionX", "I", true, false);
      hookClassVariable(
          methodNode, "client", "sh", "I", "Game/Client", "localRegionY", "I", true, false);
      hookClassVariable(
          methodNode, "client", "Ki", "I", "Game/Client", "planeWidth", "I", true, false);
      hookClassVariable(
          methodNode, "client", "sk", "I", "Game/Client", "planeHeight", "I", true, false);
      hookClassVariable(
          methodNode, "client", "bc", "I", "Game/Client", "planeIndex", "I", true, false);
      hookClassVariable(
          methodNode, "client", "Ub", "Z", "Game/Client", "loadingArea", "Z", true, false);

      // Last mouse activity
      // hookClassVariable(methodNode, "client", "sb", "I", "Game/Client", "lastMouseAction", "I",
      // true, true);

      // Client version
      hookStaticVariable(methodNode, "fa", "d", "I", "Game/Client", "version", "I");

      // Client modulus and exponent
      hookStaticVariable(
          methodNode,
          "s",
          "c",
          "Ljava/math/BigInteger;",
          "Game/Client",
          "exponent",
          "Ljava/math/BigInteger;");
      hookStaticVariable(
          methodNode,
          "ja",
          "K",
          "Ljava/math/BigInteger;",
          "Game/Client",
          "modulus",
          "Ljava/math/BigInteger;");

      // Shell strings
      hookStaticVariable(
          methodNode,
          "e",
          "Sb",
          "[Ljava/lang/String;",
          "Game/Renderer",
          "shellStrings",
          "[Ljava/lang/String;");

      hookClassVariable(
          methodNode,
          "client",
          "Jh",
          "Lda;",
          "Game/Client",
          "clientStream",
          "Ljava/lang/Object;",
          true,
          false);

      // Bank related vars
      hookClassVariable(
          methodNode, "client", "ci", "[I", "Game/Client", "new_bank_items", "[I", true, true);
      hookClassVariable(
          methodNode,
          "client",
          "Xe",
          "[I",
          "Game/Client",
          "new_bank_items_count",
          "[I",
          true,
          true);
      hookClassVariable(
          methodNode, "client", "ae", "[I", "Game/Client", "bank_items", "[I", true, true);
      hookClassVariable(
          methodNode, "client", "di", "[I", "Game/Client", "bank_items_count", "[I", true, true);
      hookClassVariable(
          methodNode, "client", "fj", "I", "Game/Client", "new_count_items_bank", "I", true, true);
      hookClassVariable(
          methodNode, "client", "vj", "I", "Game/Client", "count_items_bank", "I", true, true);
      hookClassVariable(
          methodNode, "client", "xg", "I", "Game/Client", "bank_active_page", "I", true, true);

      hookClassVariable(
          methodNode, "client", "sj", "I", "Game/Client", "selectedItem", "I", true, false);
      hookClassVariable(
          methodNode, "client", "Rd", "I", "Game/Client", "selectedItemSlot", "I", true, false);
    }
  }

  private void patchMenu(ClassNode node) {
    Logger.Info("Patching menu (" + node.name + ".class)");

    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      // Menu swap hook
      if (methodNode.name.equals("e") && methodNode.desc.equals("(II)V")) {
        AbstractInsnNode first = methodNode.instructions.getFirst();

        LabelNode label = new LabelNode();
        methodNode.instructions.insertBefore(first, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            first,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC, "Game/Menu", "switchList", "(Ljava/lang/Object;)Z"));
        methodNode.instructions.insertBefore(first, new JumpInsnNode(Opcodes.IFGT, label));
        methodNode.instructions.insertBefore(first, new InsnNode(Opcodes.RETURN));
        methodNode.instructions.insertBefore(first, label);
      }
    }
  }

  private void patchData(ClassNode node) {
    Logger.Info("Patching data (" + node.name + ".class)");

    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      if (methodNode.name.equals("a") && methodNode.desc.equals("([BBZ)V")) {
        // Data hook patches
        AbstractInsnNode lastNode = methodNode.instructions.getLast();
        methodNode.instructions.insertBefore(
            lastNode,
            new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Item", "patchItemNames", "()V", false));
        methodNode.instructions.insertBefore(
            lastNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC, "Game/Item", "patchItemCommands", "()V", false));
      }
    }
  }

  private void patchApplet(ClassNode node) {
    Logger.Info("Patching applet (" + node.name + ".class)");

    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      if (methodNode.name.equals("run") && methodNode.desc.equals("()V")) {
        // Mouse and keyboard listener hooks
        AbstractInsnNode findNode = methodNode.instructions.getFirst();
        for (; ; ) {
          AbstractInsnNode next = findNode.getNext();

          if (next == null) break;

          if (findNode.getOpcode() == Opcodes.ALOAD && next.getOpcode() == Opcodes.ALOAD) {
            AbstractInsnNode invokeNode = next.getNext();
            MethodInsnNode invoke = (MethodInsnNode) invokeNode;
            methodNode.instructions.remove(next);
            methodNode.instructions.remove(invokeNode);
            if (invoke.name.equals("addMouseListener"))
              methodNode.instructions.insert(
                  findNode,
                  new FieldInsnNode(
                      Opcodes.PUTSTATIC,
                      "Game/MouseHandler",
                      "listener_mouse",
                      "Ljava/awt/event/MouseListener;"));
            else if (invoke.name.equals("addMouseMotionListener"))
              methodNode.instructions.insert(
                  findNode,
                  new FieldInsnNode(
                      Opcodes.PUTSTATIC,
                      "Game/MouseHandler",
                      "listener_mouse_motion",
                      "Ljava/awt/event/MouseMotionListener;"));
            else if (invoke.name.equals("addKeyListener"))
              methodNode.instructions.insert(
                  findNode,
                  new FieldInsnNode(
                      Opcodes.PUTSTATIC,
                      "Game/KeyboardHandler",
                      "listener_key",
                      "Ljava/awt/event/KeyListener;"));
          }
          findNode = findNode.getNext();
        }

        // Throwable crash patch
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          if (nextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.INVOKESTATIC
              && nextNode.getOpcode() == Opcodes.ATHROW) {
            MethodInsnNode call = (MethodInsnNode) insnNode;
            methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.RETURN));
          }
        }
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(IB)V")) {
        // FPS hook
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();

        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          if (nextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.GETSTATIC) {
            FieldInsnNode call = (FieldInsnNode) insnNode;
            methodNode.instructions.insertBefore(
                insnNode,
                new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Replay", "getFPS", "()I", false));
            methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ISTORE, 1));
          }
        }
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(Ljava/lang/String;Z)V")) {
        // this part shows error_game_
        // we want to call disconnect hook for instance in error_game_crash
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();

        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          if (nextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL
              && ((MethodInsnNode) insnNode).name.equals("println")) {
            LabelNode call = (LabelNode) insnNode.getNext();
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ALOAD, 1));
            methodNode.instructions.insertBefore(
                call,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Game/Client",
                    "error_game_hook",
                    "(Ljava/lang/String;)V",
                    false));
          }
        }
      }
      if (Settings.javaVersion >= 9
          && methodNode.name.equals("isDisplayable")
          && methodNode.desc.equals("()Z")) {
        Logger.Warn(
            "Applying Java 9+ compatibility fix in "
                + node.name
                + "."
                + methodNode.name
                + methodNode.desc);
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        AbstractInsnNode insnNode = insnNodeList.next().getNext();

        // To fix java 9+, just forward the call to super.isDisplayable()
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode,
            new MethodInsnNode(
                Opcodes.INVOKESPECIAL, "java/applet/Applet", "isDisplayable", "()Z", false));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.IRETURN));
      }
      if (Settings.javaVersion >= 9
          && (methodNode.name.equals("mousePressed") || methodNode.name.equals("mouseDragged"))
          && methodNode.desc.equals("(Ljava/awt/event/MouseEvent;)V")) {
        Logger.Warn(
            "Applying Java 9+ compatibility fix in "
                + node.name
                + "."
                + methodNode.name
                + methodNode.desc);
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          if (insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL
              && ((MethodInsnNode) insnNode).name.equals("isMetaDown")) {
            // Use SwingUtilities to determine if click is right click to support java 9+ right
            // clicking
            methodNode.instructions.insertBefore(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "javax/swing/SwingUtilities",
                    "isRightMouseButton",
                    "(Ljava/awt/event/MouseEvent;)Z",
                    false));
            methodNode.instructions.remove(insnNode);
          }
        }
      }
    }
  }

  private void patchClient(ClassNode node) {
    Logger.Info("Patching client (" + node.name + ".class)");

    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      // URL check removal at launch
      if (methodNode.name.equals("a") && methodNode.desc.equals("(B)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          if (insnNode.getOpcode() == Opcodes.IFEQ) {
            JumpInsnNode jmpNode = (JumpInsnNode) insnNode;
            JumpInsnNode gotoNode = new JumpInsnNode(Opcodes.GOTO, jmpNode.label);
            methodNode.instructions.insert(insnNode, gotoNode);
            methodNode.instructions.remove(jmpNode.getPrevious());
            methodNode.instructions.remove(jmpNode.getPrevious());
            methodNode.instructions.remove(jmpNode);
            break;
          }
        }
      }
      // handlePacket
      if (methodNode.name.equals("a") && methodNode.desc.equals("(III)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        AbstractInsnNode insnNode = insnNodeList.next();
        methodNode.instructions.insertBefore(
            insnNode,
            new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Replay", "patchClient", "()V", false));
        // save encrypted opcodes (used when decrypted opcode = 182)
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 3));
        methodNode.instructions.insertBefore(
            insnNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC, "Game/Replay", "saveEncOpcode", "(I)V", false));

        // fetch opcode for packets
        insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          insnNode = insnNodeList.next();

          if (insnNode.getOpcode() == Opcodes.ISTORE && ((VarInsnNode) insnNode).var == 3) {
            VarInsnNode call = (VarInsnNode) insnNode.getNext();

            // check opcode against checkpoint 182 - code for welcome screen info
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 3));
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 2));
            methodNode.instructions.insertBefore(
                call,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Game/Replay", "checkPoint", "(II)V", false));
            break;
          }
        }
      }
      // sendLogout
      if (methodNode.name.equals("B") && methodNode.desc.equals("(I)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        AbstractInsnNode insnNode = insnNodeList.next();
        methodNode.instructions.insertBefore(
            insnNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC, "Game/Replay", "closeReplayPlayback", "()V", false));
      }
      // I (I)V is where most of the interface is processed
      if (methodNode.name.equals("I") && methodNode.desc.equals("(I)V")) {
        AbstractInsnNode first = methodNode.instructions.getFirst();

        methodNode.instructions.insert(
            first, new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Client", "is_hover", "Z"));
        methodNode.instructions.insert(first, new InsnNode(Opcodes.ICONST_0));

        // Show combat menu
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          if (insnNode.getOpcode() == Opcodes.BIPUSH) {
            IntInsnNode bipush = (IntInsnNode) insnNode;

            if (bipush.operand == 9) {
              AbstractInsnNode findNode = null;

              // Hide combat menu patch
              findNode = insnNode;
              while (findNode.getOpcode() != Opcodes.ALOAD) findNode = findNode.getNext();
              LabelNode label = new LabelNode();
              LabelNode skipLabel = new LabelNode();
              methodNode.instructions.insertBefore(
                  findNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Client/Settings",
                      "updateInjectedVariables",
                      "()V",
                      false));
              methodNode.instructions.insertBefore(
                  findNode,
                  new FieldInsnNode(
                      Opcodes.GETSTATIC, "Client/Settings", "COMBAT_MENU_HIDDEN_BOOL", "Z"));
              methodNode.instructions.insertBefore(findNode, new JumpInsnNode(Opcodes.IFGT, label));
              methodNode.instructions.insertBefore(findNode, skipLabel);
              methodNode.instructions.insertBefore(findNode, new InsnNode(Opcodes.ICONST_1));
              methodNode.instructions.insertBefore(
                  findNode,
                  new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Renderer", "combat_menu_shown", "Z"));
              methodNode.instructions.insert(findNode.getNext().getNext(), label);

              // Show combat menu patch
              JumpInsnNode jumpNode = (JumpInsnNode) insnNode.getNext();
              LabelNode exitLabel = jumpNode.label;
              LabelNode runLabel = (LabelNode) jumpNode.getNext();
              label = new LabelNode();
              jumpNode.label = label;
              methodNode.instructions.insert(jumpNode, new JumpInsnNode(Opcodes.GOTO, exitLabel));
              methodNode.instructions.insert(jumpNode, new JumpInsnNode(Opcodes.IFGT, skipLabel));
              methodNode.instructions.insert(
                  jumpNode,
                  new FieldInsnNode(
                      Opcodes.GETSTATIC, "Client/Settings", "COMBAT_MENU_SHOWN_BOOL", "Z"));
              methodNode.instructions.insert(
                  jumpNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Client/Settings",
                      "updateInjectedVariables",
                      "()V",
                      false));
              methodNode.instructions.insert(jumpNode, label);
              methodNode.instructions.insert(jumpNode, new JumpInsnNode(Opcodes.GOTO, runLabel));

              findNode = insnNode.getPrevious();
              while (findNode.getOpcode() != Opcodes.BIPUSH) findNode = findNode.getPrevious();
              methodNode.instructions.insertBefore(findNode, new InsnNode(Opcodes.ICONST_0));
              methodNode.instructions.insertBefore(
                  findNode,
                  new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Renderer", "combat_menu_shown", "Z"));

              break;
            }
          }
        }
      }
      if (methodNode.name.equals("a")
          && methodNode.desc.equals("(ILjava/lang/String;Ljava/lang/String;Z)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        int xteaIndex = 0;
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Dump login xtea keys
          if (xteaIndex < 4 && insnNode.getOpcode() == Opcodes.IASTORE) {
            methodNode.instructions.insertBefore(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Game/Replay", "hookXTEAKey", "(I)I", false));
            xteaIndex++;
          }
        }

        insnNodeList = methodNode.instructions.iterator();

        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();
          AbstractInsnNode twoNextNodes = nextNode.getNext();

          if (nextNode == null || twoNextNodes == null) break;

          if (insnNode.getOpcode() == Opcodes.GETSTATIC
              && ((FieldInsnNode) insnNode).name.equals("il")
              && nextNode.getOpcode() == Opcodes.SIPUSH
              && ((IntInsnNode) nextNode).operand == 439
              && twoNextNodes.getOpcode() == Opcodes.AALOAD) {
            // just do the logic in client
            MethodInsnNode call = (MethodInsnNode) (twoNextNodes.getNext());
            // loginresponse
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 11));
            // reconnecting
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 4));
            // xtea keys
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ALOAD, 8));
            methodNode.instructions.insertBefore(
                call,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Game/Client", "login_attempt_hook", "(IZ[I)V", false));
            break;
          }
        }
      }
      if (methodNode.name.equals("u") && methodNode.desc.equals("(I)V")) {
        // Replay pause hook
        // TODO: Not sure but it seems like it gets broken upon starting another replay sometimes?
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        AbstractInsnNode findNode = insnNodeList.next();

        LabelNode label = new LabelNode();
        methodNode.instructions.insertBefore(findNode, new InsnNode(Opcodes.ICONST_0));
        methodNode.instructions.insertBefore(
            findNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Replay", "paused", "Z"));
        methodNode.instructions.insertBefore(findNode, new JumpInsnNode(Opcodes.IFEQ, label));
        methodNode.instructions.insertBefore(findNode, new InsnNode(Opcodes.RETURN));
        methodNode.instructions.insertBefore(findNode, label);
        methodNode.instructions.insertBefore(
            findNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC, "Game/Replay", "disconnect_hook", "()V", false));
      }
      if (methodNode.name.equals("J") && methodNode.desc.equals("(I)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Chat command patch
          if (insnNode.getOpcode() == Opcodes.SIPUSH) {
            IntInsnNode call = (IntInsnNode) insnNode;
            if (call.operand == 627) {
              AbstractInsnNode jmpNode = insnNode;
              while (jmpNode.getOpcode() != Opcodes.IFEQ) jmpNode = jmpNode.getNext();

              AbstractInsnNode insertNode = insnNode;
              while (insertNode.getOpcode() != Opcodes.INVOKEVIRTUAL)
                insertNode = insertNode.getPrevious();

              JumpInsnNode jmp = (JumpInsnNode) jmpNode;
              methodNode.instructions.insert(insertNode, new VarInsnNode(Opcodes.ASTORE, 2));
              methodNode.instructions.insert(
                  insertNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Game/Client",
                      "processChatCommand",
                      "(Ljava/lang/String;)Ljava/lang/String;"));
              methodNode.instructions.insert(insertNode, new VarInsnNode(Opcodes.ALOAD, 2));
            }
          }
        }

        insnNodeList = methodNode.instructions.iterator();

        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();
          AbstractInsnNode twoNextNode = nextNode.getNext();

          if (nextNode == null || twoNextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.IMUL
              && nextNode.getOpcode() == Opcodes.IADD
              && twoNextNode.getOpcode() == Opcodes.PUTFIELD
              && ((FieldInsnNode) twoNextNode).name.equals("ug")) {
            // entry point when its true to close it
            // intercept first time camera rotation
            // TODO: the setting constant at first may give bug in camera so im leaving it out for
            // the
            // moment
            // AbstractInsnNode call = twoNextNode.getNext();
            // methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ALOAD, 0));
            // this constant is from the one in Camera
            // methodNode.instructions.insertBefore(call, new IntInsnNode(Opcodes.BIPUSH, 126));
            // methodNode.instructions.insertBefore(call, new FieldInsnNode(Opcodes.PUTFIELD,
            // "client",
            // "ug", "I"));
            break;
          }
        }
      }
      if (methodNode.name.equals("h") && methodNode.desc.equals("(B)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Private chat command patch
          if (insnNode.getOpcode() == Opcodes.GETFIELD) {
            FieldInsnNode field = (FieldInsnNode) insnNode;
            if (field.owner.equals("client")
                && field.name.equals("Ob")
                && insnNode.getPrevious().getPrevious().getOpcode() != Opcodes.INVOKEVIRTUAL) {
              insnNode = insnNode.getPrevious().getPrevious();
              methodNode.instructions.insert(
                  insnNode,
                  new FieldInsnNode(Opcodes.PUTFIELD, "client", "Ob", "Ljava/lang/String;"));
              methodNode.instructions.insert(
                  insnNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Game/Client",
                      "processPrivateCommand",
                      "(Ljava/lang/String;)Ljava/lang/String;"));
              methodNode.instructions.insert(
                  insnNode,
                  new FieldInsnNode(Opcodes.GETFIELD, "client", "Ob", "Ljava/lang/String;"));
              methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
              methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
              break;
            }
          }
        }
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(IIIIIIII)V")) {
        // Draw NPC hook
        AbstractInsnNode insnNode = methodNode.instructions.getLast();
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 8));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 1));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 7));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 4));

        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "e", "Mb", "[Ljava/lang/String;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Tb", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 6));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "t", "I"));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        // extended npc hook to include current hits and max hits
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Tb", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 6));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "B", "I"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Tb", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 6));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "G", "I"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Tb", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 6));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "t", "I"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Tb", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 6));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "b", "I"));
        methodNode.instructions.insertBefore(
            insnNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC, "Game/Client", "drawNPC", "(IIIILjava/lang/String;IIII)V"));
      }
      if (methodNode.name.equals("b") && methodNode.desc.equals("(IIIIIIII)V")) {
        // Draw Player hook
        AbstractInsnNode insnNode = methodNode.instructions.getLast();
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 5));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 6));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 2));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 7));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "rg", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 8));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "C", "Ljava/lang/String;"));
        // extended player hook to include current hits and max hits
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "rg", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 8));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "B", "I"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "rg", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 8));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "G", "I"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "rg", "[Lta;"));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 8));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.AALOAD));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETFIELD, "ta", "b", "I"));
        methodNode.instructions.insertBefore(
            insnNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC, "Game/Client", "drawPlayer", "(IIIILjava/lang/String;III)V"));
      }
      if (methodNode.name.equals("b") && methodNode.desc.equals("(IIIIIII)V")) {
        // Draw Item hook
        // ILOAD 4 is item id
        AbstractInsnNode insnNode = methodNode.instructions.getLast();
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 3));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 7));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 5));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 1));
        methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 4));
        methodNode.instructions.insertBefore(
            insnNode,
            new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Client", "drawItem", "(IIIII)V"));
      }
      if (methodNode.name.equals("L") && methodNode.desc.equals("(I)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Right click bounds fix
          if (insnNode.getOpcode() == Opcodes.SIPUSH) {
            IntInsnNode call = (IntInsnNode) insnNode;
            AbstractInsnNode nextNode = insnNode.getNext();

            if (call.operand == 510) {
              call.operand = 512 - call.operand;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
            } else if (call.operand == 315) {
              call.operand = 334 - call.operand;
              methodNode.instructions.insertBefore(
                  insnNode,
                  new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "height_client", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
            } else if (call.operand == -316) {
              call.operand = 334 - (call.operand * -1);
              methodNode.instructions.insertBefore(
                  insnNode,
                  new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "height_client", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.INEG));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
            }
          }
        }

        // Hook that gives out the message on X action such as npcs, items and prints them top left
        // corner
        // TODO: use the hook
        insnNodeList = methodNode.instructions.iterator();

        LabelNode lblNode = null;
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          // this part checks to see if there's a string to render for the two cases below
          if (insnNode.getOpcode() == Opcodes.ALOAD
              && ((VarInsnNode) insnNode).var == 3
              && nextNode.getOpcode() == Opcodes.IFNONNULL) {
            lblNode = ((JumpInsnNode) nextNode.getNext()).label;
          }

          // is_hover = 1, also getfield client.li:ba
          if (insnNode.getOpcode() == Opcodes.GETFIELD) {
            FieldInsnNode fieldNode = ((FieldInsnNode) insnNode);
            if (fieldNode.owner.equals("client")
                && fieldNode.name.equals("li")
                && nextNode.getOpcode() == Opcodes.ALOAD
                && ((VarInsnNode) nextNode).var == 5) {
              methodNode.instructions.insert(fieldNode, new VarInsnNode(Opcodes.ASTORE, 5));
              methodNode.instructions.insert(
                  fieldNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Game/Client",
                      "mouse_action_hook",
                      "(Ljava/lang/String;)Ljava/lang/String;",
                      false));
              methodNode.instructions.insert(fieldNode, new VarInsnNode(Opcodes.ALOAD, 5));
              methodNode.instructions.insert(
                  fieldNode, new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Client", "is_hover", "Z"));
              methodNode.instructions.insert(fieldNode, new InsnNode(Opcodes.ICONST_1));
              continue;
            }
          }

          // is_hover = 0
          if (insnNode instanceof LabelNode
              && lblNode != null
              && ((LabelNode) insnNode).equals(lblNode)) {
            methodNode.instructions.insert(
                insnNode, new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Client", "is_hover", "Z"));
            methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ICONST_0));
          }
        }
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(ZZ)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Friends chat mouse fix
          if (insnNode.getOpcode() == Opcodes.SIPUSH) {
            IntInsnNode call = (IntInsnNode) insnNode;
            if (call.operand == 489 || call.operand == 429) {
              call.operand = 512 - call.operand;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
            }
            if (call.operand == -430) {
              call.operand = 512 - (call.operand * -1);
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.INEG));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
            }
          }
        }
      }
      if (methodNode.name.equals("i") && methodNode.desc.equals("(I)V")) {
        AbstractInsnNode lastNode = methodNode.instructions.getLast().getPrevious();

        // Send combat style option
        LabelNode label = new LabelNode();

        methodNode.instructions.insert(lastNode, label);

        // Format
        methodNode.instructions.insert(
            lastNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "da", "b", "(I)V", false));
        methodNode.instructions.insert(lastNode, new IntInsnNode(Opcodes.SIPUSH, 21294));
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Jh", "Lda;"));
        methodNode.instructions.insert(lastNode, new VarInsnNode(Opcodes.ALOAD, 0));

        // Write byte
        methodNode.instructions.insert(
            lastNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "ja", "c", "(II)V", false));
        methodNode.instructions.insert(lastNode, new IntInsnNode(Opcodes.BIPUSH, -80));
        methodNode.instructions.insert(
            lastNode,
            new FieldInsnNode(Opcodes.GETSTATIC, "Client/Settings", "COMBAT_STYLE_INT", "I"));
        methodNode.instructions.insert(
            lastNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC,
                "Client/Settings",
                "updateInjectedVariables",
                "()V",
                false)); // TODO Remove this line when COMBAT_STYLE_INT is eliminated
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "da", "f", "Lja;"));
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Jh", "Lda;"));
        methodNode.instructions.insert(lastNode, new VarInsnNode(Opcodes.ALOAD, 0));

        // Create Packet
        methodNode.instructions.insert(
            lastNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "da", "b", "(II)V", false));
        methodNode.instructions.insert(lastNode, new InsnNode(Opcodes.ICONST_0));
        methodNode.instructions.insert(lastNode, new IntInsnNode(Opcodes.BIPUSH, 29));
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Jh", "Lda;"));
        methodNode.instructions.insert(lastNode, new VarInsnNode(Opcodes.ALOAD, 0));

        // Skip combat packet if style is already controlled
        methodNode.instructions.insert(lastNode, new JumpInsnNode(Opcodes.IF_ICMPLE, label));
        methodNode.instructions.insert(lastNode, new InsnNode(Opcodes.ICONST_0));
        methodNode.instructions.insert(
            lastNode,
            new FieldInsnNode(Opcodes.GETSTATIC, "Client/Settings", "COMBAT_STYLE_INT", "I"));
        methodNode.instructions.insert(
            lastNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC,
                "Client/Settings",
                "updateInjectedVariables",
                "()V",
                false)); // TODO Remove this line when COMBAT_STYLE_INT is eliminated

        // Client init_game
        methodNode.instructions.insert(
            lastNode,
            new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Client", "init_game", "()V", false));
      }
      if (methodNode.name.equals("o") && methodNode.desc.equals("(I)V")) {
        // Client.init_login patch
        AbstractInsnNode findNode = methodNode.instructions.getLast();
        methodNode.instructions.insertBefore(
            findNode,
            new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Client", "init_login", "()V", false));
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(B)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Camera view distance crash fix
          if (insnNode.getOpcode() == Opcodes.SIPUSH) {
            IntInsnNode call = (IntInsnNode) insnNode;
            if (call.operand == 15000) {
              call.operand = 32767;
            }
          }
        }

        // Client.init patch
        AbstractInsnNode findNode = methodNode.instructions.getFirst();
        methodNode.instructions.insertBefore(findNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insertBefore(
            findNode,
            new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Client", "instance", "Ljava/lang/Object;"));
        methodNode.instructions.insertBefore(
            findNode,
            new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Client", "init", "()V", false));
      }
      if (methodNode.name.equals("G") && methodNode.desc.equals("(I)V")) {
        // TODO: This can be shortened, I'll fix it another time

        // NPC Dialogue keyboard
        AbstractInsnNode lastNode = methodNode.instructions.getLast().getPrevious();

        LabelNode label = new LabelNode();

        methodNode.instructions.insert(lastNode, label);

        // Hide dialogue
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.PUTFIELD, "client", "Ph", "Z"));
        methodNode.instructions.insert(lastNode, new InsnNode(Opcodes.ICONST_0));
        methodNode.instructions.insert(lastNode, new VarInsnNode(Opcodes.ALOAD, 0));

        // Format
        methodNode.instructions.insert(
            lastNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "da", "b", "(I)V", false));
        methodNode.instructions.insert(lastNode, new IntInsnNode(Opcodes.SIPUSH, 21294));
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Jh", "Lda;"));
        methodNode.instructions.insert(lastNode, new VarInsnNode(Opcodes.ALOAD, 0));

        // Write byte
        methodNode.instructions.insert(
            lastNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "ja", "c", "(II)V", false));
        methodNode.instructions.insert(lastNode, new IntInsnNode(Opcodes.BIPUSH, 115));
        methodNode.instructions.insert(
            lastNode,
            new FieldInsnNode(Opcodes.GETSTATIC, "Game/KeyboardHandler", "dialogue_option", "I"));
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "da", "f", "Lja;"));
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Jh", "Lda;"));
        methodNode.instructions.insert(lastNode, new VarInsnNode(Opcodes.ALOAD, 0));

        // Create Packet
        methodNode.instructions.insert(
            lastNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "da", "b", "(II)V", false));
        methodNode.instructions.insert(lastNode, new InsnNode(Opcodes.ICONST_0));
        methodNode.instructions.insert(lastNode, new IntInsnNode(Opcodes.BIPUSH, 116));
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Jh", "Lda;"));
        methodNode.instructions.insert(lastNode, new VarInsnNode(Opcodes.ALOAD, 0));

        // Check if dialogue option is pressed
        methodNode.instructions.insert(lastNode, new JumpInsnNode(Opcodes.IF_ICMPGE, label));
        // Menu option count
        methodNode.instructions.insert(
            lastNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Id", "I"));
        methodNode.instructions.insert(lastNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insert(
            lastNode,
            new FieldInsnNode(Opcodes.GETSTATIC, "Game/KeyboardHandler", "dialogue_option", "I"));
        methodNode.instructions.insert(lastNode, new JumpInsnNode(Opcodes.IFLT, label));
        methodNode.instructions.insert(
            lastNode,
            new FieldInsnNode(Opcodes.GETSTATIC, "Game/KeyboardHandler", "dialogue_option", "I"));
      }
      if (methodNode.name.equals("f") && methodNode.desc.equals("(I)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        boolean roofHidePatched = false;
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Hide Roof option
          if (insnNode.getOpcode() == Opcodes.BIPUSH) {
            IntInsnNode field = (IntInsnNode) insnNode;

            if (!roofHidePatched && field.operand == 118) {
              JumpInsnNode end =
                  (JumpInsnNode)
                      insnNode
                          .getNext()
                          .getNext()
                          .getNext()
                          .getNext()
                          .getNext()
                          .getNext()
                          .getNext();
              AbstractInsnNode ifStart =
                  insnNode
                      .getPrevious()
                      .getPrevious()
                      .getPrevious()
                      .getPrevious()
                      .getPrevious()
                      .getPrevious()
                      .getPrevious()
                      .getPrevious()
                      .getPrevious()
                      .getPrevious();
              methodNode.instructions.insertBefore(
                  ifStart,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Client/Settings",
                      "updateInjectedVariables",
                      "()V",
                      false));
              methodNode.instructions.insertBefore(
                  ifStart,
                  new FieldInsnNode(Opcodes.GETSTATIC, "Client/Settings", "HIDE_ROOFS_BOOL", "Z"));
              methodNode.instructions.insertBefore(
                  ifStart, new JumpInsnNode(Opcodes.IFGT, end.label));
              roofHidePatched = true;
            }
          }

          // Move wilderness skull
          if (insnNode.getOpcode() == Opcodes.SIPUSH) {
            IntInsnNode call = (IntInsnNode) insnNode;
            if (call.operand == 465 || call.operand == 453) {
              call.operand = 512 - call.operand;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(
                  insnNode, new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Client", "is_in_wild", "Z"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ICONST_1));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
            }
          }

          // unset is_in_wild flag if not in wild
          AbstractInsnNode nextNode = insnNode.getNext();
          if (insnNode.getOpcode() == Opcodes.ICONST_M1
              && nextNode.getOpcode() == Opcodes.ILOAD
              && ((VarInsnNode) nextNode).var == 2) {
            methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.ICONST_0));
            methodNode.instructions.insertBefore(
                insnNode, new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Client", "is_in_wild", "Z"));
          }
        }

        // Retro FPS overlay
        insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          if (insnNode.getOpcode() == Opcodes.INVOKESPECIAL
              && ((MethodInsnNode) insnNode).name.equals("l")
              && ((MethodInsnNode) insnNode).desc.equals("(I)V")) {
            InsnNode call = (InsnNode) insnNode.getNext();
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ALOAD, 0));
            methodNode.instructions.insertBefore(
                call, new FieldInsnNode(Opcodes.GETFIELD, "client", "li", "Lba;"));
            methodNode.instructions.insertBefore(
                call,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Game/Client",
                    "retroFPSHook",
                    "(Ljava/lang/Object;)V",
                    false));
            break;
          }
        }
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(IIZ)Z")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Move the load screen text dialogue
          if (insnNode.getOpcode() == Opcodes.SIPUSH) {
            IntInsnNode call = (IntInsnNode) insnNode;
            if (call.operand == 256) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 192) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "height", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IADD));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 19));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            }
          }
        }
      }
      if (methodNode.name.equals("d") && methodNode.desc.equals("(B)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Center logout dialogue
          if (insnNode.getOpcode() == Opcodes.SIPUSH || insnNode.getOpcode() == Opcodes.BIPUSH) {
            IntInsnNode call = (IntInsnNode) insnNode;
            if (call.operand == 256) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 173) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "height", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 126) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 130));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 137) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "height", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 36));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            }
          }
        }
      }
      if (methodNode.name.equals("j") && methodNode.desc.equals("(I)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Center welcome box
          if (insnNode.getOpcode() == Opcodes.SIPUSH || insnNode.getOpcode() == Opcodes.BIPUSH) {
            IntInsnNode call = (IntInsnNode) insnNode;
            if (call.operand == 256) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 167) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "height", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 6));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 56) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 200));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == -87) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.INEG));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 169));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 426) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IADD));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 170));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 106) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ISUB));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 150));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            } else if (call.operand == 406) {
              call.operand = 2;
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Renderer", "width", "I"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IADD));
              methodNode.instructions.insert(insnNode, new IntInsnNode(Opcodes.SIPUSH, 150));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.IDIV));
            }
          }
        }
      }
      if (methodNode.name.equals("k") && methodNode.desc.equals("(B)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          // Save settings from combat menu
          if (insnNode.getOpcode() == Opcodes.PUTFIELD) {
            FieldInsnNode field = (FieldInsnNode) insnNode;

            if (field.owner.equals("client") && field.name.equals("Fg")) {
              methodNode.instructions.insert(
                  insnNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC, "Client/Settings", "save", "()V", false));
              methodNode.instructions.insert(
                  insnNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Client/Settings",
                      "outputInjectedVariables",
                      "()V",
                      false));
              methodNode.instructions.insert(
                  insnNode,
                  new FieldInsnNode(Opcodes.PUTSTATIC, "Client/Settings", "COMBAT_STYLE_INT", "I"));
              methodNode.instructions.insert(
                  insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Fg", "I"));
              methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
            }
          }
        }
      }
      if (methodNode.name.equals("a")
          && methodNode.desc.equals(
              "(ZLjava/lang/String;ILjava/lang/String;IILjava/lang/String;Ljava/lang/String;)V")) {
        AbstractInsnNode first = methodNode.instructions.getFirst();

        methodNode.instructions.insertBefore(first, new VarInsnNode(Opcodes.ALOAD, 7));
        methodNode.instructions.insertBefore(first, new VarInsnNode(Opcodes.ALOAD, 4));
        methodNode.instructions.insertBefore(first, new VarInsnNode(Opcodes.ILOAD, 5));
        methodNode.instructions.insertBefore(first, new VarInsnNode(Opcodes.ALOAD, 8));
        methodNode.instructions.insertBefore(
            first,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC,
                "Game/Client",
                "messageHook",
                "(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)V"));

        // Replay seeking don't show messages hook
        // LabelNode label = new LabelNode();
        // methodNode.instructions.insertBefore(first, new InsnNode(Opcodes.ICONST_0));
        // methodNode.instructions.insertBefore(first, new FieldInsnNode(Opcodes.GETSTATIC,
        // "Game/Replay", "isSeeking", "Z"));
        // methodNode.instructions.insertBefore(first, new JumpInsnNode(Opcodes.IFEQ, label));
        // methodNode.instructions.insertBefore(first, new InsnNode(Opcodes.RETURN));
        // methodNode.instructions.insertBefore(first, label);
      }
      if (methodNode.name.equals("b") && methodNode.desc.equals("(ZI)V")) {
        // Fix on swap between command and use, if 635 is received make it 650 by hook
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          if (nextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.ISTORE && ((VarInsnNode) insnNode).var == 3) {
            VarInsnNode call = (VarInsnNode) nextNode;
            methodNode.instructions.insertBefore(nextNode, new VarInsnNode(Opcodes.ILOAD, 3));
            methodNode.instructions.insertBefore(
                nextNode,
                new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Client", "swapUseMenuHook", "(I)I"));
            methodNode.instructions.insertBefore(nextNode, new VarInsnNode(Opcodes.ISTORE, 3));
            break;
          }
        }

        // Throwable crash patch
        insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          if (nextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.INVOKESTATIC
              && nextNode.getOpcode() == Opcodes.ATHROW) {
            MethodInsnNode call = (MethodInsnNode) insnNode;
            methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.RETURN));
          }
        }

        // Fix on sleep, so packets are not managed directly
        insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode prevNode = insnNode.getPrevious();

          if (prevNode == null) continue;

          // patch before the sequence of command checks
          if (insnNode.getOpcode() == Opcodes.ASTORE
              && ((VarInsnNode) insnNode).var == 9
              && prevNode.getOpcode() == Opcodes.INVOKEVIRTUAL) {
            VarInsnNode call = (VarInsnNode) insnNode;
            LabelNode label = ((LabelNode) insnNode.getNext());

            methodNode.instructions.insert(call, new VarInsnNode(Opcodes.ISTORE, 4));
            methodNode.instructions.insert(
                call, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Client", "sleepBagIdx", "I"));
            methodNode.instructions.insert(call, new VarInsnNode(Opcodes.ISTORE, 3));
            methodNode.instructions.insert(call, new IntInsnNode(Opcodes.SIPUSH, (short) 640));
            methodNode.instructions.insert(call, new JumpInsnNode(Opcodes.IF_ICMPNE, label));
            methodNode.instructions.insert(call, new InsnNode(Opcodes.ICONST_1));
            methodNode.instructions.insert(
                call, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Client", "sleepCmdSent", "Z"));
          }
        }

        // Turn off sleep cmd flag
        insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();
          AbstractInsnNode twoNextNodes = nextNode.getNext();

          if (nextNode == null || twoNextNodes == null) break;

          if (insnNode.getOpcode() == Opcodes.ALOAD
              && nextNode.getOpcode() == Opcodes.GETFIELD
              && twoNextNodes.getOpcode() == Opcodes.BIPUSH
              && ((IntInsnNode) twoNextNodes).operand == 90) {
            VarInsnNode call = (VarInsnNode) insnNode;
            methodNode.instructions.insert(
                call, new FieldInsnNode(Opcodes.PUTSTATIC, "Game/Client", "sleepCmdSent", "Z"));
            methodNode.instructions.insert(call, new InsnNode(Opcodes.ICONST_0));
          }
        }
      }
      if (methodNode.name.equals("C") && methodNode.desc.equals("(I)V")) {
        // Hook updateBankItems
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode prevNode = insnNode.getPrevious();

          if (prevNode == null) continue;

          if (insnNode.getOpcode() == Opcodes.ISTORE && prevNode.getOpcode() == Opcodes.GETSTATIC) {
            VarInsnNode call = (VarInsnNode) insnNode;
            methodNode.instructions.insert(
                call,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Game/Bank", "updateBankItemsHook", "()V"));
            break;
          }
        }
        // plus hook final bank items
        insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode prevNode = insnNode.getPrevious();

          if (prevNode == null) continue;

          if (insnNode.getOpcode() == Opcodes.ISTORE && prevNode.getOpcode() == Opcodes.IDIV) {
            VarInsnNode call = (VarInsnNode) insnNode;
            methodNode.instructions.insert(
                call,
                new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Bank", "finalBankItemsHook", "()V"));
            break;
          }
        }
      }
      if (methodNode.name.equals("b") && methodNode.desc.equals("(IBI)V")) {
        // hook first time opened bank interface
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          if (nextNode == null) continue;
          if (insnNode.getOpcode() == Opcodes.ICONST_1
              && nextNode.getOpcode() == Opcodes.PUTFIELD
              && ((FieldInsnNode) nextNode).name.equals("Fe")) {
            InsnNode call = (InsnNode) insnNode;
            methodNode.instructions.insertBefore(
                call,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Game/Bank", "openedBankInterfaceHook", "()V"));
            break;
          }
        }

        // hook onto npc attack info
        insnNodeList = methodNode.instructions.iterator();
        // two times it gets found, first is one for player second for npc
        int pos = -1;
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();
          AbstractInsnNode twoNextNode = nextNode.getNext();

          if (nextNode == null || twoNextNode == null) break;
          if (insnNode.getOpcode() == Opcodes.SIPUSH && ((IntInsnNode) insnNode).operand == -235) {
            pos = 0; // player combat hook
            continue;
          }
          if (insnNode.getOpcode() == Opcodes.BIPUSH
              && ((IntInsnNode) insnNode).operand == 104
              && nextNode.getOpcode() == Opcodes.ILOAD) {
            pos = 1; // npc combat hook
            continue;
          }
          if (insnNode.getOpcode() == Opcodes.ALOAD
              && ((VarInsnNode) insnNode).var == 7
              && nextNode.getOpcode() == Opcodes.ILOAD
              && ((VarInsnNode) nextNode).var == 9
              && twoNextNode.getOpcode() == Opcodes.PUTFIELD
              && ((FieldInsnNode) twoNextNode).owner.equals("ta")
              && ((FieldInsnNode) twoNextNode).name.equals("u")) {
            methodNode.instructions.insert(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Game/Client",
                    "inCombatHook",
                    "(IIIIILjava/lang/Object;)V"));
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ALOAD, 7));
            // indicate packet was from player cmd
            if (pos == 0) {
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ICONST_0));
            }
            // indicate packet was from npc cmd
            else if (pos == 1) {
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.ICONST_1));
            }
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ILOAD, 6));
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ILOAD, 11));
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ILOAD, 10));
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ILOAD, 9));
            continue;
          }
        }

        // setLoadingArea
        insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();
          AbstractInsnNode twoNextNode = nextNode.getNext();

          if (nextNode == null || twoNextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.ILOAD
              && ((VarInsnNode) insnNode).var == 5
              && nextNode.getOpcode() == Opcodes.IFNE
              && twoNextNode.getOpcode() == Opcodes.GOTO) {
            methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 5));
            methodNode.instructions.insertBefore(
                insnNode,
                new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Client", "isLoadingHook", "(Z)V"));
          }
        }

        // hook onto received menu options
        insnNodeList = methodNode.instructions.iterator();
        LabelNode lblNode = null;
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();
          AbstractInsnNode twoNextNode = nextNode.getNext();

          if (nextNode == null || twoNextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.ALOAD
              && ((VarInsnNode) insnNode).var == 0
              && nextNode.getOpcode() == Opcodes.GETFIELD
              && ((FieldInsnNode) nextNode).owner.equals("client")
              && ((FieldInsnNode) nextNode).name.equals("ah")
              && twoNextNode.getOpcode() == Opcodes.ILOAD
              && ((VarInsnNode) twoNextNode).var == 5
              && insnNode.getPrevious().getOpcode() == Opcodes.IF_ICMPLE) {
            // in here is part where menu options are received, is a loop
            lblNode = ((JumpInsnNode) insnNode.getPrevious()).label;
            continue;
          }

          if (lblNode != null
              && insnNode instanceof LabelNode
              && ((LabelNode) insnNode).equals(lblNode)
              && twoNextNode.getOpcode() == Opcodes.RETURN) {
            InsnNode call = (InsnNode) twoNextNode;
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ALOAD, 0));
            methodNode.instructions.insertBefore(
                call, new FieldInsnNode(Opcodes.GETFIELD, "client", "ah", "[Ljava/lang/String;"));
            // could also be client.Id but more lines would be needed
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 4));
            methodNode.instructions.insertBefore(
                call,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Game/Client",
                    "receivedOptionsHook",
                    "([Ljava/lang/String;I)V"));
          }
        }
      }
      // hook onto selected menu option
      if (methodNode.name.equals("G") && methodNode.desc.equals("(I)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();
          AbstractInsnNode twoNextNode = nextNode.getNext();

          if (nextNode == null || twoNextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.ALOAD
              && ((VarInsnNode) insnNode).var == 0
              && nextNode.getOpcode() == Opcodes.GETFIELD
              && ((FieldInsnNode) nextNode).owner.equals("client")
              && ((FieldInsnNode) nextNode).name.equals("Jh")
              && twoNextNode.getOpcode() == Opcodes.BIPUSH
              && ((IntInsnNode) twoNextNode).operand == 116) {
            VarInsnNode call = (VarInsnNode) insnNode;
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ALOAD, 0));
            methodNode.instructions.insertBefore(
                call, new FieldInsnNode(Opcodes.GETFIELD, "client", "ah", "[Ljava/lang/String;"));
            methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 2));
            methodNode.instructions.insertBefore(
                call,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Game/Client",
                    "selectedOptionHook",
                    "([Ljava/lang/String;I)V"));
          }
        }
      }
      // hook menu item
      if (methodNode.name.equals("a") && methodNode.desc.equals("(IZ)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        LabelNode firstLabel = null;
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode prevNode = insnNode.getPrevious();
          AbstractInsnNode twoPrevNodes = null;
          if (prevNode != null) twoPrevNodes = prevNode.getPrevious();

          if (insnNode.getNext() == null) continue;

          if (prevNode == null || twoPrevNodes == null) continue;

          if (insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL
              && prevNode.getOpcode() == Opcodes.LDC
              && prevNode instanceof LdcInsnNode
              && ((LdcInsnNode) prevNode).cst instanceof String
              && ((String) ((LdcInsnNode) prevNode).cst).equals("")
              && twoPrevNodes.getOpcode() == Opcodes.AALOAD) {
            methodNode.instructions.insert(prevNode, new InsnNode(Opcodes.RETURN));
            methodNode.instructions.insert(
                prevNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Game/Client",
                    "redrawMenuHook",
                    "(Ljava/lang/Object;IILjava/lang/String;Ljava/lang/String;)V"));
            methodNode.instructions.insert(prevNode, new InsnNode(Opcodes.AALOAD));
            methodNode.instructions.insert(prevNode, new VarInsnNode(Opcodes.ILOAD, 6));
            methodNode.instructions.insert(
                prevNode, new FieldInsnNode(Opcodes.GETSTATIC, "ac", "x", "[Ljava/lang/String;"));
            methodNode.instructions.insert(prevNode, new InsnNode(Opcodes.AALOAD));
            methodNode.instructions.insert(prevNode, new VarInsnNode(Opcodes.ILOAD, 6));
            methodNode.instructions.insert(
                prevNode, new FieldInsnNode(Opcodes.GETSTATIC, "lb", "ac", "[Ljava/lang/String;"));
            methodNode.instructions.insert(prevNode, new VarInsnNode(Opcodes.ILOAD, 6));
            methodNode.instructions.insert(prevNode, new VarInsnNode(Opcodes.ILOAD, 5));
            methodNode.instructions.insert(
                prevNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "zh", "Lwb;"));
            methodNode.instructions.insert(prevNode, new VarInsnNode(Opcodes.ALOAD, 0));
            continue;
          }
        }
      }
      // hook onto (windowed) server message hook
      if (methodNode.name.equals("l") && methodNode.desc.equals("(B)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          if (insnNode.getOpcode() == Opcodes.PUTSTATIC) {
            methodNode.instructions.insert(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Game/Client",
                    "serverMessageHook",
                    "(Ljava/lang/String;)V"));
            methodNode.instructions.insert(
                insnNode,
                new FieldInsnNode(Opcodes.GETFIELD, "client", "Cj", "Ljava/lang/String;"));
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
            break;
          }
        }
      }
      if (methodNode.name.equals("x") && methodNode.desc.equals("(I)V")) {
        // Login button press hook
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();

        AbstractInsnNode findNode = null;
        int count = 0;
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          if (insnNode.getOpcode() == Opcodes.PUTFIELD) {
            FieldInsnNode field = (FieldInsnNode) insnNode;
            if (count != 1
                && field.owner.equals("client")
                && field.name.equals("wh")
                && field.desc.equals("Ljava/lang/String;")) {
              findNode = insnNode.getNext();
              count++;
            }
          }
        }

        methodNode.instructions.insertBefore(
            findNode,
            new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Client", "login_hook", "()V", false));
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(ZI)V")) {
        // Disconnect hook (::closecon)
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();

        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          if (nextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.SIPUSH
              && ((IntInsnNode) insnNode).operand == -6924
              && nextNode.getOpcode() == Opcodes.INVOKEVIRTUAL) {
            // entry point when its true to close it
            methodNode.instructions.insertBefore(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Game/Client", "disconnect_hook", "()V", false));
            break;
          }
        }
      }
      if (methodNode.name.equals("s") && methodNode.desc.equals("(I)V")) {
        // bypass npc attack on left option, regardless of level difference if user wants it that
        // way
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();

        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode prevNode = insnNode.getPrevious();

          if (prevNode == null) continue;

          if (insnNode.getOpcode() == Opcodes.ILOAD
              && ((VarInsnNode) insnNode).var == 12
              && prevNode.getOpcode() == Opcodes.GETFIELD
              && ((FieldInsnNode) prevNode).owner.equals("ta")
              && ((FieldInsnNode) prevNode).name.equals("b")) {
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ILOAD, 12));
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ISTORE, 12));
            methodNode.instructions.insert(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Game/Client", "attack_menu_hook", "(I)I", false));
            break;
          }
        }
      }
      // add on info for objects
      if (methodNode.name.equals("s") && methodNode.desc.equals("(I)V")) {
        boolean foundExaminePos = false;

        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        AbstractInsnNode insnNode = insnNodeList.next();

        while (!foundExaminePos && insnNodeList.hasNext()) {
          insnNode = insnNodeList.next();
          if (insnNode.getOpcode() == Opcodes.SIPUSH && ((IntInsnNode) insnNode).operand == 3400) {
            foundExaminePos = true;
          }
        }
        if (foundExaminePos) {
          while (insnNodeList.hasNext()) {
            insnNode = insnNodeList.next();

            if (insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL
                && ((MethodInsnNode) insnNode).name.equals("toString")) {
              // id
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 10));
              // direction
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "bg", "[I"));
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 9));
              methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.IALOAD));
              // x
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Se", "[I"));
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 9));
              methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.IALOAD));
              // y
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "ye", "[I"));
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 9));
              methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.IALOAD));
              methodNode.instructions.insertBefore(
                  insnNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Game/Client",
                      "appendDetailsHook",
                      "(IIII)Ljava/lang/String;",
                      false));
              methodNode.instructions.insertBefore(
                  insnNode,
                  new MethodInsnNode(
                      Opcodes.INVOKEVIRTUAL,
                      "java/lang/StringBuilder",
                      "append",
                      "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
                      false));
              break;
            }
          }
        }
      }
      // add on info for wall objects
      if (methodNode.name.equals("s") && methodNode.desc.equals("(I)V")) {
        boolean foundExaminePos = false;

        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        AbstractInsnNode insnNode = insnNodeList.next();

        while (!foundExaminePos && insnNodeList.hasNext()) {
          insnNode = insnNodeList.next();
          if (insnNode.getOpcode() == Opcodes.SIPUSH && ((IntInsnNode) insnNode).operand == 3300) {
            foundExaminePos = true;
          }
        }
        if (foundExaminePos) {
          while (insnNodeList.hasNext()) {
            insnNode = insnNodeList.next();

            if (insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL
                && ((MethodInsnNode) insnNode).name.equals("toString")) {
              // id
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 10));
              // direction
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Hj", "[I"));
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 9));
              methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.IALOAD));
              // x
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "Jd", "[I"));
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 9));
              methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.IALOAD));
              // y
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
              methodNode.instructions.insertBefore(
                  insnNode, new FieldInsnNode(Opcodes.GETFIELD, "client", "yk", "[I"));
              methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 9));
              methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.IALOAD));
              methodNode.instructions.insertBefore(
                  insnNode,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC,
                      "Game/Client",
                      "appendDetailsHook",
                      "(IIII)Ljava/lang/String;",
                      false));
              methodNode.instructions.insertBefore(
                  insnNode,
                  new MethodInsnNode(
                      Opcodes.INVOKEVIRTUAL,
                      "java/lang/StringBuilder",
                      "append",
                      "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
                      false));
              break;
            }
          }
        }
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(ILjava/lang/String;)V")) {
        // hook onto sound effect played
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();

        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();

          if (insnNode.getOpcode() == Opcodes.PUTSTATIC) {
            methodNode.instructions.insert(
                insnNode,
                new FieldInsnNode(
                    Opcodes.PUTSTATIC, "Game/Client", "lastSoundEffect", "Ljava/lang/String;"));
            methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ALOAD, 2));

            insnNode = insnNodeList.next();
            LabelNode label = new LabelNode();
            methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.ICONST_0));
            methodNode.instructions.insertBefore(
                insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Replay", "isSeeking", "Z"));
            methodNode.instructions.insertBefore(insnNode, new JumpInsnNode(Opcodes.IFEQ, label));
            methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.RETURN));
            methodNode.instructions.insertBefore(insnNode, label);
            break;
          }
        }
      }
      // drawGame
      if (methodNode.name.equals("f") && methodNode.desc.equals("(I)V")) {
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        AbstractInsnNode insnNode = insnNodeList.next();
        LabelNode label = new LabelNode();
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.ICONST_0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Replay", "isSeeking", "Z"));
        methodNode.instructions.insertBefore(insnNode, new JumpInsnNode(Opcodes.IFEQ, label));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.RETURN));
        methodNode.instructions.insertBefore(insnNode, label);
      }

      // drawTextBox
      if (methodNode.name.equals("a")
          && methodNode.desc.equals("(Ljava/lang/String;BLjava/lang/String;)V")) {
        // hook onto sound effect played
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();

        AbstractInsnNode insnNode = insnNodeList.next();
        LabelNode label = new LabelNode();
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.ICONST_0));
        methodNode.instructions.insertBefore(
            insnNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Replay", "isRestarting", "Z"));
        methodNode.instructions.insertBefore(insnNode, new JumpInsnNode(Opcodes.IFEQ, label));
        methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.RETURN));
        methodNode.instructions.insertBefore(insnNode, label);
      }

      if (methodNode.name.equals("e") && methodNode.desc.equals("(I)V")) {
        // handleGameInput
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();

        AbstractInsnNode insnNode = insnNodeList.next();
        methodNode.instructions.insertBefore(
            insnNode,
            new MethodInsnNode(Opcodes.INVOKESTATIC, "Game/Client", "update", "()V", false));
      }
    }
  }

  private void patchRenderer(ClassNode node) {
    Logger.Info("Patching renderer (" + node.name + ".class)");

    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      // Renderer present hook
      if (methodNode.desc.equals("(Ljava/awt/Graphics;III)V")) {
        AbstractInsnNode findNode = methodNode.instructions.getFirst();
        FieldInsnNode imageNode = null;

        LabelNode label = new LabelNode();
        methodNode.instructions.insertBefore(findNode, new InsnNode(Opcodes.ICONST_0));
        methodNode.instructions.insertBefore(
            findNode, new FieldInsnNode(Opcodes.GETSTATIC, "Game/Replay", "isSeeking", "Z"));
        methodNode.instructions.insertBefore(findNode, new JumpInsnNode(Opcodes.IFEQ, label));
        methodNode.instructions.insertBefore(findNode, new InsnNode(Opcodes.RETURN));
        methodNode.instructions.insertBefore(findNode, label);

        while (findNode.getOpcode() != Opcodes.POP) {
          findNode = findNode.getNext();
          if (findNode == null) {
            Logger.Error("Unable to find present hook");
            break;
          }
        }

        while (findNode.getOpcode() != Opcodes.INVOKESPECIAL) {
          if (findNode.getOpcode() == Opcodes.GETFIELD) imageNode = (FieldInsnNode) findNode;

          AbstractInsnNode prev = findNode.getPrevious();
          methodNode.instructions.remove(findNode);
          findNode = prev;
        }

        methodNode.instructions.insert(
            findNode,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC,
                "Game/Renderer",
                "present",
                "(Ljava/awt/Graphics;Ljava/awt/Image;)V",
                false));
        methodNode.instructions.insert(
            findNode,
            new FieldInsnNode(Opcodes.GETFIELD, node.name, imageNode.name, imageNode.desc));
        methodNode.instructions.insert(findNode, new VarInsnNode(Opcodes.ALOAD, 0));
        methodNode.instructions.insert(findNode, new VarInsnNode(Opcodes.ALOAD, 1));
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(IILjava/lang/String;IIBI)V")) {
        AbstractInsnNode start = methodNode.instructions.getFirst();
        while (start != null) {
          if (start.getOpcode() == Opcodes.ALOAD
              && start.getNext().getOpcode() == Opcodes.ILOAD
              && start.getNext().getNext().getOpcode() == Opcodes.INVOKEVIRTUAL
              && start.getNext().getNext().getNext().getOpcode() == Opcodes.ISTORE) {
            break;
          }

          start = start.getNext();
        }
        start = start.getPrevious();

        LabelNode finishLabel = ((JumpInsnNode) start.getPrevious().getPrevious()).label;
        LabelNode failLabel = new LabelNode();

        methodNode.instructions.insertBefore(start, new IntInsnNode(Opcodes.BIPUSH, 126));
        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ALOAD, 3));
        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ILOAD, 10));
        methodNode.instructions.insertBefore(
            start, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/String", "charAt", "(I)C"));
        methodNode.instructions.insertBefore(start, new JumpInsnNode(Opcodes.IF_ICMPNE, failLabel));

        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ILOAD, 10));
        methodNode.instructions.insertBefore(start, new InsnNode(Opcodes.ICONST_5));
        methodNode.instructions.insertBefore(start, new InsnNode(Opcodes.IADD));
        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ALOAD, 3));
        methodNode.instructions.insertBefore(
            start, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I"));
        methodNode.instructions.insertBefore(start, new JumpInsnNode(Opcodes.IF_ICMPGE, failLabel));

        methodNode.instructions.insertBefore(start, new IntInsnNode(Opcodes.BIPUSH, 126));
        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ALOAD, 3));
        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ILOAD, 10));
        methodNode.instructions.insertBefore(start, new InsnNode(Opcodes.ICONST_5));
        methodNode.instructions.insertBefore(start, new InsnNode(Opcodes.IADD));
        methodNode.instructions.insertBefore(
            start, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/String", "charAt", "(I)C"));
        methodNode.instructions.insertBefore(start, new JumpInsnNode(Opcodes.IF_ICMPNE, failLabel));

        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ALOAD, 3));
        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ILOAD, 10));
        methodNode.instructions.insertBefore(start, new InsnNode(Opcodes.ICONST_1));
        methodNode.instructions.insertBefore(start, new InsnNode(Opcodes.IADD));
        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ILOAD, 10));
        methodNode.instructions.insertBefore(start, new InsnNode(Opcodes.ICONST_5));
        methodNode.instructions.insertBefore(start, new InsnNode(Opcodes.IADD));
        methodNode.instructions.insertBefore(
            start,
            new MethodInsnNode(
                Opcodes.INVOKEVIRTUAL, "java/lang/String", "substring", "(II)Ljava/lang/String;"));
        methodNode.instructions.insertBefore(
            start,
            new MethodInsnNode(
                Opcodes.INVOKESTATIC, "java/lang/Integer", "parseInt", "(Ljava/lang/String;)I"));
        methodNode.instructions.insertBefore(start, new VarInsnNode(Opcodes.ISTORE, 4));
        methodNode.instructions.insertBefore(start, new IincInsnNode(10, 5));

        methodNode.instructions.insertBefore(start, new JumpInsnNode(Opcodes.GOTO, finishLabel));

        methodNode.instructions.insertBefore(start, failLabel);
      }
      if (methodNode.name.equals("a") && methodNode.desc.equals("(ILjava/lang/String;IIII)V")) {
        // method hook for drawstringCenter, reserved testing
      }
    }
  }

  private void patchRandom(ClassNode node) {
    Logger.Info("Patching random (" + node.name + ".class)");

    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      if (methodNode.name.equals("a")) {
        // System.out.println(methodNode.desc);
        if (methodNode.desc.equals("(ILtb;)V")) {
          Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
          while (insnNodeList.hasNext()) {
            AbstractInsnNode insnNode = insnNodeList.next();
            AbstractInsnNode nextNode = insnNode.getNext();

            if (nextNode == null) break;

            if (insnNode.getOpcode() == Opcodes.ALOAD && nextNode.getOpcode() == Opcodes.ICONST_0) {
              VarInsnNode call = (VarInsnNode) insnNode;
              Logger.Info("Patching validation...");

              methodNode.instructions.insert(
                  insnNode,
                  new MethodInsnNode(
                      Opcodes.INVOKEVIRTUAL, "java/util/Random", "nextBytes", "([B)V"));
              methodNode.instructions.insert(insnNode, new VarInsnNode(Opcodes.ALOAD, 2));
              methodNode.instructions.insert(
                  insnNode,
                  new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/util/Random", "<init>", "()V"));
              methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.DUP));
              methodNode.instructions.insert(
                  insnNode, new TypeInsnNode(Opcodes.NEW, "java/util/Random"));
            }
          }
        }
      }
    }
  }

  private void patchGameApplet(ClassNode node) {
    Logger.Info("Patching GameApplet (" + node.name + ".class)");

    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      if (methodNode.name.equals("a")) {
        if (methodNode.desc.equals("(Ljava/net/URL;ZZ)[B")) {
          Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
          AbstractInsnNode insnNode = insnNodeList.next();
          methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
          methodNode.instructions.insertBefore(
              insnNode,
              new MethodInsnNode(
                  Opcodes.INVOKESTATIC,
                  "Game/GameApplet",
                  "cacheURLHook",
                  "(Ljava/net/URL;)Ljava/net/URL;"));
          methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ASTORE, 0));
        }
        if (methodNode.desc.equals("(Z)V")) {
          // Disconnect hook (::lostcon)
          Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
          AbstractInsnNode insnNode = insnNodeList.next();
          methodNode.instructions.insertBefore(
              insnNode,
              new MethodInsnNode(
                  Opcodes.INVOKESTATIC, "Game/Client", "disconnect_hook", "()V", false));
        }
        if (methodNode.desc.equals("([BIII)V")) {
          // dump whole input stream!
          Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
          while (insnNodeList.hasNext()) {
            AbstractInsnNode insnNode = insnNodeList.next();
            AbstractInsnNode nextNode = insnNode.getNext();

            // entry point
            if (insnNode.getOpcode() == Opcodes.ILOAD
                && ((VarInsnNode) insnNode).var == 5
                && nextNode.getOpcode() == Opcodes.ILOAD
                && ((VarInsnNode) nextNode).var == 7) {
              VarInsnNode call = (VarInsnNode) insnNode;
              methodNode.instructions.insertBefore(
                  call, new VarInsnNode(Opcodes.ALOAD, 1)); // byte[]
              methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 2)); // n
              methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 3)); // n2
              // methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 4)); //
              // n3
              // methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 6)); //
              // n4
              methodNode.instructions.insertBefore(call, new VarInsnNode(Opcodes.ILOAD, 5)); // n5
              methodNode.instructions.insertBefore(
                  call, new VarInsnNode(Opcodes.ILOAD, 7)); // bytes read
              methodNode.instructions.insertBefore(
                  call,
                  new MethodInsnNode(
                      Opcodes.INVOKESTATIC, "Game/Replay", "dumpRawInputStream", "([BIIII)V"));
              break;
            }
          }
        }
      }
      if (methodNode.name.equals("run") && methodNode.desc.equals("()V")) {

        // dump whole output stream!
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          // entry point
          if (insnNode.getOpcode() == Opcodes.ALOAD
              && ((VarInsnNode) insnNode).var == 0
              && nextNode.getOpcode() == Opcodes.GETFIELD
              && ((FieldInsnNode) nextNode).name.equals("Q")
              && ((FieldInsnNode) nextNode).desc.equals("Ljava/io/OutputStream;")) {
            methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0));
            methodNode.instructions.insertBefore(
                insnNode, new FieldInsnNode(Opcodes.GETFIELD, "da", "Y", "[B"));
            methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 2));
            methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ILOAD, 1));
            methodNode.instructions.insertBefore(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC, "Game/Replay", "dumpRawOutputStream", "([BII)V"));
            break;
          }
        }
      }
    }
  }

  private void patchRendererHelper(ClassNode node) {
    Logger.Info("Patching renderer helper (" + node.name + ".class)");

    Iterator<MethodNode> methodNodeList = node.methods.iterator();
    while (methodNodeList.hasNext()) {
      MethodNode methodNode = methodNodeList.next();

      if (methodNode.name.equals("c") && methodNode.desc.equals("(I)V")) {
        // Throwable crash patch - a condition of indexoutbounds was reported on this method
        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          AbstractInsnNode nextNode = insnNode.getNext();

          if (nextNode == null) break;

          if (insnNode.getOpcode() == Opcodes.INVOKESTATIC
              && nextNode.getOpcode() == Opcodes.ATHROW) {
            MethodInsnNode call = (MethodInsnNode) insnNode;
            methodNode.instructions.insert(call, new InsnNode(Opcodes.RETURN));
          }
        }
      }
    }
  }

  /**
   * TODO: Complete JavaDoc
   *
   * @param methodNode
   * @param owner The class of the variable to be hooked
   * @param var The variable to be hooked
   * @param desc
   * @param newClass The class the hooked variable will be stored in
   * @param newVar The variable name the hooked variable will be stored in
   * @param newDesc
   * @param canRead Specifies if the hooked variable should be readable
   * @param canWrite Specifies if the hooked variable should be writable
   */
  private void hookClassVariable(
      MethodNode methodNode,
      String owner,
      String var,
      String desc,
      String newClass,
      String newVar,
      String newDesc,
      boolean canRead,
      boolean canWrite) {
    Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
    while (insnNodeList.hasNext()) {
      AbstractInsnNode insnNode = insnNodeList.next();

      int opcode = insnNode.getOpcode();
      if (opcode == Opcodes.GETFIELD || opcode == Opcodes.PUTFIELD) {
        FieldInsnNode field = (FieldInsnNode) insnNode;
        if (field.owner.equals(owner) && field.name.equals(var) && field.desc.equals(desc)) {
          if (opcode == Opcodes.GETFIELD && canWrite) {
            methodNode.instructions.insert(
                insnNode, new FieldInsnNode(Opcodes.GETSTATIC, newClass, newVar, newDesc));
            methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.POP));
          } else if (opcode == Opcodes.PUTFIELD && canRead) {
            methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.DUP_X1));
            methodNode.instructions.insert(
                insnNode, new FieldInsnNode(Opcodes.PUTSTATIC, newClass, newVar, newDesc));
          }
        }
      }
    }
  }

  private void hookConditionalClassVariable(
      MethodNode methodNode,
      String owner,
      String var,
      String desc,
      String newClass,
      String newVar,
      String newDesc,
      boolean canRead,
      boolean canWrite,
      String boolTrigger) {
    Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
    while (insnNodeList.hasNext()) {
      AbstractInsnNode insnNode = insnNodeList.next();

      int opcode = insnNode.getOpcode();
      if (opcode == Opcodes.GETFIELD || opcode == Opcodes.PUTFIELD) {
        FieldInsnNode field = (FieldInsnNode) insnNode;
        if (field.owner.equals(owner) && field.name.equals(var) && field.desc.equals(desc)) {
          if (opcode == Opcodes.GETFIELD && canWrite) {
            LabelNode label = new LabelNode();
            methodNode.instructions.insert(insnNode, label);
            methodNode.instructions.insert(
                insnNode, new FieldInsnNode(Opcodes.GETSTATIC, newClass, newVar, newDesc));
            methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.POP));
            methodNode.instructions.insert(insnNode, new JumpInsnNode(Opcodes.IFEQ, label));
            methodNode.instructions.insert(
                insnNode,
                new FieldInsnNode(Opcodes.GETSTATIC, "Client/Settings", boolTrigger, "Z"));
            methodNode.instructions.insert(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Client/Settings",
                    "updateInjectedVariables",
                    "()V",
                    false));
          } else if (opcode == Opcodes.PUTFIELD && canRead) {
            LabelNode label_end = new LabelNode();
            LabelNode label = new LabelNode();
            methodNode.instructions.insertBefore(insnNode, new InsnNode(Opcodes.DUP_X1));
            methodNode.instructions.insert(insnNode, label_end);
            methodNode.instructions.insert(insnNode, new InsnNode(Opcodes.POP));
            methodNode.instructions.insert(insnNode, label);
            methodNode.instructions.insert(insnNode, new JumpInsnNode(Opcodes.GOTO, label_end));
            methodNode.instructions.insert(
                insnNode, new FieldInsnNode(Opcodes.PUTSTATIC, newClass, newVar, newDesc));
            methodNode.instructions.insert(insnNode, new JumpInsnNode(Opcodes.IFEQ, label));
            methodNode.instructions.insert(
                insnNode,
                new FieldInsnNode(Opcodes.GETSTATIC, "Client/Settings", boolTrigger, "Z"));
            methodNode.instructions.insert(
                insnNode,
                new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    "Client/Settings",
                    "updateInjectedVariables",
                    "()V",
                    false));
          }
        }
      }
    }
  }

  /**
   * TODO: Complete JavaDoc
   *
   * @param methodNode
   * @param owner The class of the variable to be hooked
   * @param var The variable to be hooked
   * @param desc
   * @param newClass The class the hooked variable will be stored in
   * @param newVar The variable name the hooked variable will be stored in
   * @param newDesc
   */
  private void hookStaticVariable(
      MethodNode methodNode,
      String owner,
      String var,
      String desc,
      String newClass,
      String newVar,
      String newDesc) {
    Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
    while (insnNodeList.hasNext()) {
      AbstractInsnNode insnNode = insnNodeList.next();

      int opcode = insnNode.getOpcode();
      if (opcode == Opcodes.GETSTATIC || opcode == Opcodes.PUTSTATIC) {
        FieldInsnNode field = (FieldInsnNode) insnNode;
        if (field.owner.equals(owner) && field.name.equals(var) && field.desc.equals(desc)) {
          field.owner = newClass;
          field.name = newVar;
          field.desc = newDesc;
        }
      }
    }
  }

  private void dumpClass(ClassNode node) {
    BufferedWriter writer = null;

    try {
      File file = new File(Settings.Dir.DUMP + "/" + node.name + ".dump");
      writer = new BufferedWriter(new FileWriter(file));

      writer.write(decodeAccess(node.access) + node.name + " extends " + node.superName + ";\n");
      writer.write("\n");

      Iterator<FieldNode> fieldNodeList = node.fields.iterator();
      while (fieldNodeList.hasNext()) {
        FieldNode fieldNode = fieldNodeList.next();
        writer.write(
            decodeAccess(fieldNode.access) + fieldNode.desc + " " + fieldNode.name + ";\n");
      }

      writer.write("\n");

      Iterator<MethodNode> methodNodeList = node.methods.iterator();
      while (methodNodeList.hasNext()) {
        MethodNode methodNode = methodNodeList.next();
        writer.write(
            decodeAccess(methodNode.access) + methodNode.name + " " + methodNode.desc + ":\n");

        Iterator<AbstractInsnNode> insnNodeList = methodNode.instructions.iterator();
        while (insnNodeList.hasNext()) {
          AbstractInsnNode insnNode = insnNodeList.next();
          String instruction = decodeInstruction(insnNode);
          writer.write(instruction);
        }
        writer.write("\n");
      }

      writer.close();
    } catch (Exception e) {
      try {
        writer.close();
      } catch (Exception e2) {
      }
    }
  }

  private String decodeAccess(int access) {
    String res = "";

    if ((access & Opcodes.ACC_PUBLIC) == Opcodes.ACC_PUBLIC) res += "public ";
    if ((access & Opcodes.ACC_PRIVATE) == Opcodes.ACC_PRIVATE) res += "private ";
    if ((access & Opcodes.ACC_PROTECTED) == Opcodes.ACC_PROTECTED) res += "protected ";

    if ((access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC) res += "static ";
    if ((access & Opcodes.ACC_FINAL) == Opcodes.ACC_FINAL) res += "final ";
    if ((access & Opcodes.ACC_VOLATILE) == Opcodes.ACC_VOLATILE) res += "protected ";
    if ((access & Opcodes.ACC_SYNCHRONIZED) == Opcodes.ACC_SYNCHRONIZED) res += "synchronized ";
    if ((access & Opcodes.ACC_ABSTRACT) == Opcodes.ACC_ABSTRACT) res += "abstract ";
    if ((access & Opcodes.ACC_INTERFACE) == Opcodes.ACC_INTERFACE) res += "interface ";

    return res;
  }

  private String decodeInstruction(AbstractInsnNode insnNode) {
    insnNode.accept(mp);
    StringWriter sw = new StringWriter();
    printer.print(new PrintWriter(sw));
    printer.getText().clear();
    return sw.toString();
  }

  public static JClassPatcher getInstance() {
    if (instance == null) {
      synchronized (JClassPatcher.class) {
        instance = new JClassPatcher();
      }
    }
    return instance;
  }
}