From a608419e38cf02362df4cfb77a3587108f76bfc6 Mon Sep 17 00:00:00 2001 From: Juuxel <6596629+Juuxel@users.noreply.github.com> Date: Thu, 17 Dec 2020 18:38:14 +0200 Subject: Add screen networking API --- .../io/github/cottonmc/test/TestDescription.java | 20 ++- .../cottonmc/cotton/gui/SyncedGuiDescription.java | 35 +++++ .../cottonmc/cotton/gui/impl/LibGuiCommon.java | 10 ++ .../cotton/gui/impl/ScreenNetworkingImpl.java | 142 +++++++++++++++++++++ .../cotton/gui/impl/client/LibGuiClient.java | 2 + .../cotton/gui/networking/NetworkSide.java | 13 ++ .../cotton/gui/networking/ScreenNetworking.java | 96 ++++++++++++++ .../cotton/gui/networking/package-info.java | 7 + src/main/resources/fabric.mod.json | 4 +- 9 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/impl/LibGuiCommon.java create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/impl/ScreenNetworkingImpl.java create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/networking/NetworkSide.java create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/networking/ScreenNetworking.java create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/networking/package-info.java diff --git a/GuiTest/src/main/java/io/github/cottonmc/test/TestDescription.java b/GuiTest/src/main/java/io/github/cottonmc/test/TestDescription.java index b3e0c80..e0602be 100644 --- a/GuiTest/src/main/java/io/github/cottonmc/test/TestDescription.java +++ b/GuiTest/src/main/java/io/github/cottonmc/test/TestDescription.java @@ -1,14 +1,19 @@ package io.github.cottonmc.test; import io.github.cottonmc.cotton.gui.SyncedGuiDescription; +import io.github.cottonmc.cotton.gui.networking.NetworkSide; +import io.github.cottonmc.cotton.gui.networking.ScreenNetworking; import io.github.cottonmc.cotton.gui.widget.*; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.screen.ScreenHandlerContext; import net.minecraft.screen.ScreenHandlerType; import net.minecraft.text.LiteralText; +import net.minecraft.util.Identifier; public class TestDescription extends SyncedGuiDescription { - + private static final Identifier TEST_MESSAGE = new Identifier("libgui", "test"); + private static final Identifier UNREGISTERED_ON_SERVER = new Identifier("libgui", "unregistered_on_server"); + public TestDescription(ScreenHandlerType type, int syncId, PlayerInventory playerInventory, ScreenHandlerContext context) { super(type, syncId, playerInventory, getBlockInventory(context, GuiBlockEntity.INVENTORY_SIZE), null); @@ -16,7 +21,14 @@ public class TestDescription extends SyncedGuiDescription { root.add(WItemSlot.of(blockInventory, 0, 4, 1), 0, 1); - root.add(new WButton(new LiteralText("Button A")), 0, 3, 4, 1); + WButton buttonA = new WButton(new LiteralText("Button A")); + + buttonA.setOnClick(() -> { + ScreenNetworking.of(this, NetworkSide.CLIENT).send(TEST_MESSAGE, buf -> {}); + ScreenNetworking.of(this, NetworkSide.CLIENT).send(UNREGISTERED_ON_SERVER, buf -> {}); + }); + + root.add(buttonA, 0, 3, 4, 1); root.add(new WButton(new LiteralText("Button B")), 5, 3, 4, 1); root.add(new WButton(new LiteralText("Button C")), 0, 5, 4, 1); root.add(new WButton(new LiteralText("Button D")), 5, 5, 4, 1); @@ -29,5 +41,9 @@ public class TestDescription extends SyncedGuiDescription { System.out.println(root.toString()); this.getRootPanel().validate(this); + + ScreenNetworking.of(this, NetworkSide.SERVER).receive(TEST_MESSAGE, buf -> { + System.out.println("Received on the server!"); + }); } } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/SyncedGuiDescription.java b/src/main/java/io/github/cottonmc/cotton/gui/SyncedGuiDescription.java index ec779a6..fae723e 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/SyncedGuiDescription.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/SyncedGuiDescription.java @@ -2,6 +2,9 @@ package io.github.cottonmc.cotton.gui; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.InventoryProvider; @@ -18,10 +21,13 @@ import net.minecraft.screen.ScreenHandlerContext; import net.minecraft.screen.ScreenHandlerType; import net.minecraft.screen.slot.Slot; import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; import net.minecraft.world.World; import io.github.cottonmc.cotton.gui.client.BackgroundPainter; import io.github.cottonmc.cotton.gui.client.LibGui; +import io.github.cottonmc.cotton.gui.networking.NetworkSide; import io.github.cottonmc.cotton.gui.widget.WGridPanel; import io.github.cottonmc.cotton.gui.widget.WLabel; import io.github.cottonmc.cotton.gui.widget.WPanel; @@ -513,4 +519,33 @@ public class SyncedGuiDescription extends ScreenHandler implements GuiDescriptio public void setTitleAlignment(HorizontalAlignment titleAlignment) { this.titleAlignment = titleAlignment; } + + /** + * Gets the network side this GUI description runs on. + * + * @return this GUI's network side + * @since 3.3.0 + */ + public final NetworkSide getNetworkSide() { + return world instanceof ServerWorld ? NetworkSide.SERVER : NetworkSide.CLIENT; + } + + /** + * Gets the packet sender corresponding to this GUI's network side. + * + * @return the packet sender + * @since 3.3.0 + */ + public final PacketSender getPacketSender() { + if (getNetworkSide() == NetworkSide.SERVER) { + return ServerPlayNetworking.getSender((ServerPlayerEntity) playerInventory.player); + } else { + return getClientPacketSender(); + } + } + + @Environment(EnvType.CLIENT) + private PacketSender getClientPacketSender() { + return ClientPlayNetworking.getSender(); + } } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/LibGuiCommon.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/LibGuiCommon.java new file mode 100644 index 0000000..5e87287 --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/LibGuiCommon.java @@ -0,0 +1,10 @@ +package io.github.cottonmc.cotton.gui.impl; + +import net.fabricmc.api.ModInitializer; + +public final class LibGuiCommon implements ModInitializer { + @Override + public void onInitialize() { + ScreenNetworkingImpl.init(); + } +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/ScreenNetworkingImpl.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/ScreenNetworkingImpl.java new file mode 100644 index 0000000..a8d8921 --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/ScreenNetworkingImpl.java @@ -0,0 +1,142 @@ +package io.github.cottonmc.cotton.gui.impl; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.util.Identifier; + +import io.github.cottonmc.cotton.gui.SyncedGuiDescription; +import io.github.cottonmc.cotton.gui.networking.NetworkSide; +import io.github.cottonmc.cotton.gui.networking.ScreenNetworking; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +public class ScreenNetworkingImpl implements ScreenNetworking { + // Packet structure: + // syncId: int + // message: identifier + // rest: buf + + public static final Identifier SCREEN_MESSAGE_S2C = new Identifier("libgui", "screen_message_s2c"); + public static final Identifier SCREEN_MESSAGE_C2S = new Identifier("libgui", "screen_message_c2s"); + + private static final Logger LOGGER = LogManager.getLogger(); + private static final Map instanceCache = new WeakHashMap<>(); + + private final Map messages = new HashMap<>(); + private SyncedGuiDescription description; + private final NetworkSide side; + + private ScreenNetworkingImpl(SyncedGuiDescription description, NetworkSide side) { + this.description = description; + this.side = side; + } + + public void receive(Identifier message, MessageReceiver receiver) { + Objects.requireNonNull(message, "message"); + Objects.requireNonNull(receiver, "receiver"); + + if (!messages.containsKey(message)) { + messages.put(message, receiver); + } else { + throw new IllegalStateException("Message " + message + " on side " + side + " already registered"); + } + } + + @Override + public void send(Identifier message, Consumer writer) { + Objects.requireNonNull(message, "message"); + Objects.requireNonNull(writer, "writer"); + + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeVarInt(description.syncId); + buf.writeIdentifier(message); + writer.accept(buf); + description.getPacketSender().sendPacket(side == NetworkSide.SERVER ? SCREEN_MESSAGE_S2C : SCREEN_MESSAGE_C2S, buf); + } + + public static void init() { + ServerPlayNetworking.registerGlobalReceiver(SCREEN_MESSAGE_C2S, (server, player, networkHandler, buf, responseSender) -> { + handle(server, player, buf); + }); + } + + @Environment(EnvType.CLIENT) + public static void initClient() { + ClientPlayNetworking.registerGlobalReceiver(SCREEN_MESSAGE_S2C, (client, networkHandler, buf, responseSender) -> { + handle(client, client.player, buf); + }); + } + + private static void handle(Executor executor, PlayerEntity player, PacketByteBuf buf) { + ScreenHandler screenHandler = player.currentScreenHandler; + + // Packet data + int syncId = buf.readVarInt(); + Identifier messageId = buf.readIdentifier(); + + if (!(screenHandler instanceof SyncedGuiDescription)) { + LOGGER.error("Received message packet for screen handler {} which is not a SyncedGuiDescription", screenHandler); + return; + } else if (syncId != screenHandler.syncId) { + LOGGER.error("Received message for sync ID {}, current sync ID: {}", syncId, screenHandler.syncId); + return; + } + + ScreenNetworkingImpl networking = instanceCache.get(screenHandler); + + if (networking != null) { + MessageReceiver receiver = networking.messages.get(messageId); + + if (receiver != null) { + buf.retain(); + executor.execute(() -> receiver.onMessage(buf)); + } else { + LOGGER.warn("Message {} not registered for {} on side {}", messageId, screenHandler, networking.side); + } + } else { + LOGGER.warn("GUI description {} does not use networking", screenHandler); + } + } + + public static ScreenNetworking of(SyncedGuiDescription description, NetworkSide networkSide) { + Objects.requireNonNull(description, "description"); + Objects.requireNonNull(networkSide, "networkSide"); + + if (description.getNetworkSide() == networkSide) { + return instanceCache.computeIfAbsent(description, it -> new ScreenNetworkingImpl(description, networkSide)); + } else { + return DummyNetworking.INSTANCE; + } + } + + private static final class DummyNetworking extends ScreenNetworkingImpl { + static final DummyNetworking INSTANCE = new DummyNetworking(); + + private DummyNetworking() { + super(null, null); + } + + @Override + public void receive(Identifier message, MessageReceiver receiver) { + // NO-OP + } + + @Override + public void send(Identifier message, Consumer writer) { + // NO-OP + } + } +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/LibGuiClient.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/LibGuiClient.java index c9e0010..91ce7b7 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/LibGuiClient.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/LibGuiClient.java @@ -8,6 +8,7 @@ import net.minecraft.resource.ResourceType; import blue.endless.jankson.Jankson; import blue.endless.jankson.JsonElement; import blue.endless.jankson.JsonObject; +import io.github.cottonmc.cotton.gui.impl.ScreenNetworkingImpl; import io.github.cottonmc.jankson.JanksonFactory; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -27,6 +28,7 @@ public class LibGuiClient implements ClientModInitializer { public void onInitializeClient() { config = loadConfig(); + ScreenNetworkingImpl.initClient(); ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(NinePatchInternals.MetadataLoader.INSTANCE); } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/networking/NetworkSide.java b/src/main/java/io/github/cottonmc/cotton/gui/networking/NetworkSide.java new file mode 100644 index 0000000..e3816ac --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/networking/NetworkSide.java @@ -0,0 +1,13 @@ +package io.github.cottonmc.cotton.gui.networking; + +/** + * The sides of a network connection. + * + * @since 3.3.0 + */ +public enum NetworkSide { + /** The logical client. */ + CLIENT, + /** The logical server. */ + SERVER +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/networking/ScreenNetworking.java b/src/main/java/io/github/cottonmc/cotton/gui/networking/ScreenNetworking.java new file mode 100644 index 0000000..2a41b29 --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/networking/ScreenNetworking.java @@ -0,0 +1,96 @@ +package io.github.cottonmc.cotton.gui.networking; + +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.Identifier; + +import io.github.cottonmc.cotton.gui.SyncedGuiDescription; +import io.github.cottonmc.cotton.gui.impl.ScreenNetworkingImpl; + +import java.util.function.Consumer; + +/** + * {@code ScreenNetworking} handles screen-related network messages sent between the server and the client. + * + *

Registering a message receiver

+ * {@linkplain MessageReceiver Message receivers} can be registered by calling {@link #receive(Identifier, MessageReceiver)} + * on a {@code ScreenNetworking} for the receiving side. The {@code message} ID is a unique ID that matches between + * the sender and the receiver. + * + *

Message receivers should be registered in the constructor of a {@link SyncedGuiDescription}. + * + *

Sending messages

+ * Messages can be sent by calling {@link #send(Identifier, Consumer)} on a {@code ScreenNetworking} + * for the sending side. The {@code message} ID should match up with a receiver registered on the opposite + * side. + * + *

Example

+ *
+ * {@code
+ * private static final Identifier MESSAGE_ID = new Identifier("my_mod", "some_message");
+ *
+ * // Receiver
+ * ScreenNetworking.of(this, NetworkSide.SERVER).receive(MESSAGE_ID, buf -> {
+ * 	   // Example data: a lucky number as an int
+ *     System.out.println("Your lucky number is " + buf.readInt() + "!");
+ * });
+ *
+ * // Sending
+ *
+ * // We're sending from a button. The packet data is our lucky number, 123.
+ * WButton button = ...;
+ * button.setOnClick(() -> {
+ *     ScreenNetworking.of(this, NetworkSide.CLIENT).send(MESSAGE_ID, buf -> buf.writeInt(123));
+ * });
+ * }
+ * 
+ * + * @since 3.3.0 + */ +public interface ScreenNetworking { + /** + * Gets a networking handler for the GUI description that is active on the specified side. + * + * @param description the GUI description + * @param networkSide the network side + * @return the network handler + * @throws NullPointerException if either parameter is null + */ + static ScreenNetworking of(SyncedGuiDescription description, NetworkSide networkSide) { + return ScreenNetworkingImpl.of(description, networkSide); + } + + /** + * Registers a message receiver for the message. + * + * @param message the screen message ID + * @param receiver the message receiver + * @throws IllegalStateException if the message has already been registered + * @throws NullPointerException if either parameter is null + */ + void receive(Identifier message, MessageReceiver receiver); + + /** + * Sends a screen message to the other side of the connection. + * + * @param message the screen message ID + * @param writer a writer that writes the message contents to a packet buffer; + * should not read the buffer + * @throws NullPointerException if either parameter is null + */ + void send(Identifier message, Consumer writer); + + /** + * A handler for received screen messages. + */ + @FunctionalInterface + interface MessageReceiver { + /** + * Handles a received screen message. + * + *

This method should only read from the buffer, not write to it. + * + * @param buf the message packet buffer + */ + void onMessage(PacketByteBuf buf); + } +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/networking/package-info.java b/src/main/java/io/github/cottonmc/cotton/gui/networking/package-info.java new file mode 100644 index 0000000..e38e08d --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/networking/package-info.java @@ -0,0 +1,7 @@ +/** + * Screen networking helpers. + * + * @see io.github.cottonmc.cotton.gui.networking.ScreenNetworking + * @since 3.3.0 + */ +package io.github.cottonmc.cotton.gui.networking; diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index c8f0ffe..ca3cde8 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -18,6 +18,7 @@ "environment": "*", "entrypoints": { + "main": ["io.github.cottonmc.cotton.gui.impl.LibGuiCommon"], "client": ["io.github.cottonmc.cotton.gui.impl.client.LibGuiClient"], "modmenu": ["io.github.cottonmc.cotton.gui.impl.client.modmenu.ModMenuSupport"] }, @@ -28,7 +29,8 @@ "fabricloader": ">=0.8.8", "fabric": "*", "minecraft": ">=1.16.1", - "jankson": "^3.0.0" + "jankson": "^3.0.0", + "fabric-networking-api-v1": "^1.0.0" }, "suggests": { "flamingo": "*" -- cgit