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

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

Версия(и) 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
Просмотры
1,593
Первый выпуск
Обновление
Оценка
5.00 звёзд 3 оценок

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

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

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