[1.7-1.16]Пакетная система CodeChickenLib

7,099
324
1,509
Данный урок писался для 1.7.10 и код тестировался на этой версии. Но в библиотеке не изменился api в 1.8, наверное, тоже пойдет
Update! Юзаю эту либу на 1.11.2 - интерфейс не поменялся, только несколько новых методов добавилось для записи данных в пакет
Update! Юзаю эту либу на 1.14.4 и 1.15.2 - поменялась регистрация хандлеров, апи в целом осталось тем же
Введение
Предполагаю, что тему могут читать совсем новички в моддинге, поэтому поясню, что такое пакеты и зачем они могут быть нужны.​
Майнкрафт состоит из сервера и клиента(даже в синглплеере), которым соответствует клиентская и серверная стороны кода. Пакет - это сообщение, отправляемое с одной стороны на другую.
Каждая сторона имеет какую-то свою информацию. Например, клиент знает, что игрок нажимает левую кнопку мыши, но сервер не знает об этом. Поэтому клиент должен отправить на сервер пакет о действии атаки. Сервер примет пакет и соответствующе отреагирует.
Подобное поведение мы и будет реализовывать для примера.

Для начала расскажу о плюсах и минусах использования пакетной системы CodeChickenLib

Плюсы:
  • Простота использования
  • Каждый пакет не требуется регистрировать отдельно
  • Хороша для небольшого количества пакетов
  • Получается мало кода
  • CCL на первой странице курса по популярности и на второй по закачкам, библиотека уже содержится во множестве сборок
  • Апи не зависит от версии майнкрафта - ваши моды легче портировать
Минусы:
  • При большом количестве пакетов обработчики сильно распухают

Архитектура пакетной системы CodeChickenLib
1600282720329.png
Реализации интерфейсов IClientPacketHandler и IServerPacketHandler - сущности, отвечающие за обработку принятых пакетов со стороны клиента и со стороны сервера.
PacketCustom - класс пакетов. Каждый пакет имеет канал назначения("строка") и тип-идентификатор(целое число >=1). В пакет могут быть записаны какие-то данные при помощи write-методов и прочтены при помощи read-методов
Java:
//Каждому write-методу в списке соответствует read-метод
writeBoolean
writeByte
writeShort
writeInt
writeFloat
writeDouble
writeLong
writeChar
writeVarInt //формат переменной длины(в битах), подробнее тут https://wiki.vg/Data_types
writeVarShort //формат переменной длины
writeVarLong//формат переменной длины
writeArray //записывает массив байтов byte[]
writeString
writeUUID
writeEnum
writeResourceLocation
writePos //записывает BlockPos
writeVector
writeNBTTagCompound
writeItemStack
writeFluidStack

Добавление библиотеки в проект
Тут есть два варианта.
Скачать можно тут: CodeChicken Lib 1.8.+ или из репозитория. Выберите версию deobf для разработки, для релиза - universal.
После того, как скачали, помещаем библиотеку в папку ./libs/ проекта и обновляем/пересобираем gradle-проект. В intellij idea достаточно нажать на 1575200696518.pngRrefresh all gradle projects. В эклипсе не пробовал, но пересборка проекта и повторный импорт точно подействуют.
Для старых версий forge может потребоваться добавить в build.gradle зависимость от папки libs
Gradle (Groovy):
dependencies {
    compile fileTree(dir: './libs/', include: '*.jar')
}
Спасибо @Agravaine :j за совет
Gradle (Groovy):
repositories {
    maven {
        name = "chickenbones"
        url = "https://chickenbones.net/maven" //или https://maven.covers1624.net/
    }
}

dependencies {
    compile "codechicken:CodeChickenLib:<version>:deobf"
}
значения <version>, например, 1.12.2-3.2.3.358
Этот способ может не работать на последних версиях mdk для 1.12.2. Используйте mdk версии 14.23.5.2847 или первый способ добавления.

1600282518844.png
Реализуемая фича состоит в том, чтобы хоткеем [H]+[1..9] заюзать зелья, лежащие в девяти верхних слотах инвентаря.​
Поэтому в пакет нужно будет записать номер нажатой клавиши, чтобы сервер знал, из какого слота нужно юзать зельку.
Для отслеживания нажатия клавиш будем использовать обработчик InputEvent.KeyInputEvent
Подробнее о событиях: Что такое события и как их ловить. Короткая и простая тема.

Обработчик события:
@SideOnly(Side.CLIENT)
@Mod.EventBusSubscriber(modid = Main.modid, value = Side.CLIENT)
public class ClientUpdate {
    private static KeyBinding key = new KeyBinding("potionUse", Keyboard.KEY_H, "HotKeys");

    public static void init() { //нужно вызвать это в preinit клиента
        ClientRegistry.registerKeyBinding(key);
    }

    @SubscribeEvent
    public static void onKeyPress(InputEvent.KeyInputEvent e) {
        if (Keyboard.isKeyDown(key.getKeyCode())) {
            KeyBinding[] keyBindsHotbar = Minecraft.getMinecraft().gameSettings.keyBindsHotbar;

            for (int key = 0; key < keyBindsHotbar.length; key++)
                if (Keyboard.isKeyDown(keyBindsHotbar[key].getKeyCode()))
                    new PacketCustom(Main.modid, 1)//Создаем пакет с типом-идентификатором 1
                            .writeInt(key)//Записываем в него индекс хоткея слота инвентаря
                            .sendToServer();//Отправляем
        }
    }
}
Создание нового пакета - new PacketCustom(Main.modid, 1) - выглядит несколько громоздко, если у нас будет больше одного пакета. Поэтому сделаем вспомогательный метод:
Java:
public static PacketCustom createPacket(int type) {
    return new PacketCustom(Main.modid, type);
}

