- 808
- 3
- 127
О чем речь?
В последних версиях Minecraft'a разработчики всеми способами борются против того, чтобы моддеры изменяли классы в minecraft.jar. Для этого была выстроена огромная система, с помощью которой можно изменять (трансформировать) классы во время запуска игры. Однако, чтобы этим пользоваться, нужны некоторые познания в устройстве JVM и её байткода, а так же умение использовать библиотеку ASM.
Я написал небольшой мод, с помощью которого возможно вставлять хуки (вызовы своих статических методов) в код майнкрафта, форджа и других модов, не обременяя себя лишними познаниями. Если вы встроите его в сборку своего мода, она вырастет меньше, чем на 100 КБ.
Что можно сделать с помощью таких хуков?
Практически всё. Если немного пораскинуть мозгами, то 95% требующихся изменений вполне реально сделать вставкой хука (личная статистика, а не число из головы!) без особых костылей, а оставшиеся 5% - заменив с помощью хука всё содержимое метода.
Примеры конструкций, которые можно вставить через этот мод:
Короче говоря, вставить можно что угодно. Главное ограничение - код можно вставить только в начало или конец метода (точнее, в любую точку выхода, в том числе через return). Ну и в качестве костыля есть вставка по номеру строки, но я крайне не рекомендую ей пользоваться: она может сломаться от чего угодно.
Где мне взять этот мод и как его подключить?
GloomyFolken/HookLib
Чтобы подключить кормод в IDE, нужно закинуть в src сорцы с гитхаба, а потом открыть настройки запуска:
В IntelliJ IDEA: Run -> Edit configurations
В Eclipse: Run -> Run (Debug) configurations
И в VM arguments написать:
Указываются IFMLLoadingPlugin'ы через запятую. Замените название из примера на название своего HookLoader'a. О том, как его создать, читайте ниже.
Работает это на всех версиях майна, начиная с 1.6. Если вы используете 1.8+, замените импорты во всем пакете gloomyfolken.hooklib.minecraft: cpw.mods.fml на net.minecraftforge.fml.
Как, собственно, вставлять хуки?
Не так быстро. Сначала нужно сделать кормод (мод, который запускается до майнкрафта). "Главный" класс кормода - это класс, реализующий интерфейс IFMLLoadingPlugin. На деле большая часть возможностей кормодов не понадобится, нам всего лишь нужно выполнить свой код до запуска майна. Поэтому удобно наследовать мою базовую реализацию этого класса. Всё, что нужно сделать - зарегистрировать класс с хуками.
Итак, наконец-то можно приступать к написанию хуков. Вставляются они установкой аннотации @Hook над методом. Возьмем самый простой пример: мы хотим выводить размер окна при каждом ресайзе. Для этого достаточно такого кода:
Разберём подробнее.
Я научился вставлять хуки, но нужный мне метод/поле - private или protected. Что делать?
Какие ещё есть фичи?
Как запаковать получившийся кормод в .jar?
Практически также, как обычный мод, нужно лишь добавить в кормод два файла:
1) META-INF/MANIFEST.MF с таким содержимым:
2) methods.bin (в корне архива с модом). Это файл, который содержит обфусцированные названия методов. Копируете из src/gloomyfolken/hooklib/helper файл methods{xx}.bin (в зависимости от нужной вам версии майнкрафта), переименовываете и кладете в архив с модом. А в идеале - сгенерируйте methods.bin для нужной вам версии MCP самостоятельно. Для этого в сорцах лежит скриптик DictionaryGenerator. Без этого могут возникнуть внезапные ошибки уровня "can not find target method of hook" из-за того, что я генерировал для другой версии MCP.
Саму хуклибу придется паковать в ваш мод, так как в фордже нет механизма зависимостей для coremod'ов. Из-за этого убедительно прошу сменить название пакета gloomyfolken.hooklib, если вы собираетесь выкладывать мод в паблик, чтобы избежать конфиликтов между разными версиями хуклибы.
Всё сломалось. Почему?
P.S: текущая версия библиотеки тщательно тестировалась только с Minecraft 1.6.4, могут быть проблемы с более новыми версиями игры. Про все возникающие проблемы пишите здесь или на гитхабе.
В последних версиях Minecraft'a разработчики всеми способами борются против того, чтобы моддеры изменяли классы в minecraft.jar. Для этого была выстроена огромная система, с помощью которой можно изменять (трансформировать) классы во время запуска игры. Однако, чтобы этим пользоваться, нужны некоторые познания в устройстве JVM и её байткода, а так же умение использовать библиотеку ASM.
Я написал небольшой мод, с помощью которого возможно вставлять хуки (вызовы своих статических методов) в код майнкрафта, форджа и других модов, не обременяя себя лишними познаниями. Если вы встроите его в сборку своего мода, она вырастет меньше, чем на 100 КБ.
Что можно сделать с помощью таких хуков?
Практически всё. Если немного пораскинуть мозгами, то 95% требующихся изменений вполне реально сделать вставкой хука (личная статистика, а не число из головы!) без особых костылей, а оставшиеся 5% - заменив с помощью хука всё содержимое метода.
Примеры конструкций, которые можно вставить через этот мод:
Код:
public void foo(String someParameter){
MyHooksClass.onPreFoo(this, someParameter); // хук
bar(); // тело метода
MyHooksClass.onPostFoo(this, someParameter); // хук
}
Код:
public void foo(String someParameter){
if (MyHooksClass.onPreFoo(someParameter)) return; // хук
bar(); // тело метода
}
Код:
public String foo(String someParameter){
String local = bar(); // тело метода
return MyHooksClass.foo(local); // хук, передается значение, которое изначально шло в return
// return local; <-- оригинальный код, никогда не вызывается
}
Код:
public int getSomeInt(){
return 0;
// return someInt <-- оригинальный код, никогда не вызывается
}
Короче говоря, вставить можно что угодно. Главное ограничение - код можно вставить только в начало или конец метода (точнее, в любую точку выхода, в том числе через return). Ну и в качестве костыля есть вставка по номеру строки, но я крайне не рекомендую ей пользоваться: она может сломаться от чего угодно.
Где мне взять этот мод и как его подключить?
GloomyFolken/HookLib
Чтобы подключить кормод в IDE, нужно закинуть в src сорцы с гитхаба, а потом открыть настройки запуска:
В IntelliJ IDEA: Run -> Edit configurations
В Eclipse: Run -> Run (Debug) configurations
И в VM arguments написать:
Код:
-Dfml.coreMods.load=gloomyfolken.hooklib.example.ExampleHookLoader
Работает это на всех версиях майна, начиная с 1.6. Если вы используете 1.8+, замените импорты во всем пакете gloomyfolken.hooklib.minecraft: cpw.mods.fml на net.minecraftforge.fml.
Как, собственно, вставлять хуки?
Не так быстро. Сначала нужно сделать кормод (мод, который запускается до майнкрафта). "Главный" класс кормода - это класс, реализующий интерфейс IFMLLoadingPlugin. На деле большая часть возможностей кормодов не понадобится, нам всего лишь нужно выполнить свой код до запуска майна. Поэтому удобно наследовать мою базовую реализацию этого класса. Всё, что нужно сделать - зарегистрировать класс с хуками.
Код:
public class ExampleHookLoader extends HookLoader {
// не пишем в этом классе ВООБЩЕ ничего кроме того что есть в примере.
// Если вы случайно здесь загрузите какой-то класс майнкрафта, то все сломается.
// включает саму HookLib'у. Делать это можно только в одном из HookLoader'ов.
// При желании, можно включить gloomyfolken.hooklib.minecraft.HookLibPlugin и не указывать здесь это вовсе.
@Override
public String[] getASMTransformerClass() {
return new String[]{PrimaryClassTransformer.class.getName()};
}
@Override
public void registerHooks() {
//регистрируем класс, где есть методы с аннотацией @Hook
//обязательно нужно оставить название класса в виде строки, AnnotationHooks.class все сломает
registerHookContainer("gloomyfolken.hooklib.example.AnnotationHooks");
}
}
Итак, наконец-то можно приступать к написанию хуков. Вставляются они установкой аннотации @Hook над методом. Возьмем самый простой пример: мы хотим выводить размер окна при каждом ресайзе. Для этого достаточно такого кода:
Код:
@Hook
@SideOnly(Side.CLIENT)
public static void resize(Minecraft mc, int x, int y){
System.out.println("Resize, x=" + x + ", y=" + y);
}
- Модификаторы public и static обязательны для хук-метода, это должно быть очевидно.
- Если у аннотации @Hook не указано дополнительных элементов, то все параметры установлены по умолчанию. Это значит, что, во-первых, хук вставляется в начало метода, а, во-вторых, после вызова хука выполнение продолжается.
- Название целевого метода по умолчанию определяется названием хук-метода (в нашем случае - resize). Его можно задать отдельно, указав в аннотации элемент targetMethod = "targetMethodName".
- Этот же элемент аннотации нужно использовать, чтобы вставить хук в конструктор или инициализацию класса. Для вставки в конструктор нужно указать targetMethod = "<init>", в инициализацию класса = targetMethod = "<clinit>".
- Класс, куда вставлять хук, определяется первым параметром хук-метода (в нашем случае - Minecraft).
- Список параметров целевого метода определяется последующими параметрами хук-метода (в нашем случае - int, int).
- Первым аргументом хук-метод получает this (или null, если целевой метод статический).
- @SideOnly(Side.CLIENT) указан, чтобы этот метод не вызвал ошибку загрузки класса на сервере. Это нужно писать для всех хуков, которые вставляются в классы в пакете net.minecraft.client.
Код:
@Hook(injectOnExit = true, returnCondition = ReturnCondition.ALWAYS)
public static int getTotalArmorValue(ForgeHooks fh, EntityPlayer player, @ReturnValue int returnValue) {
return returnValue/2;
}
- injectOnExit = true значит, что хук вставляется на выходе из метода
- returnCondition = ReturnCondition.ALWAYS значит, что после вызова хук-метода сразу же вызывается return.
- Благодаря аннотации @ReturnValue перехватывается значение, которое изначально шло в return, и передается хук-методу.
- Целевой метод в итоге вернёт то, что вернул хук-метод, то есть уменьшенный вдвое показатель брони. Это можно изменить с помощью некоторых элементов аннотации @Hook.
Код:
@Hook(returnCondition = ReturnCondition.ON_TRUE, intReturnConstant = 100)
public static boolean getPortalCooldown(EntityPlayer player) {
return player.dimension == 0;
}
- returnCondition = ReturnCondition.ON_TRUE значит, что после вызова хук-метода вызывается return только если хук-метод вернул true.
- intReturnConstant = 100 задаёт значение, которое вернет целевой метод. Объект нельзя сделать элементом аннотации, поэтому есть аналогичные элементы для остальных примитивов и для String.
- Сам хук-метод проверяет измерение, в котором находится игрок, чтобы хук влиял только когда игрок в основном мире.
Код:
@Hook(injectOnExit = true, returnCondition = ReturnCondition.ON_TRUE, returnAnotherMethod = "getBrightness")
public static boolean getBrightnessForRender(Entity entity, float f, @ReturnValue int oldValue) {
return entity.height > 1.5f;
}
public static int getBrightness(Entity entity, float f, int oldValue) {
int j = ((oldValue >> 20)&15)/2;
int k = ((oldValue >> 4)&15)/2;
return j << 20 | k << 4;
}
- Хук вставляется на выходе из метода и вызывает return только когда хук-метод возвращает true, это уже проходили.
- returnAnotherMethod = "getBrightness" значит, что при вызове return после вызова хук-метода будет возвращено значение, которое вернет метод getBrightness в этом же классе и с этими же параметрами.
- Стоит отметить, что если бы мы не перехватывали возвращаемое значение (не добавляли @ReturnValue int oldValue), то и в метод getBrightness не надо было бы добавлять третий параметр.
Код:
@Hook(createMethod = true, returnCondition = ALWAYS)
public static int idDropped(BlockDirt blockDirt, int meta, Random rng, int fortune) {
return 2;
}
- createMethod = true значит, что при провалившейся попытке найти целевой метод для вставки хука создается новый метод.
- Список аргументов создаваемого метода берется из аргументов хук-метода.
- Возвращаемый тип создаваемого метода можно задать через targetMethodReturnType.
- Если targetMethodReturnType не указан, то возвращаемый тип берется из возвращаемого типа хук-метода.
Я научился вставлять хуки, но нужный мне метод/поле - private или protected. Что делать?
Просто измените модификатор доступа в сорцах - фордж объявляет всё public при запуске. Это абсолютно безопасно.
За этот мини-гайд спасибо Dahaka.
Инструкция по использованию Access Transformers (>= 1.7.10)
1. Создать папку src/main/resourses/META-INF.
2. Создать в ней файл <modid>_at.cfg.
3. Заполнить файл:
4. Добавить в build.gradle
Если 1.10.2, то еще и это:
5. Пересобрать проект.
Инструкция по использованию Access Transformers (>= 1.7.10)
1. Создать папку src/main/resourses/META-INF.
2. Создать в ней файл <modid>_at.cfg.
3. Заполнить файл:
Код:
# Это комментарий
# Все поля в классе Item будут публичными
public net.minecraft.item.Item *
# Все методы в классе Item будут публичными (опасно, может не собраться из-за того, что в производных классах эти методы будут приватными)
public net.minecraft.item.Item *()
#
# Следущие скопипащены из TinkersConstruct
# Если указывать поля и методы по одиночке, то процесс сборки не будет столь долгим, но это не точно
#
# Какое-то конкретное поле
public net.minecraft.item.ItemPickaxe field_150915_c
# Какой-то конкретный метод
public net.minecraft.entity.monster.EntitySlime func_70799_a(I)V
# Какой-то конкретный метод
protected net.minecraft.entity.item.EntityItemFrame func_110131_b(Lnet/minecraft/item/ItemStack;)V
Код:
jar {
manifest {
attributes 'FMLAT': '<modid>_at.cfg'
}
}
Код:
artifacts {
archives deobfJar
}
Код:
gradlew clean setupDecompWorkspace --refresh-dependencies
Какие ещё есть фичи?
- (новое) Обязательные хуки. Если добавить в аннотацию isMandatory = true, то при провале попытки вставки хука крашнется игра (иначе просто ругается в лог).
- Приоритет хука. Можно добавить элемент аннотации priority = HookPriority.HIGH, например. Хуки с более высоким приоритетом вызываются раньше.
- Явно заданный тип, возвращаемый целевым методом. С точки зрения JVM могут быть методы, которые отличаются только возращаемым типом. На практике компилятор таких методов не генерируют, но в некоторых случаях они могут встретиться (например, это можно сделать при обфускации через ProGuard). На этот случай есть элемент аннотации returnType. P.S: упс, я забыл добавить к нему поддержку примитивов и массивов.
- Вставка по номеру строки. Можно написать что-нибудь вроде injectOnLine = 1337, и хук будет вставлен в начало строки с номером 1337. На строке для этого должна быть какая-нибудь инструкция (не пустая строка / комментарий / скобка / етц). Главная проблема в том, что в MCP и в Minecraft'e номера строк разные. Для этого можно завести static final boolean MCP = true, при сборке мода заменять её на false, и писать injectOnLine = (MCP ? 1337 : 1488).
- Возврат null. Кроме примитивной константы и строки можно вернуть ещё null, делается это через returnNull = true.
- Перехват локальной переменной. Кроме аннотации @ReturnValue есть аналогичная @LocalVariable(номер_переменной). Названия переменных в коде могут не сохраняться, так что вот так вот. Чтобы узнать номер, рекомендую использовать методы в классе VariableIdHelper.
- Это не моя фича, но можно посмотреть, какой код сгенерируется после применения хука. Поможет в этом опция, которая сохраняет трансформированные классы на диск. Чтобы активировать её, в VM arguments пропишите
Код:
-Dlegacy.debugClassLoading=true -Dlegacy.debugClassLoadingSave=true
- Регистрация хука вручную, без аннотации. Удобно использовать из вашего HookLoader'а, делается это как-то так:
Код:
HookLoader.registerHook(AsmHook.newBuilder()
.setTargetClass("net.minecraft.client.renderer.texture.TextureManager")
.setTargetMethod("bindTexture")
.addTargetMethodParameters("net.minecraft.util.ResourceLocation")
.setHookClass(HOOKS_CLASS)
.setHookMethod("bindTexture")
.addHookMethodParameter("net.minecraft.util.ResourceLocation", 1)
.setReturnCondition(ReturnCondition.ALWAYS)
.build());
Как запаковать получившийся кормод в .jar?
Практически также, как обычный мод, нужно лишь добавить в кормод два файла:
1) META-INF/MANIFEST.MF с таким содержимым:
Код:
Manifest-Version: 1.0
FMLCorePlugin: name.of.YourHookLoader
FMLCorePluginContainsFMLMod: true
Created-By: 1.7.0 (Oracle Corporation)
Саму хуклибу придется паковать в ваш мод, так как в фордже нет механизма зависимостей для coremod'ов. Из-за этого убедительно прошу сменить название пакета gloomyfolken.hooklib, если вы собираетесь выкладывать мод в паблик, чтобы избежать конфиликтов между разными версиями хуклибы.
1) Чтобы в MANIFEST.MF появились нужные вам строки, добавьте в build.gradle следующий код:
2) Положите methods.bin в папку src/main/resources.
Код:
jar {
manifest {
attributes 'FMLCorePlugin': 'name.of.YourHookLoader'
attributes 'FMLCorePluginContainsFMLMod': 'true'
}
}
Всё сломалось. Почему?
1. За порядком загрузки классов всё же придется следить. Если вы загрузите ваш класс с хуками раньше, чем начнет грузиться майнкрафт, то вызовете этим загрузку классов майна в неподходящий момент и всё сломается.
1.1. Не надо пытаться объединить HookLoader и класс с хуками. HookLoader грузится до начала загрузки майна, контейнер с хуками - после!
1.2 Не надо пытаться написать что-то вроде registerHookContainer(MyHooks.class.getName()). Это тоже вызовет загрузку класса с хуками раньше чем надо.
2. Если все хуки работают в IDE, но ни один - в собранном моде, то, скорее всего, вы не положили нужный methods.bin куда надо.
3. Если все хуки работают в IDE, но какой-то один ломается в собранном моде, то, скорее всего, вам нужно самостоятельно сгенерировать methods.bin для вашей версии MCP. Как это сделать читайте выше.
4. Если на клиенте все работает, а при попытке запустить на сервере вылетает ошибка загрузки какого-то клиентского класса (например, EntityClientPlayerMP), то проблема в том, что вы не прописали у хук-метода @SideOnly(Side.CLIENT). Это обязательно нужно делать если вы вставляете хуки в классы, которые лежат в net.minecraft.client. Дополнительно было бы хорошей идеей вынести все такие хуки в отдельный класс и вызывать для него registerHookContainer() только если выполняется условие FMLLaunchHandler.side().isClient().
5. Если вы осилили весь этот текст, немножко подумали, но хуки все равно не работают - не бойтесь спросить в этой теме.
1.1. Не надо пытаться объединить HookLoader и класс с хуками. HookLoader грузится до начала загрузки майна, контейнер с хуками - после!
1.2 Не надо пытаться написать что-то вроде registerHookContainer(MyHooks.class.getName()). Это тоже вызовет загрузку класса с хуками раньше чем надо.
2. Если все хуки работают в IDE, но ни один - в собранном моде, то, скорее всего, вы не положили нужный methods.bin куда надо.
3. Если все хуки работают в IDE, но какой-то один ломается в собранном моде, то, скорее всего, вам нужно самостоятельно сгенерировать methods.bin для вашей версии MCP. Как это сделать читайте выше.
4. Если на клиенте все работает, а при попытке запустить на сервере вылетает ошибка загрузки какого-то клиентского класса (например, EntityClientPlayerMP), то проблема в том, что вы не прописали у хук-метода @SideOnly(Side.CLIENT). Это обязательно нужно делать если вы вставляете хуки в классы, которые лежат в net.minecraft.client. Дополнительно было бы хорошей идеей вынести все такие хуки в отдельный класс и вызывать для него registerHookContainer() только если выполняется условие FMLLaunchHandler.side().isClient().
5. Если вы осилили весь этот текст, немножко подумали, но хуки все равно не работают - не бойтесь спросить в этой теме.
P.S: текущая версия библиотеки тщательно тестировалась только с Minecraft 1.6.4, могут быть проблемы с более новыми версиями игры. Про все возникающие проблемы пишите здесь или на гитхабе.
Последнее редактирование: