Миксины! Хуклиба на максималках.

Миксины! Хуклиба на максималках.

Версия(и) Minecraft
1.7.10, 1.12
mixin.png

Предисловие
Наконец я нашел мотивацию написать такую статью по моим любимым миксинам.
Статья предназначена не для новичков, а для людей, уже пытавших счастье в написании кормодов и возможно пользовавшихся хуклибой. Для более полного понимания происходящего можно ознакомиться с данными материалами:
Миксины в массы, так сказать!
Статья состоит в основном из адаптированного перевода официальной вики и явадоков, моих комментариев и пары примеров.

И да, кто-то может сказать, что юзать миксины для пары хуков - это как забивать гвоздь микроскопом, но что вы мне сделаете я в другом городе.

Назначение
Миксины - чрезвычайно мощный инструмент разработки, предназначенный для модификации и дополнения уже написанного скомпилированного кода (хуки в исходный код майнкрафта, другие моды, плагины и тд), основанный на преобразовании байткода с помощью ASM.

Миксины в основном предназначены для объемных проектов, меняющих много исходного кода. Но они существенно облегчают жизнь, если грамотно настроить рабочее пространство и знать как их использовать. С помощью них можно с легкостью изменить логику работы ванильных механик и добавить свои. Также миксины могут стать интересной альтернативой кузнецкой Capability.

Возможности
С помощью миксинов можно:
  • Изменять, дополнять методы классов (частично и полностью) множеством способов
  • Дополнять классы своими полями и методами
  • Не волноваться об обфускации и маппингах
  • Практически не волноваться с совместимостью модов, также использующих Mixins
  • Почти не волноваться обо всех гадких вещах, связанных с модификацией байткода
И вы думаете этого мало?

Начало работы
Большинство примеров и действий подкреплены кодом из репозитория. https://github.com/GlassSpirit/JustMixins/tree/master/SweetMixin_Forge_1_12

Подготовка рабочей среды
Нам понадобятся:
  1. Как ни странно среда разработки (я использую IntelliJ IDEA)
  2. Настоятельно рекомендую установить плагин Minecraft Dev Minecraft Dev for IntelliJ
Плагин позволяет быстро создавать новые проекты для основных платформ (forge, spigot, sponge), добавляет маленькие косметические штуки, а также крайне сильно упрощает работу с миксинами (всевозможные подсказки, автодополнения, проверка правильности миксинов и тд).

Создадим же новый Forge проект!

Изменения в build.gradle
Первым делом шаманим над build.gradle. Добавим репозиторий и зависимость mixins, также изменим buildscript - добавим туда помимо плагина forgegradle плагин mixingradle, который занимается магическими вещами, связанными с ремаппингом полей при сборке.
Рассмотрим пример на версии 1.12.
build.gradle:
buildscript {
   repositories {
       jcenter()
       maven {
           name = 'forge'
           url = 'https://files.minecraftforge.net/maven'
       }
       maven {
           name = 'sponge'
           url = 'https://repo.spongepowered.org/maven'
       }
   }
   dependencies {
       classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT'
       classpath 'org.spongepowered:mixingradle:0.6-SNAPSHOT'
   }
}

apply plugin: 'net.minecraftforge.gradle.forge'
apply plugin: 'org.spongepowered.mixin'

repositories {
   mavenCentral()
   maven {
       name = 'sponge'
       url = 'http://repo.spongepowered.org/maven'
   }
}

dependencies {
    compile 'org.spongepowered:mixin:0.8.1-SNAPSHOT'
}
Для того, чтобы мод правильно собирался в джарник, добавляем refmap (ту самую информацию о маппингах, которая генерируется автоматически) и информацию о миксине в MANIFEST.MF.
Не забываем добавить название нашего мода!

build.gradle:
sourceSets {
   main {
       ext.refMap = 'mixins.[название мода].refmap.json'
   }
}

