Собственные события. Часть вторая: Внедрение

Собственные события. Часть вторая: Внедрение

Версия(и) Minecraft
1.12
Приветствую. Вы читаете вторую часть статьи, посвящённой созданию собственных событий. Здесь я подробно разжую процесс трансформации классов и добавления своего кода для вызова событий. Для трансформации будет использована библиотека ASM, но ничего сложного не будет. Исходники в моём репозитории.


Собственные события

Часть вторая

Forge даёт нам возможность использовать трансформеры для классов, которые загружаются раньше майнкрафта и любой модификации (кроме кормодов), а это значит что мы можем модифицировать что угодно. В рамках создания событий рассмотрим их внедрение в игру на паре примеров.

Но прежде чем начать я, мягко говоря, требую от вас ознакомится с понятием байт-кода. Вот хорошая статья на Хабре: Java Bytecode Fundamentals. Про создание кормодов и использование библиотеки ASM на форуме есть статья [Не просто][Не легко]Хуки, авторства @JustAGod. Для работы с байт-кодом вам необходимо иметь возможность его просматривать, я рекомендую Bytecode Outline (плагин для Eclipse). Он позволит просматривать байт-код в удобном мнемоническом представлении с использованием имён опкодов. Вам так же неплохо бы иметь представления о самой библиотеке ASM и для тех кто can speak english, оставляю ссылку на официальный сайт (спецификация): Using the ASM.

Создание кормода

Для модификации исходного кода нам потребуется использовать трансформер (класс с интерфейсом IClassTransformer), который будет загружаться через плагин (реализация IFMLLoadingPlugin). Плагин и трансформер принято называть кормодом – по сути модификацией внутри модификации, которая будет загружена раньше основного мода для применения трансформаций. Плагин может содержать контейнер (реализация интерфейса ModContaner), который позволит снабдить его всеми атрибутами мода (описание и пр., кормод появится в списке модификаций).

Создадим его. В новом пакете coremod создайте класс с реализацией IFMLLoadingPlugin. Я назвал свой FMLCustomEventsPlugin:
Java:
@TransformerExclusions({"ru.austeretony.events.coremod"})
@MCVersion("1.12.2")
public class FMLCustomEventsPlugin implements IFMLLoadingPlugin {
    
    @Override
    public String[] getASMTransformerClass() {
    
        //Передача полного имени класса-трансформера.
        return new String[] {};
    }

    @Override
    public String getModContainerClass() {
    
        return null;
    }

    @Override
    public String getSetupClass() {
    
        return null;
    }

    @Override
    public void injectData(Map<String, Object> data) {}

    @Override
    public String getAccessTransformerClass() {
    
        return null;
    }
}


Теперь нужен класс-трансформер с интерфейсом IClassTransformer, в котором и будет происходить работа по изменению классов. Мой CustomEventsTransformer:
Java:
public class CustomEventsClassTransformer implements IClassTransformer {

    //Логгер для отладки.
    public static final Logger LOGGER = LogManager.getLogger();

    @Override
    public byte[] transform(String name, String transformedName, byte[] basicClass) {
    
        //Тут мы будем изменять классы.
    
        return basicClass;
    }
}


В методе getASMTransformerClass() вашей реализации IFMLLoadingPlugin требуется вернуть полное имя класса-трансформера:
Java:
    @Override
    public String[] getASMTransformerClass() {
    
        //Передача полного имени класса-трансформера.
        return new String[] {"ru.austeretony.events.coremod.CustomEventsClassTransformer"};
    }


Для подгрузки плагина в IDE добавьте новый аргумент VM, в котором нужно указать путь к классу-плагину: -Dfml.coreMods.load=ru.austeretony.events.coremod.FMLCustomEventsPlugin

Чтобы плагин грузился форджем после компиляции мода в сторонних клиентах добавьте этот блок в build.gradle:
Код:
jar {
   manifest {
       attributes 'FMLCorePlugin': 'ru.austeretony.events.coremod.FMLCustomEventsPlugin'
       attributes 'FMLCorePluginContainsFMLMod': 'true'
   }
}


В нём также требуется указать путь к плагину. Во второй строчке при наличии контейнера для кормода поставьте false. В примере контейнера не будет. На этом подготовка завершена.

Основы трансформации

Прежде чем наложить руки на святое и перейти к ковырянию классов майна потренируемся работать с байт-кодом и делать простенькие трансформации. Зная как вы относитесь к прочтению подготовительного материала, я опишу процесс трансформации максимально подробно на простеньком примере.

Итак, допустим у нас есть такой класс TestClass:
Java:
public class TestClass {

    public static final Logger LOGGER = LogManager.getLogger();

    private int someInt = 5;

    public TestClass() {
            
        this.addTo(10);
    }

    public void addTo(int value) {
    
        this.someInt = this.someInt + value;
        log();
    }

    public void log() {
    
        LOGGER.info("***************Вам не узнать значение поля <someInt>!***************");
    }
}


Попробуем вставить в него свой код, но сначала посмотрим как класс выглядит в байт-коде. Вот что показывает плагин Bytecode Outline:
Java:
// class version 52.0 (52)
// access flags 0x21//Флаги модификаторов доступа
public class ru/austeretony/events/test/TestClass {//Полное имя класса.

  // compiled from: TestClass.java

  // access flags 0x19
  public final static Lorg/apache/logging/log4j/Logger; LOGGER//Поле логгера с именем и дескриптором класса, инициализируется в статическом блоке как константа.

  // access flags 0x2
  private I someInt//Поле переменной с именем и дескриптором. Обратите внимание, оно не статично и инициализируется в конструкторе.

  // access flags 0x8
  static <clinit>()V//Статический блок инициализации.
   L0//Номер строки
    LINENUMBER 8 L0//LINENUMBER - указатели номеров строк.
    INVOKESTATIC org/apache/logging/log4j/LogManager.getLogger()Lorg/apache/logging/log4j/Logger;//INVOKESTATIC - вызов статического метода. Вызов getLogger(), который вернёт объект типа Logger для присвоения константе LOGGER.
    PUTSTATIC ru/austeretony/events/test/TestClass.LOGGER : Lorg/apache/logging/log4j/Logger;//PUTSTATIC - установка значения константы.
    RETURN//RETURN - выход из блока - любой метод типа void, конструктор или статический блок инициализации неявно содержит его в конце.
    MAXSTACK = 1
    MAXLOCALS = 0//Кол-во локальных переменных блока.

  // access flags 0x1
  public <init>()V//Блок конструктора класса. При отсутствии конструктора будет неявно сгенерирован по умолчанию.
   L0
    LINENUMBER 12 L0
    ALOAD 0//A - приставка опкодов для  работы с ссылочными объектами. Загрузка локальной переменной объекта класса, происходит при доступе к полям класса или вызове его методов.
    INVOKESPECIAL java/lang/Object.<init>()V//INVOKESPECIAL - вызов суперконструктора класса или приватного метода, доступного только в этом классе - Object (все классы унаследованы от него).
   L1
    LINENUMBER 10 L1
    ALOAD 0
    ICONST_5//ICONST_5 - загрузка константы типа int - число 5.
    PUTFIELD ru/austeretony/events/test/TestClass.someInt : I//PUTFIELD - установка значения поля - someInt.
   L2
    LINENUMBER 14 L2
    ALOAD 0
    BIPUSH 10//BIPUSH - загруза константы типа int.
    INVOKEVIRTUAL ru/austeretony/events/test/TestClass.addTo(I)V//INVOKEVIRTUAL - вызов публичного метода класса по ссылке - вызов addTo().
   L3
    LINENUMBER 15 L3
    RETURN//Выход из блока конструктора.
   L4
    LOCALVARIABLE this Lru/austeretony/events/test/TestClass; L0 L4 0//LOCALVARIABLE - локальная переменная блока - любой метод и конструктор содержит переменную объекта класса (this).
    MAXSTACK = 2
    MAXLOCALS = 1//Кол-во локальных переменных блока.

  // access flags 0x2
  private addTo(I)V//Название метода и его дескриптор.
   L0
    LINENUMBER 19 L0
    ALOAD 0
    ALOAD 0
    GETFIELD ru/austeretony/events/test/TestClass.someInt : I//GETFIELD - получение значение поля - значения someInt.
    ILOAD 1//I - приставка опкодов для работы с int - загрузка значения локальной переменной с индексом 1 (значение аргумента метода).
    IADD//IADD - сложение переменных типа int.
    PUTFIELD ru/austeretony/events/test/TestClass.someInt : I//Установка нового значения поля someInt.
   L1
    LINENUMBER 20 L1
    ALOAD 0
    INVOKEVIRTUAL ru/austeretony/events/test/TestClass.log()V//Вызов метода класса log()
   L2
    LINENUMBER 21 L2
    RETURN//Выход из блока метода.
   L3
    LOCALVARIABLE this Lru/austeretony/events/test/TestClass; L0 L3 0//LOCALVARIABLE - локальная переменная блока - объект класса.
    LOCALVARIABLE value I L0 L3 1//Локальная переменная - значение аргумента метода.
    MAXSTACK = 3
    MAXLOCALS = 2

  // access flags 0x2
  private log()V//Блок метода log()
   L0
    LINENUMBER 25 L0
    GETSTATIC ru/austeretony/events/test/TestClass.LOGGER : Lorg/apache/logging/log4j/Logger;//GETSTATIC - получение статического поля - LOGGER.
    LDC "\u0412\u0430\u043c \u043d\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u044f <someInt>!"//LDC - загрузка строковой константы.
    INVOKEINTERFACE org/apache/logging/log4j/Logger.info(Ljava/lang/String;)V//INVOKEINTERFACE - вызов метода интерфейса - Logger#info().
   L1
    LINENUMBER 26 L1
    RETURN
   L2
    LOCALVARIABLE this Lru/austeretony/events/test/TestClass; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}


Понятно… Я добавил комментарии для большей ясности, сравните класс в привычном виде и в таком. Естественно это не байт-код, а лишь его представление в виде имён опкодов, это делает проектирование хуков и их внедрение максимально простым. Имена опкодов операций можно найти в классе Opcodes, там же указаны классы, которые применяются для создания инструкций с этими опкодами. После названия методов в заголовках можно увидеть дескрипторы. Стандартный вид дескриптора – ()V – что значит метод не имеет аргументов и возвращаемого типа (тип void). Примитивы в дескрипторах обозначаются единственной заглавной буквой (I – int, Z – boolean и т.д.). Объекты имеют вид – L<имя класса>; - например: Lnet/minecraft/client/Minecraft;. В конце каждого блока методов указываются размер стека и локальные переменные.

Я добавил объявление и инициализацию TestClass в главный класс мода для его создания при загрузке самого мода для отладочных нужд:
private TestClass testClass = new TestClass();

При инициализации главного класса создаётся TestClass и мы видим в консоли сообщение, которое выводится при вызове метода TestClass#log(). Примерно такое:
[21:35:45] [main/INFO] [ru.austeretony.events.test.TestClass]: ***************Вам не узнать значение поля <someInt>!***************

Допустим класс не доступен для прямых изменений. Попробуем вставить в метод addTo() хук, который выведет в консоль значения поля и значение переданной ему переменной. Вот как это могло бы выглядеть, если бы могли сделать это без этих ваших asm’ов:
Java:
    public void addTo(int value) {
    
        this.someInt = this.someInt + value;
        LOGGER.info("Значение поля <someValue> равно: " + this.someInt + ", переданного аргумента: " + value);//Вставка
        log();
    }


Добавить такую конструкцию не сложно, однако всегда можно упростить себе задачу (как это делает фордж) – вынести эту довольно массивную строку в собственный класс в статический метод и внедрить в нужное место его вызов. Так и поступим. Создаём класс TestInjections и метод showValues() с двумя аргументами, которые будут получать значения переменных:
Java:
public class TestInjections {

    public static final Logger LOGGER = LogManager.getLogger();

    public static void showValues(int field, int local) {
    
        LOGGER.info("***Внедрённый код***");
        LOGGER.info("Значение поля <someValue> равно: " + field + ", переданного аргумента: " + local);
        LOGGER.info("***Внедрённый код***");
    }
}


Теперь ручная вставка была бы такой:
Java:
    public void addTo(int value) {
    
        this.someInt = this.someInt + value;
        //Добавленная строка
        TestInjections.showValues(this.someInt, value);
        log();
    }


Такую трансформацию произвести гораздо проще. Давайте сравним байт-коды метода без хука, и с ним. Стандартный метод:
Java:
  // access flags 0x1
  public addTo(I)V
   L0
    LINENUMBER 19 L0
    ALOAD 0
    ALOAD 0
    GETFIELD ru/austeretony/events/test/TestClass.someInt : I
    ILOAD 1
    IADD
    PUTFIELD ru/austeretony/events/test/TestClass.someInt : I
   L1
    LINENUMBER 20 L1
    ALOAD 0
    INVOKEVIRTUAL ru/austeretony/events/test/TestClass.log()V
   L2
    LINENUMBER 21 L2
    RETURN
   L3
    LOCALVARIABLE this Lru/austeretony/events/test/TestClass; L0 L3 0
    LOCALVARIABLE value I L0 L3 1
    MAXSTACK = 3
    MAXLOCALS = 2


С вызовом статического метода:
Java:
  // access flags 0x1
  public addTo(I)V
   L0
    LINENUMBER 19 L0
    ALOAD 0
    ALOAD 0
    GETFIELD ru/austeretony/events/test/TestClass.someInt : I
    ILOAD 1
    IADD
    PUTFIELD ru/austeretony/events/test/TestClass.someInt : I
   L1
    LINENUMBER 20 L1//Наш вызов.
    ALOAD 0//Загрузка this (индекс 0)
    GETFIELD ru/austeretony/events/test/TestClass.someInt : I//Загрузка значения поля someInt
    ILOAD 1//Загрузка значения аргумента (индекс 1)
    INVOKESTATIC ru/austeretony/events/test/TestInjections.showValues(II)V//Вызов статического showValues()
   L2
    LINENUMBER 21 L2
    ALOAD 0
    INVOKEVIRTUAL ru/austeretony/events/test/TestClass.log()V
   L3
    LINENUMBER 22 L3
    RETURN
   L4
    LOCALVARIABLE this Lru/austeretony/events/test/TestClass; L0 L4 0
    LOCALVARIABLE value I L0 L4 1
    MAXSTACK = 3
    MAXLOCALS = 2


Отличается он от стандартного наличием четырёх новых опкодов в новой строке #2 – инструкций, которые нужно добавить.

Давайте попробуем трансформировать класс. В классе CustomEventsClassTransformer (ваша реализация IClassTransformer) создаём метод patchTestClass() с аргументом в виде массива байтов – тут будет происходить трансформация. В transform() проверяем имя текущего класса и при его совпадении с TestClass возвращаем patchTestClass(), который вернёт модифицированный массив байтов класса:
Java:
    @Override
    public byte[] transform(String name, String transformedName, byte[] basicClass) {
    
        switch (name) {
    
            //Демонстрация трансформеров.
            case "ru.austeretony.events.test.TestClass":
        
                LOGGER.info("<TestClass> transformation attempt...");
        
                return patchTestClass(basicClass);   
        }
    
        return basicClass;
    }

    //Трансформация TestClass
    public byte[] patchTestClass(byte[] bytes) {
    
        ClassNode classNode = new ClassNode();//Новый узел для класса.
        ClassReader classReader = new ClassReader(bytes);//Ридер для байтов трансформируемого класса.
        classReader.accept(classNode, 0);//Добавление ClassNode в ридер для внесения изменений.
    
        //Трансформация.
    
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);//Создание райтера.   
        classNode.accept(writer);//Запись изменёного класса.
            
        //Возвращение записанного класса в байтовом представлении для дальнейшей работы.
        return writer.toByteArray();
    }


Для трансформации я использовал возможности ASM Tree API и вот какой алгоритм у меня получился:
  • Создание цикла для перебора методов класса и поиск искомого по названию (addTo()) и дескриптору (I)V.
  • При нахождении искомого метода создаётся цикл по его инструкциям (узлам), в котором производится поиск подходящего узла, перед которым будут добавлены новые. Исходя из анализа байт-кода метода и желаемой позиции производится требуется достичь второго узла с опкодом ALOAD и добавить инструкции перед ним, но так как он встречается дважды будет проще достичь узла INVOKEVIRTUAL (вызов log()), так как он встречается единожды и при добавлении инструкций получить предыдущий узел.
  • При нахождении узла с опкодом INVOKEVIRTUAL создаём новый список для инструкций, последовательно (в соответствии с байт-кодом модифицированного метода) добавляем их. По завершению происходит добавление списка в инструкции метода для предудущего (относительно текущего) узла (текущий INVOKESTATIC, предыдущий (нужен) ALOAD). Выходим из циклов.
А вот код трансформера. Имена классов для создания узлов с нужными опкодами указаны в классе Opcodes:
Java:
//Трансформация TestClass
    public byte[] patchTestClass(byte[] bytes) {
    
        /*
         * Опкоды всех операций и используемые объекты для создания инструкций с данным опкодом указаны в классе Opcodes.
         */
    
        ClassNode classNode = new ClassNode();//Новый узел для класса.
        ClassReader classReader = new ClassReader(bytes);//Ридер для байтов трансформируемого класса.
        classReader.accept(classNode, 0);//Добавление ClassNode в ридер для внесения изменений.
    
         String targetMethodName = "addTo";//Имя трансформируемого метода.
            
         //Начало перебора методов класса.
        for (MethodNode methodNode : classNode.methods) {
        
            //Если имя текушего метода соответствует имени целевого и его дискриптору, работаем с ним.
            if (methodNode.name.equals(targetMethodName) && methodNode.desc.equals("(I)V")) {
                            
                AbstractInsnNode currentNode = null;//Ссылка на текущий узел.
            
                //Получение итератора для узлов метода, все узлы приводятся к их суперклассу AbstractInsnNode.
                Iterator<AbstractInsnNode> iteratorNode = methodNode.instructions.iterator();
          
                //Вам тут делать нечего, если вы не можете в циклы...
                while (iteratorNode.hasNext()) {
                
                    currentNode = iteratorNode.next();
                                    
                    //При нахождении нужного узла работаем с ним.
                    if (currentNode.getOpcode() == Opcodes.INVOKEVIRTUAL) {//узел вызова метода log().
                                
                        //Создание нового списка инструкций для метода.
                        InsnList nodesList = new InsnList();
                    
                        //Добавление первой инструкции: загрузка ссылки на объект класса (локальная переменная this, индекс 0 (всегда)) для получения доступа к полю someInt. Требует создание узла VarInsnNode.
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 0));
                    
                        //Добавление второй инструкции: загрузка (получение) поля someInt из класса TestClass с дескриптором I (int). Требуется создание узла FieldInsnNode.
                        nodesList.add(new FieldInsnNode(Opcodes.GETFIELD, "ru/austeretony/events/test/TestClass", "someInt", "I"));
                    
                        //Добавление третьей инструкции: загрузка локальной переменной типа int под индексом 1 (значение, переданное методу как аргумент). Требует создание узла VarInsnNode.
                        nodesList.add(new VarInsnNode(Opcodes.ILOAD, 1));
                    
                        //Добавление четвёртой инструкции: вызов статического метода с указанным именем и дескриптором из указанного класса (создание нового узла MethodInsnNode для метода),
                        //последний флаг определяет является ли класс, из которого вызывается метод, интерфейсом.
                        nodesList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "ru/austeretony/events/test/TestInjections", "showValues", "(II)V", false));                    
                    
                        //Добавление списка новых инструкций перед загрузкой ссылки на объект класса для вызова log().
                        //Так как текущий узел это вызов метода, то вставку требуется произвести перед предыдущим узлом.
                        methodNode.instructions.insertBefore(currentNode.getPrevious(), nodesList);
                    
                        break;//Выход из цикла перебора узлов метода.
                    }
                }
            
                break;//Выход из цикла перебора методов.
            }
        }
    
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);//Создание райтера.   
        classNode.accept(writer);//Запись изменёного класса.
            
        //Возвращение записанного класса в байтовом представлении для дальнейшей работы.
        return writer.toByteArray();
    }


