PacketAPI

PacketAPI 1.3.0

Нет прав для скачивания
Версия(и) Minecraft
1.12.2, 1.7.10

PacketAPI призвано упростить работу с пакетами.
Кратко:
  • Больше никаких обработчиков для каждого пакета, сохраняя ООП стиль и синхронность обработки
  • Регистрация пакетов без надобности указывать дескриминаторы и прочий блудняк
  • Колбэки. Отсылка запросов серверу и асинхронное ожидание ответа, а затем обработка результата синхронно и все это в одной строчке кода
  • Контроль над пакетами. Подавление спама пакетами одной аннотацией
  • Богатый функционал отправки пакетов пачками с фильтрацией и блек джеком
  • Конвертация Object <-> byte[] без боли
  • Полная поддержка BukkitAPI и Forge
  • Ленивая отправка данных без пакетов
  • Авторский метод упаковки данных, схожий с сериализацией, но с оптимизацией веса объектов до 4.5 раз

Примеры использования


пример использования колбэков:
@Mod(modid = "test")
@PacketSubscriber(channelName = "test", packets = {
    MyPacket.class
})
public class MyMod {
    public void exampleMethod() {
        Sender.callback(new MyMathCallback("2 * 2 = 4 ?"))
            .send() // sending a callback to the server
            .onResult(result -> { // when the response came back from the server
                if(result.isTrue()) {
                    System.out.println("It is true");
                } else {
                    System.out.println("It is not true");
                }
            })
            .onTimeout(() -> System.out.println("TIMEOUT")) // on timeout
            .onError(exception -> System.out.println("EXCEPTION: " + exception)); // if an exception thrown during execution
    }
}
IO:
@Packet
public class MyPacket implements IPacketOutServer, IPacketInClient {
    // writing on the server
    @Override
    public void write(EntityPlayer entityPlayer, ByteBufOutputStream bbos) throws IOException {
        ShopCategory category = new ShopCategory();
        for (int i = 0; i < 10; i++) {
            category.add(new ShopItem("Name", 1000));
        }
        writeObject(bbos, category);
    }
    // reading on the client
    @Override
    public void read(ByteBufInputStream bbis) throws IOException {
        ShopCategory category = readObject(bbis, ShopCategory.class);
    }
}
Здесь будет рассмотрен пример простой игры в угадайку числа. Клиент будет генерировать случайное число при отправке запроса серверу. Сервер же будет решать, угадал клиент или нет и отсылать ему ответ.

Пример также продемонстрирует возможность применения @ControllablePacket(period = 200L) , что фактически эквивалентно строке private static final RequestController<UUID> REQUEST_CONTROLLER = new RequestController.Periodic<>(200L);.

RequestController.Periodic даст возможность блокировать обработку пакета, если клиент слишком часто шлет пакеты этого типа. В данном примере, пакет от клиента будет обработан на сервере только в том случае, если период отправки двух одинаковых пакетов клиентом не превышает 200 миллисекунд. В обратном случае, мы все равно вызовем метод write у серверного пакета и уведомим клиента, что он пренебрегает возможностью угадать число.

Клиент-серверный мод:
@Mod(modid = MODID)
@PacketSubscriber(channelName = MODID, packets = {
    MyTestCallback.class,
    MyTestCallbackOnServer.class
})
public class MyTestMod {

    static final String MODID = "packetapiexample";

    @Mod.EventHandler
    public void event(FMLInitializationEvent event) {
        if(event.getSide() == Side.CLIENT) {
            MinecraftForge.EVENT_BUS.register(this);
        }
    }

    @SubscribeEvent
    public void event(InputEvent.KeyInputEvent event) {
        if(Keyboard.getEventKeyState() && Keyboard.isKeyDown(Keyboard.KEY_P)) {
            int v = new Random().nextInt(10);
            System.out.println(String.format("Sending a number %s for verification to the server side...", v));
            Sender.callback(new MyTestCallback(v))
                    .send()
                    .onResult(result -> {
                        if (result.isSuccess()) {
                            System.out.println("Success!");
                        } else {
                            System.out.println("Failure!");
                        }
                        System.out.println(result.getResponseMessage());
                    });
        }
    }
}
Клиентский пакет:
@Packet
public class MyTestCallback implements ICallbackOut<MyTestCallback.Result> {