jar {
   manifest.attributes(
           'TweakClass': 'org.spongepowered.asm.launch.MixinTweaker',
           'MixinConfigs': 'mixins.[название мода].json', // Файлы наших миксинов
           'FMLCorePluginContainsFMLMod': 'true',         // Говорим, что наш джарник еще и мод
           'ForceLoadAsMod': 'true',                      // Точно точно мод
   )
}
Рабочий build.gradle, который подойдет для большинства модов версии 1.12 можно найти тут.
https://github.com/GlassSpirit/JustMixins/blob/master/SweetMixin_Forge_1_12/build.gradle
В версии 1.7.10 для сборки проекта используется старая версия Gradle, которая не позволяет подключить плагин mixingradle, поэтому придется добавить пару магических строк вручную. Можно воспользоваться рабочим build.gradle из моего репозитория и изменить под свои нужды

Магический файл mixins.json
Создадим новый файл в корне папки resources с названием mixins.[название мода].json.
Именно его мы указываем в MANIFEST.MF. Он сообщает системе о версии миксинов, которую нужно использовать, о том, какие миксины нужно применить, на какой стороне, где они находятся и тд.
Его наполнение будет примерно таким:
JSON:
{
    "minVersion": "0.7.10",
    "compatibilityLevel": "JAVA_8",
    "package": "[путь и название мода].mixin",
    "refmap": "mixins.[название мода].refmap.json",
    "mixins": [
        "здесь будут",
        "классы наших миксинов",
        "относительно package"
    ],
    "client": [],
    "server": []
}
Из этих параметров нас интересуют:
minVersion - Версия Mixins, которую мы используем.
package - Папка с вашими миксинами.
refmap - Пусть к автоматически сгенерированному refmap (из build.gradle)
mixins - Перечисление всех ваших миксинов внутри package. Эти миксины будут применены и на сервере, и на клиенте. Если нужно применить миксин только на одной стороне, используй client и server.

Сборка и запуск мода
По хорошему, чтобы собрать наш мод, нужно просто нажать кнопочку build. Если все прошло хорошо, мы увидим наш джарник, а в нем файлы mixins.[название мода].json и mixins.[название мода].refmap.json, а также в магические строки в файле META-INF/MANIFEST.MF.

Для того же, чтобы запустить и протестировать мод с миксинами в рабочей среде, необходимо сделать пару несложных движений. В параметры конфигурации запуска клиента/сервера (В IntelliJ IDEA штука рядом треугольником запуска => Edit Configurations => Application => run client/server)
в параметры запуска (arguments) необходимо добавить строку
--tweakClass org.spongepowered.asm.launch.MixinTweaker --mixin mixins.[название мода].json

Запуск мода после сборки
Итак, вы собрали джарник, закинули в папку mods, но игра не запускается и в логах написано
Unable to launch java.lang.ClassNotFoundException: org.spongepowered.asm.launch.MixinTweaker?
Дело в том, что для работы системы миксинов нужна сама система миксинов, которой изначально нет на клиенте/сервере (если вы не используете SpongeForge). Вариантов решения два: либо засовывать систему миксинов в каждый мод, использующий ее, либо поставить мой вспомогательный мод JustMixins (который сам включает в себя только систему, инициализирует ее и позволяет использовать любым другим модам). Первый же вариант можно реализовать, посмотрев в build.gradle этого мода.

Создаем первый миксин
На этом этапе у вас должна быть настроена рабочая среда, создан Forge проект и добавлены основные файлы. Здесь мы рассмотрим, как создать свой первый миксин, как добавить новое поле или метод, как к ним обратиться и как быстро и просто дополнить существующие методы.

