Собственные события. Часть первая: Создание

Собственные события. Часть первая: Создание

Версия(и) Minecraft
1.12
Доброго времени суток, господа. В статье речь пойдёт о создании и использовании собственных событий. Тема довольно обширна и поэтому я решил разделить её на две части. В первой части будет подробно объяснён принцип создания и управления с внедрением в свой код. Вторая часть будет содержать подробнейший гайд по внедрению своих событий в исходный код игры с помощью библиотеки ASM – будет интересно. Статья написана для последней версии игры, но принцип един для всех. Исходники как всегда на GitHub.


Собственные события​

Часть первая​

Без использования форджевских событий не обходится почти ни одна серьёзная модификация и от версии к версии их разнообразие растёт. Используя их можно добиться практически всего, но к сожалению для всех возможных случаев событий нет, в место этого у нас есть возможность создавать и использовать свои собственные. Создание и запуск дело простое и главная задача состоит в том, чтобы внедрить его куда требуется, а требуется в большинстве случаев в исходные классы игры (в свой код внедрять события имеет смысл наверное только если вы пишите библиотеку или API). Нам придётся прибегнуть к созданию кормода и манипуляциям с байт-кодом с использованием библиотеки ASM, но в рамках создания событий сложных операций с трансформацией не будет. Вы можете задать резонный вопрос: зачем возиться с созданием событий если всё равно потребуются ASM трансформеры? На мой взгляд привычная обвязка форджевских событий при удачной конструкции и внедрении гарантирует абсолютное удобство использования. Их создание даст вам все их преимущества в виде возможности отслеживания исполнения какого либо кода, возможности повлиять на него или даже прервать и всё это в знакомой обёртке.


Внимание! В этой части рассматриваются элементарные и бесполезные вещи с точки зрения применения событий, они предназначены лишь для ознакомления с процессом их создания и использования. Самое интересное ожидает вас во второй части, но ознакомиться с этим материалом всё же советую. Вторая часть без этого тоже не обойдётся, но отвлекаться на основу я не буду.

Создание и запуск простого события

Чтобы создать событие вам потребуется создать класс-наследник Event и вызвать его срабатывание в нужном месте с помощью MinecraftForge#EVENT_BUS#post(Event). Всё. Элементарно, правда?


В первом примере попробуем отловить момент завершения использования предмета, а главное сам предмет. Для запуска мы будем использовать стандартное событие LivingEntityUseItemEvent.Tick с парой условий. Его практическая ценность стремиться к нулю, но для образовательных целей сойдёт.


В первую очередь нужно создать класс-наследник Event. Имя класса должно отражать суть события и вероятно содержать слово “Event” - например ItemUsedEvent:
Java:
public class ItemUsedEvent extends Event {
  
    /**
     * ItemUsedEvent срабатывает когда EntityLivingBase завершает использование предмета.<br>
     * Срабатывает через {@link DemoEventsInjection#onLivingUseItem(LivingEntityUseItemEvent.Tick)}<br>
     * <br>
     * {@link #livingBase} EntityLivingBase, использовавшую предмет.<br>
     * {@link #itemStack} используемый предмет.<br>
     * <br>
     * Это событие нельзя отменить. {@link Cancelable}. <br>
     * <br>
     * Это событие не имеет результата. {@link HasResult}<br>
     * <br>
     * Это событие использует {@link MinecraftForge#EVENT_BUS}.<br>
     **/
  
    private final EntityLivingBase livingBase;

    private final ItemStack itemStack;

    public ItemUsedEvent(EntityLivingBase livingBase, ItemStack itemStack) {
      
        this.livingBase = livingBase;
        this.itemStack = itemStack;
    }
  
    public EntityLivingBase getEntityLiving() {
      
        return this.livingBase;
    }
  
    public ItemStack getItemStack() {
      
        return this.itemStack;
    }
}


В конструктор необходимо добавить необходимые вам аргументы, но нужно учитывать доступность переменных в том месте, куда вы внедрите запуск. Допустим нам важно знать кто использовал предмет и что это был за предмет - добавляем EntityLivingBase и ItemStack. Класс должен содержать соответствующие поля и геттеры для них (последние по вкусу).


Идём дальше. Нужно где то разместить вызов события. Фордж размещает большинство вызовов в классах ForgeEventFactory и ForgeHooks в статических методах, которые затем уже внедряет в исходный код (вызовы исключительно клиентских событий внедряются напрямую). Делать это со статическими методами очень просто. Мы пойдём по тому же пути - создаём DemoEventFactory и размещаем в нём метод с вызовом:
Java:
public class DemoEventFactory {

    /*
     * Статические методы для внедрения в стандартные события для демонстрации.
     */
  
    public static void onLivingFinishesUsingItem(EntityLivingBase livingBase, ItemStack itemStack) {
      
        //Вызов события.
        MinecraftForge.EVENT_BUS.post(new ItemUsedEvent(livingBase, itemStack));
    }
}


