- Версия(и) Minecraft
- 1.12.2, 1.7.10
Кратко:
Примеры использования
Инструментарий
Forge client:
Отправляйтесь в Releases на Github и качайте одну из версий.
p.s. используйте jar файл, названный
Это возможно при работе с версией игры 1.7.10.
- Больше никаких обработчиков для каждого пакета, сохраняя ООП стиль и синхронность обработки
- Регистрация пакетов без надобности указывать дескриминаторы и прочий блудняк
- Колбэки. Отсылка запросов серверу и асинхронное ожидание ответа, а затем обработка результата синхронно и все это в одной строчке кода
- Контроль над пакетами. Подавление спама пакетами одной аннотацией
- Богатый функционал отправки пакетов пачками с фильтрацией и блек джеком
- Конвертация 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);
}
}
Здесь будет рассмотрен пример простой игры в угадайку числа. Клиент будет генерировать случайное число при отправке запроса серверу. Сервер же будет решать, угадал клиент или нет и отсылать ему ответ.
Пример также продемонстрирует возможность применения
RequestController.Periodic даст возможность блокировать обработку пакета, если клиент слишком часто шлет пакеты этого типа. В данном примере, пакет от клиента будет обработан на сервере только в том случае, если период отправки двух одинаковых пакетов клиентом не превышает 200 миллисекунд. В обратном случае, мы все равно вызовем метод write у серверного пакета и уведомим клиента, что он пренебрегает возможностью угадать число.
Пример также продемонстрирует возможность применения
@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.");
}
}
У нас есть задача передать клиенту все товары всех категорий магазина. Мы можем сделать это двумя способами:
- Нет, здесь и пригодится PacketAPI. Апи поддерживает нехитрую систему колбэков, которые объединяют 2 пакета в один и не вынуждают писать для этих пакетов обработчики, а позволяют проворачивать запись и чтение в 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. Здесь решается задача вывода в консоль клиенту времени с момента логина, которое хранится исключительно на сервере.
Данный пример демонстрирует работу между 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 будет записан одной строчкой кода в буфер.
Composable объекты как и Serializable поддерживают тег transient для маркировки игнорируемых данных при сериализации.
Экосистема Composable также включает в себя процесс ленивого обмена этими объектами между логическими сторонами. В качестве простого примера, ниже я приведу пример реализации решения задачи отправки каждой нажатой игроком кнопки на клавиатуре серверу.
Как видно, подобные задачи становятся намного проще и быстрее в реализации с использованием Composable.
По сути, так можно было бы передавать любое событие, отсылать обновления мира или регистра.
ВАЖНО! Composable, порядок его полей и их типы данных должны быть идентичны на обеих логических сторонах, иначе что-то да где-то вспыхнет и взорвется.
ВАЖНО! Composable не использует привычную сериализацию в Java. Упаковываются только значения его полей и некоторая мета-информация, что облегчает его в разы. Помимо этого, упаковка некоторых типов данных в Composable упрощена на сколько это было возможным. К примеру, enum передается как integer, используя его ordinal. Имейте это ввиду, когда будете работать с Composable. Если вы захотите переписать процесс упаковки какого-либо типа данных(того же enum, к примеру), то обратите внимание на текст ниже.
ВАЖНО! Упаковщик Composable изначально не поддерживает ничего, кроме примитивов, строк, enum, коллекций, массивов, карт и самих Composable. Однако, вы можете без проблем расширить этот список, зарегистрировав свой адаптер, используя
Если вам вдруг вздумается переписать логику упаковки вашего Composable от А до Я, вам будет достаточно переопределить соответствующие методы внутри своего 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 базовый исходящий колбэк
- PacketHandlerServer инструмент, обрабатывающий и отправляющий пакеты на серверной стороне
- Base packets:
- IPacketInServer базовый входящий пакет
- IPacketOutServer базовый исходящий пакет
- ICallbackInServer базовый входящий колбэк
- 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.