В ClientProxy в обработчик FMLPreInitializationEvent добавим ClientUpdate.init();, чтобы зарегистрировать хоткей.

Отправка пакета у нас готова, теперь нужно написать логику приема.
Для этого делаем реализации IClientPacketHandler и IServerPacketHandler:
Серверный обработчик пакетов:
public class ServerPacketHandler implements IServerPacketHandler {
    @Override
    public void handlePacket(PacketCustom packetCustom, EntityPlayerMP player, INetHandlerPlayServer iNetHandlerPlayServer) {
        switch (packetCustom.getType()) {//чтобы определить тип-идентификатор пакета
            case 1:
                int potionHotSlot = packetCustom.readInt();//читаем из пакета слот, из которого нужно выпить зелье
                ItemStack stack = player.inventory.getStackInSlot(9 + potionHotSlot);
                if (stack.getItem() == Items.POTIONITEM)//если в слоте зелька, то Стив выпьет ее одним глотком :D
                {
                    ItemStack stack1 = Items.POTIONITEM.onItemUseFinish(stack, player.world, player);
                    System.out.println(stack1.getCount());
                    player.inventory.setInventorySlotContents(9 + potionHotSlot,
                            stack1);
                }
                break;
            default:

        }
    }
}
На клиент мы не отправляем никакие пакеты, но сделаем для него обработчик тоже
Клиентский обработчик пакетов:
public class ClientPacketHandler implements IClientPacketHandler {
    @Override
    public void handlePacket(PacketCustom packetCustom, Minecraft minecraft, INetHandlerPlayClient iNetHandlerPlayClient) {
        switch (packetCustom.getType()) {//Определяем тип пакета
            default:
        }
    }
}
Теперь нужно зарегистрировать обработчики пакетов. Делать это нужно во время FMLPreInitializationEvent
Java:
public class CommonProxy {
    public void preinit(FMLPreInitializationEvent e) {
        //Даже, если у вас есть отдельный класс для серверного прокси, все равно регистрацию серверного обработчика делайте здесь
        PacketCustom.assignHandler(Main.modid/*имя канала не длиннее 20*/, new ServerPacketHandler());
    }
}

public class ClientProxy extends CommonProxy {
    public void preinit(FMLPreInitializationEvent e) {
        super.preinit(e);
        PacketCustom.assignHandler(Main.modid, new ClientPacketHandler());
        ClientUpdate.init();
    }
}
На новых версиях нужно в конструкторе главного класса мода вызвать PacketCustomChannelBuilder
Java:
PacketCustomChannelBuilder
    .named(name)//имя канала
    .assignClientHandler(() -> ClientPacketHandler::new)
    .assignServerHandler(() -> ServerPacketHandler::new)
    .build();

Запускаем, заходим в локальный мир, или на локальный сервер, тестируем.

Ссылки
Github: hohserg1/CCL-example

На этом все, надеюсь, вам понравилось)
 
Последнее редактирование:
2,505
81
397
Тут многие java-то знают на уровне копипаста, а ты scala суешь :)
А есть вообще преимущества над форжевскими пакетами?
 
608
5
15
И чем они отличаются вообще?
 
7,099
324
1,509
Dahaka написал(а):
Тут многие java-то знают на уровне копипаста, а ты scala суешь :)
А есть вообще преимущества над форжевскими пакетами?
Все преимущества - дело вкуса.
Мне, например, нравится интерфейсы, их простота:
2 обработчика, для клиента и сервера, их регистрация и все. Пакеты можно отправлять одной строчкой, что тоже удобно и не захломляет код.
 
7,099
324
1,509
Хах, только хотел посоветовать в твоей теме в вопросах эту либу) Жди, скоро сделаю пример для java
 

CumingSoon

Местный стендапер
1,634
12
269
Вот совсем непонятно. Даже хаскелл больше на джаву похож, чем это отродье. Переделывай
 
7,099
324
1,509
Похож-не похож, а писать на scala приятнее, т.к. очень выразительный язык.
Прикрепил пример на java, делающий то же самое. Посмотри, какой он громоздкий.
 
586
32
136
А как к примеру при нажатии кнопки давать эффект зелья тому, на кого смотришь?
 

CumingSoon

Местный стендапер
1,634
12
269
Похож-не похож, а писать на scala приятнее, т.к. очень выразительный язык.
Прикрепил пример на java, делающий то же самое. Посмотри, какой он громоздкий.

Это была ирония. Там все понятно, не знаю, к чему @Zarak придрался. Хоть объяснил бы, что не ясно
 
7,099
324
1,509
Это не относится к теме пакетов. Про сущность, на которую смотришь была тема
 
7,099
324
1,509
Точно также, но метод отправки другой
Есть sendToPlayer, sendToClients, посмотри в классе PacketCustom
 

Sainthozier

Стрелочник
623
11
369
Использование этой либы на версии 1.7.10 является актуальным на данный момент?
И как обстоят дела с безопасностью?
 
7,099
324
1,509
Конечно актуально. Безопасность такая же, как с обычными пакетами
 
7,099
324
1,509
7,099
324
1,509
А CCL есть там?
 
Сверху