Добавление новых полей и методов в существующие классы
Рассматривать будем заезженную тему в новом свете - добавление некого нового абстрактного ресурса - Эссенции. Предположим эссенцию будут иметь возможность содержать любые вещи, но мы рассмотрим игрока. Максимум эссенции игрока будет равен его уровню.
Создадим интерфейс EssenceContainer, который будут реализовывать классы, имеющие эссенцию, а также новый пакет с названием mixin и класс MixinEntityPlayer с аннотацией @Mixin. Класс можно сделать абстрактным, это позволит не реализовывать методы, взятые из целевого класса.
В аннотации @Mixin укажем целевой класс - тот, к которому будет применен данный миксин. Так как пока что мы добавляем эссенцию только игроку, целевой класс - EntityPlayer.
(!!!) Не забудьте добавить MixinEntityPlayer в mixins.[название мода].json

EssenceContainer:
package ru.glassspirit.sweetmixin;

public interface EssenceContainer {

    float getEssence();

    void setEssence(float value);

    float getMaxEssence();

    void setMaxEssence(float value);

}
MixinEntityPlayer:
package ru.glassspirit.sweetmixin.mixin;

@Mixin(EntityPlayer.class)
public abstract class MixinEntityPlayer implements EssenceContainer {

    /**
     * Наше новое поле в классе EntityPlayer - эссенция
     */
    private float essence;

    /**
     * Виртуальное поле, которое ссылается на реальное поле experienceLevel в классе EntityPlayer
     */
    @Shadow(remap = true)
    public int experienceLevel;

    @Override
    public float getEssence() {
        return this.essence;
    }

    /**
     * Изменяет текущее количество эссенции игрока. Если оно больше, чем максимально возможное для игрока - ставится максимальное
     */
    @Override
    public void setEssence(float value) {
        this.essence = Math.min(value, getMaxEssence());
    }

    /**
     * Максимальное количество эссенции игрока равно его уровню
     */
    @Override
    public float getMaxEssence() {
        return this.experienceLevel;
    }

    @Override
    public void setMaxEssence(float value) {
        // NOOP
        System.out.println("Максимальное количество эссенции зависит от уровня игрока!");
    }
}

Простым движением руки - объявлением нового поля и методов, мы говорим системе миксинов изменить байткод класса EntityPlayer, добавив совершенно новые поля и методы. Теперь у каждого игрока потенциально будет эссенция =).

Аннотация Shadow
Эта аннотация указывает на то, что данное поле или метод ссылается на существующее поле или метод в целевом классе. В данном случае в классе EntityPlayer есть публичное поле experienceLevel, и мы хотим использовать его значение в нашем миксине в методе getMaxEssence(). Аналогично можно вызывать методы или изменять значения полей целевого класса.

Параметр remap в Mixin, Shadow, Inject и всех других местах
Параметр remap (true по умолчанию) отвечает за то, подвергается ли поле ремаппингу после компиляции. Чаще всего работает такое правило: если это поле/метод из ванильного кода - ремаппинг необходим. Если пытаемся получить доступ, изменить или сделать еще что-то с кодом Forge или других модов - обязательно ставим remap = false.
Информация про обфускацию и ремаппинг, для чего нужен тот самый магический refmap, и как же все это работает, вот (базовая инфа, рус) и вот (более подробно)

Простейшее дополнение существующих методов (хуки)
Ну вот мы и добрались до самого интересного. Система миксинов в сочетании со средой разработки и плагином Minecraft Dev позволяет полностью избавить мододела от бесполезной работы по поиску опкодов, линий для вставки кода, определения параметров и прочей ереси, с которой сталкивались те, кто работал с хуклибой, а не то и с кормодами.
Разберем же наиболее используемые приемы! Продолжим наш пример с эссенцией. Да, она теперь есть у игрока, но… Она не сохраняется после выгрузки игрока из памяти, да и вообще не изменяется! Исправим же это =)

Для начала разберемся, как сохранить количество нашей эссенции на века. Для хранения параметров сущностей используется NBT, следовательно наиболее логично будет влезть в методы writeEntityToNBT и readEntityFromNBT целевого класса EntityPlayer.

Аннотация Inject
Аннотация @Inject указывает, что система миксинов должна вставить вызов нашего метода-обработчика в целевой метод в определенном месте. Параметр method указывает на метод в целевом классе, в который необходимо внедриться. Параметр at указывает, в какое место/места это нужно сделать. Чаще всего используются @At("HEAD") - самое начало метода, и @At("TAIL") - перед самым последним return из метода. Также можно внедриться после вызова определенного метода, доступа к полю, с определенным сдвигом и тд, для полного списка возможностей читай далее раздел магии.

Создадим метод onWriteEntityToNBT и onReadFromNBT, настроим аннотации и добавим необходимый нам код (в данном случае - сохранение и загрузка поля essence из NBT).

Java:
@Inject(method = "writeEntityToNBT", at = @At("TAIL"))
private void onWriteEntityToNBT(NBTTagCompound compound, CallbackInfo ci) {
   compound.setFloat("Essence", this.getEssence());
}

@Inject(method = "readEntityFromNBT", at = @At("TAIL"))
private void onReadEntityFromNBT(NBTTagCompound compound, CallbackInfo ci) {
   setEssence(compound.getFloat("Essence"));
}
Готово! Теперь, когда Minecraft будет вызывать эти методы при сохранении и загрузке игрока, он также будет сохранять и загружать значения нашей эссенции.

Использование новых методов класса
Для красоты примера добавим немного логики нашему моду-примеру. Пусть эссенция будет необходима для того, чтобы ломать блоки, а зарабатываться она будет при нанесении урона живым существам (приветик моей шизе).

Чтобы в использовать новые поля и методы, достаточно просто сделать каст объекта EntityPlayer к нашему интерфейсу (как ни странно, компилятор Java позволяет это сделать, даже если изначально класс этот интерфейс не реализует).
Создадим слушатель событий на эти два ивента:

Java:
@Mod.EventBusSubscriber
public class SweetMixinListener {

   @SubscribeEvent
   public static void onEntityKill(LivingHurtEvent event) {
       if (event.getSource().getTrueSource() instanceof EntityPlayer) {
           EssenceContainer essencePlayer = (EssenceContainer) event.getSource().getTrueSource();
           essencePlayer.setEssence(essencePlayer.getEssence() + 1.0F);
       }
   }

   @SubscribeEvent
   public static void onBlockBreak(BlockEvent.BreakEvent event) {
       if (event.getPlayer() != null) {
           EssenceContainer essencePlayer = (EssenceContainer) event.getPlayer();
           if (essencePlayer.getEssence() > 0) {
               essencePlayer.setEssence(essencePlayer.getEssence() - 1.0F);
           } else event.setCanceled(true);
       }
   }
}
Если не получается сделать прямой каст final класса (например ItemStack) к нашему интерфейсу, можно прибегнуть к хитрости - сначала сделать каст к Object, а затем к интерфейсу.

Также для доступа к полям и методам можно использовать рефлексию, но делать этого я вам крайне не советую…

Попрошу отметить, что пример создан только чтобы показать самую малость возможностей миксинов, а не для реализации полной новой механики!

Подробнее про магию
В этом разделе подробнее разберем основные возможности и подводные камни системы миксинов.

Мистический Inject - тут добавим, тут отрежем
Указывает, что система миксинов должна вставить обратный вызов к нашему обработчику в целевом методе. Используем его, когда хотим добавить какую-либо логику к существующему методу, или же прервать его выполнение раньше, чем задумано изначально.
Методы, аннотированные @Inject всегда должны быть VOID и иметь CallbackInfo/CallbackInfoReturnable в параметрах (этот объект генерируется обратным вызовом и служит дескриптором, который можно использовать при отменяемых инъекциях, подробнее далее).

Простейшее использование
Вот простейший пример использования при инъекции в void метод без параметров:

Java:
@Inject(method = "update", at = @At("HEAD"))
private void onUpdate(CallbackInfo ci) {
    Observer.instance.foo(this);
}
Получаем аргументы целевого метода
При внедрении в метод с аргументами, их можно передать в метод-обработчик, указав эти аргументы перед CallbackInfo. Пример:

Java:
/**
* Целевой метод, setPos
*/
public void setPos(int x, int y) {
    Point p = new Point(x, y);
    this.position = p;
    this.update();
}

/**
[LIST]
[*]Метод-обработчик внутри класса миксина, onSetPos.
[*]Обратим внимание на переменные int x и y перед объектом CallbackInfo
[/LIST]
*/
@Inject(method = "setPos", at = @At("HEAD"))
protected void onSetPos(int x, int y, CallbackInfo ci) {
    System.out.printf("Position is being set to (%d, %d)\n", x, y);
}
Завершающие инъекции
До этого момента наши инъекции не меняли структуру целевого метода, они просто вызывали нашу функцию-обработчик. Завершающие инъекции позволяют нам преждевременно завершить (вставить return) целевой метод.
Для этого необходимо в аннотации @Inject выставить параметр cancellable = true и в нужном нам месте вызвать cancel(). Пример:

Java:
/**
* Завершающая инъекция, обратите внимание на параметр "cancellable" в аннотации
*/
@Inject(method = "setPos", at = @At("HEAD"), cancellable = true)
private void onSetPos(int x, int y, CallbackInfo ci) {
    // Check whether setting position to origin and do some custom logic
    if (x == 0 && y == 0) {
        // Some custom logic
        this.position = Point.ORIGIN;
        this.handleOriginPosition();

        // Call update() just like the original method would have
        this.update();

        // Mark the callback as cancelled
        ci.cancel();
    }

    // Execution proceeds as normal at this point, no custom handling
}
В приведенном выше примере обработчик проверяет, установлена ли позиция в (0, 0), и если условие выполнено, вместо стандартного поведения целевого метода он выполняет некоторую логику, помечая обратный вызов как завершенный, чтобы заставить целевой метод (setPos) завершиться сразу после завершения обработчика.

Целевые методы, возвращающие тип
До сих пор мы внедрялись только в void методы. При внедрении в метод с возвращаемым типом, в обработчике вместо CallbackInfo следует указать CallbackInfoReturnable<ВозвращаемыйТип>. Он отличается от своего собрата тем, что при завершающей инъекции следует использовать не cancel(), а setReturnValue(). Пример:

Java:
@Inject(method = "getPos", at = @At("HEAD"), cancellable = true)
protected void onGetPos(CallbackInfoReturnable<Point> cir) {

    if (this.position == null) {
        // setReturnValue заменяет cancel()
        cir.setReturnValue(Point.ORIGIN);
    }

    // Отметим, что если обработчик не устанавливает возвращаемое значение,
    // целевой метод продолжит работать как задуманно
}
Больше информации про Inject тут.
https://github.com/SpongePowered/Mixin/wiki/Advanced-Mixin-Usage---Callback-Injectors

Целься… Пли! Или что же написать в @At…
Пока что мы сталкивались только с двумя точками внедрения - HEAD и TAIL.
Они особые, поскольку являются единственными точками внедрения, которые гарантированно будут успешными, потому что в методе всегда есть бы один RETURN, и, естественно, всегда есть начало. Перед тем, как мы пойдем дальше, вот несколько ключевых вещей, которые следует понимать о точках внедрения:
  1. В большинстве случаев, инжектор вставит код ПЕРЕД опкодом, найденным точкой внедрения. Вот пара примеров:
    • RETURN определяет коды операций RETURN в методе, внедрение происходит непосредственно перед возвратом метода
    • HEAD идентифицирует первый код операции в методе, внедрение происходит в самом начале метода
    • INVOKE (см. ниже) идентифицирует вызов метода, внедрение происходит непосредственно перед вызовом найденного метода
  2. Поскольку точки внедрения фактически являются запросами, они могут не возвращать результатов (не найти опкод, удовлетворяющий параметрам поиска).
  3. Хотя точки внедрения очень гибки, не стоит забывать, что код, в который мы внедряемся, в будущем может измениться (выйти новая версия мода, например).
  4. Определение более сложных точек внедрения является одним из немногих мест в системе миксинов, где вам придется испачкать руки и взглянуть на байт-код целевого метода. Это часто необходимо, чтобы выбрать наиболее подходящее место для внедрения кода (хотя плагин Minecraft Dev прекрасно справляется с большинством таких ситуаций).