Вот так вот, ничего сложного в таком хуке нет. Врятли можно разжевать это подробнее. Запустим игру и смотрим в консоль:
Код:
[23:15:48] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Трансформация <TestClass>
[23:15:48] [main/INFO] [ru.austeretony.events.test.TestInjections]: ***Внедрённый код***
[23:15:48] [main/INFO] [ru.austeretony.events.test.TestInjections]: Значение поля <someValue> равно: 15, переданного аргумента: 10
[23:15:48] [main/INFO] [ru.austeretony.events.test.TestInjections]: ***Внедрённый код***
[23:15:48] [main/INFO] [ru.austeretony.events.test.TestClass]: ***************Вам не узнать значение поля <someInt>!***************


Узнали всё таки. Как видно, трансформация класса происходит при его первом вызове (в данном случае инициализации при создании нового экземпляра в главном классе мода). После вызова addTo() из конструктора и операции сложения значения поля и значения аргумента будет вызван статический TestInjections#showValues().

Теперь неплохо бы прервать вызов метода TestClass#log(). Что можно для этого сделать? Посмотрите на байт-код, подумайте немного…

Время вышло. В данном случае достаточно вставить точку выхода return перед log().А в нашем случае добавить всего одну инструкцию к уже созданным:
Java:
                        //Добавление пятой инструкции после предыдущих: досрочный выход из метода через return для предотвращения выполнения оставшейся его части (вызова метода log()). Требуется новый узел InsnNode.
                        nodesList.add(new InsnNode(Opcodes.RETURN));


Запустите игру и теперь в консоли вы не увидите сообщение из метода log(). Ну вот, экскурс в базу трансформеров с ASM закончен. Переходим к внедрению событий.

Внедрение событий в исходный код

Данный раздел будет содержать два примера. В первом будет рассмотрено простое неотменяемое событие (целых два, да ещё и с подклассами), которые будут срабатывать при добавлении предмета в слот инвентаря и извлечении. Во втором событие будет с возможностью отмены и наличием фаз, суть его будет в возможности отмены использования клавиш F1 и F3. Интересно? Вперёд!

Пример первый

Небольшое отступление. Полезность такого события заключается например в том, что можно отследить изменения в экипировке игрока, когда он что то перемещает по слотам, это позволит с лёгкостью отследить какой предмет куда добавлен, откуда убран, был ли заменён и пр.

Итак, сначала необходимо разобраться с тем, где должен происходить вызов вашего события, а так же тем, какую информацию при его срабатывании вы хотите получить. Допустим, нужно отследить изъятие предмета из слота. Лезем в класс Slot и видим такой метод:
Java:
    public ItemStack onTake(EntityPlayer thePlayer, ItemStack stack) {

        this.onSlotChanged();
        return stack;
    }


Он срабатывает при извлечении предмета из слота и нам явно нужно работать с ним. Из аргументов видно, что нам доступен игрок, предмет, над которым висит курсор, ну и само собой все поля класса. Мы можем добавить вызов статического метода из onTake(), который запустит наше событие и будет содержать интересующие нас переменные. А лучше сразу два метода, в начале и в конце для двух фаз до изменения слота и после, мало ли – пригодится.

Прикидываем что мы имеем и создаём класс события с двумя подклассами-фазами. TakeStackSlotEvent:
Java:
public class TakeStackSlotEvent extends Event {

    /**
     * TakeStackSlotEvent срабатывает когда игрок забирает предмет из слота.<br>
     * Срабатывает через: <Pre> {@link CustomEventsFactory#onTakePre(Slot, ItemStack)}<br>
     * Срабатывает через: <Post> {@link CustomEventsFactory#onTakePost(Slot, ItemStack)}<br>
     * <br>
     * {@link #slot} изменяемый слот.<br>
     * {@link #inventory} инвентарь, которому принадлежит слот<br>
     * (не тот, в котором находится, а тот, который передан в конструктор).<br>
     * {@link #player} игрок, взаимодействующий со слотом.<br>
     * {@link #slotIndex} индекс слота в инвентаре.<br>
     * {@link #stackInSlot} предмет в слоте.<br>
     * {@link #mouseStack} предмет, над которым курсор.<br>
     * <br>
     * Это событие нельзя отменить. {@link Cancelable}. <br>
     * <br>
     * Это событие не имеет результата. {@link HasResult}<br>
     * <br>
     * Это событие использует {@link MinecraftForge#EVENT_BUS}.<br>
     **/

    private final Slot slot;

    private final IInventory inventory;

    private final EntityPlayer player;

    private final int slotIndex;

    private final ItemStack stackInSlot, mouseStack;

    public TakeStackSlotEvent(Slot slot, EntityPlayer player, ItemStack itemStack) {
    
        this.slot = slot;
        this.player = player;
        this.inventory = slot.inventory;
        this.slotIndex = slot.getSlotIndex();
        this.stackInSlot = slot.getStack();
        this.mouseStack = itemStack;
    }

    public Slot getSlot() {
    
        return this.slot;
    }

    public IInventory getInventory() {
    
        return this.inventory;
    }

    public EntityPlayer getEntityPlayer() {
    
        return this.player;
    }

    public int getSlotIndex() {
    
        return this.slotIndex;
    }

    public ItemStack getStackInSlot() {
    
        return this.stackInSlot;
    }

    public ItemStack getMouseHeldStack() {
    
        return this.mouseStack;
    }

    public static class Pre extends TakeStackSlotEvent {

        /**
         * Срабатывает до внесения изменений.
         */
    
        public Pre(Slot slot, EntityPlayer player, ItemStack itemStack) {
        
            super(slot, player, itemStack);
        }   
    }

    public static class Post extends TakeStackSlotEvent {

        /**
         * Срабатывает после внесения изменений.
         */
    
        public Post(Slot slot, EntityPlayer player, ItemStack itemStack) {
        
            super(slot, player, itemStack);
        }   
    }
}


Конструктор содержит Slot, EntityPlayer и ItemStack, ссылки на которые он получит при вызове события из статического метода, который мы будем внедрять в Slot и создадим чуть позже. Из объекта слота мы достаём все, что есть может быть нужно и создаём геттеры для удобства.

