Всем привет! Эта статья была написана из-за отсутствия адекватных русскоязычных руководств по моддингу, особенно для новых версий Minecraft. То, что я опишу ниже, будет применимо для таких загрузчиков модов, как Fabric и Forge, а также для реализации различных функций.
Для начала - немного теории. Когда вы пишете любую программу на Java и компилируете её, вы получаете файл с расширением .jar. Если открыть этот файл как архив, то внутри, среди прочего, вы найдёте файлы с расширением .class. Это и есть ваш код, но уже в виде Java байт-кода. Далее эти файлы загружаются в виртуальную машину Java (JVM) и выполняются. Любой загрузчик модов построен на том, что он модифицирует процесс загрузки классов, изменяя байт-код этих самых классов в соответствии с указаниями из модов. (Да, технически я очень упростил).
Однако не всё так просто. Зачастую код коммерческих приложений, включая Minecraft, проходит через обфускацию - процесс, при котором оригинальные названия классов, полей и методов заменяются на бессмысленные символы (например, a, b, aa). Это затрудняет обратную разработку. С Minecraft такая ситуация была долгое время, хотя в последних версиях официальные маппинги стали значительно лучше(а недавно и вовсе обфускацию отменили).
Разумеется, также понадобится среда разработки, такая как IntelliJ IDEA. Я выберу загрузчик Fabric, а в качестве инструмента для модификации байт-кода - Mixin. Всё это уже собрано в удобный шаблон проекта.
Итак, переходим в официальный репозиторий Fabric Example Mod. В ветке выбираем нужную версию игры (я выбираю 1.20.x).
После того, как скачаем, нам нужно понять какие требования к версии JDK у сборщика gradle, переходим в build.gradle и ищем:
java {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
Видно что требует jdk 21, качаем и ставим.
Так как данный репозиторий позволяет сразу запустить модуль(клиент или сервер) и протестировать что мы написали, то по умолчанию к коду не будут применяться карты обфускации, поэтому вручную их укажем.
Я уже указал:
loom {
splitEnvironmentSourceSets()
mixin {
defaultRefmapName = "your-mod-id-refmap.json"
useLegacyMixinAp = true
}
mods {
"modid" {
sourceSet sourceSets.client
}
}
}
тут я предлагаю вам стереть упоминания модуля main, в modid ибо не ясно зачем он. Видимо это что-то из API, но API мы не используем.
Так вот, вам надо вставить:
mixin {
defaultRefmapName = "your-mod-id-refmap.json"
useLegacyMixinAp = true
}
Что бы вышло так. К слову defaultRefmapName это название файла, в которым будут данные, которые сопоставляют что мы изменили в деобфусцированном коде, и где это находится в оригинальном, укажите какое угодно название.
Теперь надо скачать IDEA (у меня версия 2022.3.3 когда ещё не добавили ИИ).
Открываем IDEA и импортируем проект, скорей всего получите такое(иногда вообще ничего не будет, вручную перезагрузите gradle проект):

Собственно это происходит потому, что IDEA почему-то даже не пытается искать в системных переменных и выбирает последнюю версию. Поэтому переходим по File - Settings - Build, Execution, Deployment - Gradle - В Пункте Gradle JVM выбираем наш JDK , затем Apply и OK. Затем Project Structure - Project - SDK - выбираем наш JDK, так же Apply и OK, снова перезагружаем gradle проект.
После этого подтянуться зависимости, будет это около 10 минут. Теперь генерируем исходный код. Запускаем CMD переходим в корень проекта и выполняем gradlew genSources.
Теперь исходный код находится в .gradle/loom-cache/minecraftMaven/net/Minecraft и тут две папки клиент и сервер. Да мы при создании мода укажем что меняем, и от этого будем отталкиваться, переходим в клиент и там переходим в архив который перед форматом не содержит source , а там в net.minecraft и всё, код игры.
Теперь не много о структуре проекта. Собственно как и всегда код проекта в src, и у вас будет два модуля это main и client, но ещё можно добавить server, в зависимости от модуля, вам будут доступны соответствующие классы, но почему в main не доступно почти ничего, поэтому я просто советую его удалить(буквально нажать и delete).
В папке src -> client есть java, там не посредственно код а в resources конфиги, собственно для нас будет два главных:
-
fabric.mod.json - конфиг загрузчика, там на самом деле не много и он на почти не нужен, так как апи мы не будем использовать, а будем использовать перехватчик ClassLoader , то есть миксины.
fabric.mod.json синтаксис
"schemaVersion": 1
Назначение: Версия схемы Fabric mod metadata
Значения: Всегда 1
Всегда обязателен"id": "yourmod"
Назначение: Уникальный идентификатор мода
Формат: [a-z][a-z0-9_]
Всегда обязателен"version": "1.0.0"
Назначение: Версия мода
Формат: Semantic Versioning (major.minor.patch)
Всегда обязателен"name": "Your Awesome Mod"
Назначение: Человекочитаемое название мода
Формат: Любая строка
Обязательный пунктdescription
Назначение: Описание мода
Формат: Текст"authors": [
"YourName",
"AnotherDeveloper"
]
Альтернатива: Можно использовать "contributors"
"contributors": {
"Developer1": "Главный разработчик",
"Artist1": "Художник текстур"
}"contact": {
"homepage": "https://yourmod.site", "sources": "https://github.com/yourname/yourmod", "issues": "https://github.com/yourname/yourmod/issues", "discord": "https://discord.gg/yourmod", "email": "support@yourmod.site"
}"license": "MIT"
"icon": "assets/yourmod/icon.png"
Формат: PNG, размер 64x64 или 128x128 пикселей
Путь: Относительно папки src/main/resources/"environment": "" назначение: ГДЕ РАБОТАЕТ МОД Значения: "" - везде (клиент + сервер)
"client" - только клиент
"server" - только сервер
"dedicated_server" - только выделенный сервер-
"entrypoints": {
"main": [
"com.yourname.yourmod.YourMod"
],
"client": [
"com.yourname.yourmod.client.YourModClient"
],
"server": [
"com.yourname.yourmod.server.YourModServer"
]
}назначение: точки входа. Если не используем API , можно не указывать значения.
-
"mixins": [
"yourmod.mixins.json",
"yourmod.client.mixins.json"
]Назначение: конфиги с информацией о инъекцией кода(это мы и будем использовать)
Расширенный вариант:
"mixins": [
{
"config": "yourmod.mixins.json",
"environment": "*"
},
{
"config": "yourmod.client.mixins.json",
"environment": "client"
},
{
"config": "yourmod.server.mixins.json",
"environment": "server"
}
] -
"depends": {
"fabricloader": ">=0.15.0",
"minecraft": "~1.20.1",
"java": ">=17",
"fabric-api": "*"
}Назначение: Зависимости мода
Синтаксис: "*" - любая версия
"1.0.0" - точная версия
">=1.0.0" - версия или выше
"<=1.5.0" - версия или ниже
"1.0.0 - 2.0.0" - диапазон версий
"~1.0.0" - совместимая версия (1.0.x)Пример с зависимостью от других модов:
"depends": {
"fabricloader": ">=0.15.0",
"minecraft": "~1.20.1",
"java": ">=17",
"fabric-api": "*",
"cloth-config": ">=11.0.0",
"rei": ">=12.0.0"
} -
"suggests": {
"modmenu": ">=7.0.0",
"jei": ">=15.0.0",
"worldedit": "*"
}Назначение: рекомендация модов
-
"breaks": {
"old-version-mod": "<2.0.0",
"conflicting-mod": "*"
}Назначение: Несовместимые моды
-
"conflicts": {
"incompatible-mod": "*"
}Назначение: конфликтующие моды
-
"accessWidener": "yourmod.accesswidener"
Назначение: путь и сам файл, который содержит конфигурацию которая меняет инкапсуляцию полей и методов, меняет классы
Вот пример конфига:
{ "schemaVersion": 1, "id": "your-mod-id", "version": "${version}", "name": "Your Mod Name", "description": "Describe your mod here!", "authors": ["Your Name"], "contact": { "homepage": "https://your-homepage.com/", "sources": "https://github.com/your-username/your-repo" }, "license": "MIT", "icon": "assets/your-mod-id/icon.png", "mixins": [ "modid.client.mixins.json" ], "depends": { "fabricloader": ">=0.15.0", "minecraft": "~1.20.1", "java": ">=17", "fabric-api": "*" } } -
Конфиг миксина - Название то, что указали в конфиге.
mixin.json
required: true
Назначение: Определяет критичность миксинов для работы мода
Поведение: Если true - игра аварийно завершится при ошибке применения миксина
Рекомендация: Всегда true для основных миксинов, false только для опциональных улучшенийminVersion: "0.8.5"
Назначение: Минимальная версия библиотеки Mixin
Важно: Должна соответствовать версии Mixin в ваших зависимостях
Для Minecraft 1.20.1: Рекомендуется 0.8.5 или вышеpackage: "com.example.bettervillages.mixin"
Назначение: Корневой пакет для всех классов миксинов
Структура: Все миксины должны находиться в этом пакете или его подпакетахcompatibilityLevel: "JAVA_17"
Назначение: Указывает версию Java для компиляции миксиновrefmap: "bettervillages.mixins.refmap.json"
Назначение: Файл карты ссылок для работы в обфусцированной среде
Как работает: Хранит соответствия между названиями методов в разработке и в продакшене
Генерация: Автоматически генерируется при сборке, если настроено в build.gradle
Важность: Без refmap миксины не будут работать в собранном моде!-
mixins - Универсальные миксины:
Загружаются: На клиенте И на сервере
Использование: Для классов, существующих в обоих окружениях
Примеры: World, PlayerEntity, ItemStackclient - Клиентские миксины:
Загружаются: Только на клиенте
Использование: Для GUI, рендеринга, обработки ввода
Примеры: MinecraftClient, TitleScreen, InGameHudserver - Серверные миксины:
Загружаются: Только на сервере
Использование: Для логики сервера, AI, генерации мира defaultRequire: 1
Назначение: Количество обязательных целей для @Inject методов
Значение 1: Инъекция должна найти хотя бы одну цель (по умолчанию)
Значение 0: Инъекция опциональна, не вызовет ошибку если цель не найденаmaxShiftBy: 3
Назначение: Максимальное смещение при использовании @At(shift)
Безопасность: Предотвращает случайное смещение слишком далекоallowInjection: true
Назначение: Разрешить все инъекции
Безопасность: Можно установить false для отладкиinjectionPoints
Назначение: Список разрешенных точек инъекции
Стандартные: HEAD, TAIL, RETURN, INVOKEconformVisibility: true
Назначение: Приводит видимость перезаписанного метода к оригиналу
Пример: Если оригинальный метод protected, перезапись тоже будет protectedrequireAnnotations: true
Назначение: Требует явного указания @Overwrite аннотации
Безопасность: Предотвращает случайные перезаписи методов
Вот пример:
{ "required": true, "minVersion": "0.8", "package": "com.yourname.yourmod.mixin", "compatibilityLevel": "JAVA_21", "refmap": "client-your-mod-id-refmap.json", "client": [ "DeathScreenMixin", "ScreenMixin" ], "injectors": { "defaultRequire": 1 } }Собственно в client то, что содержит Mixin, refmap содержит то, что в build.gradle
Теперь то что касается Mixin:
Нам надо определиться какой класс мы хотим изменить. Пусть это будет класс DeathScreen , это GUI которая показывается когда игрок погибает.
Назовём его DeathScreenMixin , теперь что бы указать что мы хотим что-то сделать с DeathScreen, нужно перед сигнатурой класса указать аннотацию @Mixin с одним аргументом, value , которому передаём либо один класс либо через массив указываем набор классов. Вот пример:
package com.yourname.yourmod.mixin;
import net.minecraft.client.gui.screen.DeathScreen;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(value = DeathScreen.class)
public class DeathScreenMixin {
}
Ещё если нескольких таких классов, можно добавить второй аргумент - priority и указать значение до 1000. Собственно 0 - макс. приоритет.
Теперь, рассмотрим самое базовое, мы хотим полностью переписать метод, пусть будет render в DeathScreen, находим метод и его сигнатуру:
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
}
Собственно в нашем классе пишем:
@Overwrite
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
return;
}
Самое просто это тут же отменить метод, то есть в теле метода прописать return; Теперь что бы протестить достаточно запустить модуль client, и вот результат:

Усложнение задачи, мы хотим изменить логику кнопки возрождения, например что-то добавить к существующей логики. Для этого нам пригодится аннотация @Inject которая по умолчанию добавит наш написанный код, до той части, какую мы укажем.
Аннотация Inject
id = "id функции", позволяет по иду менять её аргументы в дальнейшем и так далее.
-
Аргумент method = "название целевой функции". Так же можно ещё задать как:
method = "render" - Один метод
method = {"render", "tick", "update"} - несколько
method ="addItem(Lnet/minecraft/item/ItemStack;)Z" -Метод с сигнатурой (для перегруженных методов) (о том что это за синтаксис узнаете чуть ниже когда буду объяснять аргументы @At
-
Аннотация @At
Аннотация @At
-
value = "одно из списка":
"HEAD" - Вставить написанную функцию в начало целевой.
"RETURN" - Вставить написанную функцию перед оператором return в целевой
"TAIL" - Вставить написанную функцию в конце(перед последним return)
"INVOKE" - Вставить написанную функцию при вызове метода(дальше будут аргументы для этого) в целевой функции
"INVOKE_ASSIGN" - Вставить написанную функцию после вызова метода с присваиванием(дальше будут аргументы для этого) в целевой функции
"FIELD" - Вставить написанную функцию при доступе к полю(дальше будут аргументы для этого) в целевой функции
"NEW" - Вставить написанную функцию при создании объекта(дальше будут аргументы для этого) в целевой функции.
"CONSTANT" - Вставить написанную функцию при использовании константы(дальше будут аргументы для этого) в целевой функции.
"INVOKE_STRING" - Вставить написанную функцию при вызове метода со строковым аргументом(дальше будут аргументы для этого) в целевой функции.
"JUMP" - Вставить написанную функцию При переходе (условные операторы)(дальше будут аргументы для этого) в целевой функции.
"ARRAY" - Вставить написанную функцию при доступе к массиву (условные операторы)(дальше будут аргументы для этого) в целевой функции.
"INVOKE_ARRAY" - Вставить написанную функцию Доступ к массиву после вызова(дальше будут аргументы для этого) в целевой функции.
target = "значение конкретное в байткоде".
для метода: "Lполный/путь/к/Классу;имяМетода(ТипыАргументов)ВозвращаемыйТип". Для поля:
"Lполный/путь/к/Классу;имяПоля:ТипПоля".
Как указывать данные:
Примитивные типы:
Z - boolean
B - byte
C - char
S - short
I - int
J - long
F - float
D - double
Ссылочные типы:
Lполный/путь/к/Классу; - например, Ljava/lang/String;
Массивы:
[Тип - например, [I для int[], [[Ljava/lang/String; для String[][]ordinal - порядковый номер вызова. Если например вызова функции несколько раз, можно указать к какой именно по счёту функции это применить.
slice - ограничение области поиска. slice = @Slice (
from = @At("HEAD"), // Начало диапазона
to = @At(value = "INVOKE", target = "...") // Конец диапазона
) Думаю тут всё понятно.-
opcode - тип байткод-инструкции. Просмотреть их можно через IDEA, view -> show bytecode, пример вызова функции:
INVOKEVIRTUAL net/minecraft/client/network/ClientPlayerEntity.getScore ()IВот то что в начале, то есть INVOKEVIRTUAL, возможно быть такое, что две функции отличаются только этим, поэтому вам придётся указать опкод, собственно их список:
Opcodes.INVOKEVIRTUAL - Виртуальный вызов метода.
Opcodes.INVOKESTATIC - Статический вызов.
Opcodes.INVOKESPECIAL - Специальный вызов (конструктор, private).
Opcodes.INVOKEINTERFACE - Интерфейсный вызов.
Opcodes.GETFIELD - Чтение поля.
Opcodes.PUTFIELD - Запись поля.
Opcodes.GETSTATIC - Чтение статического поля.
Opcodes.PUTSTATIC - Запись статического поля.
-
shift - смещение позиции инъекции. Когда функция будет найдена, смещением можно задать направление, и будет заменена не то что бы заменилось, а со смещение на N байткодов. Варианты направления:
Shift.NONE - без смещения.
Shift.BEFORE - перед целевой инструкцией.
Shift.AFTER - после целевой инструкции.
Shift.BY - Смещение на N инструкций. (в отличие от остальных, от места инъекций, after и before смещались после изменения позиции инъекции)
by = 0 - задаёт на сколько инструкций сместиться
-
cancellable = true или false, возможность досрочно завершить функцию и вернуть значение(да по умолчанию все функции void , но об этом ниже).
-
locals - захват локальных переменных. Позволяет при написании функции, получить те же аргументы, которые будут когда функцию вызовет игра. По умолчанию захват локальных перемен нет. Собственно присваиваем значение одно из enum-членов, список:
LocalCapture.NO_CAPTURE - нет захвата.
LocalCapture.CAPTURE_FAILHARD - Строгий режим. Выбросит ошибку, если фактические локальные переменные не совпадают с ожидаемыми параметрами метода.
LocalCapture.CAPTURE_FAILSOFT - Мягкий режим. Пропустит захват и выведет предупреждение при несовпадении.
LocalCapture.PRINT - Захватывает переменные и выводит их для отладки.
require = -1 - Требуемое количество успешных инъекций (-1 = не проверять).
expect = 1 - Ожидаемое количество инъекций (выдает предупреждение при несовпадении).
allow = -1 - Максимальное разрешенное количество инъекций (-1 = без ограничений).
Так же важно записав все аргументы функции, в конце указать CallbackInfo ci(если 0 аргументов, то указать его одного), но если у функции есть возвращаемое значение , то CallbackInfoReturnable<ОбъектКоторыйВозвращаетКласс> cir. и что бы отменить функцию вызываем cancel у них, а если вернуть значение, то у cir вызываем setReturnValue и передаём значение, а уже потом отмена функции.
Собственно, вот функция init из DeathScreen которая отвечает за инициализацию логики кнопок:
protected void init() {
this.ticksSinceDeath = 0;
this.buttons.clear();
Text text = this.isHardcore ? Text.translatable("deathScreen.spectate") : Text.translatable("deathScreen.respawn");
this.buttons.add((ButtonWidget)this.addDrawableChild(ButtonWidget.builder(text, (button) -> {
this.client.player.requestRespawn();
button.active = false;
}).dimensions(this.width / 2 - 100, this.height / 4 + 72, 200, 20).build()));
this.titleScreenButton = (ButtonWidget)this.addDrawableChild(ButtonWidget.builder(Text.translatable("deathScreen.titleScreen"), (button) -> {
this.client.getAbuseReportContext().tryShowDraftScreen(this.client, this, this::onTitleScreenButtonClicked, true);
}).dimensions(this.width / 2 - 100, this.height / 4 + 96, 200, 20).build());
this.buttons.add(this.titleScreenButton);
this.setButtonsActive(false);
this.scoreText = Text.translatable("deathScreen.score.value", new Object[]{Text.literal(Integer.toString(this.client.player.getScore())).formatted(Formatting.YELLOW)});
}
Тут в целом всё понятно(можете самостоятельно открыть класс, ButtonWidget это класс кнопки интерфейса), нас интересует две строки:
this.client.player.requestRespawn();
button.active = false;
А точней одна:
this.client.player.requestRespawn();
И вот тут важно знать JAVA, потому что даже просмотрев байткод функции, можно не найти упоминания вызова функции requestRespawn, ибо лямбда лежит в том же классе, но отдельно, поэтому нам надо менять не эту функцию, а:
private synthetic method_19809(Lnet/minecraft/client/gui/widget/ButtonWidget;)V
// parameter button
L0
LINENUMBER 51 L0
ALOAD 0
GETFIELD net/minecraft/client/gui/screen/DeathScreen.client : Lnet/minecraft/client/MinecraftClient;
GETFIELD net/minecraft/client/MinecraftClient.player : Lnet/minecraft/client/network/ClientPlayerEntity;
INVOKEVIRTUAL net/minecraft/client/network/ClientPlayerEntity.requestRespawn ()V
L1
LINENUMBER 52 L1
ALOAD 1
ICONST_0
PUTFIELD net/minecraft/client/gui/widget/ButtonWidget.active : Z
L2
LINENUMBER 54 L2
RETURN
L3
LOCALVARIABLE this Lnet/minecraft/client/gui/screen/DeathScreen; L0 L3 0
LOCALVARIABLE button Lnet/minecraft/client/gui/widget/ButtonWidget; L0 L3 1
MAXSTACK = 2
MAXLOCALS = 2
В нашем классе миксине, мы должны определиться с модификатором доступа функции, возвращаемых тип данных, собственно сохраняем модификатор доступа, то есть private, имя функции тоже, а возвращаемый тип, в самом конце, V, то есть void, список полный есть выше, ну а аргумент это объект класса ButtonWidget, собственно и последнее что надо, понять перед чем надо сделать инъекцию.
Собственно, вот:
INVOKEVIRTUAL net/minecraft/client/network/ClientPlayerEntity.requestRespawn ()V
Собственно target будет содержать:
Lnet/minecraft/client/network/ClientPlayerEntity;requestRespawn()V
В итоге наша функция инъекции(полный пример со всеми аргументами выше) будет выглядеть так:
@Inject(id = "tutorialInject",
method = "method_19809",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/network/ClientPlayerEntity;requestRespawn()V",
ordinal = 1,
opcode = Opcodes.INVOKEVIRTUAL,
shift = At.Shift.NONE,
by = 0),
cancellable = false,
locals = LocalCapture.NO_CAPTURE,
require = -1,
expect = 1,
allow = -1)
private void method_19809(ButtonWidget buttonWidget, CallbackInfo ci){
}
Разумеется многие значения здесь и так такие по умолчанию и писать каждый раз так не надо, в идеале вот так выглядеть должно было:
@Inject(id = "tutorialInject",
method = "method_19809",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/network/ClientPlayerEntity;requestRespawn()V",
opcode = Opcodes.INVOKEVIRTUAL))
private void method_19809(ButtonWidget buttonWidget, CallbackInfo ci){
}
Теперь мы хотим например, что бы после смерти на тех координатах что-то появилось, так как оригинальный метод использует класс MinecraftClient , а поле это в классе родителя, то создаём ещё один класс-mixin, который уже будет нацелен на родителя, то есть Screen и через новую аннотацию получаем доступ к поле-доступу.
Собственно новая аннотация это @Shadow , позволяет получить доступ к полю, к которому изначально доступа нет(оно private и так далее), собственно пишет поле копируя тип и название, и над ним @Shadow, если поле ещё и константа, то между ними @Final. Итоговый класс:
package com.yourname.yourmod.mixin;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(value = Screen.class)
public class ScreenMixin {
@Shadow
MinecraftClient client;
}
Добавляем этот класс в конфиг миксинов:
{
"required": true,
"minVersion": "0.8",
"package": "com.yourname.yourmod.mixin",
"compatibilityLevel": "JAVA_21",
"refmap": "client-your-mod-id-refmap.json",
"client": [
"DeathScreenMixin",
"ScreenMixin"
],
"injectors": {
"defaultRequire": 1
}
}
Теперь наследуем наш DeathScreenMixin от ScreenMixin. И теперь пишем логику, берём координаты и спавним блоком, вот итоговый класс:
package com.yourname.yourmod.mixin;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.client.gui.screen.DeathScreen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(DeathScreen.class)
public class DeathScreenMixin extends ScreenMixin{
@Inject(id = "tutorialInject",
method = "method_19809",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/network/ClientPlayerEntity;requestRespawn()V",
opcode = Opcodes.INVOKEVIRTUAL))
private void method_19809(ButtonWidget buttonWidget, CallbackInfo ci){
World world = this.client.player.getWorld();
BlockPos coordinateDeathPlayer = this.client.player.getBlockPos();
coordinateDeathPlayer.add(0,10,0);
world.setBlockState(coordinateDeathPlayer, Blocks.STONE.getDefaultState());
}
}
Собственно с этим думаю понятно. Но что, если мы хотим не совместить наш код с частью функцией, а заменить? Для этого есть аннотация @Redirect , по сути почти все те же аргументы, то не добавляем код, а заменяем и разница по аргументам:
Нет параметра id
нет cancellable
нет locals (по умолчанию захватывает)
А так же важно, что в аргументах указывается объект с которым работаем, то есть если вызываем функцию из объекта, в аргументах указываем объект(дальше пример будет)
Собственно изменим наш туториал так, что бы возрождения не было , для этого можно убрать ScreenMixin:
package com.yourname.yourmod.mixin;
import net.minecraft.client.gui.screen.DeathScreen;
import net.minecraft.client.gui.widget.ButtonWidget;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(DeathScreen.class)
public class DeathScreenMixin {
@Redirect(method = "method_19809",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/network/ClientPlayerEntity;requestRespawn()V",
opcode = Opcodes.INVOKEVIRTUAL))
private void method_19809(ClientPlayerEntity clientPlayerEntity){
System.out.println("REDIRECT!!!");
}
}
И вот он результат:

Кнопку нажали и всё. А в консоли - Сообщение:
[10:24:36] [Render thread/INFO] (Minecraft) [STDOUT]: REDIRECT!!!
Собственно осталась небольшая пачка аннотаций, но они не сложные:
-
Меняем аргументы вызова:
-
Одного:
@ModifyArg , вот пример:
@ModifyArg( method = "targetMethod", at = @At( value = "INVOKE", target = "Lsome/Class;someMethod(Ljava/lang/String;I)V" ), index = 1 // индекс изменяемого аргумента (начиная с 0) ) private int modifyArgument(int originalValue) { // Получаем оригинальное значение, возвращаем модифицированное return originalValue * 2; }Где интуитивно всё понятно, логика такая же, только index обозначает какой аргумент(начиная с 0) редактируем.
-
Сразу всех:
@ModifyArgs, вот пример:
@ModifyArgs - изменение нескольких аргументов java @ModifyArgs( method = "targetMethod", at = @At( value = "INVOKE", target = "Lsome/Class;someMethod(FF)V" ) ) private void modifyMultipleArguments(Args args) { // args позволяет работать со всеми аргументами вызова float arg1 = args.get(0); float arg2 = args.get(1); args.set(0, arg1 * 1.5f); // Модифицируем первый аргумент args.set(1, arg2 + 10.0f); // Модифицируем второй аргумент }Думаю тут и так всё понятно.
-
Локальные переменные:
@ModifyVariable , позволяет перехватывать локальные переменные - пример:
@ModifyVariable( method = "calculateDamage", at = @At(value = "LOAD",ordinal = 2) print = true // Отладочный вывод )Варианты @At:
"LOAD" - при загрузке переменной
"STORE" - при сохранении в переменную
"HEAD" - в начале метода
ordinal это какой по счёту действие.
-
Константы:
@ModifyConstant , позволяет найти по типу константы и её значению все поля и заменить через функцию обработчик. Пример:
@ModifyConstant( method = "someMethod", constants = { @Constant(intValue = 16), @Constant(intValue = 32) } ) private int replaceMultiple(int original) { return original * 2; }Собственно указываем сколько угодно @Constant, а вот список типов:
intValue - целые числа
floatValue - числа с плавающей точкой
doubleValue - double числа
longValue - long числа
stringValue - строки
charValue - символы
classValue - классы
nullValue - null значения
expandZero - все нулевые значения(принимает true или false)
ну а через = значение.
Ну а функция обработчик всё понятно, тип тот который обрабатываем, название original. Возвращает тот же тип что и обрабатывает.
-
Получение доступ к полю, вместо @Shadow:
Использование @Accessor, по сути создаёт геттер и сеттер для любого поля, но при этом реализация скрыта, и через вызов функции манипулируете полем, синтаксис-пример:
@Accessor("имя_поля") Тип getИмяполя(); @Accessor("имя_поля") void setИмяполя(ТипПоля значение);В функции имена поля с большой в аннотации как в классе.
-
Получение доступ к функции, (Как с полем, только к функции):
Собственно @Invoker позволяет получить доступ к полю, пример синтаксис-внизу:
@Invoker("имя_метода") ReturnType callMethodName(Параметры...);
в скобках оригинальный метод, ну а название метода, в начале invoke, и с большой буквы слитно название оригинального метода Изменение константных полей:
Получив доступ через @Shadow , указывал @Final ещё нужно указать @Mutable(третьим аргументом) и тогда сможете изменить значение. И всё, меняйте хоть сразу, хоть через логику.Добавление новых полей и методов без конфликтов с классом(существуют сугубо в миксине). Просто напишите @Unique
Возможно вы не уверены, что класс существует , тогда перед @Mixin укажите @Pseudo , а в @Mixin
Так же ещё есть @Intrinsic, в скобка значение displace если установить true, полностью переопределит метод, иначе просто добавит функцию к целевой функции. При чём надо полностью сохранять модификатор доступа, возвращаемый тип, название и аргументы функции.
-
Собственно на этом все аннотации, на самом деле ничего сложного и есть огромный плюс: Mixin отдельная библиотека и не зависит от загрузчика, то есть мод полностью построенный на Mixin, легко перенесётся на Forge и т.п. А так же, позволяет полностью менять механики и добавлять совершенно новые, в отличие от API.