Метод содержит те же аргументы, что и конструктор события, но это не обязательно. Главное что бы в том месте, куда будет внедрён статический метод, были доступны эти переменные по прямой ссылке (это намного упростит трансформацию). Это могут быть как локальные переменные, так и поля класса.


Теперь внедрение. Запускать событие будем из стандартного события LivingEntityUseItemEvent.Tick. Размещаем его в отдельном классе, символизирующем трансформируемый CustomEventsInjection, и после пары проверок для создания условий нашего события вызываем статический метод, запускающий его. Класс помечен аннотацией @EventBusSubscriber(modid) для автоматического подписывания содержащихся в нём событий:
Java:
@EventBusSubscriber(modid = EventsMain.MODID)
public class DemoEventsInjection {
  
    /*
     * Демонстрационные хуки собственных простых событий.
     * Созданы для симуляции настоящих.
     */

    @SubscribeEvent
    public static void onLivingUseItem(LivingEntityUseItemEvent.Tick event) {         
          
        if (event.getEntityLiving().getItemInUseCount() == 1) {//За тик до окончания использования предмета.
                      
            //Вызов статического метода фабрики для запуска события.
            DemoEventFactory.onLivingFinishesUsingItem(event.getEntityLiving(), event.getItem());
        }
    }
}


Тест события в отдельном классе CustomEvents:
Java:
@EventBusSubscriber(modid = EventsMain.MODID)
public class CustomEvents {

    @SubscribeEvent
    public static void onPlayerUsedItem(ItemUsedEvent event) {     
      
        //Только на сервере, только для игрока и при поедании еды.
        if (!event.getEntityLiving().world.isRemote && event.getEntityLiving() instanceof EntityPlayer && event.getItemStack().getItem() instanceof ItemFood) {
                      
            //Отправка игроку сообщения с названиеим съеденного предмета.
            ((EntityPlayer) event.getEntityLiving()).sendMessage(new TextComponentTranslation("event.itemUsed", event.getItemStack().getDisplayName()));
        }
    }
}


При съедании чего либо в чате появится сообщение об этом. Вот так просто мы можем создавать и использовать события, которые будут срабатывать через форджевский EVENT_BUS.

Создание и запуск отменяемого события


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

Как создавать событие, вы узнали из первого примера. Что бы добавить возможность отмены, пометьте класс анотацией @Canceable или переопределите метод Event#isCanceable() и верните в нём true. Теперь вы можете отменять его, но как использовать это на практике? Всё просто – метод MinecraftForge#EVENT_BUS#post(Event) возвращает логическое значение и вернёт true при отмене.


Для демонстрации возможностей отмены я создал пример, событие в котором срабатывает при открытии инвентаря. Запуск из GuiOpenEvent. Хм, что может быть полезнее?


Событие OpenInventoryEvent:
Java:
@Cancelable
public class OpenInventoryEvent extends Event {
  
    /**
     * OpenInventoryEvent срабатывает когда игрок открывает инвентарь.<br>
     * Срабатывает через {@link DemoEventsInjection#onInventoryOpen(GuiOpenEvent)}<br>
     * <br>
     * Это событие можно отменить. {@link Cancelable}. <br>
     * Отмена предотвратит открытие инвентаря.<br>
     * <br>
     * Это событие не имеет результата. {@link HasResult}<br>
     * <br>
     * Это событие использует {@link MinecraftForge#EVENT_BUS}.<br>
     **/

    public OpenInventoryEvent() {}
}


Содержательно. Теперь создаём статический метод для вызова в CustomEventsInjection. Он возвращает значение типа boolean (вернёт true при отмене события):
Java:
public class DemoEventFactory {

    /*
     * Статические методы для внедрения в стандартные события для демонстрации.
     */

    public static boolean onPlayerOpenInventory() {
      
        //Вернёт true при отмене события OpenInventoryEvent.
        return MinecraftForge.EVENT_BUS.post(new OpenInventoryEvent());
    }
}


Внедрение. Просто отменяем исходное событие, если CustomEventsInjection #onPlayerOpenInventory() вернёт true. В CustomEventsInjections:
Java:
@EventBusSubscriber(modid = EventsMain.MODID)
public class DemoEventsInjection {
  
    /*
     * Демонстрационные хуки собственных простых событий.
     * Созданы для симуляции настоящих событий.
     */
  
    @SubscribeEvent
    public static void onInventoryOpen(GuiOpenEvent event) {         
      
        if (event.getGui() instanceof GuiInventory) {
                      
            //Вызов события.
            //Если OpenInventoryEvent будет отменено, это событие так же прервётся.
            event.setCanceled(DemoEventFactory.onPlayerOpenInventory());
        }
    }
}


Использование в CustomEvents:
Java:
@EventBusSubscriber(modid = EventsMain.MODID)
public class CustomEvents {