    private final Result result = new Result();

    private int value;

    public MyTestCallback() {}

    public MyTestCallback(int value) {
        this.value = value;
    }

    @Override
    public void write(ByteBufOutputStream bbos) throws IOException {
        bbos.writeInt(value);
    }

    @Override
    public void read(ByteBufInputStream bbis) throws IOException {
        result.success = bbis.readBoolean();
        result.responseMessage = bbis.readUTF();
    }

    @Nullable
    @Override
    public Result getResult() {
        return result;
    }

    public static class Result {

        private boolean success;
        private String responseMessage;

        public boolean isSuccess() {
            return success;
        }

        public String getResponseMessage() {
            return responseMessage;
        }
    }
}
Серверный пакет:
@Packet
@ControllablePacket(period = 200L, callWriteAnyway = true)
public class MyTestCallbackOnServer implements ICallbackInServer {

    private Integer value;

    @Override
    public void read(EntityPlayer entityPlayer, ByteBufInputStream bbis, PacketCallbackSender packetCallbackSender) throws IOException {
        System.out.println("Incoming request from " + entityPlayer.getName());
        value = bbis.readInt();
    }

    @Override
    public void write(EntityPlayer entityPlayer, ByteBufOutputStream bbos) throws IOException {
        System.out.println("Sending the result back to the client side...");
        boolean success = false;
        String message = "You sent a request too often. Request rejected!";
        if (value != null) {
            message = "You haven't guessed the secret number, it's definitely not 4!";
            if(value == 4) {
                success = true;
                message = "You guessed the secret number, it really is 4!";
            }
        }
        bbos.writeBoolean(success);
        bbos.writeUTF(message);
        System.out.println("The result was sent.");
    }
}
У нас есть задача передать клиенту все товары всех категорий магазина. Мы можем сделать это двумя способами:
  1. Отослать все сразу при коннекте клиента к серверу, что будет неэффективно, по крайней мере, по одной причине - на клиента может упасть сразу очень много информации, что возможно может перегрузить его на некоторое время.
  2. Отсылать каждую из категорий по мере ее загрузки в интерфейсе. В этом же случае нам придется писать слишком сложную систему для такой просто задачи. Придется организовывать структуру пакетов, писать для каждого обработчик, придумывать как загрузить пришедшую информацию по категории в интерфейс и т.д.
Я вижу второй вариант практичнее, но означает ли это, что придется столкнуться со всем гемороем, описанным выше ?
- Нет, здесь и пригодится PacketAPI. Апи поддерживает нехитрую систему колбэков, которые объединяют 2 пакета в один и не вынуждают писать для этих пакетов обработчики, а позволяют проворачивать запись и чтение в 2 методах. Мало того, колбэки позволяют описать обработку результата в одну строчку кода прямо в моменте своей отправки. Наглядно:

Момент отправки пакета:
System.out.println("Sending a request to the server for shop items by category: " + category.toString());
Sender.callback(new PacketShopCategoryGet(category.toString()))
        .timeout(2000L)
        .send()
        .onResult(result -> {
            // it will happen when a callback will return back with the result to the client side
            System.out.println("The incoming set of shop items:");
            result.getShopItemList().forEach(System.out::println);
            System.out.println("The message from the server: " + result.getResponseMessage());
        })
        .onTimeout(() -> System.out.println("timeout"))
        .onError(exception -> System.out.println("exception: " + exception));
Клиентский пакет-запрос:
@Packet
public class PacketShopCategoryGet implements ICallbackOut<PacketShopCategoryGet.Result> {

    private final Result result = new Result();

    private String category;

    public PacketShopCategoryGet() {}