Далее, идя по стопам форджа, создаём класс для статических методов CustomEventFactory, в котором размещаем два метода для двух фаз события. Размещаем в них вызовы событий. Аргументы методов аналогичны аргументам конструктора эвента:
Java:
public class CustomEventsFactory {

    //Для внедрения в класс Slot в начало onTake().
    public static void onTakePre(Slot slot, EntityPlayer player, ItemStack itemStack) {
    
        //Требуется отсечение клиентских срабатываний, так как сервер постоянно синхронизирует инвентари и на клиенте вызов будет происходить
        //очень часто "ложно" (брать и пихать стаки будет сервер при синхронизации, а не игрок).       
        MinecraftForge.EVENT_BUS.post(new TakeStackSlotEvent.Pre(slot, player, itemStack));//Вызов срабатывания события фазы Pre
    }

    //Для внедрения в класс Slot в конец onTake() перед return.
    public static void onTakePost(Slot slot, EntityPlayer player, ItemStack itemStack) {
    
        MinecraftForge.EVENT_BUS.post(new TakeStackSlotEvent.Post(slot, player, itemStack));//Вызов срабатывания события фазы Post
    }
}


Для отлавливания момента добавления предмета в слот придётся модифицировать метод putStack():
Java:
    public void putStack(ItemStack stack) {

        this.inventory.setInventorySlotContents(this.slotIndex, stack);
        this.onSlotChanged();
    }


Тут в качестве аргумента у нас есть добавляемый ItemStack. Прискорбно, но EntityPlayer нам не додали, но постойте - можно достать его из IInventory, приведя его к InventoryPlayer при желании.

Создаём событие PutStackSlotEvent:
Java:
public class PutStackSlotEvent extends Event {

    /**
     * PutStackSlotEvent срабатывает когда игрок кладёт предмет в слот.<br>
     * Срабатывает через: <Pre> {@link CustomEventsFactory#putStackPre(Slot, ItemStack)}<br>
     * Срабатывает через: <Post> {@link CustomEventsFactory#putStackPost(Slot, ItemStack)}<br>
     * <br>
     * {@link #slot} изменяемый слот.<br>
     * {@link #inventory} инвентарь, которому принадлежит слот<br>
     * (не тот, в котором находится, а тот, который передан в конструктор).<br>
     * {@link #slotIndex} индекс слота в инвентаре.<br>
     * {@link #stackInSlot} предмет в слоте.<br>
     * {@link #stackToPut} добавляемый предмет.<br>
     * <br>
     * Это событие нельзя отменить. {@link Cancelable}. <br>
     * <br>
     * Это событие не имеет результата. {@link HasResult}<br>
     * <br>
     * Это событие использует {@link MinecraftForge#EVENT_BUS}.<br>
     **/

    private final Slot slot;

    private final IInventory inventory;

    private final int slotIndex;

    private final ItemStack stackInSlot, stackToPut;

    public PutStackSlotEvent(Slot slot, ItemStack itemStack) {
    
        this.slot = slot;
        this.inventory = slot.inventory;
        this.slotIndex = slot.getSlotIndex();
        this.stackInSlot = slot.getStack();
        this.stackToPut = itemStack;
    }

    public Slot getSlot() {
    
        return this.slot;
    }

    public IInventory getInventory() {
    
        return this.inventory;
    }

    public int getSlotIndex() {
    
        return this.slotIndex;
    }

    public ItemStack getStackInSlot() {
    
        return this.stackInSlot;
    }

    public ItemStack getStackToPut() {
    
        return this.stackToPut;
    }

    public static class Pre extends PutStackSlotEvent {

        /**
         * Срабатывает до внесения изменений.
         */
    
        public Pre(Slot slot, ItemStack itemStack) {
        
            super(slot, itemStack);
        }   
    }

    public static class Post extends PutStackSlotEvent {

        /**
         * Срабатывает после внесения изменений.
         */
    
        public Post(Slot slot, ItemStack itemStack) {
        
            super(slot, itemStack);
        }   
    }
}


В конструкторе только слот и стак.

Методы в CustomEventsFactory:
Java:
public class CustomEventsFactory {

    //Для внедрения в класс Slot в начало putStack().
    public static void putStackPre(Slot slot, ItemStack itemStack) {
    
        if (itemStack != itemStack.EMPTY && itemStack.getItem() != Items.AIR) {
        
            MinecraftForge.EVENT_BUS.post(new PutStackSlotEvent.Pre(slot, itemStack));
        }
    }

    //Для внедрения в класс Slot в конец putStack().
    public static void putStackPost(Slot slot, ItemStack itemStack) {
    
        if (itemStack != itemStack.EMPTY && itemStack.getItem() != Items.AIR) {

            MinecraftForge.EVENT_BUS.post(new PutStackSlotEvent.Post(slot, itemStack));
        }
    }
}


Простая часть закончилась, теперь нужно внедрить вызовы событий в Slot в соответствующие методы. Происходит всё в классе-трансформере в методе transform(). Тут у нас в наличии имя класса, трансформированное имя (удобно для поика обфусцированных классов по необфусцированному имени) и сам класс в виде байтов. Проверяем имя класса на входе и в зависимости от наличия обфускации (в IDE необфусцированные классы, в сторонних клиентах с обфускацией), а с классом работаем в отдельном методе:
Java:
    @Override
    public byte[] transform(String name, String transformedName, byte[] basicClass) {
    
        switch (name) {     
            
            //Трансформация Slot.class
            case "agr":
                
                LOGGER.info("Obfuscated <Slot> class transformation attempt...");

                return patchSlot(basicClass, true);
            
            case "net.minecraft.inventory.Slot":
            
                LOGGER.info("Debfuscated <Slot> class transformation attempt...");
            
                return patchSlot(basicClass, false);     
        }
    
        return basicClass;
    }

    public byte[] patchSlot(byte[] bytes, boolean obfuscated) {}


Прежде чем лезть в класс нужно прикинуть, какие изменения и в каком месте нужно сделать. У нас есть исходный метод onTake():
Java:
    public ItemStack onTake(EntityPlayer thePlayer, ItemStack stack) {

        this.onSlotChanged();
        return stack;
    }


И есть его представление в байт-коде:
Java:
  // access flags 0x1
  public onTake(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/item/ItemStack;)Lnet/minecraft/item/ItemStack;
   L0
    LINENUMBER 64 L0
    ALOAD 0
    INVOKEVIRTUAL net/minecraft/inventory/Slot.onSlotChanged()V
   L1
    LINENUMBER 65 L1
    ALOAD 2
    ARETURN
   L2
    LOCALVARIABLE this Lnet/minecraft/inventory/Slot; L0 L2 0
    LOCALVARIABLE thePlayer Lnet/minecraft/entity/player/EntityPlayer; L0 L2 1
    LOCALVARIABLE stack Lnet/minecraft/item/ItemStack; L0 L2 2
    MAXSTACK = 1
    MAXLOCALS = 3


