- Версия(и) 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:
Теперь нужен класс-трансформер с интерфейсом IClassTransformer, в котором и будет происходить работа по изменению классов. Мой CustomEventsTransformer:
В методе
Для подгрузки плагина в IDE добавьте новый аргумент VM, в котором нужно указать путь к классу-плагину:
Чтобы плагин грузился форджем после компиляции мода в сторонних клиентах добавьте этот блок в build.gradle:
В нём также требуется указать путь к плагину. Во второй строчке при наличии контейнера для кормода поставьте
Основы трансформации
Прежде чем наложить руки на святое и перейти к ковырянию классов майна потренируемся работать с байт-кодом и делать простенькие трансформации. Зная как вы относитесь к прочтению подготовительного материала, я опишу процесс трансформации максимально подробно на простеньком примере.
Итак, допустим у нас есть такой класс TestClass:
Попробуем вставить в него свой код, но сначала посмотрим как класс выглядит в байт-коде. Вот что показывает плагин Bytecode Outline:
Понятно… Я добавил комментарии для большей ясности, сравните класс в привычном виде и в таком. Естественно это не байт-код, а лишь его представление в виде имён опкодов, это делает проектирование хуков и их внедрение максимально простым. Имена опкодов операций можно найти в классе Opcodes, там же указаны классы, которые применяются для создания инструкций с этими опкодами. После названия методов в заголовках можно увидеть дескрипторы. Стандартный вид дескриптора – ()V – что значит метод не имеет аргументов и возвращаемого типа (тип void). Примитивы в дескрипторах обозначаются единственной заглавной буквой (I – int, Z – boolean и т.д.). Объекты имеют вид – L<имя класса>; - например: Lnet/minecraft/client/Minecraft;. В конце каждого блока методов указываются размер стека и локальные переменные.
Я добавил объявление и инициализацию TestClass в главный класс мода для его создания при загрузке самого мода для отладочных нужд:
При инициализации главного класса создаётся TestClass и мы видим в консоли сообщение, которое выводится при вызове метода
Допустим класс не доступен для прямых изменений. Попробуем вставить в метод
Добавить такую конструкцию не сложно, однако всегда можно упростить себе задачу (как это делает фордж) – вынести эту довольно массивную строку в собственный класс в статический метод и внедрить в нужное место его вызов. Так и поступим. Создаём класс TestInjections и метод
Теперь ручная вставка была бы такой:
Такую трансформацию произвести гораздо проще. Давайте сравним байт-коды метода без хука, и с ним. Стандартный метод:
С вызовом статического метода:
Отличается он от стандартного наличием четырёх новых опкодов в новой строке #2 – инструкций, которые нужно добавить.
Давайте попробуем трансформировать класс. В классе CustomEventsClassTransformer (ваша реализация IClassTransformer) создаём метод
Для трансформации я использовал возможности ASM Tree API и вот какой алгоритм у меня получился:
Вот так вот, ничего сложного в таком хуке нет. Врятли можно разжевать это подробнее. Запустим игру и смотрим в консоль:
Узнали всё таки. Как видно, трансформация класса происходит при его первом вызове (в данном случае инициализации при создании нового экземпляра в главном классе мода). После вызова
Теперь неплохо бы прервать вызов метода
Время вышло. В данном случае достаточно вставить точку выхода
Запустите игру и теперь в консоли вы не увидите сообщение из метода
Данный раздел будет содержать два примера. В первом будет рассмотрено простое неотменяемое событие (целых два, да ещё и с подклассами), которые будут срабатывать при добавлении предмета в слот инвентаря и извлечении. Во втором событие будет с возможностью отмены и наличием фаз, суть его будет в возможности отмены использования клавиш F1 и F3. Интересно? Вперёд!
Пример первый
Небольшое отступление. Полезность такого события заключается например в том, что можно отследить изменения в экипировке игрока, когда он что то перемещает по слотам, это позволит с лёгкостью отследить какой предмет куда добавлен, откуда убран, был ли заменён и пр.
Итак, сначала необходимо разобраться с тем, где должен происходить вызов вашего события, а так же тем, какую информацию при его срабатывании вы хотите получить. Допустим, нужно отследить изъятие предмета из слота. Лезем в класс Slot и видим такой метод:
Он срабатывает при извлечении предмета из слота и нам явно нужно работать с ним. Из аргументов видно, что нам доступен игрок, предмет, над которым висит курсор, ну и само собой все поля класса. Мы можем добавить вызов статического метода из
Прикидываем что мы имеем и создаём класс события с двумя подклассами-фазами. TakeStackSlotEvent:
Конструктор содержит Slot, EntityPlayer и ItemStack, ссылки на которые он получит при вызове события из статического метода, который мы будем внедрять в Slot и создадим чуть позже. Из объекта слота мы достаём все, чтоесть может быть нужно и создаём геттеры для удобства.
Далее, идя по стопам форджа, создаём класс для статических методов CustomEventFactory, в котором размещаем два метода для двух фаз события. Размещаем в них вызовы событий. Аргументы методов аналогичны аргументам конструктора эвента:
Для отлавливания момента добавления предмета в слот придётся модифицировать метод
Тут в качестве аргумента у нас есть добавляемый ItemStack. Прискорбно, но EntityPlayer нам не додали, но постойте - можно достать его из IInventory, приведя его к InventoryPlayer при желании.
Создаём событие PutStackSlotEvent:
В конструкторе только слот и стак.
Методы в CustomEventsFactory:
Простая часть закончилась, теперь нужно внедрить вызовы событий в Slot в соответствующие методы. Происходит всё в классе-трансформере в методе
Прежде чем лезть в класс нужно прикинуть, какие изменения и в каком месте нужно сделать. У нас есть исходный метод
И есть его представление в байт-коде:
При наличии вызовов событий он должен выглядеть так:
И его байт-код:
Всё понятно, а если нет – читайте сначала... Нам нужно вставить четыре новых инструкции (передать три локальных переменных и вызвать статический метод) в начало метода, перед узлом ALOAD 0 (переменная Slot (ссылка
Далее метод
Байт-код:
Вид после модификации:
Байт-код:
Поступаем аналогично
Алгоритм трансформации получился такой:
Обратите внимание на то, что трансформация предполагает использование обфусцированных имён классов и методов. Для работы трансформера вне IDE требуется указать обфусцированные имена объектов. Просмотреть их можно к примеру с помощью bspk MCP Mapping Viewer (для последних версий названия обфусцированных классов указаны некорректно, с методами и полями всё нормально). Дескрипторы всех методов, работающих с классами майнкрафта должны также зависеть от наличия обфускации.
Как вы наверное заметили, код содержит выводы отладочных сообщений в консоль для проверок. Вот лог процесса трансформации класса Slot:
Если осилили - поздравляю. Показанная здесь трансформация заключается в паре (пар) вставок вызовов статических методов, которая довольно проста для понимания и воспроизведения.
Испытание события. Добавим в CustomEvents конечные фазы событий добавления и изъятия предметов из слота:
Тут производится проверка на действия в инвентаре игрока и исполнение на серверной стороне. В контексте статьи содержимое событий комментировать нет смысла. Вот вам видеоролик (ЯД, по какой то причине при просмотре в браузере звук отвратительного качества): демонстрация работы событий.
Чувствуете потенциал? Самое время рассмотреть кое что посложнее…
Второй пример
Посмотрим на особенности внедрения отменяемых событий...
Надеюсь, вы найдёте статью полезной хотя бы в рамках пособия по работе с ASM. Я старался донести то, что создавать и внедрять события очень просто, а польза от них может быть огромной. Исходники на GitHub содержат полностью рабочий проект, примеры из которого рассмотрены здесь. Работоспособность трансформеров тестировалась на стороннем сервере и клиенте.
Для тех кто всё ещё считает применение ASM ниже своего достоинства (мы то знаем почему) всегда есть удобная альтернатива - [Гайд][Легко][1.6+] Модификация чужого кода при запуске (трансфомеры) от @GloomyFolken. Но я всё же рекомендую научится использовать трансформеры напрямую.
Хочу в конце извиниться за возможные ошибки (я не профи), сообщайте о неточностях в обсуждении. Я потратил очень много времени на подготовку материала, так что пожалуйста оцените статью – мне важно знать, насколько они вам интересны. Спасибо за внимание!
Собственные события
Часть вторая
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). Выходим из циклов.
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);
}
}
}
}
Тут производится проверка на действия в инвентаре игрока и исполнение на серверной стороне. В контексте статьи содержимое событий комментировать нет смысла. Вот вам видеоролик (ЯД, по какой то причине при просмотре в браузере звук отвратительного качества): демонстрация работы событий.
Чувствуете потенциал? Самое время рассмотреть кое что посложнее…
Второй пример
Посмотрим на особенности внедрения отменяемых событий...
Второй пример - создание и внедрение отменяемого события - будет дописан если будут пожелания. А ещё я сомневаюсь, что хватит допустимого кол-ва символов для статьи .
Заключение
Надеюсь, вы найдёте статью полезной хотя бы в рамках пособия по работе с ASM. Я старался донести то, что создавать и внедрять события очень просто, а польза от них может быть огромной. Исходники на GitHub содержат полностью рабочий проект, примеры из которого рассмотрены здесь. Работоспособность трансформеров тестировалась на стороннем сервере и клиенте.
Для тех кто всё ещё считает применение ASM ниже своего достоинства (мы то знаем почему) всегда есть удобная альтернатива - [Гайд][Легко][1.6+] Модификация чужого кода при запуске (трансфомеры) от @GloomyFolken. Но я всё же рекомендую научится использовать трансформеры напрямую.
Хочу в конце извиниться за возможные ошибки (я не профи), сообщайте о неточностях в обсуждении. Я потратил очень много времени на подготовку материала, так что пожалуйста оцените статью – мне важно знать, насколько они вам интересны. Спасибо за внимание!