    public PacketShopCategoryGet(String category) {
        this.category = category;
    }

    @Override
    public void write(ByteBufOutputStream bbos) throws IOException {
        bbos.writeUTF(category);
    }

    @Override
    public void read(ByteBufInputStream bbis) throws IOException {
        result.shopItemList = readObjects(bbis, ShopItem.class);
        result.responseMessage = bbis.readUTF();
    }

    @Nullable
    @Override
    public Result getResult() {
        return result;
    }

    public static class Result {
        private String responseMessage;
        private List<ShopItem> shopItemList;

        public String getResponseMessage() {
            return responseMessage;
        }

        public List<ShopItem> getShopItemList() {
            return shopItemList;
        }
    }
}
Серверный пакет-обработчик:
@Packet
public class PacketShopCategoryGetOnServer implements ICallbackInServer {

    private static final RequestController<UUID> REQUEST_CONTROLLER = new RequestController.Periodic<>(1000L);

    private List<ShopItem> shopItems;
    private long period;

    @Override
    public void read(EntityPlayer entityPlayer, ByteBufInputStream bbis, PacketCallbackSender packetCallbackSender) throws IOException {
        String category = bbis.readUTF();
        System.out.println("Incoming request shop items by category: " + category);
        long l = System.currentTimeMillis();
        REQUEST_CONTROLLER.doCompletedRequestAsync(entityPlayer.getUniqueID(), () -> {
            shopItems = ShopMod.INSTANCE.getShopItemManager().getItemListByCategory(category);
            period = System.currentTimeMillis() - l;
            System.out.println("Request processed. Sending the result back to the client side...");
            packetCallbackSender.send();
        });
    }

    @Override
    public void write(EntityPlayer entityPlayer, ByteBufOutputStream bbos) throws IOException {
        writeObjects(bbos, shopItems);
        bbos.writeUTF("The processing of request took " + period + " millis");
    }

    @Override
    public boolean handleCallback() {
        return true;
    }
}
Если вы пишете серверную часть на BukkitAPI, будь то Sponge или Spigot, вы все также можете использовать PacketAPI. Апи предоставляет два набора серверных инструментов для работы с пакетами: для Forge и для BukkitAPI.

Данный пример демонстрирует работу между BukkitAPI и Forge. Здесь решается задача вывода в консоль клиенту времени с момента логина, которое хранится исключительно на сервере.

Клиентский мод:
@Mod(modid = "timesenderexample")
@PacketSubscriber(channelName = "timesenderexample", packets = {
    PacketOnlineSend.class,
    PacketOnlineReceive.class
})
public class OnlineTimeSenderMod {
}
Плагин на BukkitAPI:
@PacketSubscriber(channelName = "timesenderexample", packets = {
    PacketOnlineSend.class,
    PacketOnlineReceive.class
})
public class OnlineTimeSenderPlugin extends JavaPlugin implements Listener {

    public static final TObjectLongMap<UUID> ONLINE_MAP = new TObjectLongHashMap<>();

    @Override
    public void onEnable() {
        getServer().getPluginManager().registerEvents(this, this);
        getServer().getScheduler().scheduleSyncRepeatingTask(this, () -> Sender.packet(new PacketOnlineSend()).toAll().send(), 0L, 20L);
    }

    @EventHandler
    public void event(PlayerLoginEvent event) {
        System.out.println("Logged in " + event.getPlayer().getName());
        ONLINE_MAP.put(event.getPlayer().getUniqueId(), System.currentTimeMillis());
    }
}
Серверный пакет, отправляющий нам количество секунд, прошедшее с нашего логина на сервера:
@Packet
public class PacketOnlineSend implements IPacketOutBukkit {
    @Override
    public void write(Player player, ByteBufOutputStream bbos) throws IOException {
        bbos.writeInt((int) ((System.currentTimeMillis() - OnlineTimeSenderPlugin.ONLINE_MAP.get(player.getUniqueId())) / 1000));
        bbos.writeUTF("I'm a string, just a string");
    }
}

