Миксины для Чайников! Практика.

Миксины для Чайников! Практика.

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

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

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

Миксины лучше писать только на Java. На других языках (scala, kotlin) писать не рекомендуется по причине того, что будет сложно реализовывать некоторые хуки, да и с java байткодом понятнее всего взаимодействовать, используя java.

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

    И это далеко не все )
Начало работы
Большинство примеров и действий подкреплены кодом из репозитория. GlassSpirit/JustMixins

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

Скопируйте проект-пример для вашей версии из репозитория

Основные изменения в build.gradle
Предпочтительно за основу взять build.gradle нужной вам версии из репозитория.
В примере на 1.7 используется форк ForgeGradle 1.2, позволяющий использовать последние версии сборщика Gradle (изначально он работает только с Gradle 2.8, который даже уже не поддерживается IntelliJ IDEA). Это позволяет нам не проводить танцы с бубном для смены IDEA JRE и без проблем пользоваться новыми возможностями.

В примере на 1.12 и 1.15 используется ForgeGradle 4+ (изначально для работы с 1.12 требовался ForgeGradle 2.3). Он позволяет нам использовать систему Миксинов и плагин MixinGradle последних версий.
Добавились репозиторий Sponge, плагин MixinGradle (не для 1.7), зависимость от Mixins необходимой версии (для 1.7 самой последней является 0.7.11, версии выше работают крайне нестабильно), процессор аннотаций Mixins (проверяет правильность написанных миксинов, генерирует карты имен).
Для версии 1.7 нет плагина MixinGradle, так что некоторые действия придется дописать.
Не забудьте изменить имена mixinSrg и mixinRefMapName под ваш мод.
build.gradle:
// Дальше начинается магический код, который понятен лишь избранным, но вроде бы он заменяет фунционал
// MixinGradle плагина, отсутствующего для ForgeGradle 1.2 (что-то связанное с генерацией и упаковкой refmap)

ext {
    mixinSrg = new File(project.buildDir, 'mixins/mixin.[название мода].srg')
    mixinRefMapName = "mixins.[название мода].refmap.json";
    mixinRefMap = new File(project.buildDir, "mixins/" + mixinRefMapName)
}

jar {
    from project.mixinRefMap
}

reobf {
    addExtraSrgFile project.mixinSrg
}

compileJava {
    options.compilerArgs += [
            '-Xlint:-processing',
            "-AoutSrgFile=${project.mixinSrg.canonicalPath}",
            "-AoutRefMapFile=${project.mixinRefMap.canonicalPath}",
            "-AreobfSrgFile=${project.file('build/srgs/mcp-srg.srg').canonicalPath}"
    ]
}

task copySrgs(type: Copy, dependsOn: 'genSrgs') {
    from plugins.getPlugin("forge").delayedFile('{SRG_DIR}')

    include '*[I]/[/I].srg'
    into 'build/srgs'
}

compileJava.dependsOn copySrgs
Для того, чтобы мод определялся как миксин, добавляем информацию о миксине в MANIFEST.MF.
build.gradle:
jar {
    manifest {
        attributes([
                "Implementation-Title"    : project.name,
                "Implementation-Version"  : project.version,
                "TweakClass": "org.spongepowered.asm.launch.MixinTweaker",
                "MixinConfigs": "mixins.[название мода].json", // Конфигурации миксинов, через зяпятую если их несколько
                "FMLCorePluginContainsFMLMod": "true",  // Не забываем указать эти два флага,
                "ForceLoadAsMod": "true",               // чтобы Forge нормально загрузил и мод, и миксины
                "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
        ])
    }
}

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

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

Запускать и дебажить мод следует через сгенерированные скрипты запуска.
На версиях 1.12+ в раздел "runs" следует добавить дополнительные аргументы запуска:
build.gradle:
// Нужно для работы миксинов в DEV среде
// Program arguments to setup and run MIXINS
args '--tweakClass', 'org.spongepowered.asm.launch.MixinTweaker', '--mixin', 'mixins.[название мода].json'
Для того, чтобы запустить и протестировать 1.7.10 версию с миксинами в рабочей среде, необходимо сделать пару несложных движений. В параметры конфигурации запуска клиента/сервера (В IntelliJ IDEA штука рядом треугольником запуска => Edit Configurations => Application => run client/server)
в параметры запуска (arguments) необходимо добавить строку
--tweakClass org.spongepowered.asm.launch.MixinTweaker --mixin mixins.[название мода].json

Запуск мода после сборки
Версии 1.7 и 1.12.

Для того, чтобы система миксинов начала работать, необходимо добавить и развернуть саму либу миксинов, которой изначально нет на клиенте/сервере. В среде разработки это происходило за счет зависимости от Mixins и аргументов запуска.
Вариантов решения два: либо засовывать систему миксинов в каждый мод, использующий ее, либо поставить мой вспомогательный мод JustMixins
Правильнее всего будет поставить нормальный мод для инициализации миксинов (Grimoire для 1.7.10 и MixinBootstrap для 1.12+).

Версии 1.15+.
Версии 1.15 и 1.16 уже включают в себя систему миксинов, так что сторонних действий не требуется.

Создаем первый миксин
На этом этапе у вас должна быть настроена рабочая среда, создан 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 тут.
SpongePowered/Mixin

Целься… Пли! Или что же написать в @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).

Больше информации о точках внедрения тут, а также явадоках к ним.
SpongePowered/Mixin.

Таинственный 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;
}

Меняют все! ModifyArg(s), ModifyVariable, ModifyConstant
С помощью этих аннотаций можно изменить значения входных параметров при вызове метода, изменить значение локальной переменной внутри метода или изменить значение констант.
Ссылки на JavaDocs:
ModifyArg
ModifyArgs
ModifyVariable
ModifyConstant

Прочие трюки

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


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

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

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

Смотрим как деланы Grimoire патчи, ставим сам Grimoire и делам так же.
Автор
GlassSpirit
Просмотры
16,671
Первый выпуск
Обновление
Оценка
4.83 звёзд 6 оценок

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

Последние обновления

  1. Обновление JustMixins и примеров SweetMixin

    Обновлены JustMixins и примеры SweetMixin, добавлен пример для 1.15+ (использование форка FG 1.2...
  2. Полировка текста и поддержка 1.15+

    Отредактировано большинство неточностей, добавлены некоторые параграфы, добавлена потенциальная...

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

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