Иконка ресурса

Снова трансформеры? 2020-11-26

Нет прав для скачивания
29
20
Workbench61 добавил(а) новый ресурс:

Снова трансформеры? - Набор полезных методов для трансформирования классов

Если вы как я, часто пишете кормоды, или просто интересуетесь темой.
Хуклибы и миксины, для меня, это слишком много и непонятно. Они большие, требуют относительно долгого сетапа, и их невозможно дебажить.
Суть моего проекта - набор сниппетов (live templates), которые я могу вызвать простым шорткатом, в любом месте проекта, над которым я работаю, и получить моментальный результат. Плюсом это дает мне больше независимости и полный контроль над своим кодом.

Сам код:
Набор функций для...

Узнать больше об этом ресурсе...

Если вы как я, часто пишете кормоды, или просто интересуетесь темой.
Хуклибы и миксины, для меня, это слишком много и непонятно. Они большие, требуют относительно долгого сетапа, и их невозможно дебажить.
Суть моего проекта - набор сниппетов (live templates), которые я могу вызвать простым шорткатом, в любом месте проекта, над которым я работаю, и получить моментальный результат. Плюсом это дает мне больше независимости и полный контроль над своим кодом.

Сам код:
Набор функций для работы с ASM tree API (ClassNode, MethodNode, InsnList и т.д.)

Простой класс для ремаппинга методов и полей, чтобы ваши моды не ломались при смене маппингов в воркспейсе
Опционально реализует интерфейс yopoyka/code-snippets кастомизируйте по вашему желанию.
Использование: GradleMcp.instance.fromSrg("func_72326_a") вернет значение согласно маппингам используемым в вашем проекте, либо переданное значение если маппинги не найдены.

Для новичков:
Если вы еще не знаете как реализовать и задействовать IFMLLoadingPlugin
Ваш build.gradle
Gradle (Groovy):
jar {
    manifest {
        attributes 'FMLCorePlugin': 'путь.к.вашему.классу'
    }
}

runClient {
    systemProperty 'fml.coreMods.load', 'путь.к.вашему.классу'
}

runServer {
    systemProperty 'fml.coreMods.load', 'путь.к.вашему.классу,на.самом.деле.плагины.можно.добавлять.через.запятую'
}

В вашем классе реализующем интрефейс IFMLLoadingPlugin определяем метод String[] getASMTransformerClass() и добавляем аннотации
Java:
@IFMLLoadingPlugin.Name("My Coremod")
@IFMLLoadingPlugin.MCVersion("1.7.10") // версия майнкрафта
@IFMLLoadingPlugin.SortingIndex(1001) // чтобы работать с srg именами а не с оригинальными a b az
public class Coremod implements IFMLLoadingPlugin {

    @Override
    public String[] getASMTransformerClass() {
        return new String[] { "ваши.класс.трансформеры" };
    }
}

В моем репозиторие имеется простой трансформер yopoyka/code-snippets вида:
Java:
public class BasicClassTransformer implements IClassTransformer {
    protected java.util.Map<String, net.minecraft.launchwrapper.IClassTransformer> transformers = new java.util.HashMap<>();
    {
        map.put("имя.класса.для.трансформации", (n, name, basicClass) -> {
            // делаем с классом что хотим
            return basicClass;
        })
    }
  
    @Override
    public byte[] transform(String name, String transformedName, byte[] basicClass) {
        if (transformers.isEmpty()) return basicClass;

        final net.minecraft.launchwrapper.IClassTransformer transformer = transformers.remove(transformedName);
        if (transformer != null)
            return transformer.transform(name, transformedName, basicClass);

        return basicClass;
    }
}

Примеры:
Создаем или импортируем все статические элементы класса yopoyka/code-snippets
Подробнее с его структурой можно ознакомиться на гитхабе

Java:
package examples;

import static yopoyka.mctool.Asm.*;

