From 502a73665413fd11bcb7a7d258e1c56c2ec7c3ea Mon Sep 17 00:00:00 2001
From: TexTrue <65154269+TexBlock@users.noreply.github.com>
Date: Sun, 20 Oct 2024 15:44:16 +0800
Subject: [PATCH] wip part 2
---
.../{networking => }/KessokuNetworking.java | 4 +-
.../lib/api/networking/LoginPacketSender.java | 54 +++
.../{util => }/PacketByteBufHelper.java | 2 +-
.../lib/api/networking/PacketSender.java | 64 ++++
.../api/networking/PayloadTypeRegistry.java | 56 +++
...rConfigurationNetworkHandlerExtension.java | 2 +-
.../client/C2SConfigurationChannelEvent.java | 51 +++
.../client/C2SPlayChannelEvent.java | 51 +++
.../ClientConfigurationConnectionEvent.java | 78 +++++
.../client/ClientConfigurationNetworking.java | 275 +++++++++++++++
.../client/ClientLoginConnectionEvent.java | 80 +++++
.../client/ClientLoginNetworking.java | 145 ++++++++
.../client/ClientPlayConnectionEvent.java | 62 ++++
.../client/ClientPlayNetworking.java | 282 +++++++++++++++
.../server/S2CConfigurationChannelEvent.java | 50 +++
.../server/S2CPlayChannelEvent.java | 50 +++
.../ServerConfigurationConnectionEvent.java | 67 ++++
.../server/ServerConfigurationNetworking.java | 269 +++++++++++++++
.../server/ServerLoginConnectionEvent.java | 73 ++++
.../server/ServerLoginNetworking.java | 179 ++++++++++
.../server/ServerPlayConnectionEvent.java | 59 ++++
.../server/ServerPlayNetworking.java | 326 ++++++++++++++++++
.../api/networking/util/ChannelRegister.java | 56 ---
.../util/NetworkHandlerExtension.java | 7 -
.../AbstractChanneledNetworkAddon.java | 224 ++++++++++++
.../impl/networking/AbstractNetworkAddon.java | 145 ++++++++
.../impl/networking/ChannelInfoHolder.java | 13 +
.../CustomPayloadPacketCodecExtension.java | 7 +
.../networking/CustomPayloadTypeProvider.java | 9 +
.../networking/GlobalReceiverRegistry.java | 208 +++++++++++
.../networking/NetworkHandlerExtension.java | 5 +
.../lib/impl/networking/NetworkingImpl.java | 31 ++
.../networking/PacketCallbackListener.java | 12 +
...Impl.java => PayloadTypeRegistryImpl.java} | 14 +-
.../impl/networking/RegistrationPayload.java | 80 +++++
.../client/ClientCommonNetworkAddon.java | 64 ++++
.../ClientConfigurationNetworkAddon.java | 121 +++++++
.../client/ClientLoginNetworkAddon.java | 99 ++++++
.../client/ClientNetworkingImpl.java | 166 +++++++++
.../client/ClientPlayNetworkAddon.java | 93 +++++
.../common/CommonPacketHandler.java | 11 +
.../networking/common/CommonPacketsImpl.java | 79 +++++
.../common/CommonRegisterPayload.java | 36 ++
.../common/CommonVersionPayload.java | 24 ++
...PacketByteBufLoginQueryRequestPayload.java | 12 +
...acketByteBufLoginQueryResponsePayload.java | 11 +
.../networking/payload/PayloadHelper.java | 28 ++
.../networking/server/QueryIdFactory.java | 22 ++
.../ServerConfigurationNetworkAddon.java | 174 ++++++++++
.../server/ServerLoginNetworkAddon.java | 191 ++++++++++
.../server/ServerNetworkingImpl.java | 45 +++
.../server/ServerPlayNetworkAddon.java | 128 +++++++
.../ClientCommonNetworkHandlerAccessor.java | 13 +
...ntConfigurationNetworkHandlerAccessor.java | 13 +
.../ClientLoginNetworkHandlerAccessor.java | 13 +
.../client/ConnectScreenAccessor.java | 13 +
.../client/MinecraftClientAccessor.java | 15 +
.../ServerCommonNetworkHandlerAccessor.java | 17 +
.../ServerLoginNetworkHandlerAccessor.java | 17 +
.../main/resources/architectury.common.json | 5 +
.../kessoku-networking.common.mixins.json | 19 +
networking/fabric/build.gradle | 2 +-
.../kessoku/lib/KessokuNetworkingFabric.java | 4 -
.../fabric/CommonPacketsImplFabric.java | 65 ++++
.../fabric/KessokuNetworkingFabric.java | 20 ++
.../fabric/ClientConnectionMixin.java | 74 ++++
.../fabric/CommandManagerMixin.java | 39 +++
.../fabric/CustomPayloadC2SPacketMixin.java | 41 +++
.../fabric/CustomPayloadPacketCodecMixin.java | 47 +++
.../fabric/CustomPayloadS2CPacketMixin.java | 50 +++
.../LoginQueryRequestS2CPacketMixin.java | 28 ++
.../LoginQueryResponseC2SPacketMixin.java | 34 ++
.../fabric/PacketCodecDispatcherMixin.java | 34 ++
.../networking/fabric/PlayerManagerMixin.java | 21 ++
.../ServerCommonNetworkHandlerMixin.java | 42 +++
...erverConfigurationNetworkHandlerMixin.java | 148 +++++++-
.../ServerLoginNetworkHandlerMixin.java | 74 ++++
.../fabric/ServerPlayNetworkHandlerMixin.java | 51 +++
.../ClientCommonNetworkHandlerMixin.java | 35 ++
...lientConfigurationNetworkHandlerMixin.java | 47 +++
.../ClientLoginNetworkHandlerMixin.java | 50 +++
.../client/ClientPlayNetworkHandlerMixin.java | 47 +++
.../fabric/src/main/resources/fabric.mod.json | 39 +++
.../kessoku-networking.fabric.mixins.json | 29 ++
networking/neo/build.gradle | 3 +-
.../neoforge/CommonPacketsImplNeoForge.java | 84 +++++
.../neoforge/KessokuNetworkingNeoForge.java | 51 +++
...erverConfigurationNetworkHandlerMixin.java | 148 ++++++++
.../kessoku-networking.neoforge.mixins.json | 13 +
89 files changed, 5756 insertions(+), 83 deletions(-)
rename networking/common/src/main/java/band/kessoku/lib/api/{networking => }/KessokuNetworking.java (78%)
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/LoginPacketSender.java
rename networking/common/src/main/java/band/kessoku/lib/api/networking/{util => }/PacketByteBufHelper.java (99%)
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/PacketSender.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/PayloadTypeRegistry.java
rename networking/common/src/main/java/band/kessoku/lib/api/networking/{util => }/ServerConfigurationNetworkHandlerExtension.java (96%)
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/client/C2SConfigurationChannelEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/client/C2SPlayChannelEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientConfigurationConnectionEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientConfigurationNetworking.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientLoginConnectionEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientLoginNetworking.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientPlayConnectionEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientPlayNetworking.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/server/S2CConfigurationChannelEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/server/S2CPlayChannelEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerConfigurationConnectionEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerConfigurationNetworking.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerLoginConnectionEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerLoginNetworking.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerPlayConnectionEvent.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerPlayNetworking.java
delete mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/util/ChannelRegister.java
delete mode 100644 networking/common/src/main/java/band/kessoku/lib/api/networking/util/NetworkHandlerExtension.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/AbstractChanneledNetworkAddon.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/ChannelInfoHolder.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/CustomPayloadPacketCodecExtension.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/CustomPayloadTypeProvider.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/GlobalReceiverRegistry.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/NetworkHandlerExtension.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/NetworkingImpl.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/PacketCallbackListener.java
rename networking/common/src/main/java/band/kessoku/lib/impl/networking/{ChannelRegisterImpl.java => PayloadTypeRegistryImpl.java} (67%)
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/RegistrationPayload.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientCommonNetworkAddon.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientConfigurationNetworkAddon.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientLoginNetworkAddon.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientNetworkingImpl.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientPlayNetworkAddon.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonPacketHandler.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonPacketsImpl.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonRegisterPayload.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonVersionPayload.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PacketByteBufLoginQueryRequestPayload.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PacketByteBufLoginQueryResponsePayload.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PayloadHelper.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/server/QueryIdFactory.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerConfigurationNetworkAddon.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerLoginNetworkAddon.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerNetworkingImpl.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerPlayNetworkAddon.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientCommonNetworkHandlerAccessor.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientConfigurationNetworkHandlerAccessor.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientLoginNetworkHandlerAccessor.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ConnectScreenAccessor.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/MinecraftClientAccessor.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/server/ServerCommonNetworkHandlerAccessor.java
create mode 100644 networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/server/ServerLoginNetworkHandlerAccessor.java
create mode 100644 networking/common/src/main/resources/architectury.common.json
create mode 100644 networking/common/src/main/resources/kessoku-networking.common.mixins.json
delete mode 100644 networking/fabric/src/main/java/band/kessoku/lib/KessokuNetworkingFabric.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/impl/networking/fabric/CommonPacketsImplFabric.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/impl/networking/fabric/KessokuNetworkingFabric.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ClientConnectionMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CommandManagerMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadC2SPacketMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadPacketCodecMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadS2CPacketMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/LoginQueryRequestS2CPacketMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/LoginQueryResponseC2SPacketMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/PacketCodecDispatcherMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/PlayerManagerMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerCommonNetworkHandlerMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerLoginNetworkHandlerMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerPlayNetworkHandlerMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientCommonNetworkHandlerMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientConfigurationNetworkHandlerMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientLoginNetworkHandlerMixin.java
create mode 100644 networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientPlayNetworkHandlerMixin.java
create mode 100644 networking/fabric/src/main/resources/fabric.mod.json
create mode 100644 networking/fabric/src/main/resources/kessoku-networking.fabric.mixins.json
create mode 100644 networking/neo/src/main/java/band/kessoku/lib/impl/networking/neoforge/CommonPacketsImplNeoForge.java
create mode 100644 networking/neo/src/main/java/band/kessoku/lib/impl/networking/neoforge/KessokuNetworkingNeoForge.java
create mode 100644 networking/neo/src/main/java/band/kessoku/lib/mixin/networking/neoforge/ServerConfigurationNetworkHandlerMixin.java
create mode 100644 networking/neo/src/main/resources/kessoku-networking.neoforge.mixins.json
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/KessokuNetworking.java b/networking/common/src/main/java/band/kessoku/lib/api/KessokuNetworking.java
similarity index 78%
rename from networking/common/src/main/java/band/kessoku/lib/api/networking/KessokuNetworking.java
rename to networking/common/src/main/java/band/kessoku/lib/api/KessokuNetworking.java
index 80568673..bb0017de 100644
--- a/networking/common/src/main/java/band/kessoku/lib/api/networking/KessokuNetworking.java
+++ b/networking/common/src/main/java/band/kessoku/lib/api/KessokuNetworking.java
@@ -1,9 +1,9 @@
-package band.kessoku.lib.api.networking;
+package band.kessoku.lib.api;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
-public class KessokuNetworking {
+public final class KessokuNetworking {
public static final String MOD_ID = "kessoku_networking";
public static final String NAME = "Kessoku Networking API";
public static final Marker MARKER = MarkerFactory.getMarker("[" + NAME + "]");
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/LoginPacketSender.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/LoginPacketSender.java
new file mode 100644
index 00000000..28193b3b
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/LoginPacketSender.java
@@ -0,0 +1,54 @@
+package band.kessoku.lib.api.networking;
+
+import java.util.Objects;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.util.Identifier;
+
+/**
+ * Represents something that supports sending packets to login channels.
+ * @see PacketSender
+ */
+@ApiStatus.NonExtendable
+public interface LoginPacketSender extends PacketSender {
+ /**
+ * Creates a packet for sending to a login channel.
+ *
+ * @param channelName the id of the channel
+ * @param buf the content of the packet
+ * @return the created packet
+ */
+ Packet> createPacket(Identifier channelName, PacketByteBuf buf);
+
+ /**
+ * Sends a packet to a channel.
+ *
+ * @param channel the id of the channel
+ * @param buf the content of the packet
+ */
+ default void sendPacket(Identifier channel, PacketByteBuf buf) {
+ Objects.requireNonNull(channel, "Channel cannot be null");
+ Objects.requireNonNull(buf, "Payload cannot be null");
+
+ this.sendPacket(this.createPacket(channel, buf));
+ }
+
+ /**
+ * Sends a packet to a channel.
+ *
+ * @param channel the id of the channel
+ * @param buf the content of the packet
+ * @param callback an optional callback to execute after the packet is sent, may be {@code null}
+ */
+ default void sendPacket(Identifier channel, PacketByteBuf buf, @Nullable PacketCallbacks callback) {
+ Objects.requireNonNull(channel, "Channel cannot be null");
+ Objects.requireNonNull(buf, "Payload cannot be null");
+
+ this.sendPacket(this.createPacket(channel, buf), callback);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/util/PacketByteBufHelper.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/PacketByteBufHelper.java
similarity index 99%
rename from networking/common/src/main/java/band/kessoku/lib/api/networking/util/PacketByteBufHelper.java
rename to networking/common/src/main/java/band/kessoku/lib/api/networking/PacketByteBufHelper.java
index e928b7ce..90bca5bc 100644
--- a/networking/common/src/main/java/band/kessoku/lib/api/networking/util/PacketByteBufHelper.java
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/PacketByteBufHelper.java
@@ -1,4 +1,4 @@
-package band.kessoku.lib.api.networking.util;
+package band.kessoku.lib.api.networking;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/PacketSender.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/PacketSender.java
new file mode 100644
index 00000000..61ca59d1
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/PacketSender.java
@@ -0,0 +1,64 @@
+package band.kessoku.lib.api.networking;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.text.Text;
+
+/**
+ * Represents something that supports sending packets to channels.
+ * Any packets sent must be {@linkplain PayloadTypeRegistry registered} in the appropriate registry.
+ */
+@ApiStatus.NonExtendable
+public interface PacketSender {
+ /**
+ * Creates a packet from a packet payload.
+ *
+ * @param payload the packet payload
+ */
+ Packet> createPacket(CustomPayload payload);
+
+ /**
+ * Sends a packet.
+ *
+ * @param packet the packet
+ */
+ default void sendPacket(Packet> packet) {
+ sendPacket(packet, null);
+ }
+
+ /**
+ * Sends a packet.
+ * @param payload the payload
+ */
+ default void sendPacket(CustomPayload payload) {
+ sendPacket(createPacket(payload));
+ }
+
+ /**
+ * Sends a packet.
+ *
+ * @param packet the packet
+ * @param callback an optional callback to execute after the packet is sent, may be {@code null}.
+ */
+ void sendPacket(Packet> packet, @Nullable PacketCallbacks callback);
+
+ /**
+ * Sends a packet.
+ *
+ * @param payload the payload
+ * @param callback an optional callback to execute after the packet is sent, may be {@code null}.
+ */
+ default void sendPacket(CustomPayload payload, @Nullable PacketCallbacks callback) {
+ sendPacket(createPacket(payload), callback);
+ }
+
+ /**
+ * Disconnects the player.
+ * @param disconnectReason the reason for disconnection
+ */
+ void disconnect(Text disconnectReason);
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/PayloadTypeRegistry.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/PayloadTypeRegistry.java
new file mode 100644
index 00000000..bd95d9da
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/PayloadTypeRegistry.java
@@ -0,0 +1,56 @@
+package band.kessoku.lib.api.networking;
+
+import band.kessoku.lib.impl.networking.PayloadTypeRegistryImpl;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.RegistryByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.packet.CustomPayload;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * A registry for payload types.
+ */
+@ApiStatus.NonExtendable
+public interface PayloadTypeRegistry {
+
+ /**
+ * Registers a custom payload type.
+ *
+ *
This must be done on both the sending and receiving side, usually during mod initialization
+ * and before registering a packet handler.
+ *
+ * @param id the id of the payload type
+ * @param codec the codec for the payload type
+ * @param the payload type
+ * @return the registered payload type
+ */
+ CustomPayload.Type super B, T> register(CustomPayload.Id id, PacketCodec super B, T> codec);
+
+ /**
+ * @return the {@link PayloadTypeRegistry} instance for the client to server configuration channel.
+ */
+ static PayloadTypeRegistry configC2S() {
+ return PayloadTypeRegistryImpl.CONFIG_C2S;
+ }
+
+ /**
+ * @return the {@link PayloadTypeRegistry} instance for the server to client configuration channel.
+ */
+ static PayloadTypeRegistry configS2C() {
+ return PayloadTypeRegistryImpl.CONFIG_S2C;
+ }
+
+ /**
+ * @return the {@link PayloadTypeRegistry} instance for the client to server play channel.
+ */
+ static PayloadTypeRegistry playC2S() {
+ return PayloadTypeRegistryImpl.PLAY_C2S;
+ }
+
+ /**
+ * @return the {@link PayloadTypeRegistry} instance for the server to client play channel.
+ */
+ static PayloadTypeRegistry playS2C() {
+ return PayloadTypeRegistryImpl.PLAY_S2C;
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/util/ServerConfigurationNetworkHandlerExtension.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/ServerConfigurationNetworkHandlerExtension.java
similarity index 96%
rename from networking/common/src/main/java/band/kessoku/lib/api/networking/util/ServerConfigurationNetworkHandlerExtension.java
rename to networking/common/src/main/java/band/kessoku/lib/api/networking/ServerConfigurationNetworkHandlerExtension.java
index cf6692a0..9e55d5d9 100644
--- a/networking/common/src/main/java/band/kessoku/lib/api/networking/util/ServerConfigurationNetworkHandlerExtension.java
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/ServerConfigurationNetworkHandlerExtension.java
@@ -1,4 +1,4 @@
-package band.kessoku.lib.api.networking.util;
+package band.kessoku.lib.api.networking;
import net.minecraft.server.network.ServerPlayerConfigurationTask;
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/client/C2SConfigurationChannelEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/C2SConfigurationChannelEvent.java
new file mode 100644
index 00000000..918be50c
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/C2SConfigurationChannelEvent.java
@@ -0,0 +1,51 @@
+package band.kessoku.lib.api.networking.client;
+
+import java.util.List;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientConfigurationNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.event.api.Event;
+import band.kessoku.lib.api.networking.PacketSender;
+
+/**
+ * Offers access to events related to the indication of a connected server's ability to receive packets in certain channels.
+ */
+public final class C2SConfigurationChannelEvent {
+ /**
+ * An event for the client configuration network handler receiving an update indicating the connected server's ability to receive packets in certain channels.
+ * This event may be invoked at any time after login and up to disconnection.
+ */
+ public static final Event REGISTER = Event.of(registers -> (handler, sender, client, channels) -> {
+ for (Register callback : registers) {
+ callback.onChannelRegister(handler, sender, client, channels);
+ }
+ });
+
+ /**
+ * An event for the client configuration network handler receiving an update indicating the connected server's lack of ability to receive packets in certain channels.
+ * This event may be invoked at any time after login and up to disconnection.
+ */
+ public static final Event UNREGISTER = Event.of(unregisters -> (handler, sender, client, channels) -> {
+ for (Unregister callback : unregisters) {
+ callback.onChannelUnregister(handler, sender, client, channels);
+ }
+ });
+
+ /**
+ * @see C2SConfigurationChannelEvent#REGISTER
+ */
+ @FunctionalInterface
+ public interface Register {
+ void onChannelRegister(ClientConfigurationNetworkHandler handler, PacketSender sender, MinecraftClient client, List channels);
+ }
+
+ /**
+ * @see C2SConfigurationChannelEvent#UNREGISTER
+ */
+ @FunctionalInterface
+ public interface Unregister {
+ void onChannelUnregister(ClientConfigurationNetworkHandler handler, PacketSender sender, MinecraftClient client, List channels);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/client/C2SPlayChannelEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/C2SPlayChannelEvent.java
new file mode 100644
index 00000000..3b038d24
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/C2SPlayChannelEvent.java
@@ -0,0 +1,51 @@
+package band.kessoku.lib.api.networking.client;
+
+import java.util.List;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.event.api.Event;
+import band.kessoku.lib.api.networking.PacketSender;
+
+/**
+ * Offers access to events related to the indication of a connected server's ability to receive packets in certain channels.
+ */
+public final class C2SPlayChannelEvent {
+ /**
+ * An event for the client play network handler receiving an update indicating the connected server's ability to receive packets in certain channels.
+ * This event may be invoked at any time after login and up to disconnection.
+ */
+ public static final Event REGISTER = Event.of(registers -> (handler, sender, client, channels) -> {
+ for (Register callback : registers) {
+ callback.onChannelRegister(handler, sender, client, channels);
+ }
+ });
+
+ /**
+ * An event for the client play network handler receiving an update indicating the connected server's lack of ability to receive packets in certain channels.
+ * This event may be invoked at any time after login and up to disconnection.
+ */
+ public static final Event UNREGISTER = Event.of(unregisters -> (handler, sender, client, channels) -> {
+ for (Unregister callback : unregisters) {
+ callback.onChannelUnregister(handler, sender, client, channels);
+ }
+ });
+
+ /**
+ * @see C2SPlayChannelEvent#REGISTER
+ */
+ @FunctionalInterface
+ public interface Register {
+ void onChannelRegister(ClientPlayNetworkHandler handler, PacketSender sender, MinecraftClient client, List channels);
+ }
+
+ /**
+ * @see C2SPlayChannelEvent#UNREGISTER
+ */
+ @FunctionalInterface
+ public interface Unregister {
+ void onChannelUnregister(ClientPlayNetworkHandler handler, PacketSender sender, MinecraftClient client, List channels);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientConfigurationConnectionEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientConfigurationConnectionEvent.java
new file mode 100644
index 00000000..c5bc6971
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientConfigurationConnectionEvent.java
@@ -0,0 +1,78 @@
+package band.kessoku.lib.api.networking.client;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientConfigurationNetworkHandler;
+import net.minecraft.network.packet.CustomPayload;
+
+import band.kessoku.lib.event.api.Event;
+
+/**
+ * Offers access to events related to the configuration connection to a server on a logical client.
+ */
+public final class ClientConfigurationConnectionEvent {
+ /**
+ * Event indicating a connection entering the CONFIGURATION state, ready for registering channel handlers.
+ *
+ * No packets should be sent when this event is invoked.
+ *
+ * @see ClientConfigurationNetworking#registerReceiver(CustomPayload.Id, ClientConfigurationNetworking.ConfigurationPayloadHandler)
+ */
+ public static final Event INIT = Event.of(inits -> (handler, client) -> {
+ for (ClientConfigurationConnectionEvent.Init callback : inits) {
+ callback.onConfigurationInit(handler, client);
+ }
+ });
+
+ /**
+ * An event called after the connection has been initialized and is ready to start sending and receiving configuration packets.
+ *
+ * Packets may be sent during this event.
+ */
+ public static final Event START = Event.of(starts -> (handler, client) -> {
+ for (ClientConfigurationConnectionEvent.Start callback : starts) {
+ callback.onConfigurationStart(handler, client);
+ }
+ });
+
+ /**
+ * An event called after the ReadyS2CPacket has been received, just before switching to the PLAY state.
+ *
+ * No packets should be sent when this event is invoked.
+ */
+ public static final Event COMPLETE = Event.of(completes -> (handler, client) -> {
+ for (ClientConfigurationConnectionEvent.Complete callback : completes) {
+ callback.onConfigurationComplete(handler, client);
+ }
+ });
+
+ /**
+ * An event for the disconnection of the client configuration network handler.
+ *
+ * No packets should be sent when this event is invoked.
+ */
+ public static final Event DISCONNECT = Event.of(disconnects -> (handler, client) -> {
+ for (ClientConfigurationConnectionEvent.Disconnect callback : disconnects) {
+ callback.onConfigurationDisconnect(handler, client);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Init {
+ void onConfigurationInit(ClientConfigurationNetworkHandler handler, MinecraftClient client);
+ }
+
+ @FunctionalInterface
+ public interface Start {
+ void onConfigurationStart(ClientConfigurationNetworkHandler handler, MinecraftClient client);
+ }
+
+ @FunctionalInterface
+ public interface Complete {
+ void onConfigurationComplete(ClientConfigurationNetworkHandler handler, MinecraftClient client);
+ }
+
+ @FunctionalInterface
+ public interface Disconnect {
+ void onConfigurationDisconnect(ClientConfigurationNetworkHandler handler, MinecraftClient client);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientConfigurationNetworking.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientConfigurationNetworking.java
new file mode 100644
index 00000000..fb701abc
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientConfigurationNetworking.java
@@ -0,0 +1,275 @@
+package band.kessoku.lib.api.networking.client;
+
+import java.util.Objects;
+import java.util.Set;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientConfigurationNetworkHandler;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.thread.ThreadExecutor;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.api.networking.PayloadTypeRegistry;
+import band.kessoku.lib.api.networking.server.ServerConfigurationNetworking;
+import band.kessoku.lib.api.networking.server.ServerPlayNetworking;
+import band.kessoku.lib.impl.networking.client.ClientConfigurationNetworkAddon;
+import band.kessoku.lib.impl.networking.client.ClientNetworkingImpl;
+
+/**
+ * Offers access to configuration stage client-side networking functionalities.
+ *
+ * Client-side networking functionalities include receiving clientbound packets,
+ * sending serverbound packets, and events related to client-side network handlers.
+ * Packets received by this class must be registered to {@link
+ * PayloadTypeRegistry#configS2C()} on both ends.
+ * Packets sent by this class must be registered to {@link
+ * PayloadTypeRegistry#configC2S()} on both ends.
+ * Packets must be registered before registering any receivers.
+ *
+ *
This class should be only used on the physical client and for the logical client.
+ *
+ *
See {@link ServerPlayNetworking} for information on how to use the packet
+ * object-based API.
+ *
+ * @see ServerConfigurationNetworking
+ */
+public final class ClientConfigurationNetworking {
+ /**
+ * Registers a handler for a packet type.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ *
If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterGlobalReceiver(CustomPayload.Id)} to unregister the existing handler.
+ *
+ * @param type the packet type
+ * @param handler the handler
+ * @return false if a handler is already registered to the channel
+ * @throws IllegalArgumentException if the codec for {@code type} has not been {@linkplain PayloadTypeRegistry#configS2C() registered} yet
+ * @see ClientConfigurationNetworking#unregisterGlobalReceiver(CustomPayload.Id)
+ * @see ClientConfigurationNetworking#registerReceiver(CustomPayload.Id, ConfigurationPayloadHandler)
+ */
+ public static boolean registerGlobalReceiver(CustomPayload.Id type, ConfigurationPayloadHandler handler) {
+ return ClientNetworkingImpl.CONFIG.registerGlobalReceiver(type.id(), handler);
+ }
+
+ /**
+ * Removes the handler for a packet type.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * The {@code type} is guaranteed not to have an associated handler after this call.
+ *
+ * @param id the packet id
+ * @return the previous handler, or {@code null} if no handler was bound to the channel,
+ * or it was not registered using {@link #registerGlobalReceiver(CustomPayload.Id, ConfigurationPayloadHandler)}
+ * @see ClientConfigurationNetworking#registerGlobalReceiver(CustomPayload.Id, ConfigurationPayloadHandler)
+ * @see ClientConfigurationNetworking#unregisterReceiver(Identifier)
+ */
+ @Nullable
+ public static ClientConfigurationNetworking.ConfigurationPayloadHandler> unregisterGlobalReceiver(CustomPayload.Id> id) {
+ return ClientNetworkingImpl.CONFIG.unregisterGlobalReceiver(id.id());
+ }
+
+ /**
+ * Gets all channel names which global receivers are registered for.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * @return all channel names which global receivers are registered for.
+ */
+ public static Set getGlobalReceivers() {
+ return ClientNetworkingImpl.CONFIG.getChannels();
+ }
+
+ /**
+ * Registers a handler for a packet type.
+ *
+ * If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterReceiver(Identifier)} to unregister the existing handler.
+ *
+ *
For example, if you only register a receiver using this method when a {@linkplain ClientLoginNetworking#registerGlobalReceiver(Identifier, ClientLoginNetworking.LoginQueryRequestHandler)}
+ * login query has been received, you should use {@link ClientPlayConnectionEvent#INIT} to register the channel handler.
+ *
+ * @param id the payload id
+ * @param handler the handler
+ * @return {@code false} if a handler is already registered for the type
+ * @throws IllegalArgumentException if the codec for {@code type} has not been {@linkplain PayloadTypeRegistry#configS2C() registered} yet
+ * @throws IllegalStateException if the client is not connected to a server
+ * @see ClientPlayConnectionEvent#INIT
+ */
+ public static boolean registerReceiver(CustomPayload.Id id, ConfigurationPayloadHandler handler) {
+ final ClientConfigurationNetworkAddon addon = ClientNetworkingImpl.getClientConfigurationAddon();
+
+ if (addon != null) {
+ return addon.registerChannel(id.id(), handler);
+ }
+
+ throw new IllegalStateException("Cannot register receiver while not configuring!");
+ }
+
+ /**
+ * Removes the handler for a packet type.
+ *
+ * The {@code type} is guaranteed not to have an associated handler after this call.
+ *
+ * @param id the payload id to unregister
+ * @return the previous handler, or {@code null} if no handler was bound to the channel,
+ * or it was not registered using {@link #registerReceiver(CustomPayload.Id, ConfigurationPayloadHandler)}
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ @Nullable
+ public static ClientConfigurationNetworking.ConfigurationPayloadHandler> unregisterReceiver(Identifier id) {
+ final ClientConfigurationNetworkAddon addon = ClientNetworkingImpl.getClientConfigurationAddon();
+
+ if (addon != null) {
+ return addon.unregisterChannel(id);
+ }
+
+ throw new IllegalStateException("Cannot unregister receiver while not configuring!");
+ }
+
+ /**
+ * Gets all the channel names that the client can receive packets on.
+ *
+ * @return All the channel names that the client can receive packets on
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ public static Set getReceived() throws IllegalStateException {
+ final ClientConfigurationNetworkAddon addon = ClientNetworkingImpl.getClientConfigurationAddon();
+
+ if (addon != null) {
+ return addon.getReceivableChannels();
+ }
+
+ throw new IllegalStateException("Cannot get a list of channels the client can receive packets on while not configuring!");
+ }
+
+ /**
+ * Gets all channel names that the connected server declared the ability to receive a packets on.
+ *
+ * @return All the channel names the connected server declared the ability to receive a packets on
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ public static Set getSendable() throws IllegalStateException {
+ final ClientConfigurationNetworkAddon addon = ClientNetworkingImpl.getClientConfigurationAddon();
+
+ if (addon != null) {
+ return addon.getSendableChannels();
+ }
+
+ throw new IllegalStateException("Cannot get a list of channels the server can receive packets on while not configuring!");
+ }
+
+ /**
+ * Checks if the connected server declared the ability to receive a packet on a specified channel name.
+ *
+ * @param channelName the channel name
+ * @return {@code true} if the connected server has declared the ability to receive a packet on the specified channel.
+ * False if the client is not in game.
+ */
+ public static boolean canSend(Identifier channelName) throws IllegalArgumentException {
+ final ClientConfigurationNetworkAddon addon = ClientNetworkingImpl.getClientConfigurationAddon();
+
+ if (addon != null) {
+ return addon.getSendableChannels().contains(channelName);
+ }
+
+ throw new IllegalStateException("Cannot get a list of channels the server can receive packets on while not configuring!");
+ }
+
+ /**
+ * Checks if the connected server declared the ability to receive a packet on a specified channel name.
+ * This returns {@code false} if the client is not in game.
+ *
+ * @param type the packet type
+ * @return {@code true} if the connected server has declared the ability to receive a packet on the specified channel
+ */
+ public static boolean canSend(CustomPayload.Id> type) {
+ return canSend(type.id());
+ }
+
+ /**
+ * Gets the packet sender which sends packets to the connected server.
+ *
+ * @return the client's packet sender
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ public static PacketSender getSender() throws IllegalStateException {
+ final ClientConfigurationNetworkAddon addon = ClientNetworkingImpl.getClientConfigurationAddon();
+
+ if (addon != null) {
+ return addon;
+ }
+
+ throw new IllegalStateException("Cannot get PacketSender while not configuring!");
+ }
+
+ /**
+ * Sends a packet to the connected server.
+ *
+ * Any packets sent must be {@linkplain PayloadTypeRegistry#configC2S() registered}.
+ *
+ * @param payload to be sent
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ public static void send(CustomPayload payload) {
+ Objects.requireNonNull(payload, "Payload cannot be null");
+ Objects.requireNonNull(payload.getId(), "CustomPayload#getId() cannot return null for payload class: " + payload.getClass());
+
+ final ClientConfigurationNetworkAddon addon = ClientNetworkingImpl.getClientConfigurationAddon();
+
+ if (addon != null) {
+ addon.sendPacket(payload);
+ return;
+ }
+
+ throw new IllegalStateException("Cannot send packet while not configuring!");
+ }
+
+ /**
+ * A packet handler utilizing {@link CustomPayload}.
+ * @param the type of the packet
+ */
+ @FunctionalInterface
+ public interface ConfigurationPayloadHandler {
+ /**
+ * Handles the incoming packet.
+ *
+ * Unlike {@link ClientPlayNetworking.PlayPayloadHandler} this method is executed on {@linkplain io.netty.channel.EventLoop netty's event loops}.
+ * Modification to the game should be {@linkplain ThreadExecutor#submit(Runnable) scheduled}.
+ *
+ *
An example usage of this:
+ *
{@code
+ * // use PayloadTypeRegistry for registering the payload
+ * ClientConfigurationNetworking.registerReceiver(OVERLAY_PACKET_TYPE, (payload, context) -> {
+ *
+ * });
+ * }
+ *
+ * @param payload the packet payload
+ * @param context the configuration networking context
+ * @see CustomPayload
+ */
+ void receive(T payload, Context context);
+ }
+
+ @ApiStatus.NonExtendable
+ public interface Context {
+ /**
+ * @return The MinecraftClient instance
+ */
+ MinecraftClient client();
+
+ /**
+ * @return The ClientConfigurationNetworkHandler instance
+ */
+ ClientConfigurationNetworkHandler networkHandler();
+
+ /**
+ * @return The packet sender
+ */
+ PacketSender responseSender();
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientLoginConnectionEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientLoginConnectionEvent.java
new file mode 100644
index 00000000..6be75165
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientLoginConnectionEvent.java
@@ -0,0 +1,80 @@
+package band.kessoku.lib.api.networking.client;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientLoginNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.event.api.Event;
+
+/**
+ * Offers access to events related to the connection to a server on the client while the server is processing the client's login request.
+ */
+public final class ClientLoginConnectionEvent {
+ /**
+ * Event indicating a connection entered the LOGIN state, ready for registering query request handlers.
+ * This event may be used by mods to prepare their client side state.
+ * This event does not guarantee that a login attempt will be successful.
+ *
+ * @see ClientLoginNetworking#registerReceiver(Identifier, ClientLoginNetworking.LoginQueryRequestHandler)
+ */
+ public static final Event INIT = Event.of(inits -> (handler, client) -> {
+ for (Init callback : inits) {
+ callback.onLoginStart(handler, client);
+ }
+ });
+
+ /**
+ * An event for when the client has started receiving login queries.
+ * A client can only start receiving login queries when a server has sent the first login query.
+ * Vanilla servers will typically never make the client enter this login phase, but it is not a guarantee that the
+ * connected server is a vanilla server since a modded server or proxy may have no login queries to send to the client
+ * and therefore bypass the login query phase.
+ * If this event is fired then it is a sign that a server is not a vanilla server or the server is behind a proxy which
+ * is capable of handling login queries.
+ *
+ * This event may be used to {@link ClientLoginNetworking.LoginQueryRequestHandler register login query handlers}
+ * which may be used to send a response to a server.
+ *
+ *
No packets should be sent when this event is invoked.
+ */
+ public static final Event QUERY_START = Event.of(queryStarts -> (handler, client) -> {
+ for (QueryStart callback : queryStarts) {
+ callback.onLoginQueryStart(handler, client);
+ }
+ });
+
+ /**
+ * An event for when the client's login process has ended due to disconnection.
+ *
+ * No packets should be sent when this event is invoked.
+ */
+ public static final Event DISCONNECT = Event.of(disconnects -> (handler, client) -> {
+ for (Disconnect callback : disconnects) {
+ callback.onLoginDisconnect(handler, client);
+ }
+ });
+
+ /**
+ * @see ClientLoginConnectionEvent#INIT
+ */
+ @FunctionalInterface
+ public interface Init {
+ void onLoginStart(ClientLoginNetworkHandler handler, MinecraftClient client);
+ }
+
+ /**
+ * @see ClientLoginConnectionEvent#QUERY_START
+ */
+ @FunctionalInterface
+ public interface QueryStart {
+ void onLoginQueryStart(ClientLoginNetworkHandler handler, MinecraftClient client);
+ }
+
+ /**
+ * @see ClientLoginConnectionEvent#DISCONNECT
+ */
+ @FunctionalInterface
+ public interface Disconnect {
+ void onLoginDisconnect(ClientLoginNetworkHandler handler, MinecraftClient client);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientLoginNetworking.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientLoginNetworking.java
new file mode 100644
index 00000000..319318b7
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientLoginNetworking.java
@@ -0,0 +1,145 @@
+package band.kessoku.lib.api.networking.client;
+
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientLoginNetworkHandler;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.listener.PacketListener;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.server.ServerLoginNetworking;
+import band.kessoku.lib.impl.networking.client.ClientNetworkingImpl;
+
+/**
+ * Offers access to login stage client-side networking functionalities.
+ *
+ * The Minecraft login protocol only allows the client to respond to a server's request, but not initiate one of its own.
+ *
+ * @see ClientPlayNetworking
+ * @see ServerLoginNetworking
+ */
+public final class ClientLoginNetworking {
+ /**
+ * Registers a handler to a query request channel.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ *
If a handler is already registered to the {@code channel}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterGlobalReceiver(Identifier)} to unregister the existing handler.
+ *
+ * @param channelName the id of the channel
+ * @param queryHandler the handler
+ * @return false if a handler is already registered to the channel
+ * @see ClientLoginNetworking#unregisterGlobalReceiver(Identifier)
+ * @see ClientLoginNetworking#registerReceiver(Identifier, LoginQueryRequestHandler)
+ */
+ public static boolean registerGlobalReceiver(Identifier channelName, LoginQueryRequestHandler queryHandler) {
+ return ClientNetworkingImpl.LOGIN.registerGlobalReceiver(channelName, queryHandler);
+ }
+
+ /**
+ * Removes the handler of a query request channel.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ *
The {@code channel} is guaranteed not to have a handler after this call.
+ *
+ * @param channelName the id of the channel
+ * @return the previous handler, or {@code null} if no handler was bound to the channel
+ * @see ClientLoginNetworking#registerGlobalReceiver(Identifier, LoginQueryRequestHandler)
+ * @see ClientLoginNetworking#unregisterReceiver(Identifier)
+ */
+ @Nullable
+ public static ClientLoginNetworking.LoginQueryRequestHandler unregisterGlobalReceiver(Identifier channelName) {
+ return ClientNetworkingImpl.LOGIN.unregisterGlobalReceiver(channelName);
+ }
+
+ /**
+ * Gets all query request channel names which global receivers are registered for.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * @return all channel names which global receivers are registered for.
+ */
+ public static Set getGlobalReceivers() {
+ return ClientNetworkingImpl.LOGIN.getChannels();
+ }
+
+ /**
+ * Registers a handler to a query request channel.
+ *
+ * If a handler is already registered to the {@code channelName}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterReceiver(Identifier)} to unregister the existing handler.
+ *
+ * @param channelName the id of the channel
+ * @param queryHandler the handler
+ * @return false if a handler is already registered to the channel name
+ * @throws IllegalStateException if the client is not logging in
+ */
+ public static boolean registerReceiver(Identifier channelName, LoginQueryRequestHandler queryHandler) throws IllegalStateException {
+ final ClientConnection connection = ClientNetworkingImpl.getLoginConnection();
+
+ if (connection != null) {
+ final PacketListener packetListener = connection.getPacketListener();
+
+ if (packetListener instanceof ClientLoginNetworkHandler) {
+ return ClientNetworkingImpl.getAddon(((ClientLoginNetworkHandler) packetListener)).registerChannel(channelName, queryHandler);
+ }
+ }
+
+ throw new IllegalStateException("Cannot register receiver while client is not logging in!");
+ }
+
+ /**
+ * Removes the handler of a query request channel.
+ *
+ *
The {@code channelName} is guaranteed not to have a handler after this call.
+ *
+ * @param channelName the id of the channel
+ * @return the previous handler, or {@code null} if no handler was bound to the channel name
+ * @throws IllegalStateException if the client is not logging in
+ */
+ @Nullable
+ public static LoginQueryRequestHandler unregisterReceiver(Identifier channelName) throws IllegalStateException {
+ final ClientConnection connection = ClientNetworkingImpl.getLoginConnection();
+
+ if (connection != null) {
+ final PacketListener packetListener = connection.getPacketListener();
+
+ if (packetListener instanceof ClientLoginNetworkHandler) {
+ return ClientNetworkingImpl.getAddon(((ClientLoginNetworkHandler) packetListener)).unregisterChannel(channelName);
+ }
+ }
+
+ throw new IllegalStateException("Cannot unregister receiver while client is not logging in!");
+ }
+
+ private ClientLoginNetworking() {
+ }
+
+ @FunctionalInterface
+ public interface LoginQueryRequestHandler {
+ /**
+ * Handles an incoming query request from a server.
+ *
+ *
This method is executed on {@linkplain io.netty.channel.EventLoop netty's event loops}.
+ * Modification to the game should be {@linkplain net.minecraft.util.thread.ThreadExecutor#submit(Runnable) scheduled} using the provided Minecraft client instance.
+ *
+ *
The return value of this method is a completable future that may be used to delay the login process to the server until a task {@link CompletableFuture#isDone() is done}.
+ * The future should complete in reasonably time to prevent disconnection by the server.
+ * If your request processes instantly, you may use {@link CompletableFuture#completedFuture(Object)} to wrap your response for immediate sending.
+ *
+ * @param client the client
+ * @param handler the network handler that received this packet
+ * @param buf the payload of the packet
+ * @param callbacksConsumer listeners to be called when the response packet is sent to the server
+ * @return a completable future which contains the payload to respond to the server with.
+ * If the future contains {@code null}, then the server will be notified that the client did not understand the query.
+ */
+ CompletableFuture<@Nullable PacketByteBuf> receive(MinecraftClient client, ClientLoginNetworkHandler handler, PacketByteBuf buf, Consumer callbacksConsumer);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientPlayConnectionEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientPlayConnectionEvent.java
new file mode 100644
index 00000000..d1eddf34
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientPlayConnectionEvent.java
@@ -0,0 +1,62 @@
+package band.kessoku.lib.api.networking.client;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.network.packet.CustomPayload;
+
+import band.kessoku.lib.event.api.Event;
+import band.kessoku.lib.api.networking.PacketSender;
+
+/**
+ * Offers access to events related to the connection to a server on a logical client.
+ */
+public final class ClientPlayConnectionEvent {
+ /**
+ * Event indicating a connection entered the PLAY state, ready for registering channel handlers.
+ *
+ * @see ClientPlayNetworking#registerReceiver(CustomPayload.Id, ClientPlayNetworking.PlayPayloadHandler)
+ */
+ public static final Event INIT = Event.of(inits -> (handler, client) -> {
+ for (Init callback : inits) {
+ callback.onPlayInit(handler, client);
+ }
+ });
+
+ /**
+ * An event for notification when the client play network handler is ready to send packets to the server.
+ *
+ * At this stage, the network handler is ready to send packets to the server.
+ * Since the client's local state has been set up.
+ */
+ public static final Event JOIN = Event.of(joins -> (handler, sender, client) -> {
+ for (Join callback : joins) {
+ callback.onPlayReady(handler, sender, client);
+ }
+ });
+
+ /**
+ * An event for the disconnection of the client play network handler.
+ *
+ * No packets should be sent when this event is invoked.
+ */
+ public static final Event DISCONNECT = Event.of(disconnects -> (handler, client) -> {
+ for (Disconnect callback : disconnects) {
+ callback.onPlayDisconnect(handler, client);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Init {
+ void onPlayInit(ClientPlayNetworkHandler handler, MinecraftClient client);
+ }
+
+ @FunctionalInterface
+ public interface Join {
+ void onPlayReady(ClientPlayNetworkHandler handler, PacketSender sender, MinecraftClient client);
+ }
+
+ @FunctionalInterface
+ public interface Disconnect {
+ void onPlayDisconnect(ClientPlayNetworkHandler handler, MinecraftClient client);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientPlayNetworking.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientPlayNetworking.java
new file mode 100644
index 00000000..b05d07c0
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/client/ClientPlayNetworking.java
@@ -0,0 +1,282 @@
+package band.kessoku.lib.api.networking.client;
+
+import java.util.Objects;
+import java.util.Set;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.network.listener.ServerCommonPacketListener;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.api.networking.PayloadTypeRegistry;
+import band.kessoku.lib.api.networking.server.ServerPlayNetworking;
+import band.kessoku.lib.impl.networking.client.ClientNetworkingImpl;
+import band.kessoku.lib.impl.networking.client.ClientPlayNetworkAddon;
+
+/**
+ * Offers access to play stage client-side networking functionalities.
+ *
+ * Client-side networking functionalities include receiving clientbound packets,
+ * sending serverbound packets, and events related to client-side network handlers.
+ * Packets received by this class must be registered to {@link PayloadTypeRegistry#playS2C()} on both ends.
+ * Packets sent by this class must be registered to {@link PayloadTypeRegistry#playC2S()} on both ends.
+ * Packets must be registered before registering any receivers.
+ *
+ *
This class should be only used on the physical client and for the logical client.
+ *
+ *
See {@link ServerPlayNetworking} for information on how to use the payload
+ * object-based API.
+ *
+ * @see ClientLoginNetworking
+ * @see ClientConfigurationNetworking
+ * @see ServerPlayNetworking
+ */
+public final class ClientPlayNetworking {
+ /**
+ * Registers a handler for a payload type.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ *
If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterGlobalReceiver(Identifier)} to unregister the existing handler.
+ *
+ * @param type the payload type
+ * @param handler the handler
+ * @return false if a handler is already registered to the channel
+ * @throws IllegalArgumentException if the codec for {@code type} has not been {@linkplain PayloadTypeRegistry#playS2C() registered} yet
+ * @see ClientPlayNetworking#unregisterGlobalReceiver(Identifier)
+ * @see ClientPlayNetworking#registerReceiver(CustomPayload.Id, PlayPayloadHandler)
+ */
+ public static boolean registerGlobalReceiver(CustomPayload.Id type, PlayPayloadHandler handler) {
+ return ClientNetworkingImpl.PLAY.registerGlobalReceiver(type.id(), handler);
+ }
+
+ /**
+ * Removes the handler for a payload type.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * The {@code type} is guaranteed not to have an associated handler after this call.
+ *
+ * @param id the payload id
+ * @return the previous handler, or {@code null} if no handler was bound to the channel,
+ * or it was not registered using {@link #registerGlobalReceiver(CustomPayload.Id, PlayPayloadHandler)}
+ * @see ClientPlayNetworking#registerGlobalReceiver(CustomPayload.Id, PlayPayloadHandler)
+ * @see ClientPlayNetworking#unregisterReceiver(Identifier)
+ */
+ @Nullable
+ public static ClientPlayNetworking.PlayPayloadHandler> unregisterGlobalReceiver(Identifier id) {
+ return ClientNetworkingImpl.PLAY.unregisterGlobalReceiver(id);
+ }
+
+ /**
+ * Gets all channel names which global receivers are registered for.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * @return all channel names which global receivers are registered for.
+ */
+ public static Set getGlobalReceivers() {
+ return ClientNetworkingImpl.PLAY.getChannels();
+ }
+
+ /**
+ * Registers a handler for a payload type.
+ *
+ * If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterReceiver(Identifier)} to unregister the existing handler.
+ *
+ *
For example, if you only register a receiver using this method when a {@linkplain ClientLoginNetworking#registerGlobalReceiver(Identifier, ClientLoginNetworking.LoginQueryRequestHandler)}
+ * login query has been received, you should use {@link ClientPlayConnectionEvent#INIT} to register the channel handler.
+ *
+ * @param type the payload type
+ * @param handler the handler
+ * @return {@code false} if a handler is already registered for the type
+ * @throws IllegalArgumentException if the codec for {@code type} has not been {@linkplain PayloadTypeRegistry#playS2C() registered} yet
+ * @throws IllegalStateException if the client is not connected to a server
+ * @see ClientPlayConnectionEvent#INIT
+ */
+ public static boolean registerReceiver(CustomPayload.Id type, PlayPayloadHandler handler) {
+ final ClientPlayNetworkAddon addon = ClientNetworkingImpl.getClientPlayAddon();
+
+ if (addon != null) {
+ return addon.registerChannel(type.id(), handler);
+ }
+
+ throw new IllegalStateException("Cannot register receiver while not in game!");
+ }
+
+ /**
+ * Removes the handler for a payload id.
+ *
+ * The {@code type} is guaranteed not to have an associated handler after this call.
+ *
+ * @param id the payload id
+ * @return the previous handler, or {@code null} if no handler was bound to the channel,
+ * or it was not registered using {@link #registerReceiver(CustomPayload.Id, PlayPayloadHandler)}
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ @Nullable
+ public static ClientPlayNetworking.PlayPayloadHandler> unregisterReceiver(Identifier id) {
+ final ClientPlayNetworkAddon addon = ClientNetworkingImpl.getClientPlayAddon();
+
+ if (addon != null) {
+ return addon.unregisterChannel(id);
+ }
+
+ throw new IllegalStateException("Cannot unregister receiver while not in game!");
+ }
+
+ /**
+ * Gets all the channel names that the client can receive packets on.
+ *
+ * @return All the channel names that the client can receive packets on
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ public static Set getReceived() throws IllegalStateException {
+ final ClientPlayNetworkAddon addon = ClientNetworkingImpl.getClientPlayAddon();
+
+ if (addon != null) {
+ return addon.getReceivableChannels();
+ }
+
+ throw new IllegalStateException("Cannot get a list of channels the client can receive packets on while not in game!");
+ }
+
+ /**
+ * Gets all channel names that the connected server declared the ability to receive a packets on.
+ *
+ * @return All the channel names the connected server declared the ability to receive a packets on
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ public static Set getSendable() throws IllegalStateException {
+ final ClientPlayNetworkAddon addon = ClientNetworkingImpl.getClientPlayAddon();
+
+ if (addon != null) {
+ return addon.getSendableChannels();
+ }
+
+ throw new IllegalStateException("Cannot get a list of channels the server can receive packets on while not in game!");
+ }
+
+ /**
+ * Checks if the connected server declared the ability to receive a payload on a specified channel name.
+ *
+ * @param channelName the channel name
+ * @return {@code true} if the connected server has declared the ability to receive a payload on the specified channel.
+ * False if the client is not in game.
+ */
+ public static boolean canSend(Identifier channelName) throws IllegalArgumentException {
+ // You cant send without a client player, so this is fine
+ if (MinecraftClient.getInstance().getNetworkHandler() != null) {
+ return ClientNetworkingImpl.getAddon(MinecraftClient.getInstance().getNetworkHandler()).getSendableChannels().contains(channelName);
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if the connected server declared the ability to receive a payload on a specified channel name.
+ * This returns {@code false} if the client is not in game.
+ *
+ * @param type the payload type
+ * @return {@code true} if the connected server has declared the ability to receive a payload on the specified channel
+ */
+ public static boolean canSend(CustomPayload.Id> type) {
+ return canSend(type.id());
+ }
+
+ /**
+ * Creates a payload which may be sent to the connected server.
+ *
+ * @param packet the fabric payload
+ * @return a new payload
+ */
+ public static Packet createC2SPacket(T packet) {
+ return ClientNetworkingImpl.createC2SPacket(packet);
+ }
+
+ /**
+ * Gets the payload sender which sends packets to the connected server.
+ *
+ * @return the client's payload sender
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ public static PacketSender getSender() throws IllegalStateException {
+ // You cant send without a client player, so this is fine
+ if (MinecraftClient.getInstance().getNetworkHandler() != null) {
+ return ClientNetworkingImpl.getAddon(MinecraftClient.getInstance().getNetworkHandler());
+ }
+
+ throw new IllegalStateException("Cannot get payload sender when not in game!");
+ }
+
+ /**
+ * Sends a payload to the connected server.
+ *
+ * Any packets sent must be {@linkplain PayloadTypeRegistry#playC2S() registered}.
+ *
+ * @param payload the payload
+ * @throws IllegalStateException if the client is not connected to a server
+ */
+ public static void send(CustomPayload payload) {
+ Objects.requireNonNull(payload, "Payload cannot be null");
+ Objects.requireNonNull(payload.getId(), "CustomPayload#getId() cannot return null for payload class: " + payload.getClass());
+
+ // You cant send without a client player, so this is fine
+ if (MinecraftClient.getInstance().getNetworkHandler() != null) {
+ MinecraftClient.getInstance().getNetworkHandler().sendPacket(createC2SPacket(payload));
+ return;
+ }
+
+ throw new IllegalStateException("Cannot send packets when not in game!");
+ }
+
+ /**
+ * A thread-safe payload handler utilizing {@link CustomPayload}.
+ * @param the type of the payload
+ */
+ @FunctionalInterface
+ public interface PlayPayloadHandler {
+ /**
+ * Handles the incoming payload. This is called on the render thread, and can safely
+ * call client methods.
+ *
+ * An example usage of this is to display an overlay message:
+ *
{@code
+ * // use PayloadTypeRegistry for registering the payload
+ * ClientPlayNetworking.registerReceiver(OVERLAY_PACKET_TYPE, (payload, context) -> {
+ * context.client().inGameHud.setOverlayMessage(payload.message(), true);
+ * });
+ * }
+ *
+ * The network handler can be accessed via {@link ClientPlayerEntity#networkHandler}.
+ *
+ * @param payload the packet payload
+ * @param context the play networking context
+ * @see CustomPayload
+ */
+ void receive(T payload, Context context);
+ }
+
+ @ApiStatus.NonExtendable
+ public interface Context {
+ /**
+ * @return The MinecraftClient instance
+ */
+ MinecraftClient client();
+
+ /**
+ * @return The player that received the payload
+ */
+ ClientPlayerEntity player();
+
+ /**
+ * @return The packet sender
+ */
+ PacketSender responseSender();
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/server/S2CConfigurationChannelEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/S2CConfigurationChannelEvent.java
new file mode 100644
index 00000000..31a06898
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/S2CConfigurationChannelEvent.java
@@ -0,0 +1,50 @@
+package band.kessoku.lib.api.networking.server;
+
+import java.util.List;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.event.api.Event;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerConfigurationNetworkHandler;
+import net.minecraft.util.Identifier;
+
+/**
+ * Offers access to events related to the indication of a connected client's ability to receive packets in certain channels.
+ */
+public final class S2CConfigurationChannelEvent {
+ /**
+ * An event for the server configuration network handler receiving an update indicating the connected client's ability to receive packets in certain channels.
+ * This event may be invoked at any time after login and up to disconnection.
+ */
+ public static final Event REGISTER = Event.of(registers -> (handler, sender, server, channels) -> {
+ for (Register callback : registers) {
+ callback.onChannelRegister(handler, sender, server, channels);
+ }
+ });
+
+ /**
+ * An event for the server configuration network handler receiving an update indicating the connected client's lack of ability to receive packets in certain channels.
+ * This event may be invoked at any time after login and up to disconnection.
+ */
+ public static final Event UNREGISTER = Event.of(unregisters -> (handler, sender, server, channels) -> {
+ for (Unregister callback : unregisters) {
+ callback.onChannelUnregister(handler, sender, server, channels);
+ }
+ });
+
+ /**
+ * @see S2CConfigurationChannelEvent#REGISTER
+ */
+ @FunctionalInterface
+ public interface Register {
+ void onChannelRegister(ServerConfigurationNetworkHandler handler, PacketSender sender, MinecraftServer server, List channels);
+ }
+
+ /**
+ * @see S2CConfigurationChannelEvent#UNREGISTER
+ */
+ @FunctionalInterface
+ public interface Unregister {
+ void onChannelUnregister(ServerConfigurationNetworkHandler handler, PacketSender sender, MinecraftServer server, List channels);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/server/S2CPlayChannelEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/S2CPlayChannelEvent.java
new file mode 100644
index 00000000..013d18db
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/S2CPlayChannelEvent.java
@@ -0,0 +1,50 @@
+package band.kessoku.lib.api.networking.server;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.event.api.Event;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerPlayNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import java.util.List;
+
+/**
+ * Offers access to events related to the indication of a connected client's ability to receive packets in certain channels.
+ */
+public final class S2CPlayChannelEvent {
+ /**
+ * An event for the server play network handler receiving an update indicating the connected client's ability to receive packets in certain channels.
+ * This event may be invoked at any time after login and up to disconnection.
+ */
+ public static final Event REGISTER = Event.of(registers -> (handler, sender, server, channels) -> {
+ for (Register callback : registers) {
+ callback.onChannelRegister(handler, sender, server, channels);
+ }
+ });
+
+ /**
+ * An event for the server play network handler receiving an update indicating the connected client's lack of ability to receive packets in certain channels.
+ * This event may be invoked at any time after login and up to disconnection.
+ */
+ public static final Event UNREGISTER = Event.of(unregisters -> (handler, sender, server, channels) -> {
+ for (Unregister callback : unregisters) {
+ callback.onChannelUnregister(handler, sender, server, channels);
+ }
+ });
+
+ /**
+ * @see S2CPlayChannelEvent#REGISTER
+ */
+ @FunctionalInterface
+ public interface Register {
+ void onChannelRegister(ServerPlayNetworkHandler handler, PacketSender sender, MinecraftServer server, List channels);
+ }
+
+ /**
+ * @see S2CPlayChannelEvent#UNREGISTER
+ */
+ @FunctionalInterface
+ public interface Unregister {
+ void onChannelUnregister(ServerPlayNetworkHandler handler, PacketSender sender, MinecraftServer server, List channels);
+ }
+}
\ No newline at end of file
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerConfigurationConnectionEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerConfigurationConnectionEvent.java
new file mode 100644
index 00000000..edd3b16a
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerConfigurationConnectionEvent.java
@@ -0,0 +1,67 @@
+package band.kessoku.lib.api.networking.server;
+
+import band.kessoku.lib.event.api.Event;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerConfigurationNetworkHandler;
+
+/**
+ * Offers access to events related to the connection to a client on a logical server while a client is configuring.
+ */
+public final class ServerConfigurationConnectionEvent {
+ /**
+ * Event fired before any vanilla configuration has taken place.
+ *
+ * This event is executed on {@linkplain io.netty.channel.EventLoop netty's event loops}.
+ *
+ *
Task queued during this event will complete before vanilla configuration starts.
+ */
+ public static final Event BEFORE_CONFIGURE = Event.of(beforeConfigures -> (handler, server) -> {
+ for (Configure callback : beforeConfigures) {
+ callback.onSendConfiguration(handler, server);
+ }
+ });
+
+ /**
+ * Event fired during vanilla configuration.
+ *
+ * This event is executed on {@linkplain io.netty.channel.EventLoop netty's event loops}.
+ *
+ *
An example usage of this:
+ *
{@code
+ * ServerConfigurationConnectionEvents.CONFIGURE.register((handler, server) -> {
+ * if (ServerConfigurationNetworking.canSend(handler, ConfigurationPacket.PACKET_TYPE)) {
+ * handler.addTask(new TestConfigurationTask("Example data"));
+ * } else {
+ * // You can opt to disconnect the client if it cannot handle the configuration task
+ * handler.disconnect(Text.literal("Network test configuration not supported by client"));
+ * }
+ * });
+ * }
+ */
+ public static final Event CONFIGURE = Event.of(configures -> (handler, server) -> {
+ for (Configure callback : configures) {
+ callback.onSendConfiguration(handler, server);
+ }
+ });
+
+ /**
+ * An event for the disconnection of the server configuration network handler.
+ *
+ * No packets should be sent when this event is invoked.
+ */
+ public static final Event DISCONNECT = Event.of(disconnects -> (handler, server) -> {
+ for (Disconnect callback : disconnects) {
+ callback.onConfigureDisconnect(handler, server);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Configure {
+ void onSendConfiguration(ServerConfigurationNetworkHandler handler, MinecraftServer server);
+ }
+
+ @FunctionalInterface
+ public interface Disconnect {
+ void onConfigureDisconnect(ServerConfigurationNetworkHandler handler, MinecraftServer server);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerConfigurationNetworking.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerConfigurationNetworking.java
new file mode 100644
index 00000000..e523ad7f
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerConfigurationNetworking.java
@@ -0,0 +1,269 @@
+package band.kessoku.lib.api.networking.server;
+
+import java.util.Objects;
+import java.util.Set;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.listener.ClientCommonPacketListener;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerConfigurationNetworkHandler;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.thread.ThreadExecutor;
+
+import band.kessoku.lib.api.networking.PayloadTypeRegistry;
+import band.kessoku.lib.impl.networking.server.ServerNetworkingImpl;
+import band.kessoku.lib.mixin.networking.accessor.server.ServerCommonNetworkHandlerAccessor;
+
+/**
+ * Offers access to configuration stage server-side networking functionalities.
+ *
+ * Server-side networking functionalities include receiving serverbound packets, sending clientbound packets, and events related to server-side network handlers.
+ * Packets received by this class must be registered to {@link PayloadTypeRegistry#configC2S()} on both ends.
+ * Packets sent by this class must be registered to {@link PayloadTypeRegistry#configS2C()} on both ends.
+ * Packets must be registered before registering any receivers.
+ *
+ *
This class should be only used for the logical server.
+ *
+ *
See {@link ServerPlayNetworking} for information on sending and receiving play phase packets.
+ *
+ *
See the documentation on each class for more information.
+ *
+ * @see ServerLoginNetworking
+ * @see ServerConfigurationNetworking
+ */
+public final class ServerConfigurationNetworking {
+ /**
+ * Registers a handler for a payload type.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ *
If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterReceiver(ServerConfigurationNetworkHandler, Identifier)} to unregister the existing handler.
+ *
+ * @param type the packet type
+ * @param handler the handler
+ * @return {@code false} if a handler is already registered to the channel
+ * @throws IllegalArgumentException if the codec for {@code type} has not been {@linkplain PayloadTypeRegistry#configC2S() registered} yet
+ * @see ServerConfigurationNetworking#unregisterGlobalReceiver(Identifier)
+ * @see ServerConfigurationNetworking#registerReceiver(ServerConfigurationNetworkHandler, CustomPayload.Id, ConfigurationPacketHandler)
+ */
+ public static boolean registerGlobalReceiver(CustomPayload.Id type, ConfigurationPacketHandler handler) {
+ return ServerNetworkingImpl.CONFIG.registerGlobalReceiver(type.id(), handler);
+ }
+
+ /**
+ * Removes the handler for a payload type.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * The {@code type} is guaranteed not to have an associated handler after this call.
+ *
+ * @param id the packet payload id
+ * @return the previous handler, or {@code null} if no handler was bound to the channel,
+ * or it was not registered using {@link #registerGlobalReceiver(CustomPayload.Id, ConfigurationPacketHandler)}
+ * @see ServerConfigurationNetworking#registerGlobalReceiver(CustomPayload.Id, ConfigurationPacketHandler)
+ * @see ServerConfigurationNetworking#unregisterReceiver(ServerConfigurationNetworkHandler, Identifier)
+ */
+ @Nullable
+ public static ServerConfigurationNetworking.ConfigurationPacketHandler> unregisterGlobalReceiver(Identifier id) {
+ return ServerNetworkingImpl.CONFIG.unregisterGlobalReceiver(id);
+ }
+
+ /**
+ * Gets all channel names which global receivers are registered for.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * @return all channel names which global receivers are registered for.
+ */
+ public static Set getGlobalReceivers() {
+ return ServerNetworkingImpl.CONFIG.getChannels();
+ }
+
+ /**
+ * Registers a handler for a payload type.
+ * This method differs from {@link ServerConfigurationNetworking#registerGlobalReceiver(CustomPayload.Id, ConfigurationPacketHandler)} since
+ * the channel handler will only be applied to the client represented by the {@link ServerConfigurationNetworkHandler}.
+ *
+ * If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterReceiver(ServerConfigurationNetworkHandler, Identifier)} to unregister the existing handler.
+ *
+ * @param networkHandler the network handler
+ * @param type the packet type
+ * @param handler the handler
+ * @return {@code false} if a handler is already registered to the channel name
+ * @throws IllegalArgumentException if the codec for {@code type} has not been {@linkplain PayloadTypeRegistry#configC2S() registered} yet
+ * @see ServerPlayConnectionEvent#INIT
+ */
+ public static boolean registerReceiver(ServerConfigurationNetworkHandler networkHandler, CustomPayload.Id type, ConfigurationPacketHandler handler) {
+ return ServerNetworkingImpl.getAddon(networkHandler).registerChannel(type.id(), handler);
+ }
+
+ /**
+ * Removes the handler for a payload type.
+ *
+ * The {@code type} is guaranteed not to have an associated handler after this call.
+ *
+ * @param id the id of the payload
+ * @return the previous handler, or {@code null} if no handler was bound to the channel,
+ * or it was not registered using {@link #registerReceiver(ServerConfigurationNetworkHandler, CustomPayload.Id, ConfigurationPacketHandler)}
+ */
+ @Nullable
+ public static ServerConfigurationNetworking.ConfigurationPacketHandler> unregisterReceiver(ServerConfigurationNetworkHandler networkHandler, Identifier id) {
+ return ServerNetworkingImpl.getAddon(networkHandler).unregisterChannel(id);
+ }
+
+ /**
+ * Gets all the channel names that the server can receive packets on.
+ *
+ * @param handler the network handler
+ * @return All the channel names that the server can receive packets on
+ */
+ public static Set getReceived(ServerConfigurationNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Server configuration network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getReceivableChannels();
+ }
+
+ /**
+ * Gets all channel names that a connected client declared the ability to receive a packets on.
+ *
+ * @param handler the network handler
+ * @return {@code true} if the connected client has declared the ability to receive a packet on the specified channel
+ */
+ public static Set getSendable(ServerConfigurationNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Server configuration network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getSendableChannels();
+ }
+
+ /**
+ * Checks if the connected client declared the ability to receive a packet on a specified channel name.
+ *
+ * @param handler the network handler
+ * @param channelName the channel name
+ * @return {@code true} if the connected client has declared the ability to receive a packet on the specified channel
+ */
+ public static boolean canSend(ServerConfigurationNetworkHandler handler, Identifier channelName) {
+ Objects.requireNonNull(handler, "Server configuration network handler cannot be null");
+ Objects.requireNonNull(channelName, "Channel name cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getSendableChannels().contains(channelName);
+ }
+
+ /**
+ * Checks if the connected client declared the ability to receive a specific type of packet.
+ *
+ * @param handler the network handler
+ * @param id the payload id
+ * @return {@code true} if the connected client has declared the ability to receive a specific type of packet
+ */
+ public static boolean canSend(ServerConfigurationNetworkHandler handler, CustomPayload.Id> id) {
+ Objects.requireNonNull(handler, "Server configuration network handler cannot be null");
+ Objects.requireNonNull(id, "Payload id cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getSendableChannels().contains(id.id());
+ }
+
+ /**
+ * Creates a packet which may be sent to a connected client.
+ *
+ * @param payload the payload
+ * @return a new packet
+ */
+ public static Packet createS2CPacket(CustomPayload payload) {
+ Objects.requireNonNull(payload, "Payload cannot be null");
+ Objects.requireNonNull(payload.getId(), "CustomPayload#getId() cannot return null for payload class: " + payload.getClass());
+
+ return ServerNetworkingImpl.createS2CPacket(payload);
+ }
+
+ /**
+ * Gets the packet sender which sends packets to the connected client.
+ *
+ * @param handler the network handler, representing the connection to the player/client
+ * @return the packet sender
+ */
+ public static PacketSender getSender(ServerConfigurationNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Server configuration network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler);
+ }
+
+ /**
+ * Sends a packet to a configuring player.
+ *
+ * Any packets sent must be {@linkplain PayloadTypeRegistry#configS2C() registered}.
+ *
+ * @param handler the network handler to send the packet to
+ * @param payload to be sent
+ */
+ public static void send(ServerConfigurationNetworkHandler handler, CustomPayload payload) {
+ Objects.requireNonNull(handler, "Server configuration handler cannot be null");
+ Objects.requireNonNull(payload, "Payload cannot be null");
+ Objects.requireNonNull(payload.getId(), "CustomPayload#getId() cannot return null for payload class: " + payload.getClass());
+
+ handler.sendPacket(createS2CPacket(payload));
+ }
+
+ // Helper methods
+
+ /**
+ * Returns the Minecraft Server of a server configuration network handler.
+ *
+ * @param handler the server configuration network handler
+ */
+ public static MinecraftServer getServer(ServerConfigurationNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Network handler cannot be null");
+
+ return ((ServerCommonNetworkHandlerAccessor) handler).getServer();
+ }
+
+ /**
+ * A packet handler utilizing {@link CustomPayload}.
+ * @param the type of the packet
+ */
+ @FunctionalInterface
+ public interface ConfigurationPacketHandler {
+ /**
+ * Handles an incoming packet.
+ *
+ * Unlike {@link ServerPlayNetworking.PlayPayloadHandler} this method is executed on {@linkplain io.netty.channel.EventLoop netty's event loops}.
+ * Modification to the game should be {@linkplain ThreadExecutor#submit(Runnable) scheduled} using the Minecraft server instance from {@link ServerConfigurationNetworking#getServer(ServerConfigurationNetworkHandler)}.
+ *
+ *
An example usage of this:
+ *
{@code
+ * // use PayloadTypeRegistry for registering the payload
+ * ServerConfigurationNetworking.registerReceiver(BOOM_PACKET_TYPE, (payload, context) -> {
+ *
+ * });
+ * }
+ *
+ *
+ * @param payload the packet payload
+ * @param context the configuration networking context
+ * @see CustomPayload
+ */
+ void receive(T payload, Context context);
+ }
+
+ @ApiStatus.NonExtendable
+ public interface Context {
+ /**
+ * @return The MinecraftServer instance
+ */
+ MinecraftServer server();
+
+ /**
+ * @return The ServerConfigurationNetworkHandler instance
+ */
+ ServerConfigurationNetworkHandler networkHandler();
+
+ /**
+ * @return The packet sender
+ */
+ PacketSender responseSender();
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerLoginConnectionEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerLoginConnectionEvent.java
new file mode 100644
index 00000000..e211cb7c
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerLoginConnectionEvent.java
@@ -0,0 +1,73 @@
+package band.kessoku.lib.api.networking.server;
+
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerLoginNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.LoginPacketSender;
+import band.kessoku.lib.event.api.Event;
+
+/**
+ * Offers access to events related to the connection to a client on a logical server while a client is logging in.
+ */
+public final class ServerLoginConnectionEvent {
+ /**
+ * Event indicating a connection entered the LOGIN state, ready for registering query response handlers.
+ *
+ * @see ServerLoginNetworking#registerReceiver(ServerLoginNetworkHandler, Identifier, ServerLoginNetworking.LoginQueryResponseHandler)
+ */
+ public static final Event INIT = Event.of(inits -> (handler, server) -> {
+ for (Init callback : inits) {
+ callback.onLoginInit(handler, server);
+ }
+ });
+
+ /**
+ * An event for the start of login queries of the server login network handler.
+ * This event may be used to register {@link ServerLoginNetworking.LoginQueryResponseHandler login query response handlers}
+ * using {@link ServerLoginNetworking#registerReceiver(ServerLoginNetworkHandler, Identifier, ServerLoginNetworking.LoginQueryResponseHandler)}
+ * since this event is fired just before the first login query response is processed.
+ *
+ * You may send login queries to the connected client using the provided {@link LoginPacketSender}.
+ */
+ public static final Event QUERY_START = Event.of(queryStarts -> (handler, server, sender, synchronizer) -> {
+ for (QueryStart callback : queryStarts) {
+ callback.onLoginStart(handler, server, sender, synchronizer);
+ }
+ });
+
+ /**
+ * An event for the disconnection of the server login network handler.
+ *
+ * No packets should be sent when this event is invoked.
+ */
+ public static final Event DISCONNECT = Event.of(disconnects -> (handler, server) -> {
+ for (Disconnect callback : disconnects) {
+ callback.onLoginDisconnect(handler, server);
+ }
+ });
+
+ /**
+ * @see ServerLoginConnectionEvent#INIT
+ */
+ @FunctionalInterface
+ public interface Init {
+ void onLoginInit(ServerLoginNetworkHandler handler, MinecraftServer server);
+ }
+
+ /**
+ * @see ServerLoginConnectionEvent#QUERY_START
+ */
+ @FunctionalInterface
+ public interface QueryStart {
+ void onLoginStart(ServerLoginNetworkHandler handler, MinecraftServer server, LoginPacketSender sender, ServerLoginNetworking.LoginSynchronizer synchronizer);
+ }
+
+ /**
+ * @see ServerLoginConnectionEvent#DISCONNECT
+ */
+ @FunctionalInterface
+ public interface Disconnect {
+ void onLoginDisconnect(ServerLoginNetworkHandler handler, MinecraftServer server);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerLoginNetworking.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerLoginNetworking.java
new file mode 100644
index 00000000..97f0475d
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerLoginNetworking.java
@@ -0,0 +1,179 @@
+package band.kessoku.lib.api.networking.server;
+
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerLoginNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.impl.networking.server.ServerNetworkingImpl;
+import band.kessoku.lib.mixin.networking.accessor.server.ServerLoginNetworkHandlerAccessor;
+
+/**
+ * Offers access to login stage server-side networking functionalities.
+ *
+ * Server-side networking functionalities include receiving serverbound query responses and sending clientbound query requests.
+ *
+ * @see ServerPlayNetworking
+ * @see ServerConfigurationNetworking
+ */
+public final class ServerLoginNetworking {
+ /**
+ * Registers a handler to a query response channel.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ *
If a handler is already registered to the {@code channel}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterGlobalReceiver(Identifier)} to unregister the existing handler.
+ *
+ * @param channelName the id of the channel
+ * @param channelHandler the handler
+ * @return false if a handler is already registered to the channel
+ * @see ServerLoginNetworking#unregisterGlobalReceiver(Identifier)
+ * @see ServerLoginNetworking#registerReceiver(ServerLoginNetworkHandler, Identifier, LoginQueryResponseHandler)
+ */
+ public static boolean registerGlobalReceiver(Identifier channelName, LoginQueryResponseHandler channelHandler) {
+ return ServerNetworkingImpl.LOGIN.registerGlobalReceiver(channelName, channelHandler);
+ }
+
+ /**
+ * Removes the handler of a query response channel.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ *
The {@code channel} is guaranteed not to have a handler after this call.
+ *
+ * @param channelName the id of the channel
+ * @return the previous handler, or {@code null} if no handler was bound to the channel
+ * @see ServerLoginNetworking#registerGlobalReceiver(Identifier, LoginQueryResponseHandler)
+ * @see ServerLoginNetworking#unregisterReceiver(ServerLoginNetworkHandler, Identifier)
+ */
+ @Nullable
+ public static ServerLoginNetworking.LoginQueryResponseHandler unregisterGlobalReceiver(Identifier channelName) {
+ return ServerNetworkingImpl.LOGIN.unregisterGlobalReceiver(channelName);
+ }
+
+ /**
+ * Gets all channel names which global receivers are registered for.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * @return all channel names which global receivers are registered for.
+ */
+ public static Set getGlobalReceivers() {
+ return ServerNetworkingImpl.LOGIN.getChannels();
+ }
+
+ /**
+ * Registers a handler to a query response channel.
+ *
+ * If a handler is already registered to the {@code channelName}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterReceiver(ServerLoginNetworkHandler, Identifier)} to unregister the existing handler.
+ *
+ * @param networkHandler the handler
+ * @param channelName the id of the channel
+ * @param responseHandler the handler
+ * @return false if a handler is already registered to the channel name
+ */
+ public static boolean registerReceiver(ServerLoginNetworkHandler networkHandler, Identifier channelName, LoginQueryResponseHandler responseHandler) {
+ Objects.requireNonNull(networkHandler, "Network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(networkHandler).registerChannel(channelName, responseHandler);
+ }
+
+ /**
+ * Removes the handler of a query response channel.
+ *
+ *
The {@code channelName} is guaranteed not to have a handler after this call.
+ *
+ * @param channelName the id of the channel
+ * @return the previous handler, or {@code null} if no handler was bound to the channel name
+ */
+ @Nullable
+ public static ServerLoginNetworking.LoginQueryResponseHandler unregisterReceiver(ServerLoginNetworkHandler networkHandler, Identifier channelName) {
+ Objects.requireNonNull(networkHandler, "Network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(networkHandler).unregisterChannel(channelName);
+ }
+
+ // Helper methods
+
+ /**
+ * Returns the Minecraft Server of a server login network handler.
+ *
+ * @param handler the server login network handler
+ */
+ public static MinecraftServer getServer(ServerLoginNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Network handler cannot be null");
+
+ return ((ServerLoginNetworkHandlerAccessor) handler).getServer();
+ }
+
+ @FunctionalInterface
+ public interface LoginQueryResponseHandler {
+ /**
+ * Handles an incoming query response from a client.
+ *
+ *
This method is executed on {@linkplain io.netty.channel.EventLoop netty's event loops}.
+ * Modification to the game should be {@linkplain net.minecraft.util.thread.ThreadExecutor#submit(Runnable) scheduled} using the provided Minecraft client instance.
+ *
+ *
Whether the client understood the query should be checked before reading from the payload of the packet.
+ * @param server the server
+ * @param handler the network handler that received this packet, representing the player/client who sent the response
+ * @param understood whether the client understood the packet
+ * @param buf the payload of the packet
+ * @param synchronizer the synchronizer which may be used to delay log-in till a {@link Future} is completed.
+ * @param responseSender the packet sender
+ */
+ void receive(MinecraftServer server, ServerLoginNetworkHandler handler, boolean understood, PacketByteBuf buf, LoginSynchronizer synchronizer, PacketSender responseSender);
+ }
+
+ /**
+ * Allows blocking client log-in until all futures passed into {@link LoginSynchronizer#waitFor(Future)} are completed.
+ */
+ @FunctionalInterface
+ @ApiStatus.NonExtendable
+ public interface LoginSynchronizer {
+ /**
+ * Allows blocking client log-in until the {@code future} is {@link Future#isDone() done}.
+ *
+ *
Since packet reception happens on netty's event loops, this allows handlers to
+ * perform logic on the Server Thread, etc. For instance, a handler can prepare an
+ * upcoming query request or check necessary login data on the server thread.
+ *
+ * Here is an example where the player log-in is blocked so that a credential check and
+ * building of a followup query request can be performed properly on the logical server
+ * thread before the player successfully logs in:
+ *
{@code
+ * ServerLoginNetworking.registerGlobalReceiver(CHECK_CHANNEL, (server, handler, understood, buf, synchronizer, responseSender) -> {
+ * if (!understood) {
+ * handler.disconnect(Text.literal("Only accept clients that can check!"));
+ * return;
+ * }
+ *
+ * String checkMessage = buf.readString(32767);
+ *
+ * // Just send the CompletableFuture returned by the server's submit method
+ * synchronizer.waitFor(server.submit(() -> {
+ * LoginInfoChecker checker = LoginInfoChecker.get(server);
+ *
+ * if (!checker.check(handler.getConnectionInfo(), checkMessage)) {
+ * handler.disconnect(Text.literal("Invalid credentials!"));
+ * return;
+ * }
+ *
+ * responseSender.send(UPCOMING_CHECK, checker.buildSecondQueryPacket(handler, checkMessage));
+ * }));
+ * });
+ * }
+ * Usually it is enough to pass the return value for {@link net.minecraft.util.thread.ThreadExecutor#submit(Runnable)} for {@code future}.
+ *
+ * @param future the future that must be done before the player can log in
+ */
+ void waitFor(Future> future);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerPlayConnectionEvent.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerPlayConnectionEvent.java
new file mode 100644
index 00000000..10304ce7
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerPlayConnectionEvent.java
@@ -0,0 +1,59 @@
+package band.kessoku.lib.api.networking.server;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.event.api.Event;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerPlayNetworkHandler;
+
+/**
+ * Offers access to events related to the connection to a client on a logical server while a client is in game.
+ */
+public final class ServerPlayConnectionEvent {
+ /**
+ * Event indicating a connection entered the PLAY state, ready for registering channel handlers.
+ *
+ * @see ServerPlayNetworking#registerReceiver(ServerPlayNetworkHandler, CustomPayload.Id, ServerPlayNetworking.PlayPayloadHandler)
+ */
+ public static final Event INIT = Event.of(inits -> (handler, server) -> {
+ for (Init callback : inits) {
+ callback.onPlayInit(handler, server);
+ }
+ });
+
+ /**
+ * An event for notification when the server play network handler is ready to send packets to the client.
+ *
+ * At this stage, the network handler is ready to send packets to the client.
+ */
+ public static final Event JOIN = Event.of(joins -> (handler, sender, server) -> {
+ for (Join callback : joins) {
+ callback.onPlayReady(handler, sender, server);
+ }
+ });
+
+ /**
+ * An event for the disconnection of the server play network handler.
+ *
+ * No packets should be sent when this event is invoked.
+ */
+ public static final Event DISCONNECT = Event.of(disconnects -> (handler, server) -> {
+ for (Disconnect callback : disconnects) {
+ callback.onPlayDisconnect(handler, server);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Init {
+ void onPlayInit(ServerPlayNetworkHandler handler, MinecraftServer server);
+ }
+
+ @FunctionalInterface
+ public interface Join {
+ void onPlayReady(ServerPlayNetworkHandler handler, PacketSender sender, MinecraftServer server);
+ }
+
+ @FunctionalInterface
+ public interface Disconnect {
+ void onPlayDisconnect(ServerPlayNetworkHandler handler, MinecraftServer server);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerPlayNetworking.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerPlayNetworking.java
new file mode 100644
index 00000000..49a65f0c
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/api/networking/server/ServerPlayNetworking.java
@@ -0,0 +1,326 @@
+package band.kessoku.lib.api.networking.server;
+
+import java.util.Objects;
+import java.util.Set;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.api.networking.PayloadTypeRegistry;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.listener.ClientCommonPacketListener;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerPlayNetworkHandler;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.impl.networking.server.ServerNetworkingImpl;
+
+/**
+ * Offers access to play stage server-side networking functionalities.
+ *
+ * Server-side networking functionalities include receiving serverbound packets, sending clientbound packets, and events related to server-side network handlers.
+ * Packets received by this class must be registered to {@link PayloadTypeRegistry#playC2S()} on both ends.
+ * Packets sent by this class must be registered to {@link PayloadTypeRegistry#playS2C()} on both ends.
+ * Packets must be registered before registering any receivers.
+ *
+ *
This class should be only used for the logical server.
+ *
+ *
Packet object-based API
+ *
+ * This class provides a registration method, utilizing packet objects, {@link #registerGlobalReceiver(CustomPayload.Id, PlayPayloadHandler)}.
+ * This handler executes the callback in the server thread, ensuring thread safety.
+ *
+ *
This payload object-based API involves three classes:
+ *
+ *
+ * - A class implementing {@link CustomPayload} that is "sent" over the network
+ * - {@link CustomPayload.Type} instance, which represents the packet's type (and its codec)
+ * - {@link PlayPayloadHandler}, which handles the packet (usually implemented as a functional interface)
+ *
+ *
+ * See the documentation on each class for more information.
+ *
+ * @see ServerLoginNetworking
+ * @see ServerConfigurationNetworking
+ */
+public final class ServerPlayNetworking {
+ /**
+ * Registers a handler for a payload type.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ *
If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterGlobalReceiver(Identifier)} to unregister the existing handler.
+ *
+ * @param type the packet type
+ * @param handler the handler
+ * @return {@code false} if a handler is already registered to the channel
+ * @throws IllegalArgumentException if the codec for {@code type} has not been {@linkplain PayloadTypeRegistry#playC2S() registered} yet
+ * @see ServerPlayNetworking#unregisterGlobalReceiver(Identifier)
+ */
+ public static boolean registerGlobalReceiver(CustomPayload.Id type, PlayPayloadHandler handler) {
+ return ServerNetworkingImpl.PLAY.registerGlobalReceiver(type.id(), handler);
+ }
+
+ /**
+ * Removes the handler for a payload type.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * The {@code id} is guaranteed not to have an associated handler after this call.
+ *
+ * @param id the payload id
+ * @return the previous handler, or {@code null} if no handler was bound to the channel,
+ * or it was not registered using {@link #registerGlobalReceiver(CustomPayload.Id, PlayPayloadHandler)}
+ * @see ServerPlayNetworking#registerGlobalReceiver(CustomPayload.Id, PlayPayloadHandler)
+ * @see ServerPlayNetworking#unregisterReceiver(ServerPlayNetworkHandler, Identifier)
+ */
+ @Nullable
+ public static ServerPlayNetworking.PlayPayloadHandler> unregisterGlobalReceiver(Identifier id) {
+ return ServerNetworkingImpl.PLAY.unregisterGlobalReceiver(id);
+ }
+
+ /**
+ * Gets all channel names which global receivers are registered for.
+ * A global receiver is registered to all connections, in the present and future.
+ *
+ * @return all channel names which global receivers are registered for.
+ */
+ public static Set getGlobalReceivers() {
+ return ServerNetworkingImpl.PLAY.getChannels();
+ }
+
+ /**
+ * Registers a handler for a payload type.
+ * This method differs from {@link ServerPlayNetworking#registerGlobalReceiver(CustomPayload.Id, PlayPayloadHandler)} since
+ * the channel handler will only be applied to the player represented by the {@link ServerPlayNetworkHandler}.
+ *
+ * For example, if you only register a receiver using this method when a {@linkplain ServerLoginNetworking#registerGlobalReceiver(Identifier, ServerLoginNetworking.LoginQueryResponseHandler)}
+ * login response has been received, you should use {@link ServerPlayConnectionEvent#INIT} to register the channel handler.
+ *
+ *
If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
+ * Use {@link #unregisterReceiver(ServerPlayNetworkHandler, Identifier)} to unregister the existing handler.
+ *
+ * @param networkHandler the network handler
+ * @param type the packet type
+ * @param handler the handler
+ * @return {@code false} if a handler is already registered to the channel name
+ * @throws IllegalArgumentException if the codec for {@code type} has not been {@linkplain PayloadTypeRegistry#playC2S() registered} yet
+ * @see ServerPlayConnectionEvent#INIT
+ */
+ public static boolean registerReceiver(ServerPlayNetworkHandler networkHandler, CustomPayload.Id type, PlayPayloadHandler handler) {
+ return ServerNetworkingImpl.getAddon(networkHandler).registerChannel(type.id(), handler);
+ }
+
+ /**
+ * Removes the handler for a packet type.
+ *
+ * The {@code id} is guaranteed not to have an associated handler after this call.
+ *
+ * @param id the id of the payload
+ * @return the previous handler, or {@code null} if no handler was bound to the channel,
+ * or it was not registered using {@link #registerReceiver(ServerPlayNetworkHandler, CustomPayload.Id, PlayPayloadHandler)}
+ */
+ @Nullable
+ public static ServerPlayNetworking.PlayPayloadHandler> unregisterReceiver(ServerPlayNetworkHandler networkHandler, Identifier id) {
+ return ServerNetworkingImpl.getAddon(networkHandler).unregisterChannel(id);
+ }
+
+ /**
+ * Gets all the channel names that the server can receive packets on.
+ *
+ * @param player the player
+ * @return All the channel names that the server can receive packets on
+ */
+ public static Set getReceived(ServerPlayerEntity player) {
+ Objects.requireNonNull(player, "Server player entity cannot be null");
+
+ return getReceived(player.networkHandler);
+ }
+
+ /**
+ * Gets all the channel names that the server can receive packets on.
+ *
+ * @param handler the network handler
+ * @return All the channel names that the server can receive packets on
+ */
+ public static Set getReceived(ServerPlayNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Server play network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getReceivableChannels();
+ }
+
+ /**
+ * Gets all channel names that the connected client declared the ability to receive a packets on.
+ *
+ * @param player the player
+ * @return All the channel names the connected client declared the ability to receive a packets on
+ */
+ public static Set getSendable(ServerPlayerEntity player) {
+ Objects.requireNonNull(player, "Server player entity cannot be null");
+
+ return getSendable(player.networkHandler);
+ }
+
+ /**
+ * Gets all channel names that a connected client declared the ability to receive a packets on.
+ *
+ * @param handler the network handler
+ * @return {@code true} if the connected client has declared the ability to receive a packet on the specified channel
+ */
+ public static Set getSendable(ServerPlayNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Server play network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getSendableChannels();
+ }
+
+ /**
+ * Checks if the connected client declared the ability to receive a packet on a specified channel name.
+ *
+ * @param player the player
+ * @param channelName the channel name
+ * @return {@code true} if the connected client has declared the ability to receive a packet on the specified channel
+ */
+ public static boolean canSend(ServerPlayerEntity player, Identifier channelName) {
+ Objects.requireNonNull(player, "Server player entity cannot be null");
+
+ return canSend(player.networkHandler, channelName);
+ }
+
+ /**
+ * Checks if the connected client declared the ability to receive a specific type of packet.
+ *
+ * @param player the player
+ * @param type the packet type
+ * @return {@code true} if the connected client has declared the ability to receive a specific type of packet
+ */
+ public static boolean canSend(ServerPlayerEntity player, CustomPayload.Id> type) {
+ Objects.requireNonNull(player, "Server player entity cannot be null");
+
+ return canSend(player.networkHandler, type.id());
+ }
+
+ /**
+ * Checks if the connected client declared the ability to receive a packet on a specified channel name.
+ *
+ * @param handler the network handler
+ * @param channelName the channel name
+ * @return {@code true} if the connected client has declared the ability to receive a packet on the specified channel
+ */
+ public static boolean canSend(ServerPlayNetworkHandler handler, Identifier channelName) {
+ Objects.requireNonNull(handler, "Server play network handler cannot be null");
+ Objects.requireNonNull(channelName, "Channel name cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getSendableChannels().contains(channelName);
+ }
+
+ /**
+ * Checks if the connected client declared the ability to receive a specific type of packet.
+ *
+ * @param handler the network handler
+ * @param type the packet type
+ * @return {@code true} if the connected client has declared the ability to receive a specific type of packet
+ */
+ public static boolean canSend(ServerPlayNetworkHandler handler, CustomPayload.Id> type) {
+ Objects.requireNonNull(handler, "Server play network handler cannot be null");
+ Objects.requireNonNull(type, "Packet type cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getSendableChannels().contains(type.id());
+ }
+
+ /**
+ * Creates a packet which may be sent to a connected client.
+ *
+ * @param packet the packet
+ * @return a new packet
+ */
+ public static Packet createS2CPacket(T packet) {
+ return ServerNetworkingImpl.createS2CPacket(packet);
+ }
+
+ /**
+ * Gets the packet sender which sends packets to the connected client.
+ *
+ * @param player the player
+ * @return the packet sender
+ */
+ public static PacketSender getSender(ServerPlayerEntity player) {
+ Objects.requireNonNull(player, "Server player entity cannot be null");
+
+ return getSender(player.networkHandler);
+ }
+
+ /**
+ * Gets the packet sender which sends packets to the connected client.
+ *
+ * @param handler the network handler, representing the connection to the player/client
+ * @return the packet sender
+ */
+ public static PacketSender getSender(ServerPlayNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Server play network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler);
+ }
+
+ /**
+ * Sends a packet to a player.
+ *
+ * Any packets sent must be {@linkplain PayloadTypeRegistry#playS2C() registered}.
+ *
+ * @param player the player to send the packet to
+ * @param payload the payload to send
+ */
+ public static void send(ServerPlayerEntity player, CustomPayload payload) {
+ Objects.requireNonNull(player, "Server player entity cannot be null");
+ Objects.requireNonNull(payload, "Payload cannot be null");
+ Objects.requireNonNull(payload.getId(), "CustomPayload#getId() cannot return null for payload class: " + payload.getClass());
+
+ player.networkHandler.sendPacket(createS2CPacket(payload));
+ }
+
+ /**
+ * A thread-safe packet handler utilizing {@link CustomPayload}.
+ * @param the type of the packet
+ */
+ @FunctionalInterface
+ public interface PlayPayloadHandler {
+ /**
+ * Handles the incoming packet. This is called on the server thread, and can safely
+ * manipulate the world.
+ *
+ * An example usage of this is to create an explosion where the player is looking:
+ *
{@code
+ * // use PayloadTypeRegistry for registering the payload
+ * ServerPlayNetworking.registerReceiver(BoomPayload.ID, (payload, context) -> {
+ * ModPacketHandler.createExplosion(context.player(), payload.fire());
+ * });
+ * }
+ *
+ * The network handler can be accessed via {@link ServerPlayerEntity#networkHandler}.
+ *
+ * @param payload the packet payload
+ * @param context the play networking context
+ * @see CustomPayload
+ */
+ void receive(T payload, Context context);
+ }
+
+ @ApiStatus.NonExtendable
+ public interface Context {
+ /**
+ * @return The MinecraftServer instance
+ */
+ MinecraftServer server();
+
+ /**
+ * @return The player that received the packet
+ */
+ ServerPlayerEntity player();
+
+ /**
+ * @return The packet sender
+ */
+ PacketSender responseSender();
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/util/ChannelRegister.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/util/ChannelRegister.java
deleted file mode 100644
index 9a7fb872..00000000
--- a/networking/common/src/main/java/band/kessoku/lib/api/networking/util/ChannelRegister.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package band.kessoku.lib.api.networking.util;
-
-import band.kessoku.lib.impl.networking.ChannelRegisterImpl;
-import net.minecraft.network.PacketByteBuf;
-import net.minecraft.network.RegistryByteBuf;
-import net.minecraft.network.codec.PacketCodec;
-import net.minecraft.network.packet.CustomPayload;
-import org.jetbrains.annotations.ApiStatus;
-
-/**
- * A registry for payload types.
- */
-@ApiStatus.NonExtendable
-public interface ChannelRegister {
-
- /**
- * Registers a custom payload type.
- *
- *
This must be done on both the sending and receiving side, usually during mod initialization
- * and before registering a packet handler.
- *
- * @param id the id of the payload type
- * @param codec the codec for the payload type
- * @param the payload type
- * @return the registered payload type
- */
- CustomPayload.Type super B, T> register(CustomPayload.Id id, PacketCodec super B, T> codec);
-
- /**
- * @return the {@link ChannelRegister} instance for the client to server configuration channel.
- */
- static ChannelRegister configC2S() {
- return ChannelRegisterImpl.CONFIG_C2S;
- }
-
- /**
- * @return the {@link ChannelRegister} instance for the server to client configuration channel.
- */
- static ChannelRegister configS2C() {
- return ChannelRegisterImpl.CONFIG_S2C;
- }
-
- /**
- * @return the {@link ChannelRegister} instance for the client to server play channel.
- */
- static ChannelRegister playC2S() {
- return ChannelRegisterImpl.PLAY_C2S;
- }
-
- /**
- * @return the {@link ChannelRegister} instance for the server to client play channel.
- */
- static ChannelRegister playS2C() {
- return ChannelRegisterImpl.PLAY_S2C;
- }
-}
diff --git a/networking/common/src/main/java/band/kessoku/lib/api/networking/util/NetworkHandlerExtension.java b/networking/common/src/main/java/band/kessoku/lib/api/networking/util/NetworkHandlerExtension.java
deleted file mode 100644
index 52e6ffe3..00000000
--- a/networking/common/src/main/java/band/kessoku/lib/api/networking/util/NetworkHandlerExtension.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package band.kessoku.lib.api.networking.util;
-
-import band.kessoku.lib.impl.networking.AbstractNetworkAddon;
-
-public interface NetworkHandlerExtension {
- AbstractNetworkAddon> getAddon();
-}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/AbstractChanneledNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/AbstractChanneledNetworkAddon.java
new file mode 100644
index 00000000..042a0c1c
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/AbstractChanneledNetworkAddon.java
@@ -0,0 +1,224 @@
+package band.kessoku.lib.impl.networking;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import band.kessoku.lib.impl.networking.common.CommonPacketHandler;
+import band.kessoku.lib.impl.networking.common.CommonRegisterPayload;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.PacketSender;
+
+/**
+ * A network addon which is aware of the channels the other side may receive.
+ *
+ * @param the channel handler type
+ */
+public abstract class AbstractChanneledNetworkAddon extends AbstractNetworkAddon implements PacketSender, CommonPacketHandler {
+ // The maximum number of channels that a connecting client can register.
+ private static final int MAX_CHANNELS = Integer.getInteger("kessokulib.networking.maxChannels", 8192);
+ // The maximum length of a channel name a connecting client can use, 128 is the default and minimum value.
+ private static final int MAX_CHANNEL_NAME_LENGTH = Math.max(Integer.getInteger("kessokulib.networking.maxChannelNameLength", GlobalReceiverRegistry.DEFAULT_CHANNEL_NAME_MAX_LENGTH), GlobalReceiverRegistry.DEFAULT_CHANNEL_NAME_MAX_LENGTH);
+
+ protected final ClientConnection connection;
+ protected final GlobalReceiverRegistry receiver;
+ protected final Set sendableChannels;
+
+ protected int commonVersion = -1;
+
+ protected AbstractChanneledNetworkAddon(GlobalReceiverRegistry receiver, ClientConnection connection, String description) {
+ super(receiver, description);
+ this.connection = connection;
+ this.receiver = receiver;
+ this.sendableChannels = Collections.synchronizedSet(new HashSet<>());
+ }
+
+ protected void registerPendingChannels(ChannelInfoHolder holder, NetworkPhase state) {
+ final Collection pending = holder.kessokulib$getPendingChannelsNames(state);
+
+ if (!pending.isEmpty()) {
+ register(new ArrayList<>(pending));
+ pending.clear();
+ }
+ }
+
+ // always supposed to handle async!
+ public boolean handle(CustomPayload payload) {
+ final Identifier channelName = payload.getId().id();
+ this.logger.debug("Handling inbound packet from channel with name \"{}\"", channelName);
+
+ // Handle reserved packets
+ if (payload instanceof RegistrationPayload registrationPayload) {
+ if (NetworkingImpl.REGISTER_CHANNEL.equals(channelName)) {
+ this.receiveRegistration(true, registrationPayload);
+ return true;
+ }
+
+ if (NetworkingImpl.UNREGISTER_CHANNEL.equals(channelName)) {
+ this.receiveRegistration(false, registrationPayload);
+ return true;
+ }
+ }
+
+ @Nullable H handler = this.getHandler(channelName);
+
+ if (handler == null) {
+ return false;
+ }
+
+ try {
+ this.receive(handler, payload);
+ } catch (Throwable ex) {
+ this.logger.error("Encountered exception while handling in channel with name \"{}\"", channelName, ex);
+ throw ex;
+ }
+
+ return true;
+ }
+
+ protected abstract void receive(H handler, CustomPayload payload);
+
+ protected void sendInitialChannelRegistrationPacket() {
+ final RegistrationPayload payload = createRegistrationPayload(RegistrationPayload.REGISTER, this.getReceivableChannels());
+
+ if (payload != null) {
+ this.sendPacket(payload);
+ }
+ }
+
+ @Nullable
+ protected RegistrationPayload createRegistrationPayload(CustomPayload.Id id, Collection channels) {
+ if (channels.isEmpty()) {
+ return null;
+ }
+
+ return new RegistrationPayload(id, new ArrayList<>(channels));
+ }
+
+ // wrap in try with res (buf)
+ protected void receiveRegistration(boolean register, RegistrationPayload payload) {
+ if (register) {
+ register(payload.channels());
+ } else {
+ unregister(payload.channels());
+ }
+ }
+
+ void register(List ids) {
+ ids.forEach(this::registerChannel);
+ schedule(() -> this.invokeRegisterEvent(ids));
+ }
+
+ private void registerChannel(Identifier id) {
+ if (this.sendableChannels.size() >= MAX_CHANNELS) {
+ throw new IllegalArgumentException("Cannot register more than " + MAX_CHANNELS + " channels");
+ }
+
+ if (id.toString().length() > MAX_CHANNEL_NAME_LENGTH) {
+ throw new IllegalArgumentException("Channel name is too long");
+ }
+
+ this.sendableChannels.add(id);
+ }
+
+ void unregister(List ids) {
+ this.sendableChannels.removeAll(ids);
+ schedule(() -> this.invokeUnregisterEvent(ids));
+ }
+
+ @Override
+ public void sendPacket(Packet> packet, PacketCallbacks callback) {
+ Objects.requireNonNull(packet, "Packet cannot be null");
+
+ this.connection.send(packet, callback);
+ }
+
+ @Override
+ public void disconnect(Text disconnectReason) {
+ Objects.requireNonNull(disconnectReason, "Disconnect reason cannot be null");
+
+ this.connection.disconnect(disconnectReason);
+ }
+
+ /**
+ * Schedules a task to run on the main thread.
+ */
+ protected abstract void schedule(Runnable task);
+
+ protected abstract void invokeRegisterEvent(List ids);
+
+ protected abstract void invokeUnregisterEvent(List ids);
+
+ public Set getSendableChannels() {
+ return Collections.unmodifiableSet(this.sendableChannels);
+ }
+
+ // Common packet handlers
+
+ @Override
+ public void kessokulib$onCommonVersionPacket(int negotiatedVersion) {
+ assert negotiatedVersion == 1; // We only support version 1 for now
+
+ commonVersion = negotiatedVersion;
+ this.logger.debug("Negotiated common packet version {}", commonVersion);
+ }
+
+ @Override
+ public void kessokulib$onCommonRegisterPacket(CommonRegisterPayload payload) {
+ if (payload.version() != kessokulib$getNegotiatedVersion()) {
+ throw new IllegalStateException("Negotiated common packet version: %d but received packet with version: %d".formatted(commonVersion, payload.version()));
+ }
+
+ final String currentPhase = getPhase();
+
+ if (currentPhase == null) {
+ // We don't support receiving the register packet during this phase. See getPhase() for supported phases.
+ // The normal case where the play channels are sent during configuration is handled in the client/common configuration packet handlers.
+ logger.warn("Received common register packet for phase {} in network state: {}", payload.phase(), receiver.getPhase());
+ return;
+ }
+
+ if (!payload.phase().equals(currentPhase)) {
+ // We need to handle receiving the play phase during configuration!
+ throw new IllegalStateException("Register packet received for phase (%s) on handler for phase(%s)".formatted(payload.phase(), currentPhase));
+ }
+
+ register(new ArrayList<>(payload.channels()));
+ }
+
+ @Override
+ public CommonRegisterPayload kessokulib$createRegisterPayload() {
+ return new CommonRegisterPayload(kessokulib$getNegotiatedVersion(), getPhase(), this.getReceivableChannels());
+ }
+
+ @Override
+ public int kessokulib$getNegotiatedVersion() {
+ if (commonVersion == -1) {
+ throw new IllegalStateException("Not yet negotiated common packet version");
+ }
+
+ return commonVersion;
+ }
+
+ @Nullable
+ private String getPhase() {
+ return switch (receiver.getPhase()) {
+ case PLAY -> CommonRegisterPayload.PLAY_PHASE;
+ case CONFIGURATION -> CommonRegisterPayload.CONFIGURATION_PHASE;
+ default -> null; // We don't support receiving this packet on any other phase
+ };
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/AbstractNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/AbstractNetworkAddon.java
index 3451fd33..d1967010 100644
--- a/networking/common/src/main/java/band/kessoku/lib/impl/networking/AbstractNetworkAddon.java
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/AbstractNetworkAddon.java
@@ -1,9 +1,154 @@
package band.kessoku.lib.impl.networking;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
/**
* A network addon is a simple abstraction to hold information about a player's registered channels.
*
* @param the channel handler type
*/
public abstract class AbstractNetworkAddon {
+ protected final GlobalReceiverRegistry receiver;
+ protected final Logger logger;
+ // A lock is used due to possible access on netty's event loops and game thread at same times such as during dynamic registration
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+ // Sync map should be fine as there is little read write competition
+ // All access to this map is guarded by the lock
+ private final Map handlers = new HashMap<>();
+ private final AtomicBoolean disconnected = new AtomicBoolean(); // blocks redundant disconnect notifications
+
+ protected AbstractNetworkAddon(GlobalReceiverRegistry receiver, String description) {
+ this.receiver = receiver;
+ this.logger = LoggerFactory.getLogger(description);
+ }
+
+ public final void lateInit() {
+ this.receiver.startSession(this);
+ invokeInitEvent();
+ }
+
+ protected abstract void invokeInitEvent();
+
+ public final void endSession() {
+ this.receiver.endSession(this);
+ }
+
+ @Nullable
+ public H getHandler(Identifier channel) {
+ Lock lock = this.lock.readLock();
+ lock.lock();
+
+ try {
+ return this.handlers.get(channel);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void assertNotReserved(Identifier channel) {
+ if (this.isReservedChannel(channel)) {
+ throw new IllegalArgumentException(String.format("Cannot (un)register handler for reserved channel with name \"%s\"", channel));
+ }
+ }
+
+ public void registerChannels(Map map) {
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ for (Map.Entry entry : map.entrySet()) {
+ assertNotReserved(entry.getKey());
+
+ boolean unique = this.handlers.putIfAbsent(entry.getKey(), entry.getValue()) == null;
+ if (unique) handleRegistration(entry.getKey());
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public boolean registerChannel(Identifier channelName, H handler) {
+ Objects.requireNonNull(channelName, "Channel name cannot be null");
+ Objects.requireNonNull(handler, "Packet handler cannot be null");
+ assertNotReserved(channelName);
+
+ receiver.assertPayloadType(channelName);
+
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ final boolean replaced = this.handlers.putIfAbsent(channelName, handler) == null;
+
+ if (replaced) {
+ this.handleRegistration(channelName);
+ }
+
+ return replaced;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public H unregisterChannel(Identifier channelName) {
+ Objects.requireNonNull(channelName, "Channel name cannot be null");
+ assertNotReserved(channelName);
+
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ final H removed = this.handlers.remove(channelName);
+
+ if (removed != null) {
+ this.handleUnregistration(channelName);
+ }
+
+ return removed;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public Set getReceivableChannels() {
+ Lock lock = this.lock.readLock();
+ lock.lock();
+
+ try {
+ return new HashSet<>(this.handlers.keySet());
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ protected abstract void handleRegistration(Identifier channelName);
+
+ protected abstract void handleUnregistration(Identifier channelName);
+
+ public final void handleDisconnect() {
+ if (disconnected.compareAndSet(false, true)) {
+ invokeDisconnectEvent();
+ endSession();
+ }
+ }
+
+ protected abstract void invokeDisconnectEvent();
+
+ /**
+ * Checks if a channel is considered a "reserved" channel.
+ * A reserved channel such as "minecraft:(un)register" has special handling and should not have any channel handlers registered for it.
+ *
+ * @param channelName the channel name
+ * @return whether the channel is reserved
+ */
+ protected abstract boolean isReservedChannel(Identifier channelName);
}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/ChannelInfoHolder.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/ChannelInfoHolder.java
new file mode 100644
index 00000000..6cd0d93a
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/ChannelInfoHolder.java
@@ -0,0 +1,13 @@
+package band.kessoku.lib.impl.networking;
+
+import java.util.Collection;
+
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.util.Identifier;
+
+public interface ChannelInfoHolder {
+ /**
+ * @return Channels which are declared as receivable by the other side but have not been declared yet.
+ */
+ Collection kessokulib$getPendingChannelsNames(NetworkPhase state);
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/CustomPayloadPacketCodecExtension.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/CustomPayloadPacketCodecExtension.java
new file mode 100644
index 00000000..bc7c4cd4
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/CustomPayloadPacketCodecExtension.java
@@ -0,0 +1,7 @@
+package band.kessoku.lib.impl.networking;
+
+import net.minecraft.network.PacketByteBuf;
+
+public interface CustomPayloadPacketCodecExtension {
+ void kessokulib$setPacketCodecProvider(CustomPayloadTypeProvider customPayloadTypeProvider);
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/CustomPayloadTypeProvider.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/CustomPayloadTypeProvider.java
new file mode 100644
index 00000000..cc7672fe
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/CustomPayloadTypeProvider.java
@@ -0,0 +1,9 @@
+package band.kessoku.lib.impl.networking;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.util.Identifier;
+
+public interface CustomPayloadTypeProvider {
+ CustomPayload.Type kessokulib$get(B packetByteBuf, Identifier identifier);
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/GlobalReceiverRegistry.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/GlobalReceiverRegistry.java
new file mode 100644
index 00000000..a2922728
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/GlobalReceiverRegistry.java
@@ -0,0 +1,208 @@
+package band.kessoku.lib.impl.networking;
+
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.NetworkSide;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+public final class GlobalReceiverRegistry {
+ public static final int DEFAULT_CHANNEL_NAME_MAX_LENGTH = 128;
+ private static final Logger LOGGER = LoggerFactory.getLogger(GlobalReceiverRegistry.class);
+
+ private final NetworkSide side;
+ private final NetworkPhase phase;
+ @Nullable
+ private final PayloadTypeRegistryImpl> payloadTypeRegistry;
+
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+ private final Map handlers = new HashMap<>();
+ private final Set> trackedAddons = new HashSet<>();
+
+ public GlobalReceiverRegistry(NetworkSide side, NetworkPhase phase, @Nullable PayloadTypeRegistryImpl> payloadTypeRegistry) {
+ this.side = side;
+ this.phase = phase;
+ this.payloadTypeRegistry = payloadTypeRegistry;
+
+ if (payloadTypeRegistry != null) {
+ assert phase == payloadTypeRegistry.getPhase();
+ assert side == payloadTypeRegistry.getSide();
+ }
+ }
+
+ @Nullable
+ public H getHandler(Identifier channelName) {
+ Lock lock = this.lock.readLock();
+ lock.lock();
+
+ try {
+ return this.handlers.get(channelName);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public boolean registerGlobalReceiver(Identifier channelName, H handler) {
+ Objects.requireNonNull(channelName, "Channel name cannot be null");
+ Objects.requireNonNull(handler, "Channel handler cannot be null");
+
+ if (NetworkingImpl.isReservedCommonChannel(channelName)) {
+ throw new IllegalArgumentException(String.format("Cannot register handler for reserved channel with name \"%s\"", channelName));
+ }
+
+ assertPayloadType(channelName);
+
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ final boolean replaced = this.handlers.putIfAbsent(channelName, handler) == null;
+
+ if (replaced) {
+ this.handleRegistration(channelName, handler);
+ }
+
+ return replaced;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Nullable
+ public H unregisterGlobalReceiver(Identifier channelName) {
+ Objects.requireNonNull(channelName, "Channel name cannot be null");
+
+ if (NetworkingImpl.isReservedCommonChannel(channelName)) {
+ throw new IllegalArgumentException(String.format("Cannot unregister packet handler for reserved channel with name \"%s\"", channelName));
+ }
+
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ final H removed = this.handlers.remove(channelName);
+
+ if (removed != null) {
+ this.handleUnregistration(channelName);
+ }
+
+ return removed;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public Map getHandlers() {
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ return new HashMap<>(this.handlers);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public Set getChannels() {
+ Lock lock = this.lock.readLock();
+ lock.lock();
+
+ try {
+ return new HashSet<>(this.handlers.keySet());
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ // State tracking methods
+
+ public void startSession(AbstractNetworkAddon addon) {
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ if (this.trackedAddons.add(addon)) {
+ addon.registerChannels(handlers);
+ }
+
+ this.logTrackedAddonSize();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public void endSession(AbstractNetworkAddon addon) {
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ this.logTrackedAddonSize();
+ this.trackedAddons.remove(addon);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * In practice, trackedAddons should never contain more than the number of players.
+ */
+ private void logTrackedAddonSize() {
+ if (LOGGER.isTraceEnabled() && this.trackedAddons.size() > 1) {
+ LOGGER.trace("{} receiver registry tracks {} addon instances", phase.getId(), trackedAddons.size());
+ }
+ }
+
+ private void handleRegistration(Identifier channelName, H handler) {
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ this.logTrackedAddonSize();
+
+ for (AbstractNetworkAddon addon : this.trackedAddons) {
+ addon.registerChannel(channelName, handler);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void handleUnregistration(Identifier channelName) {
+ Lock lock = this.lock.writeLock();
+ lock.lock();
+
+ try {
+ this.logTrackedAddonSize();
+
+ for (AbstractNetworkAddon addon : this.trackedAddons) {
+ addon.unregisterChannel(channelName);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public void assertPayloadType(Identifier channelName) {
+ if (payloadTypeRegistry == null) {
+ return;
+ }
+
+ if (payloadTypeRegistry.get(channelName) == null) {
+ throw new IllegalArgumentException(String.format("Cannot register handler as no payload type has been registered with name \"%s\" for %s %s", channelName, side, phase));
+ }
+
+ if (channelName.toString().length() > DEFAULT_CHANNEL_NAME_MAX_LENGTH) {
+ throw new IllegalArgumentException(String.format("Cannot register handler for channel with name \"%s\" as it exceeds the maximum length of 128 characters", channelName));
+ }
+ }
+
+ public NetworkPhase getPhase() {
+ return phase;
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/NetworkHandlerExtension.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/NetworkHandlerExtension.java
new file mode 100644
index 00000000..017195fd
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/NetworkHandlerExtension.java
@@ -0,0 +1,5 @@
+package band.kessoku.lib.impl.networking;
+
+public interface NetworkHandlerExtension {
+ AbstractNetworkAddon> kessokulib$getNetworkAddon();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/NetworkingImpl.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/NetworkingImpl.java
new file mode 100644
index 00000000..cce4d251
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/NetworkingImpl.java
@@ -0,0 +1,31 @@
+package band.kessoku.lib.impl.networking;
+
+import band.kessoku.lib.api.networking.PayloadTypeRegistry;
+import net.minecraft.util.Identifier;
+
+public class NetworkingImpl {
+ /**
+ * Id of packet used to register supported channels.
+ */
+ public static final Identifier REGISTER_CHANNEL = Identifier.ofVanilla("register");
+
+ /**
+ * Id of packet used to unregister supported channels.
+ */
+ public static final Identifier UNREGISTER_CHANNEL = Identifier.ofVanilla("unregister");
+
+ public static boolean isReservedCommonChannel(Identifier channelName) {
+ return channelName.equals(REGISTER_CHANNEL) || channelName.equals(UNREGISTER_CHANNEL);
+ }
+
+ public static void init() {
+ PayloadTypeRegistry.configS2C().register(RegistrationPayload.REGISTER, RegistrationPayload.REGISTER_CODEC);
+ PayloadTypeRegistry.configS2C().register(RegistrationPayload.UNREGISTER, RegistrationPayload.UNREGISTER_CODEC);
+ PayloadTypeRegistry.configC2S().register(RegistrationPayload.REGISTER, RegistrationPayload.REGISTER_CODEC);
+ PayloadTypeRegistry.configC2S().register(RegistrationPayload.UNREGISTER, RegistrationPayload.UNREGISTER_CODEC);
+ PayloadTypeRegistry.playS2C().register(RegistrationPayload.REGISTER, RegistrationPayload.REGISTER_CODEC);
+ PayloadTypeRegistry.playS2C().register(RegistrationPayload.UNREGISTER, RegistrationPayload.UNREGISTER_CODEC);
+ PayloadTypeRegistry.playC2S().register(RegistrationPayload.REGISTER, RegistrationPayload.REGISTER_CODEC);
+ PayloadTypeRegistry.playC2S().register(RegistrationPayload.UNREGISTER, RegistrationPayload.UNREGISTER_CODEC);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/PacketCallbackListener.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/PacketCallbackListener.java
new file mode 100644
index 00000000..31e7ace4
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/PacketCallbackListener.java
@@ -0,0 +1,12 @@
+package band.kessoku.lib.impl.networking;
+
+import net.minecraft.network.packet.Packet;
+
+public interface PacketCallbackListener {
+ /**
+ * Called after a packet has been sent.
+ *
+ * @param packet the packet
+ */
+ void kessokulib$sent(Packet> packet);
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/ChannelRegisterImpl.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/PayloadTypeRegistryImpl.java
similarity index 67%
rename from networking/common/src/main/java/band/kessoku/lib/impl/networking/ChannelRegisterImpl.java
rename to networking/common/src/main/java/band/kessoku/lib/impl/networking/PayloadTypeRegistryImpl.java
index 163bfd1c..6613252c 100644
--- a/networking/common/src/main/java/band/kessoku/lib/impl/networking/ChannelRegisterImpl.java
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/PayloadTypeRegistryImpl.java
@@ -1,6 +1,6 @@
package band.kessoku.lib.impl.networking;
-import band.kessoku.lib.api.networking.util.ChannelRegister;
+import band.kessoku.lib.api.networking.PayloadTypeRegistry;
import net.minecraft.network.NetworkPhase;
import net.minecraft.network.NetworkSide;
import net.minecraft.network.PacketByteBuf;
@@ -14,17 +14,17 @@
import java.util.Map;
import java.util.Objects;
-public class ChannelRegisterImpl implements ChannelRegister {
- public static final ChannelRegisterImpl CONFIG_C2S = new ChannelRegisterImpl<>(NetworkPhase.CONFIGURATION, NetworkSide.SERVERBOUND);
- public static final ChannelRegisterImpl CONFIG_S2C = new ChannelRegisterImpl<>(NetworkPhase.CONFIGURATION, NetworkSide.CLIENTBOUND);
- public static final ChannelRegisterImpl PLAY_C2S = new ChannelRegisterImpl<>(NetworkPhase.PLAY, NetworkSide.SERVERBOUND);
- public static final ChannelRegisterImpl PLAY_S2C = new ChannelRegisterImpl<>(NetworkPhase.PLAY, NetworkSide.CLIENTBOUND);
+public class PayloadTypeRegistryImpl implements PayloadTypeRegistry {
+ public static final PayloadTypeRegistryImpl CONFIG_C2S = new PayloadTypeRegistryImpl<>(NetworkPhase.CONFIGURATION, NetworkSide.SERVERBOUND);
+ public static final PayloadTypeRegistryImpl CONFIG_S2C = new PayloadTypeRegistryImpl<>(NetworkPhase.CONFIGURATION, NetworkSide.CLIENTBOUND);
+ public static final PayloadTypeRegistryImpl PLAY_C2S = new PayloadTypeRegistryImpl<>(NetworkPhase.PLAY, NetworkSide.SERVERBOUND);
+ public static final PayloadTypeRegistryImpl PLAY_S2C = new PayloadTypeRegistryImpl<>(NetworkPhase.PLAY, NetworkSide.CLIENTBOUND);
private final Map> packetTypes = new HashMap<>();
private final NetworkPhase state;
private final NetworkSide side;
- private ChannelRegisterImpl(NetworkPhase state, NetworkSide side) {
+ private PayloadTypeRegistryImpl(NetworkPhase state, NetworkSide side) {
this.state = state;
this.side = side;
}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/RegistrationPayload.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/RegistrationPayload.java
new file mode 100644
index 00000000..49316d43
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/RegistrationPayload.java
@@ -0,0 +1,80 @@
+package band.kessoku.lib.impl.networking;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import band.kessoku.lib.api.KessokuLib;
+import band.kessoku.lib.api.KessokuNetworking;
+import io.netty.util.AsciiString;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.InvalidIdentifierException;
+
+public record RegistrationPayload(Id id, List channels) implements CustomPayload {
+ public static final CustomPayload.Id REGISTER = new CustomPayload.Id<>(NetworkingImpl.REGISTER_CHANNEL);
+ public static final CustomPayload.Id UNREGISTER = new CustomPayload.Id<>(NetworkingImpl.UNREGISTER_CHANNEL);
+ public static final PacketCodec REGISTER_CODEC = codec(REGISTER);
+ public static final PacketCodec UNREGISTER_CODEC = codec(UNREGISTER);
+
+ private RegistrationPayload(Id id, PacketByteBuf buf) {
+ this(id, read(buf));
+ }
+
+ private void write(PacketByteBuf buf) {
+ boolean first = true;
+
+ for (Identifier channel : channels) {
+ if (first) {
+ first = false;
+ } else {
+ buf.writeByte(0);
+ }
+
+ buf.writeBytes(channel.toString().getBytes(StandardCharsets.US_ASCII));
+ }
+ }
+
+ private static List read(PacketByteBuf buf) {
+ List ids = new ArrayList<>();
+ StringBuilder active = new StringBuilder();
+
+ while (buf.isReadable()) {
+ byte b = buf.readByte();
+
+ if (b != 0) {
+ active.append(AsciiString.b2c(b));
+ } else {
+ addId(ids, active);
+ active = new StringBuilder();
+ }
+ }
+
+ addId(ids, active);
+
+ return Collections.unmodifiableList(ids);
+ }
+
+ private static void addId(List ids, StringBuilder sb) {
+ String literal = sb.toString();
+
+ try {
+ ids.add(Identifier.of(literal));
+ } catch (InvalidIdentifierException ex) {
+ KessokuLib.getLogger().warn(KessokuNetworking.MARKER, "Received invalid channel identifier \"{}\"", literal);
+ }
+ }
+
+ @Override
+ public Id extends CustomPayload> getId() {
+ return id;
+ }
+
+ private static PacketCodec codec(Id id) {
+ return CustomPayload.codecOf(RegistrationPayload::write, buf -> new RegistrationPayload(id, buf));
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientCommonNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientCommonNetworkAddon.java
new file mode 100644
index 00000000..39b4caeb
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientCommonNetworkAddon.java
@@ -0,0 +1,64 @@
+package band.kessoku.lib.impl.networking.client;
+
+import java.util.Collections;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientCommonNetworkHandler;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.impl.networking.AbstractChanneledNetworkAddon;
+import band.kessoku.lib.impl.networking.GlobalReceiverRegistry;
+import band.kessoku.lib.impl.networking.NetworkingImpl;
+import band.kessoku.lib.impl.networking.RegistrationPayload;
+
+abstract class ClientCommonNetworkAddon extends AbstractChanneledNetworkAddon {
+ protected final T handler;
+ protected final MinecraftClient client;
+
+ protected boolean isServerReady = false;
+
+ protected ClientCommonNetworkAddon(GlobalReceiverRegistry receiver, ClientConnection connection, String description, T handler, MinecraftClient client) {
+ super(receiver, connection, description);
+ this.handler = handler;
+ this.client = client;
+ }
+
+ public void onServerReady() {
+ this.isServerReady = true;
+ }
+
+ @Override
+ protected void handleRegistration(Identifier channelName) {
+ // If we can already send packets, immediately send the register packet for this channel
+ if (this.isServerReady) {
+ final RegistrationPayload payload = this.createRegistrationPayload(RegistrationPayload.REGISTER, Collections.singleton(channelName));
+
+ if (payload != null) {
+ this.sendPacket(payload);
+ }
+ }
+ }
+
+ @Override
+ protected void handleUnregistration(Identifier channelName) {
+ // If we can already send packets, immediately send the unregister packet for this channel
+ if (this.isServerReady) {
+ final RegistrationPayload payload = this.createRegistrationPayload(RegistrationPayload.UNREGISTER, Collections.singleton(channelName));
+
+ if (payload != null) {
+ this.sendPacket(payload);
+ }
+ }
+ }
+
+ @Override
+ protected boolean isReservedChannel(Identifier channelName) {
+ return NetworkingImpl.isReservedCommonChannel(channelName);
+ }
+
+ @Override
+ protected void schedule(Runnable task) {
+ client.execute(task);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientConfigurationNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientConfigurationNetworkAddon.java
new file mode 100644
index 00000000..eadbe2ff
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientConfigurationNetworkAddon.java
@@ -0,0 +1,121 @@
+package band.kessoku.lib.impl.networking.client;
+
+import java.util.List;
+import java.util.Objects;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientConfigurationNetworkHandler;
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.packet.BrandCustomPayload;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.client.C2SConfigurationChannelEvent;
+import band.kessoku.lib.api.networking.client.ClientConfigurationConnectionEvent;
+import band.kessoku.lib.api.networking.client.ClientConfigurationNetworking;
+import band.kessoku.lib.api.networking.client.ClientPlayNetworking;
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.impl.networking.ChannelInfoHolder;
+import band.kessoku.lib.impl.networking.RegistrationPayload;
+import band.kessoku.lib.mixin.networking.accessor.client.ClientCommonNetworkHandlerAccessor;
+import band.kessoku.lib.mixin.networking.accessor.client.ClientConfigurationNetworkHandlerAccessor;
+
+public final class ClientConfigurationNetworkAddon extends ClientCommonNetworkAddon, ClientConfigurationNetworkHandler> {
+ private final ContextImpl context;
+ private boolean sentInitialRegisterPacket;
+ private boolean hasStarted;
+
+ public ClientConfigurationNetworkAddon(ClientConfigurationNetworkHandler handler, MinecraftClient client) {
+ super(ClientNetworkingImpl.CONFIG, ((ClientCommonNetworkHandlerAccessor) handler).getConnection(), "ClientPlayNetworkAddon for " + ((ClientConfigurationNetworkHandlerAccessor) handler).getProfile().getName(), handler, client);
+ this.context = new ContextImpl(client, handler, this);
+
+ // Must register pending channels via lateinit
+ this.registerPendingChannels((ChannelInfoHolder) this.connection, NetworkPhase.CONFIGURATION);
+ }
+
+ @Override
+ protected void invokeInitEvent() {
+ ClientConfigurationConnectionEvent.INIT.invoker().onConfigurationInit(this.handler, this.client);
+ }
+
+ @Override
+ public void onServerReady() {
+ super.onServerReady();
+ invokeStartEvent();
+ }
+
+ @Override
+ protected void receiveRegistration(boolean register, RegistrationPayload payload) {
+ super.receiveRegistration(register, payload);
+
+ if (register && !this.sentInitialRegisterPacket) {
+ this.sendInitialChannelRegistrationPacket();
+ this.sentInitialRegisterPacket = true;
+
+ this.onServerReady();
+ }
+ }
+
+ @Override
+ public boolean handle(CustomPayload payload) {
+ boolean result = super.handle(payload);
+
+ if (payload instanceof BrandCustomPayload) {
+ // If we have received this without first receiving the registration packet, its likely a vanilla server.
+ invokeStartEvent();
+ }
+
+ return result;
+ }
+
+ private void invokeStartEvent() {
+ if (!hasStarted) {
+ hasStarted = true;
+ ClientConfigurationConnectionEvent.START.invoker().onConfigurationStart(this.handler, this.client);
+ }
+ }
+
+ @Override
+ protected void receive(ClientConfigurationNetworking.ConfigurationPayloadHandler> handler, CustomPayload payload) {
+ ((ClientConfigurationNetworking.ConfigurationPayloadHandler) handler).receive(payload, this.context);
+ }
+
+ // impl details
+ @Override
+ public Packet> createPacket(CustomPayload packet) {
+ return ClientPlayNetworking.createC2SPacket(packet);
+ }
+
+ @Override
+ protected void invokeRegisterEvent(List ids) {
+ C2SConfigurationChannelEvent.REGISTER.invoker().onChannelRegister(this.handler, this, this.client, ids);
+ }
+
+ @Override
+ protected void invokeUnregisterEvent(List ids) {
+ C2SConfigurationChannelEvent.UNREGISTER.invoker().onChannelUnregister(this.handler, this, this.client, ids);
+ }
+
+ public void handleComplete() {
+ ClientConfigurationConnectionEvent.COMPLETE.invoker().onConfigurationComplete(this.handler, this.client);
+ ClientNetworkingImpl.setClientConfigurationAddon(null);
+ }
+
+ @Override
+ protected void invokeDisconnectEvent() {
+ ClientConfigurationConnectionEvent.DISCONNECT.invoker().onConfigurationDisconnect(this.handler, this.client);
+ }
+
+ public ChannelInfoHolder getChannelInfoHolder() {
+ return (ChannelInfoHolder) ((ClientCommonNetworkHandlerAccessor) handler).getConnection();
+ }
+
+ private record ContextImpl(MinecraftClient client, ClientConfigurationNetworkHandler networkHandler, PacketSender responseSender) implements ClientConfigurationNetworking.Context {
+ private ContextImpl {
+ Objects.requireNonNull(client, "client");
+ Objects.requireNonNull(networkHandler, "networkHandler");
+ Objects.requireNonNull(responseSender, "responseSender");
+ }
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientLoginNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientLoginNetworkAddon.java
new file mode 100644
index 00000000..6d4762bb
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientLoginNetworkAddon.java
@@ -0,0 +1,99 @@
+package band.kessoku.lib.impl.networking.client;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientLoginNetworkHandler;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.packet.c2s.login.LoginQueryResponseC2SPacket;
+import net.minecraft.network.packet.s2c.login.LoginQueryRequestS2CPacket;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.client.ClientLoginConnectionEvent;
+import band.kessoku.lib.api.networking.client.ClientLoginNetworking;
+import band.kessoku.lib.api.networking.PacketByteBufHelper;
+import band.kessoku.lib.impl.networking.AbstractNetworkAddon;
+import band.kessoku.lib.impl.networking.payload.PacketByteBufLoginQueryRequestPayload;
+import band.kessoku.lib.impl.networking.payload.PacketByteBufLoginQueryResponsePayload;
+import band.kessoku.lib.mixin.networking.accessor.client.ClientLoginNetworkHandlerAccessor;
+
+public final class ClientLoginNetworkAddon extends AbstractNetworkAddon {
+ private final ClientLoginNetworkHandler handler;
+ private final MinecraftClient client;
+ private boolean firstResponse = true;
+
+ public ClientLoginNetworkAddon(ClientLoginNetworkHandler handler, MinecraftClient client) {
+ super(ClientNetworkingImpl.LOGIN, "ClientLoginNetworkAddon for Client");
+ this.handler = handler;
+ this.client = client;
+ }
+
+ @Override
+ protected void invokeInitEvent() {
+ ClientLoginConnectionEvent.INIT.invoker().onLoginStart(this.handler, this.client);
+ }
+
+ public boolean handlePacket(LoginQueryRequestS2CPacket packet) {
+ PacketByteBufLoginQueryRequestPayload payload = (PacketByteBufLoginQueryRequestPayload) packet.payload();
+ return handlePacket(packet.queryId(), packet.payload().id(), payload.data());
+ }
+
+ private boolean handlePacket(int queryId, Identifier channelName, PacketByteBuf originalBuf) {
+ this.logger.debug("Handling inbound login response with id {} and channel with name {}", queryId, channelName);
+
+ if (this.firstResponse) {
+ ClientLoginConnectionEvent.QUERY_START.invoker().onLoginQueryStart(this.handler, this.client);
+ this.firstResponse = false;
+ }
+
+ @Nullable ClientLoginNetworking.LoginQueryRequestHandler handler = this.getHandler(channelName);
+
+ if (handler == null) {
+ return false;
+ }
+
+ PacketByteBuf buf = PacketByteBufHelper.slice(originalBuf);
+ List callbacks = new ArrayList<>();
+
+ try {
+ CompletableFuture<@Nullable PacketByteBuf> future = handler.receive(this.client, this.handler, buf, callbacks::add);
+ future.thenAccept(result -> {
+ LoginQueryResponseC2SPacket packet = new LoginQueryResponseC2SPacket(queryId, result == null ? null : new PacketByteBufLoginQueryResponsePayload(result));
+ ((ClientLoginNetworkHandlerAccessor) this.handler).getConnection().send(packet, new PacketCallbacks() {
+ @Override
+ public void onSuccess() {
+ callbacks.forEach(PacketCallbacks::onSuccess);
+ }
+ });
+ });
+ } catch (Throwable ex) {
+ this.logger.error("Encountered exception while handling in channel with name \"{}\"", channelName, ex);
+ throw ex;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void handleRegistration(Identifier channelName) {
+ }
+
+ @Override
+ protected void handleUnregistration(Identifier channelName) {
+ }
+
+ @Override
+ protected void invokeDisconnectEvent() {
+ ClientLoginConnectionEvent.DISCONNECT.invoker().onLoginDisconnect(this.handler, this.client);
+ }
+
+ @Override
+ protected boolean isReservedChannel(Identifier channelName) {
+ return false;
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientNetworkingImpl.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientNetworkingImpl.java
new file mode 100644
index 00000000..320cc781
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientNetworkingImpl.java
@@ -0,0 +1,166 @@
+package band.kessoku.lib.impl.networking.client;
+
+import java.util.Objects;
+
+import band.kessoku.lib.api.KessokuLib;
+import band.kessoku.lib.api.KessokuNetworking;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.multiplayer.ConnectScreen;
+import net.minecraft.client.network.ClientConfigurationNetworkHandler;
+import net.minecraft.client.network.ClientLoginNetworkHandler;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.NetworkSide;
+import net.minecraft.network.listener.ServerCommonPacketListener;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket;
+
+import band.kessoku.lib.api.networking.client.ClientConfigurationConnectionEvent;
+import band.kessoku.lib.api.networking.client.ClientConfigurationNetworking;
+import band.kessoku.lib.api.networking.client.ClientLoginNetworking;
+import band.kessoku.lib.api.networking.client.ClientPlayConnectionEvent;
+import band.kessoku.lib.api.networking.client.ClientPlayNetworking;
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.impl.networking.common.CommonPacketsImpl;
+import band.kessoku.lib.impl.networking.common.CommonRegisterPayload;
+import band.kessoku.lib.impl.networking.common.CommonVersionPayload;
+import band.kessoku.lib.impl.networking.GlobalReceiverRegistry;
+import band.kessoku.lib.impl.networking.NetworkHandlerExtension;
+import band.kessoku.lib.impl.networking.PayloadTypeRegistryImpl;
+import band.kessoku.lib.mixin.networking.accessor.client.ConnectScreenAccessor;
+import band.kessoku.lib.mixin.networking.accessor.client.MinecraftClientAccessor;
+
+public final class ClientNetworkingImpl {
+ public static final GlobalReceiverRegistry LOGIN = new GlobalReceiverRegistry<>(NetworkSide.CLIENTBOUND, NetworkPhase.LOGIN, null);
+ public static final GlobalReceiverRegistry> CONFIG = new GlobalReceiverRegistry<>(NetworkSide.CLIENTBOUND, NetworkPhase.CONFIGURATION, PayloadTypeRegistryImpl.CONFIG_S2C);
+ public static final GlobalReceiverRegistry> PLAY = new GlobalReceiverRegistry<>(NetworkSide.CLIENTBOUND, NetworkPhase.PLAY, PayloadTypeRegistryImpl.PLAY_S2C);
+
+ private static ClientPlayNetworkAddon currentPlayAddon;
+ private static ClientConfigurationNetworkAddon currentConfigurationAddon;
+
+ public static ClientPlayNetworkAddon getAddon(ClientPlayNetworkHandler handler) {
+ return (ClientPlayNetworkAddon) ((NetworkHandlerExtension) handler).kessokulib$getNetworkAddon();
+ }
+
+ public static ClientConfigurationNetworkAddon getAddon(ClientConfigurationNetworkHandler handler) {
+ return (ClientConfigurationNetworkAddon) ((NetworkHandlerExtension) handler).kessokulib$getNetworkAddon();
+ }
+
+ public static ClientLoginNetworkAddon getAddon(ClientLoginNetworkHandler handler) {
+ return (ClientLoginNetworkAddon) ((NetworkHandlerExtension) handler).kessokulib$getNetworkAddon();
+ }
+
+ public static Packet createC2SPacket(CustomPayload payload) {
+ Objects.requireNonNull(payload, "Payload cannot be null");
+ Objects.requireNonNull(payload.getId(), "CustomPayload#getId() cannot return null for payload class: " + payload.getClass());
+
+ return new CustomPayloadC2SPacket(payload);
+ }
+
+ /**
+ * Due to the way logging into an integrated or remote dedicated server will differ, we need to obtain the login client connection differently.
+ */
+ @Nullable
+ public static ClientConnection getLoginConnection() {
+ final ClientConnection connection = ((MinecraftClientAccessor) MinecraftClient.getInstance()).getConnection();
+
+ // Check if we are connecting to an integrated server. This will set the field on MinecraftClient
+ if (connection != null) {
+ return connection;
+ } else {
+ // We are probably connecting to a remote server.
+ // Check if the ConnectScreen is the currentScreen to determine that:
+ if (MinecraftClient.getInstance().currentScreen instanceof ConnectScreen) {
+ return ((ConnectScreenAccessor) MinecraftClient.getInstance().currentScreen).getConnection();
+ }
+ }
+
+ // We are not connected to a server at all.
+ return null;
+ }
+
+ @Nullable
+ public static ClientConfigurationNetworkAddon getClientConfigurationAddon() {
+ return currentConfigurationAddon;
+ }
+
+ @Nullable
+ public static ClientPlayNetworkAddon getClientPlayAddon() {
+ // Since Minecraft can be a bit weird, we need to check for the play addon in a few ways:
+ // If the client's player is set this will work
+ if (MinecraftClient.getInstance().getNetworkHandler() != null) {
+ currentPlayAddon = null; // Shouldn't need this anymore
+ return getAddon(MinecraftClient.getInstance().getNetworkHandler());
+ }
+
+ // We haven't hit the end of onGameJoin yet, use our backing field here to access the network handler
+ if (currentPlayAddon != null) {
+ return currentPlayAddon;
+ }
+
+ // We are not in play stage
+ return null;
+ }
+
+ public static void setClientPlayAddon(ClientPlayNetworkAddon addon) {
+ assert addon == null || currentConfigurationAddon == null;
+ currentPlayAddon = addon;
+ }
+
+ public static void setClientConfigurationAddon(ClientConfigurationNetworkAddon addon) {
+ currentConfigurationAddon = addon;
+ }
+
+ public static void clientInit() {
+ // Reference cleanup for the locally stored addon if we are disconnected
+ ClientPlayConnectionEvent.DISCONNECT.register((handler, client) -> {
+ currentPlayAddon = null;
+ });
+
+ ClientConfigurationConnectionEvent.DISCONNECT.register((handler, client) -> {
+ currentConfigurationAddon = null;
+ });
+
+ // Version packet
+ ClientConfigurationNetworking.registerGlobalReceiver(CommonVersionPayload.ID, (payload, context) -> {
+ int negotiatedVersion = handleVersionPacket(payload, context.responseSender());
+ ClientNetworkingImpl.getClientConfigurationAddon().kessokulib$onCommonVersionPacket(negotiatedVersion);
+ });
+
+ // Register packet
+ ClientConfigurationNetworking.registerGlobalReceiver(CommonRegisterPayload.ID, (payload, context) -> {
+ ClientConfigurationNetworkAddon addon = ClientNetworkingImpl.getClientConfigurationAddon();
+
+ if (CommonRegisterPayload.PLAY_PHASE.equals(payload.phase())) {
+ if (payload.version() != addon.kessokulib$getNegotiatedVersion()) {
+ throw new IllegalStateException("Negotiated common packet version: %d but received packet with version: %d".formatted(addon.kessokulib$getNegotiatedVersion(), payload.version()));
+ }
+
+ addon.getChannelInfoHolder().kessokulib$getPendingChannelsNames(NetworkPhase.PLAY).addAll(payload.channels());
+ KessokuLib.getLogger().debug(KessokuNetworking.MARKER, "Received accepted channels from the server");
+ context.responseSender().sendPacket(new CommonRegisterPayload(addon.kessokulib$getNegotiatedVersion(), CommonRegisterPayload.PLAY_PHASE, ClientPlayNetworking.getGlobalReceivers()));
+ } else {
+ addon.kessokulib$onCommonRegisterPacket(payload);
+ context.responseSender().sendPacket(addon.kessokulib$createRegisterPayload());
+ }
+ });
+ }
+
+ // Disconnect if there are no commonly supported versions.
+ // Client responds with the intersection of supported versions.
+ // Return the highest supported version
+ private static int handleVersionPacket(CommonVersionPayload payload, PacketSender packetSender) {
+ int version = CommonPacketsImpl.getHighestCommonVersion(payload.versions(), CommonPacketsImpl.SUPPORTED_COMMON_PACKET_VERSIONS);
+
+ if (version <= 0) {
+ throw new UnsupportedOperationException("Client does not support any requested versions from server");
+ }
+
+ packetSender.sendPacket(new CommonVersionPayload(new int[]{ version }));
+ return version;
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientPlayNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientPlayNetworkAddon.java
new file mode 100644
index 00000000..ee56591d
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/client/ClientPlayNetworkAddon.java
@@ -0,0 +1,93 @@
+package band.kessoku.lib.impl.networking.client;
+
+import java.util.List;
+import java.util.Objects;
+
+import com.mojang.logging.LogUtils;
+import org.slf4j.Logger;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.client.C2SPlayChannelEvent;
+import band.kessoku.lib.api.networking.client.ClientPlayConnectionEvent;
+import band.kessoku.lib.api.networking.client.ClientPlayNetworking;
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.impl.networking.ChannelInfoHolder;
+
+public final class ClientPlayNetworkAddon extends ClientCommonNetworkAddon, ClientPlayNetworkHandler> {
+ private final ContextImpl context;
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public ClientPlayNetworkAddon(ClientPlayNetworkHandler handler, MinecraftClient client) {
+ super(ClientNetworkingImpl.PLAY, handler.getConnection(), "ClientPlayNetworkAddon for " + handler.getProfile().getName(), handler, client);
+ this.context = new ContextImpl(client, this);
+
+ // Must register pending channels via lateinit
+ this.registerPendingChannels((ChannelInfoHolder) this.connection, NetworkPhase.PLAY);
+ }
+
+ @Override
+ protected void invokeInitEvent() {
+ ClientPlayConnectionEvent.INIT.invoker().onPlayInit(this.handler, this.client);
+ }
+
+ @Override
+ public void onServerReady() {
+ try {
+ ClientPlayConnectionEvent.JOIN.invoker().onPlayReady(this.handler, this, this.client);
+ } catch (RuntimeException e) {
+ LOGGER.error("Exception thrown while invoking ClientPlayConnectionEvents.JOIN", e);
+ }
+
+ // The client cannot send any packets, including `minecraft:register` until after GameJoinS2CPacket is received.
+ this.sendInitialChannelRegistrationPacket();
+ super.onServerReady();
+ }
+
+ @Override
+ protected void receive(ClientPlayNetworking.PlayPayloadHandler> handler, CustomPayload payload) {
+ this.client.execute(() -> {
+ ((ClientPlayNetworking.PlayPayloadHandler) handler).receive(payload, context);
+ });
+ }
+
+ // impl details
+ @Override
+ public Packet> createPacket(CustomPayload packet) {
+ return ClientPlayNetworking.createC2SPacket(packet);
+ }
+
+ @Override
+ protected void invokeRegisterEvent(List ids) {
+ C2SPlayChannelEvent.REGISTER.invoker().onChannelRegister(this.handler, this, this.client, ids);
+ }
+
+ @Override
+ protected void invokeUnregisterEvent(List ids) {
+ C2SPlayChannelEvent.UNREGISTER.invoker().onChannelUnregister(this.handler, this, this.client, ids);
+ }
+
+ @Override
+ protected void invokeDisconnectEvent() {
+ ClientPlayConnectionEvent.DISCONNECT.invoker().onPlayDisconnect(this.handler, this.client);
+ }
+
+ private record ContextImpl(MinecraftClient client, PacketSender responseSender) implements ClientPlayNetworking.Context {
+ private ContextImpl {
+ Objects.requireNonNull(client, "client");
+ Objects.requireNonNull(responseSender, "responseSender");
+ }
+
+ @Override
+ public ClientPlayerEntity player() {
+ return Objects.requireNonNull(client.player, "player");
+ }
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonPacketHandler.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonPacketHandler.java
new file mode 100644
index 00000000..4f7e3840
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonPacketHandler.java
@@ -0,0 +1,11 @@
+package band.kessoku.lib.impl.networking.common;
+
+public interface CommonPacketHandler {
+ void kessokulib$onCommonVersionPacket(int negotiatedVersion);
+
+ void kessokulib$onCommonRegisterPacket(CommonRegisterPayload payload);
+
+ CommonRegisterPayload kessokulib$createRegisterPayload();
+
+ int kessokulib$getNegotiatedVersion();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonPacketsImpl.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonPacketsImpl.java
new file mode 100644
index 00000000..f7a66af3
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonPacketsImpl.java
@@ -0,0 +1,79 @@
+package band.kessoku.lib.impl.networking.common;
+
+import band.kessoku.lib.api.networking.server.ServerPlayNetworking;
+import band.kessoku.lib.impl.networking.server.ServerConfigurationNetworkAddon;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.server.network.ServerPlayerConfigurationTask;
+
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+public class CommonPacketsImpl {
+ public static final int PACKET_VERSION_1 = 1;
+ public static final int[] SUPPORTED_COMMON_PACKET_VERSIONS = new int[]{ PACKET_VERSION_1 };
+
+ // A configuration phase task to send and receive the version packets.
+ public record CommonVersionConfigurationTask(ServerConfigurationNetworkAddon addon) implements ServerPlayerConfigurationTask {
+ public static final Key KEY = new Key(CommonVersionPayload.ID.id().toString());
+
+ @Override
+ public void sendPacket(Consumer> sender) {
+ addon.sendPacket(new CommonVersionPayload(SUPPORTED_COMMON_PACKET_VERSIONS));
+ }
+
+ @Override
+ public Key getKey() {
+ return KEY;
+ }
+ }
+
+ // A configuration phase task to send and receive the registration packets.
+ public record CommonRegisterConfigurationTask(ServerConfigurationNetworkAddon addon) implements ServerPlayerConfigurationTask {
+ public static final Key KEY = new Key(CommonRegisterPayload.ID.id().toString());
+
+ @Override
+ public void sendPacket(Consumer> sender) {
+ addon.sendPacket(new CommonRegisterPayload(addon.kessokulib$getNegotiatedVersion(), CommonRegisterPayload.PLAY_PHASE, ServerPlayNetworking.getGlobalReceivers()));
+ }
+
+ @Override
+ public Key getKey() {
+ return KEY;
+ }
+ }
+
+ protected static int getNegotiatedVersion(CommonVersionPayload payload) {
+ int version = getHighestCommonVersion(payload.versions(), SUPPORTED_COMMON_PACKET_VERSIONS);
+
+ if (version <= 0) {
+ throw new UnsupportedOperationException("server does not support any requested versions from client");
+ }
+
+ return version;
+ }
+
+ public static int getHighestCommonVersion(int[] a, int[] b) {
+ int[] as = a.clone();
+ int[] bs = b.clone();
+
+ Arrays.sort(as);
+ Arrays.sort(bs);
+
+ int ap = as.length - 1;
+ int bp = bs.length - 1;
+
+ while (ap >= 0 && bp >= 0) {
+ if (as[ap] == bs[bp]) {
+ return as[ap];
+ }
+
+ if (as[ap] > bs[bp]) {
+ ap--;
+ } else {
+ bp--;
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonRegisterPayload.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonRegisterPayload.java
new file mode 100644
index 00000000..5233650b
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonRegisterPayload.java
@@ -0,0 +1,36 @@
+package band.kessoku.lib.impl.networking.common;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.util.Identifier;
+
+public record CommonRegisterPayload(int version, String phase, Set channels) implements CustomPayload {
+ public static final CustomPayload.Id ID = new Id<>(Identifier.of("c:register"));
+ public static final PacketCodec CODEC = CustomPayload.codecOf(CommonRegisterPayload::write, CommonRegisterPayload::new);
+
+ public static final String PLAY_PHASE = "play";
+ public static final String CONFIGURATION_PHASE = "configuration";
+
+ private CommonRegisterPayload(PacketByteBuf buf) {
+ this(
+ buf.readVarInt(),
+ buf.readString(),
+ buf.readCollection(HashSet::new, PacketByteBuf::readIdentifier)
+ );
+ }
+
+ public void write(PacketByteBuf buf) {
+ buf.writeVarInt(version);
+ buf.writeString(phase);
+ buf.writeCollection(channels, PacketByteBuf::writeIdentifier);
+ }
+
+ @Override
+ public Id getId() {
+ return ID;
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonVersionPayload.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonVersionPayload.java
new file mode 100644
index 00000000..b14d3271
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/common/CommonVersionPayload.java
@@ -0,0 +1,24 @@
+package band.kessoku.lib.impl.networking.common;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.util.Identifier;
+
+public record CommonVersionPayload(int[] versions) implements CustomPayload {
+ public static final PacketCodec CODEC = CustomPayload.codecOf(CommonVersionPayload::write, CommonVersionPayload::new);
+ public static final CustomPayload.Id ID = new Id<>(Identifier.of("c:version"));
+
+ private CommonVersionPayload(PacketByteBuf buf) {
+ this(buf.readIntArray());
+ }
+
+ public void write(PacketByteBuf buf) {
+ buf.writeIntArray(versions);
+ }
+
+ @Override
+ public Id extends CustomPayload> getId() {
+ return ID;
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PacketByteBufLoginQueryRequestPayload.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PacketByteBufLoginQueryRequestPayload.java
new file mode 100644
index 00000000..0b99f8b1
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PacketByteBufLoginQueryRequestPayload.java
@@ -0,0 +1,12 @@
+package band.kessoku.lib.impl.networking.payload;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.packet.s2c.login.LoginQueryRequestPayload;
+import net.minecraft.util.Identifier;
+
+public record PacketByteBufLoginQueryRequestPayload(Identifier id, PacketByteBuf data) implements LoginQueryRequestPayload {
+ @Override
+ public void write(PacketByteBuf buf) {
+ PayloadHelper.write(buf, data());
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PacketByteBufLoginQueryResponsePayload.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PacketByteBufLoginQueryResponsePayload.java
new file mode 100644
index 00000000..bdf4eddb
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PacketByteBufLoginQueryResponsePayload.java
@@ -0,0 +1,11 @@
+package band.kessoku.lib.impl.networking.payload;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.packet.c2s.login.LoginQueryResponsePayload;
+
+public record PacketByteBufLoginQueryResponsePayload(PacketByteBuf data) implements LoginQueryResponsePayload {
+ @Override
+ public void write(PacketByteBuf buf) {
+ PayloadHelper.write(buf, data());
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PayloadHelper.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PayloadHelper.java
new file mode 100644
index 00000000..eb443b12
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/payload/PayloadHelper.java
@@ -0,0 +1,28 @@
+package band.kessoku.lib.impl.networking.payload;
+
+import net.minecraft.network.PacketByteBuf;
+
+import band.kessoku.lib.api.networking.PacketByteBufHelper;
+
+public class PayloadHelper {
+ public static void write(PacketByteBuf byteBuf, PacketByteBuf data) {
+ byteBuf.writeBytes(data.copy());
+ }
+
+ public static PacketByteBuf read(PacketByteBuf byteBuf, int maxSize) {
+ assertSize(byteBuf, maxSize);
+
+ PacketByteBuf newBuf = PacketByteBufHelper.create();
+ newBuf.writeBytes(byteBuf.copy());
+ byteBuf.skipBytes(byteBuf.readableBytes());
+ return newBuf;
+ }
+
+ private static void assertSize(PacketByteBuf buf, int maxSize) {
+ int size = buf.readableBytes();
+
+ if (size < 0 || size > maxSize) {
+ throw new IllegalArgumentException("Payload may not be larger than " + maxSize + " bytes");
+ }
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/QueryIdFactory.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/QueryIdFactory.java
new file mode 100644
index 00000000..df9d686d
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/QueryIdFactory.java
@@ -0,0 +1,22 @@
+package band.kessoku.lib.impl.networking.server;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Tracks the current query id used for login query responses.
+ */
+interface QueryIdFactory {
+ static QueryIdFactory create() {
+ return new QueryIdFactory() {
+ private final AtomicInteger currentId = new AtomicInteger();
+
+ @Override
+ public int nextId() {
+ return this.currentId.getAndIncrement();
+ }
+ };
+ }
+
+ // called async prob.
+ int nextId();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerConfigurationNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerConfigurationNetworkAddon.java
new file mode 100644
index 00000000..3b71f016
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerConfigurationNetworkAddon.java
@@ -0,0 +1,174 @@
+package band.kessoku.lib.impl.networking.server;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.network.packet.s2c.common.CommonPingS2CPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerConfigurationNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.api.networking.server.S2CConfigurationChannelEvent;
+import band.kessoku.lib.api.networking.server.ServerConfigurationConnectionEvent;
+import band.kessoku.lib.api.networking.server.ServerConfigurationNetworking;
+import band.kessoku.lib.impl.networking.AbstractChanneledNetworkAddon;
+import band.kessoku.lib.impl.networking.ChannelInfoHolder;
+import band.kessoku.lib.impl.networking.NetworkingImpl;
+import band.kessoku.lib.impl.networking.RegistrationPayload;
+import band.kessoku.lib.mixin.networking.accessor.server.ServerCommonNetworkHandlerAccessor;
+
+public final class ServerConfigurationNetworkAddon extends AbstractChanneledNetworkAddon> {
+ private final ServerConfigurationNetworkHandler handler;
+ private final MinecraftServer server;
+ private final ServerConfigurationNetworking.Context context;
+ private RegisterState registerState = RegisterState.NOT_SENT;
+
+ public ServerConfigurationNetworkAddon(ServerConfigurationNetworkHandler handler, MinecraftServer server) {
+ super(ServerNetworkingImpl.CONFIG, ((ServerCommonNetworkHandlerAccessor) handler).getConnection(), "ServerConfigurationNetworkAddon for " + handler.getDebugProfile().getName());
+ this.handler = handler;
+ this.server = server;
+ this.context = new ContextImpl(server, handler, this);
+
+ // Must register pending channels via lateinit
+ this.registerPendingChannels((ChannelInfoHolder) this.connection, NetworkPhase.CONFIGURATION);
+ }
+
+ @Override
+ protected void invokeInitEvent() {
+ }
+
+ public void preConfig() {
+ ServerConfigurationConnectionEvent.BEFORE_CONFIGURE.invoker().onSendConfiguration(handler, server);
+ }
+
+ public void config() {
+ ServerConfigurationConnectionEvent.CONFIGURE.invoker().onSendConfiguration(handler, server);
+ }
+
+ public boolean startConfiguration() {
+ if (this.registerState == RegisterState.NOT_SENT) {
+ // Send the registration packet, followed by a ping
+ this.sendInitialChannelRegistrationPacket();
+ this.sendPacket(new CommonPingS2CPacket(0xFAB71C));
+
+ this.registerState = RegisterState.SENT;
+
+ // Cancel the configuration for now, the response from the ping or registration packet will continue.
+ return true;
+ }
+
+ // We should have received a response
+ assert registerState == RegisterState.RECEIVED || registerState == RegisterState.NOT_RECEIVED;
+ return false;
+ }
+
+ @Override
+ protected void receiveRegistration(boolean register, RegistrationPayload resolvable) {
+ super.receiveRegistration(register, resolvable);
+
+ if (register && registerState == RegisterState.SENT) {
+ // We received the registration packet, thus we know this is a modded client, continue with configuration.
+ registerState = RegisterState.RECEIVED;
+ handler.sendConfigurations();
+ }
+ }
+
+ public void onPong(int parameter) {
+ if (registerState == RegisterState.SENT) {
+ // We did not receive the registration packet, thus we think this is a vanilla client, continue with configuration.
+ registerState = RegisterState.NOT_RECEIVED;
+ handler.sendConfigurations();
+ }
+ }
+
+ @Override
+ protected void receive(ServerConfigurationNetworking.ConfigurationPacketHandler> handler, CustomPayload payload) {
+ ((ServerConfigurationNetworking.ConfigurationPacketHandler) handler).receive(payload, this.context);
+ }
+
+ // impl details
+
+ @Override
+ protected void schedule(Runnable task) {
+ this.server.execute(task);
+ }
+
+ @Override
+ public Packet> createPacket(CustomPayload packet) {
+ return ServerConfigurationNetworking.createS2CPacket(packet);
+ }
+
+ @Override
+ protected void invokeRegisterEvent(List ids) {
+ S2CConfigurationChannelEvent.REGISTER.invoker().onChannelRegister(this.handler, this, this.server, ids);
+ }
+
+ @Override
+ protected void invokeUnregisterEvent(List ids) {
+ S2CConfigurationChannelEvent.UNREGISTER.invoker().onChannelUnregister(this.handler, this, this.server, ids);
+ }
+
+ @Override
+ protected void handleRegistration(Identifier channelName) {
+ // If we can already send packets, immediately send the register packet for this channel
+ if (this.registerState != RegisterState.NOT_SENT) {
+ RegistrationPayload registrationPayload = this.createRegistrationPayload(RegistrationPayload.REGISTER, Collections.singleton(channelName));
+
+ if (registrationPayload != null) {
+ this.sendPacket(registrationPayload);
+ }
+ }
+ }
+
+ @Override
+ protected void handleUnregistration(Identifier channelName) {
+ // If we can already send packets, immediately send the unregister packet for this channel
+ if (this.registerState != RegisterState.NOT_SENT) {
+ RegistrationPayload registrationPayload = this.createRegistrationPayload(RegistrationPayload.UNREGISTER, Collections.singleton(channelName));
+
+ if (registrationPayload != null) {
+ this.sendPacket(registrationPayload);
+ }
+ }
+ }
+
+ @Override
+ protected void invokeDisconnectEvent() {
+ ServerConfigurationConnectionEvent.DISCONNECT.invoker().onConfigureDisconnect(handler, server);
+ }
+
+ @Override
+ protected boolean isReservedChannel(Identifier channelName) {
+ return NetworkingImpl.isReservedCommonChannel(channelName);
+ }
+
+ @Override
+ public void sendPacket(Packet> packet, PacketCallbacks callback) {
+ handler.send(packet, callback);
+ }
+
+ private enum RegisterState {
+ NOT_SENT,
+ SENT,
+ RECEIVED,
+ NOT_RECEIVED
+ }
+
+ public ChannelInfoHolder getChannelInfoHolder() {
+ return (ChannelInfoHolder) ((ServerCommonNetworkHandlerAccessor) handler).getConnection();
+ }
+
+ private record ContextImpl(MinecraftServer server, ServerConfigurationNetworkHandler networkHandler, PacketSender responseSender) implements ServerConfigurationNetworking.Context {
+ private ContextImpl {
+ Objects.requireNonNull(server, "server");
+ Objects.requireNonNull(networkHandler, "networkHandler");
+ Objects.requireNonNull(responseSender, "responseSender");
+ }
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerLoginNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerLoginNetworkAddon.java
new file mode 100644
index 00000000..10db03ad
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerLoginNetworkAddon.java
@@ -0,0 +1,191 @@
+package band.kessoku.lib.impl.networking.server;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.network.packet.c2s.login.LoginQueryResponseC2SPacket;
+import net.minecraft.network.packet.s2c.login.LoginCompressionS2CPacket;
+import net.minecraft.network.packet.s2c.login.LoginQueryRequestS2CPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerLoginNetworkHandler;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.LoginPacketSender;
+import band.kessoku.lib.api.networking.PacketByteBufHelper;
+import band.kessoku.lib.api.networking.server.ServerLoginConnectionEvent;
+import band.kessoku.lib.api.networking.server.ServerLoginNetworking;
+import band.kessoku.lib.impl.networking.AbstractNetworkAddon;
+import band.kessoku.lib.impl.networking.payload.PacketByteBufLoginQueryRequestPayload;
+import band.kessoku.lib.impl.networking.payload.PacketByteBufLoginQueryResponsePayload;
+import band.kessoku.lib.mixin.networking.accessor.server.ServerLoginNetworkHandlerAccessor;
+
+public final class ServerLoginNetworkAddon extends AbstractNetworkAddon implements LoginPacketSender {
+ private final ClientConnection connection;
+ private final ServerLoginNetworkHandler handler;
+ private final MinecraftServer server;
+ private final QueryIdFactory queryIdFactory;
+ private final Collection> waits = new ConcurrentLinkedQueue<>();
+ private final Map channels = new ConcurrentHashMap<>();
+ private boolean firstQueryTick = true;
+
+ public ServerLoginNetworkAddon(ServerLoginNetworkHandler handler) {
+ super(ServerNetworkingImpl.LOGIN, "ServerLoginNetworkAddon for " + handler.getConnectionInfo());
+ this.connection = ((ServerLoginNetworkHandlerAccessor) handler).getConnection();
+ this.handler = handler;
+ this.server = ((ServerLoginNetworkHandlerAccessor) handler).getServer();
+ this.queryIdFactory = QueryIdFactory.create();
+ }
+
+ @Override
+ protected void invokeInitEvent() {
+ ServerLoginConnectionEvent.INIT.invoker().onLoginInit(handler, this.server);
+ }
+
+ // return true if no longer ticks query
+ public boolean queryTick() {
+ if (this.firstQueryTick) {
+ // Send the compression packet now so clients receive compressed login queries
+ this.sendCompressionPacket();
+
+ ServerLoginConnectionEvent.QUERY_START.invoker().onLoginStart(this.handler, this.server, this, this.waits::add);
+ this.firstQueryTick = false;
+ }
+
+ AtomicReference error = new AtomicReference<>();
+ this.waits.removeIf(future -> {
+ if (!future.isDone()) {
+ return false;
+ }
+
+ try {
+ future.get();
+ } catch (ExecutionException ex) {
+ Throwable caught = ex.getCause();
+ error.getAndUpdate(oldEx -> {
+ if (oldEx == null) {
+ return caught;
+ }
+
+ oldEx.addSuppressed(caught);
+ return oldEx;
+ });
+ } catch (InterruptedException | CancellationException ignored) {
+ // ignore
+ }
+
+ return true;
+ });
+
+ return this.channels.isEmpty() && this.waits.isEmpty();
+ }
+
+ private void sendCompressionPacket() {
+ // Compression is not needed for local transport
+ if (this.server.getNetworkCompressionThreshold() >= 0 && !this.connection.isLocal()) {
+ this.connection.send(new LoginCompressionS2CPacket(this.server.getNetworkCompressionThreshold()),
+ PacketCallbacks.always(() -> connection.setCompressionThreshold(server.getNetworkCompressionThreshold(), true))
+ );
+ }
+ }
+
+ /**
+ * Handles an incoming query response during login.
+ *
+ * @param packet the packet to handle
+ * @return true if the packet was handled
+ */
+ public boolean handle(LoginQueryResponseC2SPacket packet) {
+ PacketByteBufLoginQueryResponsePayload response = (PacketByteBufLoginQueryResponsePayload) packet.response();
+ return handle(packet.queryId(), response == null ? null : response.data());
+ }
+
+ private boolean handle(int queryId, @Nullable PacketByteBuf originalBuf) {
+ this.logger.debug("Handling inbound login query with id {}", queryId);
+ Identifier channel = this.channels.remove(queryId);
+
+ if (channel == null) {
+ this.logger.warn("Query ID {} was received but no query has been associated in {}!", queryId, this.connection);
+ return false;
+ }
+
+ boolean understood = originalBuf != null;
+ @Nullable ServerLoginNetworking.LoginQueryResponseHandler handler = this.getHandler(channel);
+
+ if (handler == null) {
+ return false;
+ }
+
+ PacketByteBuf buf = understood ? PacketByteBufHelper.slice(originalBuf) : PacketByteBufHelper.empty();
+
+ try {
+ handler.receive(this.server, this.handler, understood, buf, this.waits::add, this);
+ } catch (Throwable ex) {
+ this.logger.error("Encountered exception while handling in channel \"{}\"", channel, ex);
+ throw ex;
+ }
+
+ return true;
+ }
+
+ @Override
+ public Packet> createPacket(CustomPayload packet) {
+ throw new UnsupportedOperationException("Cannot send CustomPayload during login");
+ }
+
+ @Override
+ public Packet> createPacket(Identifier channelName, PacketByteBuf buf) {
+ int queryId = this.queryIdFactory.nextId();
+ return new LoginQueryRequestS2CPacket(queryId, new PacketByteBufLoginQueryRequestPayload(channelName, buf));
+ }
+
+ @Override
+ public void sendPacket(Packet> packet, PacketCallbacks callback) {
+ Objects.requireNonNull(packet, "Packet cannot be null");
+
+ this.connection.send(packet, callback);
+ }
+
+ @Override
+ public void disconnect(Text disconnectReason) {
+ Objects.requireNonNull(disconnectReason, "Disconnect reason cannot be null");
+
+ this.connection.disconnect(disconnectReason);
+ }
+
+ public void registerOutgoingPacket(LoginQueryRequestS2CPacket packet) {
+ this.channels.put(packet.queryId(), packet.payload().id());
+ }
+
+ @Override
+ protected void handleRegistration(Identifier channelName) {
+ }
+
+ @Override
+ protected void handleUnregistration(Identifier channelName) {
+ }
+
+ @Override
+ protected void invokeDisconnectEvent() {
+ ServerLoginConnectionEvent.DISCONNECT.invoker().onLoginDisconnect(this.handler, this.server);
+ }
+
+ @Override
+ protected boolean isReservedChannel(Identifier channelName) {
+ return false;
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerNetworkingImpl.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerNetworkingImpl.java
new file mode 100644
index 00000000..e2ed064a
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerNetworkingImpl.java
@@ -0,0 +1,45 @@
+package band.kessoku.lib.impl.networking.server;
+
+import java.util.Objects;
+
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.NetworkSide;
+import net.minecraft.network.listener.ClientCommonPacketListener;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.network.packet.s2c.common.CustomPayloadS2CPacket;
+import net.minecraft.server.network.ServerConfigurationNetworkHandler;
+import net.minecraft.server.network.ServerLoginNetworkHandler;
+import net.minecraft.server.network.ServerPlayNetworkHandler;
+
+import band.kessoku.lib.api.networking.server.ServerConfigurationNetworking;
+import band.kessoku.lib.api.networking.server.ServerLoginNetworking;
+import band.kessoku.lib.api.networking.server.ServerPlayNetworking;
+import band.kessoku.lib.impl.networking.GlobalReceiverRegistry;
+import band.kessoku.lib.impl.networking.NetworkHandlerExtension;
+import band.kessoku.lib.impl.networking.PayloadTypeRegistryImpl;
+
+public final class ServerNetworkingImpl {
+ public static final GlobalReceiverRegistry LOGIN = new GlobalReceiverRegistry<>(NetworkSide.SERVERBOUND, NetworkPhase.LOGIN, null);
+ public static final GlobalReceiverRegistry> CONFIG = new GlobalReceiverRegistry<>(NetworkSide.SERVERBOUND, NetworkPhase.CONFIGURATION, PayloadTypeRegistryImpl.CONFIG_C2S);
+ public static final GlobalReceiverRegistry> PLAY = new GlobalReceiverRegistry<>(NetworkSide.SERVERBOUND, NetworkPhase.PLAY, PayloadTypeRegistryImpl.PLAY_C2S);
+
+ public static ServerPlayNetworkAddon getAddon(ServerPlayNetworkHandler handler) {
+ return (ServerPlayNetworkAddon) ((NetworkHandlerExtension) handler).kessokulib$getNetworkAddon();
+ }
+
+ public static ServerLoginNetworkAddon getAddon(ServerLoginNetworkHandler handler) {
+ return (ServerLoginNetworkAddon) ((NetworkHandlerExtension) handler).kessokulib$getNetworkAddon();
+ }
+
+ public static ServerConfigurationNetworkAddon getAddon(ServerConfigurationNetworkHandler handler) {
+ return (ServerConfigurationNetworkAddon) ((NetworkHandlerExtension) handler).kessokulib$getNetworkAddon();
+ }
+
+ public static Packet createS2CPacket(CustomPayload payload) {
+ Objects.requireNonNull(payload, "Payload cannot be null");
+ Objects.requireNonNull(payload.getId(), "CustomPayload#getId() cannot return null for payload class: " + payload.getClass());
+
+ return new CustomPayloadS2CPacket(payload);
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerPlayNetworkAddon.java b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerPlayNetworkAddon.java
new file mode 100644
index 00000000..2ed8e1d8
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/impl/networking/server/ServerPlayNetworkAddon.java
@@ -0,0 +1,128 @@
+package band.kessoku.lib.impl.networking.server;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerPlayNetworkHandler;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.api.networking.PacketSender;
+import band.kessoku.lib.api.networking.server.S2CPlayChannelEvent;
+import band.kessoku.lib.api.networking.server.ServerPlayConnectionEvent;
+import band.kessoku.lib.api.networking.server.ServerPlayNetworking;
+import band.kessoku.lib.impl.networking.AbstractChanneledNetworkAddon;
+import band.kessoku.lib.impl.networking.ChannelInfoHolder;
+import band.kessoku.lib.impl.networking.NetworkingImpl;
+import band.kessoku.lib.impl.networking.RegistrationPayload;
+
+public final class ServerPlayNetworkAddon extends AbstractChanneledNetworkAddon> {
+ private final ServerPlayNetworkHandler handler;
+ private final MinecraftServer server;
+ private boolean sentInitialRegisterPacket;
+ private final ServerPlayNetworking.Context context;
+
+ public ServerPlayNetworkAddon(ServerPlayNetworkHandler handler, ClientConnection connection, MinecraftServer server) {
+ super(ServerNetworkingImpl.PLAY, connection, "ServerPlayNetworkAddon for " + handler.player.getDisplayName());
+ this.handler = handler;
+ this.server = server;
+ this.context = new ContextImpl(server, handler, this);
+
+ // Must register pending channels via lateinit
+ this.registerPendingChannels((ChannelInfoHolder) this.connection, NetworkPhase.PLAY);
+ }
+
+ @Override
+ protected void invokeInitEvent() {
+ ServerPlayConnectionEvent.INIT.invoker().onPlayInit(this.handler, this.server);
+ }
+
+ public void onClientReady() {
+ ServerPlayConnectionEvent.JOIN.invoker().onPlayReady(this.handler, this, this.server);
+
+ this.sendInitialChannelRegistrationPacket();
+ this.sentInitialRegisterPacket = true;
+ }
+
+ @Override
+ protected void receive(ServerPlayNetworking.PlayPayloadHandler> payloadHandler, CustomPayload payload) {
+ this.server.execute(() -> {
+ ((ServerPlayNetworking.PlayPayloadHandler) payloadHandler).receive(payload, ServerPlayNetworkAddon.this.context);
+ });
+ }
+
+ // impl details
+
+ @Override
+ protected void schedule(Runnable task) {
+ this.handler.player.server.execute(task);
+ }
+
+ @Override
+ public Packet> createPacket(CustomPayload packet) {
+ return ServerPlayNetworking.createS2CPacket(packet);
+ }
+
+ @Override
+ protected void invokeRegisterEvent(List ids) {
+ S2CPlayChannelEvent.REGISTER.invoker().onChannelRegister(this.handler, this, this.server, ids);
+ }
+
+ @Override
+ protected void invokeUnregisterEvent(List ids) {
+ S2CPlayChannelEvent.UNREGISTER.invoker().onChannelUnregister(this.handler, this, this.server, ids);
+ }
+
+ @Override
+ protected void handleRegistration(Identifier channelName) {
+ // If we can already send packets, immediately send the register packet for this channel
+ if (this.sentInitialRegisterPacket) {
+ RegistrationPayload registrationPayload = this.createRegistrationPayload(RegistrationPayload.REGISTER, Collections.singleton(channelName));
+
+ if (registrationPayload != null) {
+ this.sendPacket(registrationPayload);
+ }
+ }
+ }
+
+ @Override
+ protected void handleUnregistration(Identifier channelName) {
+ // If we can already send packets, immediately send the unregister packet for this channel
+ if (this.sentInitialRegisterPacket) {
+ RegistrationPayload registrationPayload = this.createRegistrationPayload(RegistrationPayload.UNREGISTER, Collections.singleton(channelName));
+
+ if (registrationPayload != null) {
+ this.sendPacket(registrationPayload);
+ }
+ }
+ }
+
+ @Override
+ protected void invokeDisconnectEvent() {
+ ServerPlayConnectionEvent.DISCONNECT.invoker().onPlayDisconnect(this.handler, this.server);
+ }
+
+ @Override
+ protected boolean isReservedChannel(Identifier channelName) {
+ return NetworkingImpl.isReservedCommonChannel(channelName);
+ }
+
+ private record ContextImpl(MinecraftServer server, ServerPlayNetworkHandler handler, PacketSender responseSender) implements ServerPlayNetworking.Context {
+ private ContextImpl {
+ Objects.requireNonNull(server, "server");
+ Objects.requireNonNull(handler, "handler");
+ Objects.requireNonNull(responseSender, "responseSender");
+ }
+
+ @Override
+ public ServerPlayerEntity player() {
+ return handler.getPlayer();
+ }
+ }
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientCommonNetworkHandlerAccessor.java b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientCommonNetworkHandlerAccessor.java
new file mode 100644
index 00000000..8e0e3784
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientCommonNetworkHandlerAccessor.java
@@ -0,0 +1,13 @@
+package band.kessoku.lib.mixin.networking.accessor.client;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.network.ClientCommonNetworkHandler;
+import net.minecraft.network.ClientConnection;
+
+@Mixin(ClientCommonNetworkHandler.class)
+public interface ClientCommonNetworkHandlerAccessor {
+ @Accessor
+ ClientConnection getConnection();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientConfigurationNetworkHandlerAccessor.java b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientConfigurationNetworkHandlerAccessor.java
new file mode 100644
index 00000000..dc782fcd
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientConfigurationNetworkHandlerAccessor.java
@@ -0,0 +1,13 @@
+package band.kessoku.lib.mixin.networking.accessor.client;
+
+import com.mojang.authlib.GameProfile;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.network.ClientConfigurationNetworkHandler;
+
+@Mixin(ClientConfigurationNetworkHandler.class)
+public interface ClientConfigurationNetworkHandlerAccessor {
+ @Accessor
+ GameProfile getProfile();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientLoginNetworkHandlerAccessor.java b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientLoginNetworkHandlerAccessor.java
new file mode 100644
index 00000000..9da400c1
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ClientLoginNetworkHandlerAccessor.java
@@ -0,0 +1,13 @@
+package band.kessoku.lib.mixin.networking.accessor.client;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.network.ClientLoginNetworkHandler;
+import net.minecraft.network.ClientConnection;
+
+@Mixin(ClientLoginNetworkHandler.class)
+public interface ClientLoginNetworkHandlerAccessor {
+ @Accessor
+ ClientConnection getConnection();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ConnectScreenAccessor.java b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ConnectScreenAccessor.java
new file mode 100644
index 00000000..212573fd
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/ConnectScreenAccessor.java
@@ -0,0 +1,13 @@
+package band.kessoku.lib.mixin.networking.accessor.client;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.gui.screen.multiplayer.ConnectScreen;
+import net.minecraft.network.ClientConnection;
+
+@Mixin(ConnectScreen.class)
+public interface ConnectScreenAccessor {
+ @Accessor
+ ClientConnection getConnection();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/MinecraftClientAccessor.java b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/MinecraftClientAccessor.java
new file mode 100644
index 00000000..3c413e8b
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/client/MinecraftClientAccessor.java
@@ -0,0 +1,15 @@
+package band.kessoku.lib.mixin.networking.accessor.client;
+
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.network.ClientConnection;
+
+@Mixin(MinecraftClient.class)
+public interface MinecraftClientAccessor {
+ @Nullable
+ @Accessor("integratedServerConnection")
+ ClientConnection getConnection();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/server/ServerCommonNetworkHandlerAccessor.java b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/server/ServerCommonNetworkHandlerAccessor.java
new file mode 100644
index 00000000..93dddefe
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/server/ServerCommonNetworkHandlerAccessor.java
@@ -0,0 +1,17 @@
+package band.kessoku.lib.mixin.networking.accessor.server;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerCommonNetworkHandler;
+
+@Mixin(ServerCommonNetworkHandler.class)
+public interface ServerCommonNetworkHandlerAccessor {
+ @Accessor
+ ClientConnection getConnection();
+
+ @Accessor
+ MinecraftServer getServer();
+}
diff --git a/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/server/ServerLoginNetworkHandlerAccessor.java b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/server/ServerLoginNetworkHandlerAccessor.java
new file mode 100644
index 00000000..abe9035e
--- /dev/null
+++ b/networking/common/src/main/java/band/kessoku/lib/mixin/networking/accessor/server/ServerLoginNetworkHandlerAccessor.java
@@ -0,0 +1,17 @@
+package band.kessoku.lib.mixin.networking.accessor.server;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerLoginNetworkHandler;
+
+@Mixin(ServerLoginNetworkHandler.class)
+public interface ServerLoginNetworkHandlerAccessor {
+ @Accessor
+ MinecraftServer getServer();
+
+ @Accessor
+ ClientConnection getConnection();
+}
diff --git a/networking/common/src/main/resources/architectury.common.json b/networking/common/src/main/resources/architectury.common.json
new file mode 100644
index 00000000..4546464d
--- /dev/null
+++ b/networking/common/src/main/resources/architectury.common.json
@@ -0,0 +1,5 @@
+{
+ "injected_interfaces": {
+ "net/minecraft/class_8610": [ "net/fabricmc/fabric/api/networking/v1/FabricServerConfigurationNetworkHandler" ]
+ }
+}
\ No newline at end of file
diff --git a/networking/common/src/main/resources/kessoku-networking.common.mixins.json b/networking/common/src/main/resources/kessoku-networking.common.mixins.json
new file mode 100644
index 00000000..62116e3d
--- /dev/null
+++ b/networking/common/src/main/resources/kessoku-networking.common.mixins.json
@@ -0,0 +1,19 @@
+{
+ "required": true,
+ "package": "band.kessoku.lib.mixin.networking",
+ "compatibilityLevel": "JAVA_21",
+ "mixins": [
+ "accessor.server.ServerCommonNetworkHandlerAccessor",
+ "accessor.server.ServerLoginNetworkHandlerAccessor"
+ ],
+ "client": [
+ "accessor.client.ClientCommonNetworkHandlerAccessor",
+ "accessor.client.ClientConfigurationNetworkHandlerAccessor",
+ "accessor.client.ClientLoginNetworkHandlerAccessor",
+ "accessor.client.ConnectScreenAccessor",
+ "accessor.client.MinecraftClientAccessor"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}
\ No newline at end of file
diff --git a/networking/fabric/build.gradle b/networking/fabric/build.gradle
index 0580c67f..9c7f091b 100644
--- a/networking/fabric/build.gradle
+++ b/networking/fabric/build.gradle
@@ -5,7 +5,7 @@ apply from: rootProject.file("gradle/scripts/klib-fabric.gradle")
base.archivesName = rootProject.name + "-networking"
kessoku {
- module("base", "common")
+ modules(["base", "event-base"], "common")
common("networking", ModPlatform.FABRIC)
shadowBundle("networking", ModPlatform.FABRIC)
diff --git a/networking/fabric/src/main/java/band/kessoku/lib/KessokuNetworkingFabric.java b/networking/fabric/src/main/java/band/kessoku/lib/KessokuNetworkingFabric.java
deleted file mode 100644
index 7a779794..00000000
--- a/networking/fabric/src/main/java/band/kessoku/lib/KessokuNetworkingFabric.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package band.kessoku.lib;
-
-public class KessokuNetworkingFabric {
-}
diff --git a/networking/fabric/src/main/java/band/kessoku/lib/impl/networking/fabric/CommonPacketsImplFabric.java b/networking/fabric/src/main/java/band/kessoku/lib/impl/networking/fabric/CommonPacketsImplFabric.java
new file mode 100644
index 00000000..749929ad
--- /dev/null
+++ b/networking/fabric/src/main/java/band/kessoku/lib/impl/networking/fabric/CommonPacketsImplFabric.java
@@ -0,0 +1,65 @@
+package band.kessoku.lib.impl.networking.fabric;
+
+import band.kessoku.lib.api.KessokuLib;
+import band.kessoku.lib.api.KessokuNetworking;
+import band.kessoku.lib.api.networking.PayloadTypeRegistry;
+import band.kessoku.lib.api.networking.ServerConfigurationNetworkHandlerExtension;
+import band.kessoku.lib.api.networking.server.ServerConfigurationConnectionEvent;
+import band.kessoku.lib.api.networking.server.ServerConfigurationNetworking;
+import band.kessoku.lib.impl.networking.common.CommonPacketsImpl;
+import band.kessoku.lib.impl.networking.common.CommonRegisterPayload;
+import band.kessoku.lib.impl.networking.common.CommonVersionPayload;
+import band.kessoku.lib.impl.networking.server.ServerConfigurationNetworkAddon;
+import band.kessoku.lib.impl.networking.server.ServerNetworkingImpl;
+import net.minecraft.network.NetworkPhase;
+
+public class CommonPacketsImplFabric extends CommonPacketsImpl {
+ public static void init() {
+ PayloadTypeRegistry.configC2S().register(CommonVersionPayload.ID, CommonVersionPayload.CODEC);
+ PayloadTypeRegistry.configS2C().register(CommonVersionPayload.ID, CommonVersionPayload.CODEC);
+ PayloadTypeRegistry.playC2S().register(CommonVersionPayload.ID, CommonVersionPayload.CODEC);
+ PayloadTypeRegistry.playS2C().register(CommonVersionPayload.ID, CommonVersionPayload.CODEC);
+ PayloadTypeRegistry.configC2S().register(CommonRegisterPayload.ID, CommonRegisterPayload.CODEC);
+ PayloadTypeRegistry.configS2C().register(CommonRegisterPayload.ID, CommonRegisterPayload.CODEC);
+ PayloadTypeRegistry.playC2S().register(CommonRegisterPayload.ID, CommonRegisterPayload.CODEC);
+ PayloadTypeRegistry.playS2C().register(CommonRegisterPayload.ID, CommonRegisterPayload.CODEC);
+
+ ServerConfigurationNetworking.registerGlobalReceiver(CommonVersionPayload.ID, (payload, context) -> {
+ ServerConfigurationNetworkAddon addon = ServerNetworkingImpl.getAddon(context.networkHandler());
+ addon.kessokulib$onCommonVersionPacket(getNegotiatedVersion(payload));
+ ((ServerConfigurationNetworkHandlerExtension) context.networkHandler()).kessokulib$completeTask(CommonPacketsImpl.CommonVersionConfigurationTask.KEY);
+ });
+
+ ServerConfigurationNetworking.registerGlobalReceiver(CommonRegisterPayload.ID, (payload, context) -> {
+ ServerConfigurationNetworkAddon addon = ServerNetworkingImpl.getAddon(context.networkHandler());
+
+ if (CommonRegisterPayload.PLAY_PHASE.equals(payload.phase())) {
+ if (payload.version() != addon.kessokulib$getNegotiatedVersion()) {
+ throw new IllegalStateException("Negotiated common packet version: %d but received packet with version: %d".formatted(addon.kessokulib$getNegotiatedVersion(), payload.version()));
+ }
+
+ // Play phase hasnt started yet, add them to the pending names.
+ addon.getChannelInfoHolder().kessokulib$getPendingChannelsNames(NetworkPhase.PLAY).addAll(payload.channels());
+ KessokuLib.getLogger().debug(KessokuNetworking.MARKER, "Received accepted channels from the client for play phase");
+ } else {
+ addon.kessokulib$onCommonRegisterPacket(payload);
+ }
+
+ ((ServerConfigurationNetworkHandlerExtension) context.networkHandler()).kessokulib$completeTask(CommonPacketsImpl.CommonRegisterConfigurationTask.KEY);
+ });
+
+ // Create a configuration task to send and receive the common packets
+ ServerConfigurationConnectionEvent.CONFIGURE.register((handler, server) -> {
+ final ServerConfigurationNetworkAddon addon = ServerNetworkingImpl.getAddon(handler);
+
+ if (ServerConfigurationNetworking.canSend(handler, CommonVersionPayload.ID)) {
+ // Tasks are processed in order.
+ ((ServerConfigurationNetworkHandlerExtension) handler).kessokulib$addTask(new CommonPacketsImpl.CommonVersionConfigurationTask(addon));
+
+ if (ServerConfigurationNetworking.canSend(handler, CommonRegisterPayload.ID)) {
+ ((ServerConfigurationNetworkHandlerExtension) handler).kessokulib$addTask(new CommonPacketsImpl.CommonRegisterConfigurationTask(addon));
+ }
+ }
+ });
+ }
+}
diff --git a/networking/fabric/src/main/java/band/kessoku/lib/impl/networking/fabric/KessokuNetworkingFabric.java b/networking/fabric/src/main/java/band/kessoku/lib/impl/networking/fabric/KessokuNetworkingFabric.java
new file mode 100644
index 00000000..ea900452
--- /dev/null
+++ b/networking/fabric/src/main/java/band/kessoku/lib/impl/networking/fabric/KessokuNetworkingFabric.java
@@ -0,0 +1,20 @@
+package band.kessoku.lib.impl.networking.fabric;
+
+import band.kessoku.lib.impl.networking.NetworkingImpl;
+import band.kessoku.lib.impl.networking.client.ClientNetworkingImpl;
+
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.api.ModInitializer;
+
+public class KessokuNetworkingFabric implements ModInitializer, ClientModInitializer {
+ @Override
+ public void onInitialize() {
+ CommonPacketsImplFabric.init();
+ NetworkingImpl.init();
+ }
+
+ @Override
+ public void onInitializeClient() {
+ ClientNetworkingImpl.clientInit();
+ }
+}
diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ClientConnectionMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ClientConnectionMixin.java
new file mode 100644
index 00000000..831b189a
--- /dev/null
+++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ClientConnectionMixin.java
@@ -0,0 +1,74 @@
+package band.kessoku.lib.mixin.networking.fabric;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import io.netty.channel.ChannelHandlerContext;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.NetworkPhase;
+import net.minecraft.network.NetworkSide;
+import net.minecraft.network.NetworkState;
+import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.listener.PacketListener;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.impl.networking.ChannelInfoHolder;
+import band.kessoku.lib.impl.networking.NetworkHandlerExtension;
+import band.kessoku.lib.impl.networking.PacketCallbackListener;
+
+@Mixin(ClientConnection.class)
+abstract class ClientConnectionMixin implements ChannelInfoHolder {
+ @Shadow
+ private PacketListener packetListener;
+
+ @Unique
+ private Map> kessokulib$playChannels;
+
+ @Inject(method = "", at = @At("RETURN"))
+ private void initAddedFields(NetworkSide side, CallbackInfo ci) {
+ this.kessokulib$playChannels = new ConcurrentHashMap<>();
+ }
+
+ @Inject(method = "sendImmediately", at = @At(value = "FIELD", target = "Lnet/minecraft/network/ClientConnection;packetsSentCounter:I"))
+ private void checkPacket(Packet> packet, PacketCallbacks callback, boolean flush, CallbackInfo ci) {
+ if (this.packetListener instanceof PacketCallbackListener) {
+ ((PacketCallbackListener) this.packetListener).kessokulib$sent(packet);
+ }
+ }
+
+ @Inject(method = "setPacketListener", at = @At("HEAD"))
+ private void unwatchAddon(NetworkState> state, PacketListener listener, CallbackInfo ci) {
+ if (this.packetListener instanceof NetworkHandlerExtension oldListener) {
+ oldListener.kessokulib$getNetworkAddon().endSession();
+ }
+ }
+
+ @Inject(method = "channelInactive", at = @At("HEAD"))
+ private void disconnectAddon(ChannelHandlerContext channelHandlerContext, CallbackInfo ci) {
+ if (packetListener instanceof NetworkHandlerExtension extension) {
+ extension.kessokulib$getNetworkAddon().handleDisconnect();
+ }
+ }
+
+ @Inject(method = "handleDisconnection", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/listener/PacketListener;onDisconnected(Lnet/minecraft/network/DisconnectionInfo;)V"))
+ private void disconnectAddon(CallbackInfo ci) {
+ if (packetListener instanceof NetworkHandlerExtension extension) {
+ extension.kessokulib$getNetworkAddon().handleDisconnect();
+ }
+ }
+
+ @Override
+ public Collection kessokulib$getPendingChannelsNames(NetworkPhase state) {
+ return this.kessokulib$playChannels.computeIfAbsent(state, (key) -> Collections.newSetFromMap(new ConcurrentHashMap<>()));
+ }
+}
diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CommandManagerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CommandManagerMixin.java
new file mode 100644
index 00000000..de6f1422
--- /dev/null
+++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CommandManagerMixin.java
@@ -0,0 +1,39 @@
+package band.kessoku.lib.mixin.networking.fabric;
+
+import com.mojang.brigadier.CommandDispatcher;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.SharedConstants;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.server.command.CommandManager;
+import net.minecraft.server.command.DebugConfigCommand;
+import net.minecraft.server.command.ServerCommandSource;
+
+import net.fabricmc.loader.api.FabricLoader;
+
+@Mixin(CommandManager.class)
+public class CommandManagerMixin {
+ @Shadow
+ @Final
+ private CommandDispatcher dispatcher;
+
+ @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/dedicated/command/BanIpCommand;register(Lcom/mojang/brigadier/CommandDispatcher;)V"))
+ private void init(CommandManager.RegistrationEnvironment environment, CommandRegistryAccess commandRegistryAccess, CallbackInfo ci) {
+ if (SharedConstants.isDevelopment) {
+ // Command is registered when isDevelopment is set.
+ return;
+ }
+
+ if (!FabricLoader.getInstance().isDevelopmentEnvironment()) {
+ // Only register this command in a dev env
+ return;
+ }
+
+ DebugConfigCommand.register(this.dispatcher);
+ }
+}
diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadC2SPacketMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadC2SPacketMixin.java
new file mode 100644
index 00000000..2d1deea1
--- /dev/null
+++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadC2SPacketMixin.java
@@ -0,0 +1,41 @@
+package band.kessoku.lib.mixin.networking.fabric;
+
+import java.util.List;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.RegistryByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket;
+
+import band.kessoku.lib.impl.networking.CustomPayloadPacketCodecExtension;
+import band.kessoku.lib.impl.networking.PayloadTypeRegistryImpl;
+
+@Mixin(CustomPayloadC2SPacket.class)
+public class CustomPayloadC2SPacketMixin {
+ @WrapOperation(
+ method = "",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/network/packet/CustomPayload;createCodec(Lnet/minecraft/network/packet/CustomPayload$CodecFactory;Ljava/util/List;)Lnet/minecraft/network/codec/PacketCodec;"
+ )
+ )
+ private static PacketCodec wrapCodec(CustomPayload.CodecFactory unknownCodecFactory, List> types, Operation> original) {
+ PacketCodec codec = original.call(unknownCodecFactory, types);
+ CustomPayloadPacketCodecExtension kessokuCodec = (CustomPayloadPacketCodecExtension) codec;
+ kessokuCodec.kessokulib$setPacketCodecProvider((packetByteBuf, identifier) -> {
+ // CustomPayloadC2SPacket does not have a separate codec for play/configuration. We know if the packetByteBuf is a PacketByteBuf we are in the play phase.
+ if (packetByteBuf instanceof RegistryByteBuf) {
+ return (CustomPayload.Type) (Object) PayloadTypeRegistryImpl.PLAY_C2S.get(identifier);
+ }
+
+ return PayloadTypeRegistryImpl.CONFIG_C2S.get(identifier);
+ });
+ return codec;
+ }
+}
diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadPacketCodecMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadPacketCodecMixin.java
new file mode 100644
index 00000000..4c219bf0
--- /dev/null
+++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadPacketCodecMixin.java
@@ -0,0 +1,47 @@
+package band.kessoku.lib.mixin.networking.fabric;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Coerce;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.util.Identifier;
+
+import band.kessoku.lib.impl.networking.CustomPayloadTypeProvider;
+import band.kessoku.lib.impl.networking.CustomPayloadPacketCodecExtension;
+
+@Mixin(targets = "net/minecraft/network/packet/CustomPayload$1")
+public abstract class CustomPayloadPacketCodecMixin implements PacketCodec, CustomPayloadPacketCodecExtension {
+ @Unique
+ private CustomPayloadTypeProvider kessokulib$customPayloadTypeProvider;
+
+ @Override
+ public void kessokulib$setPacketCodecProvider(CustomPayloadTypeProvider