    @SubscribeEvent
    @SideOnly(Side.CLIENT) 
    public static void onPlayerOpenInventory(OpenInventoryEvent event) {
      
        //Если у игрока занята правая рука, то он не сможет открыть инвентарь.
        if (Minecraft.getMinecraft().player.getHeldItemMainhand().getItem() != Items.AIR) {
                      
            //Отмена собственного события.
            event.setCanceled(true);
        }
    }
}


В случае если у игрока будет занята рука, он не сможет открыть инвентарь. Новые высоты покорились нам...

Создание и запуск модифицирующего события

Вы конечно можете отменять события и выполнять нужные действия, но допустим нужно изменить значение одной переменной (можно и больше, но это сложнее). В этом случае событие может содержать сеттер для нужного параметра, а вернуть изменённый параметр позволит статический метод внедрения.


Мне лень проявлять фантазию, поэтому примером будет событие, срабатывающее при падении игрока, которое можно будет отменить или изменить высоту падения. В качестве точки запуска используется LivingFallEvent.


Класс события PlayerFallEvent:
Java:
@Cancelable
public class PlayerFallEvent extends Event {

    /**
     * PlayerFallEvent срабатывает когда грок приземляется после падения.<br>
     * Срабатывает через {@link DemoEventsInjection#onPlayerFall(LivingFallEvent)}<br>
     * <br>
     * {@link #player} упавший игрок.<br>
     * {@link #distance} дистанция падения.<br>
     * <br>
     * Это событие можно отменить. {@link Cancelable}. <br>
     * При отмене игрок не получит урона от падения.<br>
     * <br>
     * Это событие не имеет результата. {@link HasResult}<br>
     * <br>
     * Это событие использует {@link MinecraftForge#EVENT_BUS}.<br>
     **/
  
    private final EntityPlayer player;
  
    private float distance;
  
    public PlayerFallEvent(EntityPlayer player, float distance) {
      
        this.player = player;
        this.distance = distance;
    }
  
    public EntityPlayer getPlayer() {
      
        return this.player;
    }
  
    public float getDistance() {
      
        return this.distance;
    }
  
    public void setDistance(float distance) {
      
        this.distance = distance;
    }
}


Запуск из статического метода в CustomEventsFactory. В качестве параметров возьмём игрока и дистанцию падения. Создаём локальную переменную события для его однократного срабатывания и нехитрой конструкцией возвращаем результат:
Java:
public class DemoEventFactory {

    /*
     * Статические методы для внедрения в стандартные события для демонстрации.
     */
  
    public static float onPlayerFall(EntityPlayer player, float distance) {
      
        PlayerFallEvent event = new PlayerFallEvent(player, distance);
      
        //При отмене вернёт нулевую дистанцию падения, иначе возможно изменённую.
        return MinecraftForge.EVENT_BUS.post(event) ? 0.0F : event.getDistance();
    }
}


Внедряем в CustomEventsInjection. Вызов метода должен быть однократным. Передаём значение дистанции в основное событие. При отмене нашего дистанция падения будет равна нулю – отменяем текущее в этом случае:
Java:
@EventBusSubscriber(modid = EventsMain.MODID)
public class DemoEventsInjection {
  
    /*
     * Демонстрационные хуки собственных простых событий.
     * Созданы для симуляции настоящих.
     */
  
    @SubscribeEvent
    public static void onLivingFall(LivingFallEvent event) {
      
        if (event.getEntityLiving() instanceof EntityPlayer) {
          
            //Вызов события.
            float distance = DemoEventFactory.onPlayerFall((EntityPlayer) event.getEntityLiving(), event.getDistance());
          
            //Установка дистанции падения.
            event.setDistance(distance);
          
            //Отмена события.
            event.setCanceled(distance == 0.0F);
        }
    }
}


Тест в CustomEvents:
Java:
@EventBusSubscriber(modid = EventsMain.MODID)
public class CustomEvents { 

    @SubscribeEvent
    public static void onPlayerFall(PlayerFallEvent event) {

        //Уменьшение высоты падения для смягчения урона в два раза.
        event.setDistance(event.getDistance() / 2);
    }
}


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

Заключение

На этом изучение основ завершается. Во второй части будем рассматривать принципы внедрения событий в исходные классы с помощью ASM трансформеров. Я подробно распишу этот процесс и возможно вы возьмёте эту информацию на вооружение. Весь приведённый код доступен в моём репозитории.

Прошу не топить статью за относительно бесполезный материал, пришлось поделить его так как его объём переходит все мыслимые границы (описание трансформаций размером с простыню). Обо всех недочётах и ошибках пишите в обсуждении. Спасибо за внимание!
  • Like
Реакции: Eifel, mousecray и CMTV
Автор
AustereTony
Просмотры
2,229
Первый выпуск
Обновление
Оценка
5.00 звёзд 3 оценок

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

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

Как всегда отлично
Хорошо, но пожалуйста, не забывай о 1.7.10)
AustereTony
AustereTony
Спасибо. Этот тутор подходит для всех версий 1.6+
Назад
Сверху