Клиентский пакет, получающий информацию с сервера и принтящий ее в консоль:
@Packet
public class PacketOnlineReceive implements IPacketInClient {
    @Override
    public void read(ByteBufInputStream bbis) throws IOException {
        int seconds = bbis.readInt();
        System.out.println("Your actual played time: " + seconds + " sec");
        System.out.println(bbis.readUTF());
    }
}
Composable - особый тип объектов в рамках PacketAPI. Composable похож на Serializable, но он значительно легче и быстрее, что определенно привлекает внимание. Composable освобождает разработчика от муторного описания процесса компоновки данных из объекта в буфер и обратно.

Composable не использует стандартную сериализацию Java, хоть и основан на ее идеях и принципах. Благодаря особому подходу в упаковке данных, Composable может стать легче при передаче данных больше чем в 4 раза. По умолчанию, упаковщик не сильно оптимизирует вес вашего Composable. Однако вы можете включить этот процесс для вашего Composable, пометив либо его класс, либо нужные вам поля аннотацией @Lightweight. Смотрите документацию и примеры, дабы разобраться как с этим работать и где это применимо.

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

Java:
public class MyNode implements Composable {
    private final MyNode prev;

    public MyNode(MyNode prevNode) {
        this.prev = prevNode;
    }
}
static {
    IPacket.writeObject(buffer, new MyNode(new MyNode(new MyNode)))
}

Composable объекты как и Serializable поддерживают тег transient для маркировки игнорируемых данных при сериализации.
private transient String игнорируемая_строка = "важная информация, не подверженная упаковке";

Экосистема Composable также включает в себя процесс ленивого обмена этими объектами между логическими сторонами. В качестве простого примера, ниже я приведу пример реализации решения задачи отправки каждой нажатой игроком кнопки на клавиатуре серверу.

Java:
@Mod(modid = MODID)
public class ComposableMod {

    static final String MODID = "catcherexample";

    @Mod.EventHandler
    public void event(FMLInitializationEvent event) {
        if(event.getSide().isClient()) {
            MinecraftForge.EVENT_BUS.register(this);
        } else {
            PacketAPI.getComposableCatcherBus().register(this, TestKeyComposable.class);
        }
    }

    @SubscribeEvent
    public void event(InputEvent.KeyInputEvent event) {
        // it happens on the client side
        Sender.composable(new TestKeyComposable(Keyboard.getEventKey(), Keyboard.getEventKeyState()))
                .fromClient()
                .send();
        // or PacketHandlerClient.getInstance().sendComposable(new TestKeyComposable(Keyboard.getEventKey(), Keyboard.getEventKeyState()));
    }

    @ComposableCatcher
    public void catcher(TestKeyComposable testKeyComposable, EntityPlayer entityPlayer) {
        // it happens on the server side
        System.out.println(entityPlayer.getName() + (testKeyComposable.isPressed() ? " " : " un") + "pressed the key " + Keyboard.getKeyName(testKeyComposable.getKeyCode()));
    }
}

Composable:
public class TestKeyComposable implements Composable {

    private final int keyCode;
    private final boolean isPressed;

    public TestKeyComposable(int keyCode, boolean isPressed) {
        this.keyCode = keyCode;
        this.isPressed = isPressed;
    }

    public int getKeyCode() {
        return keyCode;
    }

    public boolean isPressed() {
        return isPressed;
    }

    @Override
    public String toString() {
        return "TestKeyComposable{" +
                "keyCode=" + keyCode +
                ", isPressed=" + isPressed +
                '}';
    }
}

Как видно, подобные задачи становятся намного проще и быстрее в реализации с использованием Composable.
По сути, так можно было бы передавать любое событие, отсылать обновления мира или регистра.