При наличии вызовов событий он должен выглядеть так:
Java:
    public ItemStack onTake(EntityPlayer player, ItemStack itemStack) {
    
        CustomEventsFactory.onTakePre(this, player, itemStack);
        this.onSlotChanged();
        CustomEventsFactory.onTakePost(this, player, itemStack);
        return itemStack;
    }


И его байт-код:
Java:
  // access flags 0x1
  public onTake(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/item/ItemStack;)Lnet/minecraft/item/ItemStack;
   L0
    LINENUMBER 55 L0//Новая строка - вызов onTakePre()
    ALOAD 0
    ALOAD 1
    ALOAD 2
    INVOKESTATIC ru/austeretony/events/coremod/CustomEventsFactory.onTakePre(Lnet/minecraft/inventory/Slot;Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/item/ItemStack;)V
   L1
    LINENUMBER 56 L1
    ALOAD 0
    INVOKEVIRTUAL ru/austeretony/events/events/handler/SlotCopy.onSlotChanged()V
   L2
    LINENUMBER 57 L2//Новая строка - вызов onTakePost()
    ALOAD 0
    ALOAD 1
    ALOAD 2
    INVOKESTATIC ru/austeretony/events/coremod/CustomEventsFactory.onTakePost(Lnet/minecraft/inventory/Slot;Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/item/ItemStack;)V
   L3
    LINENUMBER 58 L3
    ALOAD 2
    ARETURN
   L4
    LOCALVARIABLE this Lru/austeretony/events/events/handler/SlotCopy; L0 L4 0
    LOCALVARIABLE player Lnet/minecraft/entity/player/EntityPlayer; L0 L4 1
    LOCALVARIABLE itemStack Lnet/minecraft/item/ItemStack; L0 L4 2
    MAXSTACK = 3
    MAXLOCALS = 3


Всё понятно, а если нет – читайте сначала... Нам нужно вставить четыре новых инструкции (передать три локальных переменных и вызвать статический метод) в начало метода, перед узлом ALOAD 0 (переменная Slot (ссылка this, имеет индекс 0 среди локальных переменных), для которой происходит вызов нестатического метода onSlotChanged() в том же классе (INVOKEVIRTUAL)). Затем вставить четыре аналогичные инструкции перед ALOAD 2. Таким образом мы добавим вызовы фаз нашего события в исходный код класса Slot. Легко? Ещё как! Это самое простое, на что способна ASM и практически всё что вам нужно для вставки хуков для запуска ваших событий.

Далее метод putStack():
Java:
    public void putStack(ItemStack stack) {

        this.inventory.setInventorySlotContents(this.slotIndex, stack);
        this.onSlotChanged();
    }


Байт-код:
Java:
  // access flags 0x1
  public putStack(Lnet/minecraft/item/ItemStack;)V
   L0
    LINENUMBER 97 L0
    ALOAD 0
    GETFIELD net/minecraft/inventory/Slot.inventory : Lnet/minecraft/inventory/IInventory;
    ALOAD 0
    GETFIELD net/minecraft/inventory/Slot.slotIndex : I
    ALOAD 1
    INVOKEINTERFACE net/minecraft/inventory/IInventory.setInventorySlotContents(ILnet/minecraft/item/ItemStack;)V
   L1
    LINENUMBER 98 L1
    ALOAD 0
    INVOKEVIRTUAL net/minecraft/inventory/Slot.onSlotChanged()V
   L2
    LINENUMBER 99 L2
    RETURN
   L3
    LOCALVARIABLE this Lnet/minecraft/inventory/Slot; L0 L3 0
    LOCALVARIABLE stack Lnet/minecraft/item/ItemStack; L0 L3 1
    MAXSTACK = 3
    MAXLOCALS = 2


Вид после модификации:
Java:
    public void putStack(ItemStack itemStack) {
    
        CustomEventsFactory.putStackPre(this, itemStack);
        this.inventory.setInventorySlotContents(this.slotIndex, itemStack);
        this.onSlotChanged();
        CustomEventsFactory.putStackPost(this, itemStack);
    }


Байт-код:
Java:
  // access flags 0x1
  public putStack(Lnet/minecraft/item/ItemStack;)V
   L0
    LINENUMBER 79 L0
    ALOAD 0
    ALOAD 1
    INVOKESTATIC ru/austeretony/events/coremod/CustomEventsFactory.putStackPre(Lnet/minecraft/inventory/Slot;Lnet/minecraft/item/ItemStack;)V
   L1
    LINENUMBER 80 L1
    ALOAD 0
    GETFIELD ru/austeretony/events/events/handler/SlotCopy.inventory : Lnet/minecraft/inventory/IInventory;
    ALOAD 0
    GETFIELD ru/austeretony/events/events/handler/SlotCopy.slotIndex : I
    ALOAD 1
    INVOKEINTERFACE net/minecraft/inventory/IInventory.setInventorySlotContents(ILnet/minecraft/item/ItemStack;)V
   L2
    LINENUMBER 81 L2
    ALOAD 0
    INVOKEVIRTUAL ru/austeretony/events/events/handler/SlotCopy.onSlotChanged()V
   L3
    LINENUMBER 82 L3
    ALOAD 0
    ALOAD 1
    INVOKESTATIC ru/austeretony/events/coremod/CustomEventsFactory.putStackPost(Lnet/minecraft/inventory/Slot;Lnet/minecraft/item/ItemStack;)V
   L4
    LINENUMBER 83 L4
    RETURN
   L5
    LOCALVARIABLE this Lru/austeretony/events/events/handler/SlotCopy; L0 L5 0
    LOCALVARIABLE itemStack Lnet/minecraft/item/ItemStack; L0 L5 1
    MAXSTACK = 3
    MAXLOCALS = 2


Поступаем аналогично onTake(), пропихиваем три инструкции перед первым узлам ALOAD. А конечные инструкции вставляем после вызова метода (опкод INVOKEVIRTUAL) onSlotChanged() (а можно и перед return).