public class Examples {
    public static void main(String[] args) throws Throwable {
        // читаем класс из массива байтов
        ClassNode classNode = read(readClass("examples/Ex"));

        // простой хук в начало метода getInt
        classNode.methods
                .stream()
                .filter(forMethod("getInt", "()I")) // ищем метод getInt возвращающий int
                .findFirst()
                .ifPresent(methodNode -> {
                    InsnList list = new InsnList(); // создаем лист вручную
                    compose( // метод собирающий Consumer'ы вместе в один большой
                            getThis(), // равносильно addInst(() -> new VarInsnNode(Opcodes.ALOAD, 0))
                            // вызываем наш статический хук
                            callStatic("examples/Examples$Hooks", "getIntHook", "(Lexamples/Ex;)V")
                    ).accept(list); // применяем наш Consumer к списку
                    // вставляем инструкции перед первой инструкцией оригинального списка
                    insertFirst(methodNode.instructions, list);
                });

        // добавляем действие для каждого return'а в методе
        classNode.methods
                .stream()
                // фильтры можно комбинировать
                .filter(forMethod("numbers").and(forMethodDesc("(I)Ljava/lang/String;")))
                .findFirst()
                .ifPresent(methodNode -> {
                    // выполяет операцию для каждой инструкции
                    // которая удовлетворяет фильтру
                    forEach(methodNode.instructions,
                            // поиск инструкций с опкодом ARETURN
                            opcode(Opcodes.ARETURN),
                            // вставляет инструкции перед каждой найденной
                            insertBefore(supplyCode(compose(
                                    getThis(), // загружает переменную с индексом 0 (ноль)
                                    // вызываем наш хук
                                    callStatic("examples/Examples$Hooks", "numbersHook", "(Ljava/lang/String;Lexamples/Ex;)Ljava/lang/String;")
                            )))
                    );
                });

        // делаем ветвление с невероятной легкостью
        classNode.methods
                .stream()
                .filter(forMethod("days"))                       // еще один способ
                .filter(forMethodDesc("(I)Ljava/lang/String;"))  // комбинировать фильтры
                .findFirst()
                .ifPresent(methodNode -> {
                    // выполяет операцию для каждой инструкции
                    // которая удовлетворяет фильтру
                    forEach(methodNode.instructions,
                            // поиск инструкций с опкодом ARETURN
                            opcode(Opcodes.ARETURN),
                            // вставляет инструкции перед каждой найденной
                            insertBefore(supplyIf( // создаем ветвление
                                    compose( // инициализуем
                                            addInst(Opcodes.DUP), // дюпаем строчку со стака
                                            // вызываем наш хук который возвращает bolean или int
                                            callStatic("examples/Examples$Hooks", "daysHook", "(Ljava/lang/String;)Z")
                                    ),
                                    jumpIfTrue(), // делаем прыжок если хук вернул значение неравное 0 (нулю)
                                    compose( // эти инструкции будут выполнены
                                             // если прыжок не был совершен
                                            addInst(() -> new LdcInsnNode("Garfield doesn't like this day")),
                                            addInst(Opcodes.ARETURN)
                                    ),
                                    nothing() // эти инструкции выполняются если прыжек был сделан
                                              // в данном случае не делаем ничего
                                              // метод продолжает выполняться
                            ))
                    );
                });

        Class<?> aClass = defineClass(write(classNode));
        Object o = aClass.newInstance();
        System.out.println(aClass.getDeclaredMethod("getInt").invoke(o));
        System.out.println();

        for (int i = 0; i < 10; i++) {
            System.out.println(aClass.getDeclaredMethod("numbers", int.class).invoke(o, i));
        }

        for (int i = 0; i < 10; i++) {
            System.out.println(aClass.getDeclaredMethod("days", int.class).invoke(o, i));
        }
    }

    public static class Hooks {
        public static void getIntHook(Ex ex) {
            System.out.println("getIntHook " + ex);
        }

        public static String numbersHook(String s, Ex ex) {
            System.out.println("numbesHook " + s + ' ' + ex);
            return "hey";
        }