ВАЖНО! Composable, порядок его полей и их типы данных должны быть идентичны на обеих логических сторонах, иначе что-то да где-то вспыхнет и взорвется.
ВАЖНО! Composable не использует привычную сериализацию в Java. Упаковываются только значения его полей и некоторая мета-информация, что облегчает его в разы. Помимо этого, упаковка некоторых типов данных в Composable упрощена на сколько это было возможным. К примеру, enum передается как integer, используя его ordinal. Имейте это ввиду, когда будете работать с Composable. Если вы захотите переписать процесс упаковки какого-либо типа данных(того же enum, к примеру), то обратите внимание на текст ниже.
ВАЖНО! Упаковщик Composable изначально не поддерживает ничего, кроме примитивов, строк, enum, коллекций, массивов, карт и самих Composable. Однако, вы можете без проблем расширить этот список, зарегистрировав свой адаптер, используя PacketAPI.getComposer().registerComposeAdapter. Важно понимать, что данный метод не предназначен для самих Composable.
Если вам вдруг вздумается переписать логику упаковки вашего Composable от А до Я, вам будет достаточно переопределить соответствующие методы внутри своего Composable.

Регистрация адаптера:
Composer composer = PacketAPI.getComposer();

composer.registerComposeAdapter(
    Vec3f.class,
    (vec3f, byteBufOutputStream1) -> {
        byteBufOutputStream1.writeFloat(vec3f.x);
        byteBufOutputStream1.writeFloat(vec3f.y);
        byteBufOutputStream1.writeFloat(vec3f.z);
    },
    byteBufInputStream -> new Vec3f(byteBufInputStream.readFloat(), byteBufInputStream.readFloat(), byteBufInputStream.readFloat())
);
// или
composer.registerComposeAdapter(UUID.class, (uuid, byteBufOutputStream1) -> byteBufOutputStream1.writeUTF(uuid.toString()), byteBufInputStream -> UUID.fromString(byteBufInputStream.readUTF()));

Инструментарий


Forge client:
  • PacketHandlerClient инструмент, обрабатывающий и отправляющий пакеты на клиентской стороне
  • Base packets:
    • IPacketInClient базовый входящий пакет
    • IPacketOutClient базовый исходящий пакет
    • ICallbackOut базовый исходящий колбэк
Forge server:
  • PacketHandlerServer инструмент, обрабатывающий и отправляющий пакеты на серверной стороне
  • Base packets:
    • IPacketInServer базовый входящий пакет
    • IPacketOutServer базовый исходящий пакет
    • ICallbackInServer базовый входящий колбэк
Bukkit:
  • PacketHandlerBukkitServer инструмент, обрабатывающий и отправляющий пакеты на Bukkit стороне
  • Base packets:
    • IPacketInBukkit базовый входящий пакет
    • IPacketOutBukkit базовый исходящий пакет
    • ICallbackInBukkit базовый входящий колбэк
Остальное:
  • Composable тип объектов, которые могут быть отправлены без пакетов. Смотрите документацию и примеры для полной информации
  • Sender один из основных инструментов для отправки исходящих пакетов, колбэков и Composable объектов. Смотрите документацию и примеры для полной информации
  • RequestController инструмент для фильтрации и откладывания обработки пакетов. Смотрите документацию и примеры для полной информации
  • Packet and PacketSubscriber аннотации для автоматической регистрации ваших пакетов. Смотрите документацию и примеры для полной информации

Установка



Отправляйтесь в Releases на Github и качайте одну из версий.

p.s. используйте jar файл, названный packetapi-@[email protected], если у вас не получается запустить игру с этой библиотекой из IDE.
Это возможно при работе с версией игры 1.7.10.
Автор
Cornell
Скачивания
30
Просмотры
1,918
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок

Последние обновления

  1. 1.3.0

    оптимизирован процесс упаковки Composable. Теперь Composable объекты легче больше чем в 4 раза...
  2. 1.2.0

    переработан проект некоторые классы были переименованы до упрощенных вариаций добавлен новый...
  3. 1.1.0: Composable

    добавлены Composable объекты добавлена ленивая отправка данных дополнена документация и...
Сверху