Алгоритм трансформации получился такой:
  • Перебор всех методов (MethodNode) класса (ClassNode) в цикле до нахождения искомого (onTake()), которому соответствует указанное имя и дескриптор.
  • По нахождению метода начинается итерация по его инструкциям (AbstractInsnNode) для поиска искомой (первый узел ALOAD). Проверка производится по опкодам.
  • При нахождении искомой инструкции создаётся новый список для инструкций и их добавление. Сначала добавляются узлы VarInsnNode, с указанием опкода операции (ALOAD для ссылочных типов) и индекса локальной переменной. Порядок определяется порядком соответствующих аргументов в статическом методе onTakePre(). В конец списка добавляется узел MethodInsnNode для нового метода, которому в конструктор передаётся опкод операции (вызов статического метода INVOKESTATIC), имя класса, в котором он расположен, имя самого метода, дескриптор и флаг принадлежности класса-владельца метода к интерфейсу. В конце созданный список добавляется к инструкциям метода перед (метод insertBefore()) текущим узлом AbstractInsnNode. При этом используется флаг, предотвращающий повторную трансформацию узла ALOAD (так как их несколько, условие сравнения опкодов вернёт true для каждого последующего и они все будут одинаково модифицированы).
  • Далее операция повторяется для того же метода для второго узла ALOAD. Для удобства ищем узел ARETURN (после второго ALOAD) и вставляем инструкции для предыдущего узла. Принцип идентичен. По завершению трансформации метода цикл разрывается и управление передаётся циклу перебора методов.
  • При нахождении циклом следующего метода putStack() производятся аналогичные манипуляции. При этом используется флаг, предотвращающий повторную трансформацию узла ALOAD (так как их несколько, условие сравнения опкодов вернёт true для каждого последующего и они все будут одинаково модифицированы). Конечные инструкции вставляются после (метод insert()) INVOKEVIRTUAL. По завершению циклы разрываются.
Ну а теперь сам код трансформера:
Java:
    //Трансформация класса Slot
    public byte[] patchSlot(byte[] bytes, boolean obfuscated) {
    
        ClassNode classNode = new ClassNode();
        ClassReader classReader = new ClassReader(bytes);
        classReader.accept(classNode, 0);
    
         String
         targetMethodName = obfuscated ? "a" : "onTake",
         entityPlayerClassName = obfuscated ? "aed" : "net/minecraft/entity/player/EntityPlayer",
         itemStackClassName = obfuscated ? "aip" : "net/minecraft/item/ItemStack",
         slotClassName = obfuscated ? "agr" : "net/minecraft/inventory/Slot";
    
        boolean
        onTakePreInjected = false,
        putStackPreInjected = false,
        putStackPostInjected = false;
    
        LOGGER.info("Class name: " + classNode.name);
        
        LOGGER.info("Injection started!");
    
        for (MethodNode methodNode : classNode.methods) {
        
            //Поик onTake(). Учтите наличие обфускации!
            if (methodNode.name.equals(targetMethodName) && methodNode.desc.equals("(L" + entityPlayerClassName + ";L" + itemStackClassName + ";)L" + itemStackClassName + ";")) {
            
                LOGGER.info("Method <onTake()> found!");
            
                AbstractInsnNode currentNode = null;
            
                Iterator<AbstractInsnNode> iteratorNode = methodNode.instructions.iterator();

                while (iteratorNode.hasNext()) {
                
                    currentNode = iteratorNode.next();
                
                    //Поиск первой загрузки ссылки на объект класса (this) - ALOAD
                    if (!onTakePreInjected && currentNode.getOpcode() == Opcodes.ALOAD) {
                    
                        LOGGER.info("Method <onTake()>: ALOAD node found!");
                    
                        InsnList nodesList = new InsnList();//Новый список.
                    
                        //Загрузка локальных перемменных в порядке онных в сигнатуре вызываемого за ними метода.
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 0));
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 1));
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 2));
                        //Вставка вызова CustomEventsFactory#onTakePre()
                        nodesList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "ru/austeretony/events/coremod/CustomEventsFactory", "onTakePre", "(L" + slotClassName + ";L" + entityPlayerClassName + ";L" + itemStackClassName + ";)V", false));
                                          
                        //Вставка инструкций в начало метода - перед загрузкой локальной переменной ссылки на объект класса (this)
                        methodNode.instructions.insertBefore(currentNode, nodesList);
                    
                        //Предотвращаем вставку этих же инструкций перед вторым узлом ALOAD.
                        onTakePreInjected = true;

                        LOGGER.info("Method <onTake()>: <onTakePre> injected before current node!");
                    }
                
                    if (currentNode.getOpcode() == Opcodes.ARETURN) {
                    
                        LOGGER.info("Method <onTake()>: ARETURN node found!");
                    
                        InsnList nodesList = new InsnList();
                    
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 0));
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 1));
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 2));
                        nodesList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "ru/austeretony/events/coremod/CustomEventsFactory", "onTakePost", "(L" + slotClassName + ";L" + entityPlayerClassName + ";L" + itemStackClassName + ";)V", false));
                    
                        //Вставка инструкций перед вторым ALOAD (текущий узел - ARETURN, получаем предыдущий (ALOAD с помощью getPrevious()).
                        methodNode.instructions.insertBefore(currentNode.getPrevious(), nodesList);

                        LOGGER.info("Method <onTake()>: <onTakePost> injected before current node!");
                    
                        break;//Выход из цикла по узлам метода onTake().
                    }
                }
            
                targetMethodName = obfuscated ? "d" : "putStack";//Меняем искомое имя метода на putStack() для его поиска в главном цикле.
            }
        
            //Поиск putStack()
            if (methodNode.name.equals(targetMethodName) && methodNode.desc.equals("(L" + itemStackClassName + ";)V")) {
            
                LOGGER.info("Method <putStack()> found!");
            
                AbstractInsnNode currentNode = null;
            
                Iterator<AbstractInsnNode> iteratorNode = methodNode.instructions.iterator();

                while (iteratorNode.hasNext()) {
                
                    currentNode = iteratorNode.next();
                
                    if (!putStackPreInjected && currentNode.getOpcode() == Opcodes.ALOAD) {
                    
                        LOGGER.info("Method <putStack()>: ALOAD node found!");
                    
                        InsnList nodesList = new InsnList();
                    
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 0));
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 1));
                        nodesList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "ru/austeretony/events/coremod/CustomEventsFactory", "putStackPre", "(L" + slotClassName + ";L" + itemStackClassName + ";)V", false));
                    
                        //Вставка инструкций перед текущим узлом (загрузкой this) - в самое начало метода.
                        methodNode.instructions.insertBefore(currentNode, nodesList);
                                            
                        LOGGER.info("Method <putStack()>: <putStackPre> injected before current node!");
                    
                        putStackPreInjected = true;
                    }
                
                    if (currentNode.getOpcode() == Opcodes.INVOKEVIRTUAL) {
                    
                        LOGGER.info("Method <putStack()>: INVOKEVIRTUAL node found!");
                    
                        InsnList nodesList = new InsnList();
                    
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 0));
                        nodesList.add(new VarInsnNode(Opcodes.ALOAD, 1));
                        nodesList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "ru/austeretony/events/coremod/CustomEventsFactory", "putStackPost", "(L" + slotClassName + ";L" + itemStackClassName + ";)V", false));             
                    
                        //Вставка новых инструкций после текущего узла (после вызова onSlotChanged()).
                        methodNode.instructions.insert(currentNode, nodesList);           
                    
                        LOGGER.info("Method <putStack()>: <putStackPost> injected after current node!");
                    
                        break;
                    }
                }
            
                break;
            }
        }
            
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);   
        classNode.accept(writer);
    
        LOGGER.info("Injection ended!");
    
        return writer.toByteArray();
    }