Какие же есть еще точки внедрения?
Помимо надежных HEAD и TAIL с RETURN, являющихся молотком и отверткой в нашем наборе инструментов, есть несколько других предопределенных точек, которые вы можете использовать:
  • INVOKE - Находит вызов указанного в target метода.
  • FIELD - Находит обращение к указанному в target полю (чтение/запить)
  • NEW - Находит опкод new
  • JUMP - Находит код операции перехода (IF любого типа) и вставляет перед ним
  • INVOKE_ASSIGN - Находит вызов метода, который возвращает значение и внедряет его сразу же после присвоения значения локальной переменной. Обратите внимание, что это единственная точка, которая внедряет обработчик после своей цели
Ищем иголку в стоге сена
Как мы уже сказали, точки внедрения - это по сути запросы, возвращающие один или несколько опкодов, которые соответствуют заданным критериям. Да да, одна точка внедрения может указывать на несколько мест, т.е. опкодов. Для примера рассмотрим семантику точки возврата RETURN. Точка возврата ВОЗВРАТА определяется следующим образом:
  • RETURN соответствует всем кодам операций RETURN в целевом методе
Таким образом, если не указать никакой дополнительной информации, процессор миксинов вставит наш @Inject обработчик перед всеми RETURNами в целевом методе!
Чтобы различать подходящие точки внедрения, каждый найденный подходящий опкод помечается номером, начинающимся с нуля. Указывая параметр ordinal внутри @At означает, что мы хотим вставить обработчик только перед определенным опкодом. Это справедливо и для всех остальных типов точек внедрения (INVOKE, FIELD).

Больше информации о точках внедрения тут, а также явадоках к ним.
https://github.com/SpongePowered/Mixin/wiki/Injection-Point-Reference.

Таинственный Redirect - перенаправляем код в другое русло
Указывает, что система миксинов должна заменить указанный вызов метода, доступ к полю или создание объекта (через ключевое слово new) на вызов нашего обработчика. К перенаправлению стоит прибегать, когда нужно заменить, или же вовсе отменить вызов какого-либо метода, изменить значение получаемого поля или создание нового объекта в целевом методе.

Перенаправляем вызов метода
Сигнатура метода-обработчика (его параметры и возвращаемый тип) должна в точности соответствовать перенаправляемому методу, с предваряющим аргументом типа объекта-владельца (чтобы указать экземпляр объекта, для которого был вызван метод). Например, мы имеем такой код и хотим перенаправить вызов someObject.bar():
Java:
public void baz(int someInt, String someString) {
    int abc = 0;
    int def = 1;
    Foo someObject = new Foo();

    // Перенаправляем этот метод
    boolean xyz = someObject.bar(abc, def);
}
Аннотация и параметры метода-обработчика в этом случае будут такими:
Java:
@Redirect(method = "baz", at = @At(value = "INVOKE", target = "LFoo;bar(ILjava/lang/String;)Z"))
public boolean barProxy(Foo someObject, int abc, int def) {
    return someBoolean;
}
В результате, изначальный вызов someObject.bar() в целевом методе заменится за вызов нашего обработчика, и переменной xyz присвоится значение, которое вернет этот обработчик.

По понятным причинам для статических методов достаточно, чтобы сигнатура просто соответствовала перенаправляемому методу.

