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 register(CustomPayload.Id id, PacketCodec 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 register(CustomPayload.Id id, PacketCodec 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 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 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 customPayloadTypeProvider) { + if (this.kessokulib$customPayloadTypeProvider != null) { + throw new IllegalStateException("Payload codec provider is already set!"); + } + + this.kessokulib$customPayloadTypeProvider = customPayloadTypeProvider; + } + + @WrapOperation(method = { + "encode(Lnet/minecraft/network/PacketByteBuf;Lnet/minecraft/network/packet/CustomPayload$Id;Lnet/minecraft/network/packet/CustomPayload;)V", + "decode(Lnet/minecraft/network/PacketByteBuf;)Lnet/minecraft/network/packet/CustomPayload;" + }, at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/CustomPayload$1;getCodec(Lnet/minecraft/util/Identifier;)Lnet/minecraft/network/codec/PacketCodec;")) + private PacketCodec wrapGetCodec(@Coerce PacketCodec instance, Identifier identifier, Operation> original, B packetByteBuf) { + if (kessokulib$customPayloadTypeProvider != null) { + CustomPayload.Type payloadType = kessokulib$customPayloadTypeProvider.kessokulib$get(packetByteBuf, identifier); + + if (payloadType != null) { + return payloadType.codec(); + } + } + + return original.call(instance, identifier); + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadS2CPacketMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadS2CPacketMixin.java new file mode 100644 index 00000000..2a74c38b --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/CustomPayloadS2CPacketMixin.java @@ -0,0 +1,50 @@ +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.s2c.common.CustomPayloadS2CPacket; + +import band.kessoku.lib.impl.networking.CustomPayloadPacketCodecExtension; +import band.kessoku.lib.impl.networking.PayloadTypeRegistryImpl; + +@Mixin(CustomPayloadS2CPacket.class) +public class CustomPayloadS2CPacketMixin { + @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;", + ordinal = 0 + ) + ) + private static PacketCodec wrapPlayCodec(CustomPayload.CodecFactory unknownCodecFactory, List> types, Operation> original) { + PacketCodec codec = original.call(unknownCodecFactory, types); + CustomPayloadPacketCodecExtension kessokuCodec = (CustomPayloadPacketCodecExtension) codec; + kessokuCodec.kessokulib$setPacketCodecProvider((packetByteBuf, identifier) -> PayloadTypeRegistryImpl.PLAY_S2C.get(identifier)); + return codec; + } + + @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;", + ordinal = 1 + ) + ) + private static PacketCodec wrapConfigCodec(CustomPayload.CodecFactory unknownCodecFactory, List> types, Operation> original) { + PacketCodec codec = original.call(unknownCodecFactory, types); + CustomPayloadPacketCodecExtension kessokuCodec = (CustomPayloadPacketCodecExtension) codec; + kessokuCodec.kessokulib$setPacketCodecProvider((packetByteBuf, identifier) -> PayloadTypeRegistryImpl.CONFIG_S2C.get(identifier)); + return codec; + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/LoginQueryRequestS2CPacketMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/LoginQueryRequestS2CPacketMixin.java new file mode 100644 index 00000000..e881766f --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/LoginQueryRequestS2CPacketMixin.java @@ -0,0 +1,28 @@ +package band.kessoku.lib.mixin.networking.fabric; + +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.CallbackInfoReturnable; + +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.s2c.login.LoginQueryRequestPayload; +import net.minecraft.network.packet.s2c.login.LoginQueryRequestS2CPacket; +import net.minecraft.util.Identifier; + +import band.kessoku.lib.impl.networking.payload.PacketByteBufLoginQueryRequestPayload; +import band.kessoku.lib.impl.networking.payload.PayloadHelper; + +@Mixin(LoginQueryRequestS2CPacket.class) +public class LoginQueryRequestS2CPacketMixin { + @Shadow + @Final + private static int MAX_PAYLOAD_SIZE; + + @Inject(method = "readPayload", at = @At("HEAD"), cancellable = true) + private static void readPayload(Identifier id, PacketByteBuf buf, CallbackInfoReturnable cir) { + cir.setReturnValue(new PacketByteBufLoginQueryRequestPayload(id, PayloadHelper.read(buf, MAX_PAYLOAD_SIZE))); + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/LoginQueryResponseC2SPacketMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/LoginQueryResponseC2SPacketMixin.java new file mode 100644 index 00000000..f94c4062 --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/LoginQueryResponseC2SPacketMixin.java @@ -0,0 +1,34 @@ +package band.kessoku.lib.mixin.networking.fabric; + +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.CallbackInfoReturnable; + +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.c2s.login.LoginQueryResponseC2SPacket; +import net.minecraft.network.packet.c2s.login.LoginQueryResponsePayload; + +import band.kessoku.lib.impl.networking.payload.PacketByteBufLoginQueryResponsePayload; +import band.kessoku.lib.impl.networking.payload.PayloadHelper; + +@Mixin(LoginQueryResponseC2SPacket.class) +public class LoginQueryResponseC2SPacketMixin { + @Shadow + @Final + private static int MAX_PAYLOAD_SIZE; + + @Inject(method = "readPayload", at = @At("HEAD"), cancellable = true) + private static void readResponse(int queryId, PacketByteBuf buf, CallbackInfoReturnable cir) { + boolean hasPayload = buf.readBoolean(); + + if (!hasPayload) { + cir.setReturnValue(null); + return; + } + + cir.setReturnValue(new PacketByteBufLoginQueryResponsePayload(PayloadHelper.read(buf, MAX_PAYLOAD_SIZE))); + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/PacketCodecDispatcherMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/PacketCodecDispatcherMixin.java new file mode 100644 index 00000000..f59e6d5b --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/PacketCodecDispatcherMixin.java @@ -0,0 +1,34 @@ +package band.kessoku.lib.mixin.networking.fabric; + +import com.llamalad7.mixinextras.sugar.Local; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.EncoderException; +import org.spongepowered.asm.mixin.Mixin; +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.codec.PacketCodec; +import net.minecraft.network.handler.PacketCodecDispatcher; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket; +import net.minecraft.network.packet.s2c.common.CustomPayloadS2CPacket; + +@Mixin(PacketCodecDispatcher.class) +public abstract class PacketCodecDispatcherMixin implements PacketCodec { + // Add the custom payload id to the error message + @Inject(method = "encode(Lio/netty/buffer/ByteBuf;Ljava/lang/Object;)V", at = @At(value = "NEW", target = "(Ljava/lang/String;Ljava/lang/Throwable;)Lio/netty/handler/codec/EncoderException;")) + public void encode(B byteBuf, V packet, CallbackInfo ci, @Local(ordinal = 1) T packetId, @Local Exception e) { + CustomPayload payload = null; + + if (packet instanceof CustomPayloadC2SPacket customPayloadC2SPacket) { + payload = customPayloadC2SPacket.payload(); + } else if (packet instanceof CustomPayloadS2CPacket customPayloadS2CPacket) { + payload = customPayloadS2CPacket.payload(); + } + + if (payload != null && payload.getId() != null) { + throw new EncoderException("Failed to encode packet '%s' (%s)".formatted(packetId, payload.getId().id().toString()), e); + } + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/PlayerManagerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/PlayerManagerMixin.java new file mode 100644 index 00000000..87b7f9a7 --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/PlayerManagerMixin.java @@ -0,0 +1,21 @@ +package band.kessoku.lib.mixin.networking.fabric; + +import org.spongepowered.asm.mixin.Mixin; +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.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; + +import band.kessoku.lib.impl.networking.server.ServerNetworkingImpl; + +@Mixin(PlayerManager.class) +abstract class PlayerManagerMixin { + @Inject(method = "onPlayerConnect", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/PlayerAbilitiesS2CPacket;(Lnet/minecraft/entity/player/PlayerAbilities;)V")) + private void handlePlayerConnection(ClientConnection connection, ServerPlayerEntity player, ConnectedClientData arg, CallbackInfo ci) { + ServerNetworkingImpl.getAddon(player.networkHandler).onClientReady(); + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerCommonNetworkHandlerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerCommonNetworkHandlerMixin.java new file mode 100644 index 00000000..c88541c4 --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerCommonNetworkHandlerMixin.java @@ -0,0 +1,42 @@ +package band.kessoku.lib.mixin.networking.fabric; + +import org.spongepowered.asm.mixin.Mixin; +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.packet.CustomPayload; +import net.minecraft.network.packet.c2s.common.CommonPongC2SPacket; +import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket; +import net.minecraft.server.network.ServerCommonNetworkHandler; + +import band.kessoku.lib.impl.networking.NetworkHandlerExtension; +import band.kessoku.lib.impl.networking.server.ServerConfigurationNetworkAddon; + +@Mixin(ServerCommonNetworkHandler.class) +public abstract class ServerCommonNetworkHandlerMixin implements NetworkHandlerExtension { + @Inject(method = "onCustomPayload", at = @At("HEAD"), cancellable = true) + private void handleCustomPayloadReceivedAsync(CustomPayloadC2SPacket packet, CallbackInfo ci) { + final CustomPayload payload = packet.payload(); + + boolean handled; + + if (kessokulib$getNetworkAddon() instanceof ServerConfigurationNetworkAddon addon) { + handled = addon.handle(payload); + } else { + // Play should be handled in ServerPlayNetworkHandlerMixin + throw new IllegalStateException("Unknown addon"); + } + + if (handled) { + ci.cancel(); + } + } + + @Inject(method = "onPong", at = @At("HEAD")) + private void onPlayPong(CommonPongC2SPacket packet, CallbackInfo ci) { + if (kessokulib$getNetworkAddon() instanceof ServerConfigurationNetworkAddon addon) { + addon.onPong(packet.getParameter()); + } + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerConfigurationNetworkHandlerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerConfigurationNetworkHandlerMixin.java index df470b98..4af8851a 100644 --- a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerConfigurationNetworkHandlerMixin.java +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerConfigurationNetworkHandlerMixin.java @@ -1,8 +1,150 @@ package band.kessoku.lib.mixin.networking.fabric; -import band.kessoku.lib.api.networking.util.ServerConfigurationNetworkHandlerExtension; +import java.util.Queue; + +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; 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.server.MinecraftServer; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerCommonNetworkHandler; +import net.minecraft.server.network.ServerConfigurationNetworkHandler; +import net.minecraft.server.network.ServerPlayerConfigurationTask; + +import band.kessoku.lib.api.networking.ServerConfigurationNetworkHandlerExtension; +import band.kessoku.lib.impl.networking.NetworkHandlerExtension; +import band.kessoku.lib.impl.networking.server.ServerConfigurationNetworkAddon; + +// We want to apply a bit earlier than other mods which may not use us in order to prevent refCount issues +@Mixin(value = ServerConfigurationNetworkHandler.class, priority = 900) +public abstract class ServerConfigurationNetworkHandlerMixin extends ServerCommonNetworkHandler implements NetworkHandlerExtension, ServerConfigurationNetworkHandlerExtension { + @Shadow + @Nullable + private ServerPlayerConfigurationTask currentTask; + + @Shadow + protected abstract void onTaskFinished(ServerPlayerConfigurationTask.Key key); + + @Shadow + @Final + private Queue tasks; + + @Shadow + public abstract boolean isConnectionOpen(); + + @Shadow + public abstract void sendConfigurations(); + + @Unique + private ServerConfigurationNetworkAddon kessokulib$addon; + + @Unique + private boolean kessokulib$sentConfiguration; + + @Unique + private boolean kessokulib$earlyTaskExecution; + + public ServerConfigurationNetworkHandlerMixin(MinecraftServer server, ClientConnection connection, ConnectedClientData arg) { + super(server, connection, arg); + } + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.kessokulib$addon = new ServerConfigurationNetworkAddon((ServerConfigurationNetworkHandler) (Object) this, this.server); + // A bit of a hack but it allows the field above to be set in case someone registers handlers during INIT event which refers to said field + this.kessokulib$addon.lateInit(); + } + + @Inject(method = "sendConfigurations", at = @At("HEAD"), cancellable = true) + private void onClientReady(CallbackInfo ci) { + // Send the initial channel registration packet + if (this.kessokulib$addon.startConfiguration()) { + assert currentTask == null; + ci.cancel(); + return; + } + + // Ready to start sending packets + if (!kessokulib$sentConfiguration) { + this.kessokulib$addon.preConfig(); + kessokulib$sentConfiguration = true; + kessokulib$earlyTaskExecution = true; + } + + // Run the early tasks + if (kessokulib$earlyTaskExecution) { + if (pollEarlyTasks()) { + ci.cancel(); + return; + } else { + kessokulib$earlyTaskExecution = false; + } + } + + // All early tasks should have been completed + assert currentTask == null; + assert tasks.isEmpty(); + + // Run the vanilla tasks. + this.kessokulib$addon.config(); + } + + @Unique + private boolean pollEarlyTasks() { + if (!kessokulib$earlyTaskExecution) { + throw new IllegalStateException("Early task execution has finished"); + } + + if (this.currentTask != null) { + throw new IllegalStateException("Task " + this.currentTask.getKey().id() + " has not finished yet"); + } + + if (!this.isConnectionOpen()) { + return false; + } + + final ServerPlayerConfigurationTask task = this.tasks.poll(); + + if (task != null) { + this.currentTask = task; + task.sendPacket(this::sendPacket); + return true; + } + + return false; + } + + @Override + public ServerConfigurationNetworkAddon kessokulib$getNetworkAddon() { + return kessokulib$addon; + } + + @Override + public void kessokulib$addTask(ServerPlayerConfigurationTask task) { + tasks.add(task); + } + + @Override + public void kessokulib$completeTask(ServerPlayerConfigurationTask.Key key) { + if (!kessokulib$earlyTaskExecution) { + onTaskFinished(key); + return; + } + + final ServerPlayerConfigurationTask.Key currentKey = this.currentTask != null ? this.currentTask.getKey() : null; + + if (!key.equals(currentKey)) { + throw new IllegalStateException("Unexpected request for task finish, current task: " + currentKey + ", requested: " + key); + } -@Mixin(ServerConfigurationNetworkHandlerExtension.class) -public class ServerConfigurationNetworkHandlerMixin implements ServerConfigurationNetworkHandlerExtension { + this.currentTask = null; + sendConfigurations(); + } } diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerLoginNetworkHandlerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerLoginNetworkHandlerMixin.java new file mode 100644 index 00000000..e568feae --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerLoginNetworkHandlerMixin.java @@ -0,0 +1,74 @@ +package band.kessoku.lib.mixin.networking.fabric; + +import com.mojang.authlib.GameProfile; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.c2s.login.LoginQueryResponseC2SPacket; +import net.minecraft.network.packet.s2c.login.LoginQueryRequestS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginNetworkHandler; + +import band.kessoku.lib.impl.networking.NetworkHandlerExtension; +import band.kessoku.lib.impl.networking.PacketCallbackListener; +import band.kessoku.lib.impl.networking.payload.PacketByteBufLoginQueryResponsePayload; +import band.kessoku.lib.impl.networking.server.ServerLoginNetworkAddon; + +@Mixin(ServerLoginNetworkHandler.class) +abstract class ServerLoginNetworkHandlerMixin implements NetworkHandlerExtension, PacketCallbackListener { + @Shadow + protected abstract void tickVerify(GameProfile profile); + + @Unique + private ServerLoginNetworkAddon addon; + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.addon = new ServerLoginNetworkAddon((ServerLoginNetworkHandler) (Object) this); + // A bit of a hack but it allows the field above to be set in case someone registers handlers during INIT event which refers to said field + this.addon.lateInit(); + } + + @Redirect(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerLoginNetworkHandler;tickVerify(Lcom/mojang/authlib/GameProfile;)V")) + private void handlePlayerJoin(ServerLoginNetworkHandler instance, GameProfile profile) { + // Do not accept the player, thereby moving into play stage until all login futures being waited on are completed + if (this.addon.queryTick()) { + this.tickVerify(profile); + } + } + + @Inject(method = "onQueryResponse", at = @At("HEAD"), cancellable = true) + private void handleCustomPayloadReceivedAsync(LoginQueryResponseC2SPacket packet, CallbackInfo ci) { + // Handle queries + if (this.addon.handle(packet)) { + ci.cancel(); + } else { + if (packet.response() instanceof PacketByteBufLoginQueryResponsePayload response) { + response.data().skipBytes(response.data().readableBytes()); + } + } + } + + @Redirect(method = "tickVerify", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getNetworkCompressionThreshold()I", ordinal = 0)) + private int removeLateCompressionPacketSending(MinecraftServer server) { + return -1; + } + + @Override + public void kessokulib$sent(Packet packet) { + if (packet instanceof LoginQueryRequestS2CPacket) { + this.addon.registerOutgoingPacket((LoginQueryRequestS2CPacket) packet); + } + } + + @Override + public ServerLoginNetworkAddon kessokulib$getNetworkAddon() { + return this.addon; + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerPlayNetworkHandlerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 00000000..3123e370 --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,51 @@ +package band.kessoku.lib.mixin.networking.fabric; + +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.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerCommonNetworkHandler; +import net.minecraft.server.network.ServerPlayNetworkHandler; + +import net.fabricmc.fabric.impl.networking.NetworkHandlerExtensions; +import net.fabricmc.fabric.impl.networking.UntrackedNetworkHandler; +import net.fabricmc.fabric.impl.networking.server.ServerPlayNetworkAddon; + +// We want to apply a bit earlier than other mods which may not use us in order to prevent refCount issues +@Mixin(value = ServerPlayNetworkHandler.class, priority = 999) +abstract class ServerPlayNetworkHandlerMixin extends ServerCommonNetworkHandler implements NetworkHandlerExtensions { + @Unique + private ServerPlayNetworkAddon addon; + + ServerPlayNetworkHandlerMixin(MinecraftServer server, ClientConnection connection, ConnectedClientData arg) { + super(server, connection, arg); + } + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.addon = new ServerPlayNetworkAddon((ServerPlayNetworkHandler) (Object) this, connection, server); + + if (!(this instanceof UntrackedNetworkHandler)) { + // A bit of a hack but it allows the field above to be set in case someone registers handlers during INIT event which refers to said field + this.addon.lateInit(); + } + } + + @Inject(method = "onCustomPayload", at = @At("HEAD"), cancellable = true) + private void handleCustomPayloadReceivedAsync(CustomPayloadC2SPacket packet, CallbackInfo ci) { + if (getAddon().handle(packet.payload())) { + ci.cancel(); + } + } + + @Override + public ServerPlayNetworkAddon getAddon() { + return this.addon; + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientCommonNetworkHandlerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientCommonNetworkHandlerMixin.java new file mode 100644 index 00000000..80dcf8d5 --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientCommonNetworkHandlerMixin.java @@ -0,0 +1,35 @@ +package band.kessoku.lib.mixin.networking.fabric.client; + +import org.spongepowered.asm.mixin.Mixin; +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.client.network.ClientCommonNetworkHandler; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.s2c.common.CustomPayloadS2CPacket; + +import band.kessoku.lib.impl.networking.NetworkHandlerExtension; +import band.kessoku.lib.impl.networking.client.ClientConfigurationNetworkAddon; +import band.kessoku.lib.impl.networking.client.ClientPlayNetworkAddon; + +@Mixin(ClientCommonNetworkHandler.class) +public abstract class ClientCommonNetworkHandlerMixin implements NetworkHandlerExtension { + @Inject(method = "onCustomPayload(Lnet/minecraft/network/packet/s2c/common/CustomPayloadS2CPacket;)V", at = @At("HEAD"), cancellable = true) + public void onCustomPayload(CustomPayloadS2CPacket packet, CallbackInfo ci) { + final CustomPayload payload = packet.payload(); + boolean handled; + + if (this.kessokulib$getNetworkAddon() instanceof ClientPlayNetworkAddon addon) { + handled = addon.handle(payload); + } else if (this.kessokulib$getNetworkAddon() instanceof ClientConfigurationNetworkAddon addon) { + handled = addon.handle(payload); + } else { + throw new IllegalStateException("Unknown network addon"); + } + + if (handled) { + ci.cancel(); + } + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientConfigurationNetworkHandlerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientConfigurationNetworkHandlerMixin.java new file mode 100644 index 00000000..5b916977 --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientConfigurationNetworkHandlerMixin.java @@ -0,0 +1,47 @@ +package band.kessoku.lib.mixin.networking.fabric.client; + +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.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import net.minecraft.client.network.ClientConfigurationNetworkHandler; +import net.minecraft.client.network.ClientConnectionState; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.s2c.config.ReadyS2CPacket; + +import band.kessoku.lib.impl.networking.NetworkHandlerExtension; +import band.kessoku.lib.impl.networking.client.ClientConfigurationNetworkAddon; +import band.kessoku.lib.impl.networking.client.ClientNetworkingImpl; + +// We want to apply a bit earlier than other mods which may not use us in order to prevent refCount issues +@Mixin(value = ClientConfigurationNetworkHandler.class, priority = 999) +public abstract class ClientConfigurationNetworkHandlerMixin extends ClientCommonNetworkHandler implements NetworkHandlerExtension { + @Unique + private ClientConfigurationNetworkAddon kessokulib$addon; + + protected ClientConfigurationNetworkHandlerMixin(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { + super(client, connection, connectionState); + } + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.kessokulib$addon = new ClientConfigurationNetworkAddon((ClientConfigurationNetworkHandler) (Object) this, this.client); + // A bit of a hack but it allows the field above to be set in case someone registers handlers during INIT event which refers to said field + ClientNetworkingImpl.setClientConfigurationAddon(this.kessokulib$addon); + this.kessokulib$addon.lateInit(); + } + + @Inject(method = "onReady", at = @At(value = "NEW", target = "(Lnet/minecraft/client/MinecraftClient;Lnet/minecraft/network/ClientConnection;Lnet/minecraft/client/network/ClientConnectionState;)Lnet/minecraft/client/network/ClientPlayNetworkHandler;")) + public void handleComplete(ReadyS2CPacket packet, CallbackInfo ci) { + this.kessokulib$addon.handleComplete(); + } + + @Override + public ClientConfigurationNetworkAddon kessokulib$getNetworkAddon() { + return kessokulib$addon; + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientLoginNetworkHandlerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientLoginNetworkHandlerMixin.java new file mode 100644 index 00000000..e04fff50 --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientLoginNetworkHandlerMixin.java @@ -0,0 +1,50 @@ +package band.kessoku.lib.mixin.networking.fabric.client; + +import org.spongepowered.asm.mixin.Final; +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.client.MinecraftClient; +import net.minecraft.client.network.ClientLoginNetworkHandler; +import net.minecraft.network.packet.s2c.login.LoginQueryRequestS2CPacket; + +import band.kessoku.lib.impl.networking.NetworkHandlerExtension; +import band.kessoku.lib.impl.networking.client.ClientLoginNetworkAddon; +import band.kessoku.lib.impl.networking.payload.PacketByteBufLoginQueryRequestPayload; + +@Mixin(ClientLoginNetworkHandler.class) +abstract class ClientLoginNetworkHandlerMixin implements NetworkHandlerExtension { + @Shadow + @Final + private MinecraftClient client; + + @Unique + private ClientLoginNetworkAddon kessokulib$addon; + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.kessokulib$addon = new ClientLoginNetworkAddon((ClientLoginNetworkHandler) (Object) this, this.client); + // A bit of a hack but it allows the field above to be set in case someone registers handlers during INIT event which refers to said field + this.kessokulib$addon.lateInit(); + } + + @Inject(method = "onQueryRequest", at = @At(value = "INVOKE", target = "Ljava/util/function/Consumer;accept(Ljava/lang/Object;)V", remap = false, shift = At.Shift.AFTER), cancellable = true) + private void handleQueryRequest(LoginQueryRequestS2CPacket packet, CallbackInfo ci) { + if (packet.payload() instanceof PacketByteBufLoginQueryRequestPayload payload) { + if (this.kessokulib$addon.handlePacket(packet)) { + ci.cancel(); + } else { + payload.data().skipBytes(payload.data().readableBytes()); + } + } + } + + @Override + public ClientLoginNetworkAddon kessokulib$getNetworkAddon() { + return this.kessokulib$addon; + } +} diff --git a/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientPlayNetworkHandlerMixin.java b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientPlayNetworkHandlerMixin.java new file mode 100644 index 00000000..492aa401 --- /dev/null +++ b/networking/fabric/src/main/java/band/kessoku/lib/mixin/networking/fabric/client/ClientPlayNetworkHandlerMixin.java @@ -0,0 +1,47 @@ +package band.kessoku.lib.mixin.networking.fabric.client; + +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.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import net.minecraft.client.network.ClientConnectionState; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket; + +import band.kessoku.lib.impl.networking.NetworkHandlerExtension; +import band.kessoku.lib.impl.networking.client.ClientNetworkingImpl; +import band.kessoku.lib.impl.networking.client.ClientPlayNetworkAddon; + +// We want to apply a bit earlier than other mods which may not use us in order to prevent refCount issues +@Mixin(value = ClientPlayNetworkHandler.class, priority = 999) +abstract class ClientPlayNetworkHandlerMixin extends ClientCommonNetworkHandler implements NetworkHandlerExtension { + @Unique + private ClientPlayNetworkAddon kessokulib$addon; + + protected ClientPlayNetworkHandlerMixin(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { + super(client, connection, connectionState); + } + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.kessokulib$addon = new ClientPlayNetworkAddon((ClientPlayNetworkHandler) (Object) this, this.client); + // A bit of a hack but it allows the field above to be set in case someone registers handlers during INIT event which refers to said field + ClientNetworkingImpl.setClientPlayAddon(this.kessokulib$addon); + this.kessokulib$addon.lateInit(); + } + + @Inject(method = "onGameJoin", at = @At("RETURN")) + private void handleServerPlayReady(GameJoinS2CPacket packet, CallbackInfo ci) { + this.kessokulib$addon.onServerReady(); + } + + @Override + public ClientPlayNetworkAddon kessokulib$getNetworkAddon() { + return this.kessokulib$addon; + } +} diff --git a/networking/fabric/src/main/resources/fabric.mod.json b/networking/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..16fbe7ca --- /dev/null +++ b/networking/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": 1, + "id": "kessoku_networking", + "version": "${version}", + "name": "Kessoku Networking", + "description": "Simple network API.", + "authors": [ + "Kessoku Tea Time" + ], + "contact": { + "homepage": "https://modrinth.com/mod/kessoku-lib", + "sources": "https://github.com/KessokuTeaTime/KessokuLib", + "issues": "https://github.com/KessokuTeaTime/KessokuLib/issues" + }, + "license": "LGPL-3.0-only", + "icon": "icon.png", + "entrypoints": { + "main": [ + "band.kessoku.lib.impl.networking.fabric.KessokuNetworkingFabric::onInitialize" + ], + "client": [ + "band.kessoku.lib.impl.networking.fabric.KessokuNetworkingFabric::onInitializeClient" + ] + }, + "environment": "*", + "depends": { + "fabricloader": ">=0.16.0", + "minecraft": "1.21", + "java": ">=21", + "fabric-api": "*" + }, + "custom": { + "modmenu": { + "badges": [ + "library" + ] + } + } +} \ No newline at end of file diff --git a/networking/fabric/src/main/resources/kessoku-networking.fabric.mixins.json b/networking/fabric/src/main/resources/kessoku-networking.fabric.mixins.json new file mode 100644 index 00000000..8279be6b --- /dev/null +++ b/networking/fabric/src/main/resources/kessoku-networking.fabric.mixins.json @@ -0,0 +1,29 @@ +{ + "required": true, + "package": "band.kessoku.lib.mixin.networking.fabric", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "ClientConnectionMixin", + "CommandManagerMixin", + "CustomPayloadC2SPacketMixin", + "CustomPayloadPacketCodecMixin", + "CustomPayloadS2CPacketMixin", + "LoginQueryRequestS2CPacketMixin", + "LoginQueryResponseC2SPacketMixin", + "PacketCodecDispatcherMixin", + "PlayerManagerMixin", + "ServerCommonNetworkHandlerMixin", + "ServerConfigurationNetworkHandlerMixin", + "ServerLoginNetworkHandlerMixin", + "ServerPlayNetworkHandlerMixin" + ], + "client": [ + "client.ClientCommonNetworkHandlerMixin", + "client.ClientConfigurationNetworkHandlerMixin", + "client.ClientLoginNetworkHandlerMixin", + "client.ClientPlayNetworkHandlerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/networking/neo/build.gradle b/networking/neo/build.gradle index c2a5208c..bf67af80 100644 --- a/networking/neo/build.gradle +++ b/networking/neo/build.gradle @@ -5,7 +5,8 @@ apply from: rootProject.file("gradle/scripts/klib-neo.gradle") base.archivesName = rootProject.name + "-networking" kessoku { - module("base", "common") + modules(["base", "event-base"], "common") + module("base", "neo") common("networking", ModPlatform.NEOFORGE) shadowBundle("networking", ModPlatform.NEOFORGE) diff --git a/networking/neo/src/main/java/band/kessoku/lib/impl/networking/neoforge/CommonPacketsImplNeoForge.java b/networking/neo/src/main/java/band/kessoku/lib/impl/networking/neoforge/CommonPacketsImplNeoForge.java new file mode 100644 index 00000000..2157b5bc --- /dev/null +++ b/networking/neo/src/main/java/band/kessoku/lib/impl/networking/neoforge/CommonPacketsImplNeoForge.java @@ -0,0 +1,84 @@ +package band.kessoku.lib.impl.networking.neoforge; + +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.PayloadTypeRegistryImpl; +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; +import net.minecraft.network.NetworkSide; +import net.minecraft.util.Identifier; + +import static band.kessoku.lib.impl.networking.GlobalReceiverRegistry.DEFAULT_CHANNEL_NAME_MAX_LENGTH; + +public class CommonPacketsImplNeoForge 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)); + } + } + }); + } + + public static void assertPayloadType(PayloadTypeRegistryImpl payloadTypeRegistry, Identifier channelName, NetworkSide side, NetworkPhase phase) { + 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)); + } + } +} diff --git a/networking/neo/src/main/java/band/kessoku/lib/impl/networking/neoforge/KessokuNetworkingNeoForge.java b/networking/neo/src/main/java/band/kessoku/lib/impl/networking/neoforge/KessokuNetworkingNeoForge.java new file mode 100644 index 00000000..84cb560f --- /dev/null +++ b/networking/neo/src/main/java/band/kessoku/lib/impl/networking/neoforge/KessokuNetworkingNeoForge.java @@ -0,0 +1,51 @@ +package band.kessoku.lib.impl.networking.neoforge; + +import band.kessoku.lib.api.KessokuLib; +import band.kessoku.lib.api.KessokuNetworking; +import band.kessoku.lib.api.base.neoforge.NeoEventUtils; +import band.kessoku.lib.api.networking.server.ServerConfigurationConnectionEvent; +import band.kessoku.lib.impl.networking.NetworkingImpl; +import band.kessoku.lib.impl.networking.client.ClientNetworkingImpl; +import band.kessoku.lib.impl.networking.common.CommonPacketsImpl; +import net.minecraft.SharedConstants; +import net.minecraft.server.command.DebugConfigCommand; +import net.minecraft.server.network.ServerConfigurationNetworkHandler; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.network.event.RegisterConfigurationTasksEvent; + +@Mod(KessokuNetworking.MOD_ID) +public class KessokuNetworkingNeoForge { + public KessokuNetworkingNeoForge(IEventBus modEventBus) { + KessokuLib.loadModule(KessokuNetworking.class); + + CommonPacketsImplNeoForge.init(); + NetworkingImpl.init(); + + if (FMLLoader.getDist().isClient()) { + ClientNetworkingImpl.clientInit(); + } + + NeoEventUtils.registerEvent(NeoForge.EVENT_BUS, RegisterCommandsEvent.class, event -> { + if (SharedConstants.isDevelopment) { + // Command is registered when isDevelopment is set. + return; + } + + if (FMLLoader.isProduction()) { + // Only register this command in a dev env + return; + } + + DebugConfigCommand.register(event.getDispatcher()); + }); + +// NeoEventUtils.registerEvent(modEventBus, RegisterConfigurationTasksEvent.class, event -> { +// ServerConfigurationNetworkHandler listener = (ServerConfigurationNetworkHandler) event.getListener(); +// ServerConfigurationConnectionEvent.CONFIGURE.invoker().onSendConfiguration(listener, listener.server); +// }); + } +} diff --git a/networking/neo/src/main/java/band/kessoku/lib/mixin/networking/neoforge/ServerConfigurationNetworkHandlerMixin.java b/networking/neo/src/main/java/band/kessoku/lib/mixin/networking/neoforge/ServerConfigurationNetworkHandlerMixin.java new file mode 100644 index 00000000..4225b38d --- /dev/null +++ b/networking/neo/src/main/java/band/kessoku/lib/mixin/networking/neoforge/ServerConfigurationNetworkHandlerMixin.java @@ -0,0 +1,148 @@ +package band.kessoku.lib.mixin.networking.neoforge; + +import band.kessoku.lib.api.networking.ServerConfigurationNetworkHandlerExtension; +import band.kessoku.lib.impl.networking.NetworkHandlerExtension; +import band.kessoku.lib.impl.networking.server.ServerConfigurationNetworkAddon; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerCommonNetworkHandler; +import net.minecraft.server.network.ServerConfigurationNetworkHandler; +import net.minecraft.server.network.ServerPlayerConfigurationTask; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +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 java.util.Queue; + +// We want to apply a bit earlier than other mods which may not use us in order to prevent refCount issues +@Mixin(value = ServerConfigurationNetworkHandler.class, priority = 900) +public abstract class ServerConfigurationNetworkHandlerMixin extends ServerCommonNetworkHandler implements NetworkHandlerExtension, ServerConfigurationNetworkHandlerExtension { + @Shadow + @Nullable + private ServerPlayerConfigurationTask currentTask; + + @Shadow + protected abstract void onTaskFinished(ServerPlayerConfigurationTask.Key key); + + @Shadow + @Final + private Queue tasks; + + @Shadow + public abstract boolean isConnectionOpen(); + + @Shadow + public abstract void sendConfigurations(); + + @Unique + private ServerConfigurationNetworkAddon kessokulib$addon; + + @Unique + private boolean kessokulib$sentConfiguration; + + @Unique + private boolean kessokulib$earlyTaskExecution; + + public ServerConfigurationNetworkHandlerMixin(MinecraftServer server, ClientConnection connection, ConnectedClientData arg) { + super(server, connection, arg); + } + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.kessokulib$addon = new ServerConfigurationNetworkAddon((ServerConfigurationNetworkHandler) (Object) this, this.server); + // A bit of a hack but it allows the field above to be set in case someone registers handlers during INIT event which refers to said field + this.kessokulib$addon.lateInit(); + } + + @Inject(method = "sendConfigurations", at = @At("HEAD"), cancellable = true) + private void onClientReady(CallbackInfo ci) { + // Send the initial channel registration packet + if (this.kessokulib$addon.startConfiguration()) { + assert currentTask == null; + ci.cancel(); + return; + } + + // Ready to start sending packets + if (!kessokulib$sentConfiguration) { + this.kessokulib$addon.preConfig(); + kessokulib$sentConfiguration = true; + kessokulib$earlyTaskExecution = true; + } + + // Run the early tasks + if (kessokulib$earlyTaskExecution) { + if (kessokulib$pollEarlyTasks()) { + ci.cancel(); + return; + } else { + kessokulib$earlyTaskExecution = false; + } + } + + // All early tasks should have been completed + assert currentTask == null; + assert tasks.isEmpty(); + + // Run the vanilla tasks. + this.kessokulib$addon.config(); + } + + @Unique + private boolean kessokulib$pollEarlyTasks() { + if (!kessokulib$earlyTaskExecution) { + throw new IllegalStateException("Early task execution has finished"); + } + + if (this.currentTask != null) { + throw new IllegalStateException("Task " + this.currentTask.getKey().id() + " has not finished yet"); + } + + if (!this.isConnectionOpen()) { + return false; + } + + final ServerPlayerConfigurationTask task = this.tasks.poll(); + + if (task != null) { + this.currentTask = task; + task.sendPacket(this::send); + return true; + } + + return false; + } + + @Override + public ServerConfigurationNetworkAddon kessokulib$getNetworkAddon() { + return kessokulib$addon; + } + + @Override + public void kessokulib$addTask(ServerPlayerConfigurationTask task) { + tasks.add(task); + } + + @Override + public void kessokulib$completeTask(ServerPlayerConfigurationTask.Key key) { + if (!kessokulib$earlyTaskExecution) { + onTaskFinished(key); + return; + } + + final ServerPlayerConfigurationTask.Key currentKey = this.currentTask != null ? this.currentTask.getKey() : null; + + if (!key.equals(currentKey)) { + throw new IllegalStateException("Unexpected request for task finish, current task: " + currentKey + ", requested: " + key); + } + + this.currentTask = null; + sendConfigurations(); + } +} diff --git a/networking/neo/src/main/resources/kessoku-networking.neoforge.mixins.json b/networking/neo/src/main/resources/kessoku-networking.neoforge.mixins.json new file mode 100644 index 00000000..f63413d4 --- /dev/null +++ b/networking/neo/src/main/resources/kessoku-networking.neoforge.mixins.json @@ -0,0 +1,13 @@ +{ + "required": true, + "package": "band.kessoku.lib.mixin.networking.neoforge", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "ServerConfigurationNetworkHandlerMixin" + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file