        public static boolean daysHook(String day) {
            if (day.equals("Monday"))
                return false;

            return true;
        }
    }

    public static byte[] readClass(String name) {
        try (InputStream is = ClassLoader.getSystemResourceAsStream(name.replace('.', '/').concat(".class"))) {
            final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int read;
            final byte[] buff = new byte[Short.MAX_VALUE];
            while ((read = is.read(buff)) > 0) {
                buffer.write(buff, 0, read);
            }
            return buffer.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static Class<?> defineClass(byte[] bytes) {
        return new ClassLoader() {
            Class<?> defineClass(byte[] bytes) {
                return defineClass(bytes, 0, bytes.length);
            }
        }.defineClass(bytes);
    };
}

Класс который мы модифицируем:
Java:
package examples;

public class Ex {
    public int getInt() {
        System.out.println("getInt");
        return 10;
    }

    public String numbers(int i) {
        switch (i) {
            case 0: return "zero";
            case 1: return "one";
            case 2: return "two";
            case 3: return "three";
            case 4: return "four";
            case 5: return "five";
            case 6: return "six";
            case 7: return "seven";
            case 8: return "eight";
            case 9: return "nine";
        }
        return "none";
    }

    public String days(int i) {
        switch (i) {
            case 0: return "Monday";
            case 1: return "Tuesday";
            case 2: return "Wednesday";
            case 3: return "Thursday";
            case 4: return "Friday";
            case 5: return "Saturday";
            case 6: return "Sunday";
        }
        return "none";
    }
}

Результат:
Код:
getIntHook examples.Ex@00000000
getInt
10

numbesHook zero examples.Ex@00000000
hey
numbesHook one examples.Ex@00000000
hey
numbesHook two examples.Ex@00000000
hey
numbesHook three examples.Ex@00000000
hey
numbesHook four examples.Ex@00000000
hey
numbesHook five examples.Ex@00000000
hey
numbesHook six examples.Ex@00000000
hey
numbesHook seven examples.Ex@00000000
hey
numbesHook eight examples.Ex@00000000
hey
numbesHook nine examples.Ex@00000000
hey

Garfield doesn't like this day
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
none
none
none
 
Последнее редактирование:
1,178
27
281
Ну честно говоря, обычные хуки можно отдебажить обычным дебагом... А вот миксины да, через хотсвап не пашут.
А вообще полезно.
 
6,085
224
1,175
Можешь пожалуйста привести несколько причин почему использовать только трансформеры плохо? Мне даже интересно стало.
Допустим, есть метод
Java:
public void kek(){
    //do something
    
    //do something another
}
И мы при помощи кормода вставляем throw new Exception("Выстрел в ногу") между //do something и //do something another.
Получается так
Java:
public void kek(){
    //do something
    throw new Exception("Выстрел в ногу");
    //do something another
}
И когда какой-то код вызовет этот метод - он свалится с исключением и не будет понятно, отчего оно там возникает, в стактрейсе не будет ни слова о нашем коварном кормоде.
Вместо просто вызова исключения вставляться может какой-то код, который иногда работает, а иногда нет. И допустим, он ломается у какого-то юзера на проде. И юзер по краш-логу не поймет, разработчику какого мода ему писать баг-репорт.

Другие проблемы носят тот же характер - кормоды вносят неопределенности, которые трудно отслеживать и которые делают вещи запутанными
 
29
20
Это конечно то за чем нужно следить, правда ни хуклибы ни миксины не решают эту проблему.
 
6,085
224
1,175
Хуклиба решает: вызовы хук-методов остаются в стактрейсе
 
29
20
Вызовы любого метода остаются в стактрейсе. Другое дело когда кодер вставляет код хука вместо вызова хука, но это уже его проблема. В крайнем случае в фордже есть system properties legacy.debugClassLoading legacy.debugClassLoadingSave которые сохранят модифицированные классы на диск и их можно посмотреть вручную.
 
Сверху