Обратите внимание на то, что трансформация предполагает использование обфусцированных имён классов и методов. Для работы трансформера вне IDE требуется указать обфусцированные имена объектов. Просмотреть их можно к примеру с помощью bspk MCP Mapping Viewer (для последних версий названия обфусцированных классов указаны некорректно, с методами и полями всё нормально). Дескрипторы всех методов, работающих с классами майнкрафта должны также зависеть от наличия обфускации.

Как вы наверное заметили, код содержит выводы отладочных сообщений в консоль для проверок. Вот лог процесса трансформации класса Slot:
Код:
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Debfuscated <Slot> class transformation attempt...
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Class name: net/minecraft/inventory/Slot
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Injection started!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <onTake()> found!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <onTake()>: ALOAD node found!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <onTake()>: <onTakePre> injected before current node!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <onTake()>: ARETURN node found!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <onTake()>: <onTakePost> injected before current node!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <putStack()> found!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <putStack()>: ALOAD node found!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <putStack()>: <putStackPre> injected before current node!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <putStack()>: INVOKEVIRTUAL node found!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Method <putStack()>: <putStackPost> injected after current node!
[14:57:02] [main/INFO] [ru.austeretony.events.coremod.CustomEventsClassTransformer]: Injection ended!


Если осилили - поздравляю. Показанная здесь трансформация заключается в паре (пар) вставок вызовов статических методов, которая довольно проста для понимания и воспроизведения.

Испытание события. Добавим в CustomEvents конечные фазы событий добавления и изъятия предметов из слота:
Java:
@EventBusSubscriber(modid = EventsMain.MODID)
public class CustomEvents {

    @SubscribeEvent
    public static void onTakeFromSlotPost(TakeStackSlotEvent.Post event) {
    
        if (event.getInventory() instanceof InventoryPlayer && !event.getEntityPlayer().world.isRemote) {   

            if (event.getSlotIndex() == 39 && event.getStackInSlot().getItem() != Items.CHAINMAIL_HELMET) {
        
                //Если после клика в слоте другой предмет - удаление эффекта.
                event.getEntityPlayer().removePotionEffect(MobEffects.NIGHT_VISION);
            }
        
            else if (event.getSlotIndex() == 38 && event.getStackInSlot().getItem() != Items.DIAMOND_CHESTPLATE) {
            
                //При изъятии удаление эффекта.
                event.getEntityPlayer().removePotionEffect(MobEffects.REGENERATION);               
            }
        }
    }

    @SubscribeEvent
    public static void onPutToSlotPost(PutStackSlotEvent.Post event) {
    
        if (event.getInventory() instanceof InventoryPlayer && !((InventoryPlayer) event.getInventory()).player.world.isRemote) {

            if (event.getSlotIndex() == 39 && event.getStackInSlot().getItem() == Items.CHAINMAIL_HELMET) {
        
                //Если после клика в слоте нужный предмет - добавление эффекта
                ((InventoryPlayer) event.getInventory()).player.addPotionEffect(new PotionEffect(MobEffects.NIGHT_VISION, 24000));//
            }
        
            else if (event.getSlotIndex() == 39 && event.getStackInSlot().getItem() != Items.CHAINMAIL_HELMET) {
            
                //Если после клика в слоте другой предмет - удаление эффекта
                ((InventoryPlayer) event.getInventory()).player.removePotionEffect(MobEffects.NIGHT_VISION);
            }
        
            if (event.getSlotIndex() == 38 && event.getStackInSlot().getItem() == Items.DIAMOND_CHESTPLATE) {
            
                //Если после клика в слоте нужный предмет - добавление эффекта
                ((InventoryPlayer) event.getInventory()).player.addPotionEffect(new PotionEffect(MobEffects.REGENERATION, 24000));                           
            }
        
            else if (event.getSlotIndex() == 38 && event.getStackInSlot().getItem() != Items.DIAMOND_CHESTPLATE) {
            
                //Если после клика в слоте другой предмет - удаление эффекта
                ((InventoryPlayer) event.getInventory()).player.removePotionEffect(MobEffects.REGENERATION);                   
            }
        }
    }
}


Тут производится проверка на действия в инвентаре игрока и исполнение на серверной стороне. В контексте статьи содержимое событий комментировать нет смысла. Вот вам видеоролик (ЯД, по какой то причине при просмотре в браузере звук отвратительного качества): демонстрация работы событий.

Чувствуете потенциал? Самое время рассмотреть кое что посложнее…

Второй пример

Посмотрим на особенности внедрения отменяемых событий...

Второй пример - создание и внедрение отменяемого события - будет дописан если будут пожелания. А ещё я сомневаюсь, что хватит допустимого кол-ва символов для статьи :D.
Заключение

Надеюсь, вы найдёте статью полезной хотя бы в рамках пособия по работе с ASM. Я старался донести то, что создавать и внедрять события очень просто, а польза от них может быть огромной. Исходники на GitHub содержат полностью рабочий проект, примеры из которого рассмотрены здесь. Работоспособность трансформеров тестировалась на стороннем сервере и клиенте.

Для тех кто всё ещё считает применение ASM ниже своего достоинства (мы то знаем почему) всегда есть удобная альтернатива - [Гайд][Легко][1.6+] Модификация чужого кода при запуске (трансфомеры) от @GloomyFolken. Но я всё же рекомендую научится использовать трансформеры напрямую.

Хочу в конце извиниться за возможные ошибки (я не профи), сообщайте о неточностях в обсуждении. Я потратил очень много времени на подготовку материала, так что пожалуйста оцените статью – мне важно знать, насколько они вам интересны. Спасибо за внимание!
Автор
AustereTony
Просмотры
1,671
Первый выпуск
Обновление
Оценка
5.00 звёзд 4 оценок

Другие ресурсы пользователя AustereTony

Последние рецензии

Многое узнал. Очень полезно
Мощно. Но они все равно будут использовать глумичное. Все мы знаем почему))
AustereTony
AustereTony
Надеюсь кто-то прозреет)
Сверху