Также, помимо аргументов перенаправляемого метода, возможно получить аргументы целевого метода (например в примере выше это будут аргументы someInt и someString), добавив их в сигнатуру обработчика в конце:
Java:
public boolean barProxy(Foo someObject, int abc, int def, int someInt, String someString)
Перенаправляем доступ к полю
Сигнатура метода обработчика зависит от того, получаем ли мы значение поля (GETFIELD, GETSTATIC), или же записываем его (PUTFIELD, PUTSTATIC).
Операция (OPCODE)Сигнатура обработчика
Чтение статического поля (GETSTATIC)private static FieldType getFieldValue()
Чтение поля объекта (GETFIELD)private FieldType getFieldValue(OwnerType owner)
Запись статического поля (PUTSTATIC)private static void setFieldValue(FieldType value)
Запись поля объекта (PUTFIELD)private void setFieldValue(OwnerType owner, FieldType value)
Также возможно получить аргументы целевого метода (например в примере выше это будут аргументы someInt и someString), добавив их в сигнатуру обработчика в конце.
Пример:
Java:
class Foo {
    private int someInt;
    private String someString;
}

public static void doSomethingWithFoo(Foo foo) {
    System.out.print(foo.someString);
    foo.someInt = 10;
}

@Redirect(method = "doSomethingWithFoo", at = @At(value = "FIELD", target = "LFoo;someString:Ljava/lang/String;", opcode = Opcodes.GETFIELD))
private String getFooString(Foo foo) {
    if (foo.someString == null || foo.someString.isEmpty()) {
        return "Empty";
    } else return foo.someString;
}

@Redirect(method = "doSomethingWithFoo", at = @At(value = "FIELD", target = "LFoo;someInt:I", opcode = Opcodes.PUTFIELD))
private void setFooInt(Foo foo, int value) {
    if (value == 0) {
        foo.someInt = magicValue;
    } else foo.someInt = value;
}
Перенаправляем создание нового объекта
===заготовочка===

Перенаправляем вызов instanceof
===заготовочка===


Меняют все! ModifyArg(s), ModifyVariable, ModifyConstant
===заготовочка===


Прочие трюки

Перезапись методов, или нужен ли Overwrite?
Полной замены логики метода можно добиться, используя завершающий Inject с точкой внедрения @At("HEAD"), и я крайне советую вам пользоваться именно этим способом.
Но если все таки хочется именно полностью заменить целевой метод, можно воспользоваться аннотацией [USER=9909]@overwrite[/USER]. Таким образом, наш метод полностью заменит собой существующий в целевом классе метод, а не вставит обратный вызов к обработчику и return, как в случае с Inject.


Частые вопросы и их решение

В: Где бы еще про эти миксины почитать, можно даже на пиндосском
О: https://github.com/SpongePowered/Mixin/wiki и явадоки к аннотациям

В: А есть еще какие-нибудь примеры использования миксинов?
О:
В: Когда я пытаюсь сделать миксин, меняющий код другого мода, в среде разработки все идет как надо, а на проде краш!!! Что делать?!?!?!
О: К сожалению, на версиях 1.12 и ниже из за способа загрузки модов, нельзя миксинами изменить классы, которые лежат в других джарниках. Тык на ишью.
Все, что нам остается - пихать наш мод в ту же квартиру (джарник), что и мод, который мы изменяем. Да, неудобно, но к таким вещам обычно прибегают только когда у мода закрытые исходы и на приватных проектах, так что терпимо.

В: Так на 1.7.10 будет работать или как?
О: Ну, пробуем на свой страх и риск, у меня вроде бы работает. Пример и build.gradle находим тут.
Автор
GlassSpirit
Просмотры
343
Первый выпуск
Обновление
Оценка
4.67 звёзд 3 оценок

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

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

Все прочитал, гайд хороший. Но я так и не увидел, честно говоря, отличий от хуклибы и где она тут "на максималках". Все перечисленное делается с помощью той же хук либы. Куда уж проще, чем в ней?
Хотя INVOKE мне понравилось, хорошая тема. Но с тем же успехом можно и просто в начале метода выполнять свой код, разницы нет. За оформление 4 звездочки.
Все по полочкам. Отлично.
Миксины довольно полезны на 1.12. Но очень мало кто умел ими пользоваться
Сверху