diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric.zip b/forge/src/main/java/com/seibel/lod/forge/fabric.zip deleted file mode 100644 index 44833cbb8..000000000 Binary files a/forge/src/main/java/com/seibel/lod/forge/fabric.zip and /dev/null differ diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/C2SPlayChannelEvents.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/C2SPlayChannelEvents.java new file mode 100644 index 000000000..e2380f8de --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/C2SPlayChannelEvents.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.client.networking.v1; + +import java.util.List; +import com.seibel.lod.forge.fabric.api.event.Event; +import com.seibel.lod.forge.fabric.api.event.EventFactory; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketSender; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.resources.ResourceLocation; + +/** + * Offers access to events related to the indication of a connected server's ability to receive packets in certain channels. + */ + +public final class C2SPlayChannelEvents { + /** + * 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 = EventFactory.createArrayBacked(Register.class, callbacks -> (handler, sender, client, channels) -> { + for (Register callback : callbacks) { + 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 = EventFactory.createArrayBacked(Unregister.class, callbacks -> (handler, sender, client, channels) -> { + for (Unregister callback : callbacks) { + callback.onChannelUnregister(handler, sender, client, channels); + } + }); + + private C2SPlayChannelEvents() { + } + + /** + * @see C2SPlayChannelEvents#REGISTER + */ + + @FunctionalInterface + public interface Register { + void onChannelRegister(ClientPacketListener handler, PacketSender sender, Minecraft client, List channels); + } + + /** + * @see C2SPlayChannelEvents#UNREGISTER + */ + + @FunctionalInterface + public interface Unregister { + void onChannelUnregister(ClientPacketListener handler, PacketSender sender, Minecraft client, List channels); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientLoginConnectionEvents.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientLoginConnectionEvents.java new file mode 100644 index 000000000..e6dcfdc43 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientLoginConnectionEvents.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.client.networking.v1; + +import com.seibel.lod.forge.fabric.api.event.Event; +import com.seibel.lod.forge.fabric.api.event.EventFactory; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl; +import net.minecraft.resources.ResourceLocation; + +/** + * 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 ClientLoginConnectionEvents { + /** + * 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(ResourceLocation, ClientLoginNetworking.LoginQueryRequestHandler) + */ + public static final Event INIT = EventFactory.createArrayBacked(Init.class, callbacks -> (handler, client) -> { + for (Init callback : callbacks) { + 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 = EventFactory.createArrayBacked(QueryStart.class, callbacks -> (handler, client) -> { + for (QueryStart callback : callbacks) { + 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 = EventFactory.createArrayBacked(Disconnect.class, callbacks -> (handler, client) -> { + for (Disconnect callback : callbacks) { + callback.onLoginDisconnect(handler, client); + } + }); + + private ClientLoginConnectionEvents() { + } + + /** + * @see ClientLoginConnectionEvents#INIT + */ + + @FunctionalInterface + public interface Init { + void onLoginStart(ClientHandshakePacketListenerImpl handler, Minecraft client); + } + + /** + * @see ClientLoginConnectionEvents#QUERY_START + */ + + @FunctionalInterface + public interface QueryStart { + void onLoginQueryStart(ClientHandshakePacketListenerImpl handler, Minecraft client); + } + + /** + * @see ClientLoginConnectionEvents#DISCONNECT + */ + + @FunctionalInterface + public interface Disconnect { + void onLoginDisconnect(ClientHandshakePacketListenerImpl handler, Minecraft client); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientLoginNetworking.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientLoginNetworking.java new file mode 100644 index 000000000..ac80b1b27 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientLoginNetworking.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.client.networking.v1; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.jetbrains.annotations.Nullable; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerLoginNetworking; +import com.seibel.lod.forge.fabric.impl.networking.client.ClientNetworkingImpl; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.PacketListener; +import net.minecraft.resources.ResourceLocation; + +/** + * 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(ResourceLocation)} 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(ResourceLocation) + * @see ClientLoginNetworking#registerReceiver(ResourceLocation, LoginQueryRequestHandler) + */ + public static boolean registerGlobalReceiver(ResourceLocation 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(ResourceLocation, LoginQueryRequestHandler) + * @see ClientLoginNetworking#unregisterReceiver(ResourceLocation) + */ + @Nullable + public static ClientLoginNetworking.LoginQueryRequestHandler unregisterGlobalReceiver(ResourceLocation 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(ResourceLocation)} 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(ResourceLocation channelName, LoginQueryRequestHandler queryHandler) throws IllegalStateException { + final Connection connection = ClientNetworkingImpl.getLoginConnection(); + + if (connection != null) { + final PacketListener packetListener = connection.getPacketListener(); + + if (packetListener instanceof ClientHandshakePacketListenerImpl) { + return ClientNetworkingImpl.getAddon(((ClientHandshakePacketListenerImpl) 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(ResourceLocation channelName) throws IllegalStateException { + final Connection connection = ClientNetworkingImpl.getLoginConnection(); + + if (connection != null) { + final PacketListener packetListener = connection.getPacketListener(); + + if (packetListener instanceof ClientHandshakePacketListenerImpl) { + return ClientNetworkingImpl.getAddon(((ClientHandshakePacketListenerImpl) 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.BlockableEventLoop#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 listenerAdder 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 FriendlyByteBuf> receive(Minecraft client, ClientHandshakePacketListenerImpl handler, FriendlyByteBuf buf, Consumer>> listenerAdder); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientPlayConnectionEvents.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientPlayConnectionEvents.java new file mode 100644 index 000000000..ad4e16a33 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientPlayConnectionEvents.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.client.networking.v1; + +import com.seibel.lod.forge.fabric.api.event.Event; +import com.seibel.lod.forge.fabric.api.event.EventFactory; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketSender; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.resources.ResourceLocation; + +/** + * Offers access to events related to the connection to a server on a logical client. + */ + +public final class ClientPlayConnectionEvents { + /** + * Event indicating a connection entered the PLAY state, ready for registering channel handlers. + * + * @see ClientPlayNetworking#registerReceiver(ResourceLocation, ClientPlayNetworking.PlayChannelHandler) + */ + public static final Event INIT = EventFactory.createArrayBacked(Init.class, callbacks -> (handler, client) -> { + for (Init callback : callbacks) { + 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 setup. + */ + public static final Event JOIN = EventFactory.createArrayBacked(Join.class, callbacks -> (handler, sender, client) -> { + for (Join callback : callbacks) { + 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 = EventFactory.createArrayBacked(Disconnect.class, callbacks -> (handler, client) -> { + for (Disconnect callback : callbacks) { + callback.onPlayDisconnect(handler, client); + } + }); + + private ClientPlayConnectionEvents() { + } + + + @FunctionalInterface + public interface Init { + void onPlayInit(ClientPacketListener handler, Minecraft client); + } + + + @FunctionalInterface + public interface Join { + void onPlayReady(ClientPacketListener handler, PacketSender sender, Minecraft client); + } + + + @FunctionalInterface + public interface Disconnect { + void onPlayDisconnect(ClientPacketListener handler, Minecraft client); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientPlayNetworking.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientPlayNetworking.java new file mode 100644 index 000000000..7ebcfe9c1 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/ClientPlayNetworking.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.client.networking.v1; + +import java.util.Objects; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketSender; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerPlayNetworking; +import com.seibel.lod.forge.fabric.impl.networking.client.ClientNetworkingImpl; +import com.seibel.lod.forge.fabric.impl.networking.client.ClientPlayNetworkAddon; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.resources.ResourceLocation; + +/** + * 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. + * + *

This class should be only used on the physical client and for the logical client. + * + * @see ClientLoginNetworking + * @see ServerPlayNetworking + */ + +public final class ClientPlayNetworking { + /** + * Registers a handler to a 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(ResourceLocation)} 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 ClientPlayNetworking#unregisterGlobalReceiver(ResourceLocation) + * @see ClientPlayNetworking#registerReceiver(ResourceLocation, PlayChannelHandler) + */ + public static boolean registerGlobalReceiver(ResourceLocation channelName, PlayChannelHandler channelHandler) { + return ClientNetworkingImpl.PLAY.registerGlobalReceiver(channelName, channelHandler); + } + + /** + * Removes the handler of a 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 ClientPlayNetworking#registerGlobalReceiver(ResourceLocation, PlayChannelHandler) + * @see ClientPlayNetworking#unregisterReceiver(ResourceLocation) + */ + @Nullable + public static PlayChannelHandler unregisterGlobalReceiver(ResourceLocation channelName) { + return ClientNetworkingImpl.PLAY.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 ClientNetworkingImpl.PLAY.getChannels(); + } + + /** + * Registers a handler to a channel. + * + *

If a handler is already registered to the {@code channel}, this method will return {@code false}, and no change will be made. + * Use {@link #unregisterReceiver(ResourceLocation)} to unregister the existing handler. + * + *

For example, if you only register a receiver using this method when a {@linkplain ClientLoginNetworking#registerGlobalReceiver(ResourceLocation, ClientLoginNetworking.LoginQueryRequestHandler)} + * login query has been received, you should use {@link ClientPlayConnectionEvents#INIT} to register the channel handler. + * + * @param channelName the id of the channel + * @return false if a handler is already registered to the channel + * @throws IllegalStateException if the client is not connected to a server + * @see ClientPlayConnectionEvents#INIT + */ + public static boolean registerReceiver(ResourceLocation channelName, PlayChannelHandler channelHandler) { + final ClientPlayNetworkAddon addon = ClientNetworkingImpl.getClientPlayAddon(); + + if (addon != null) { + return addon.registerChannel(channelName, channelHandler); + } + + throw new IllegalStateException("Cannot register receiver while not in game!"); + } + + /** + * Removes the handler of a 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 + * @throws IllegalStateException if the client is not connected to a server + */ + @Nullable + public static PlayChannelHandler unregisterReceiver(ResourceLocation channelName) throws IllegalStateException { + final ClientPlayNetworkAddon addon = ClientNetworkingImpl.getClientPlayAddon(); + + if (addon != null) { + return addon.unregisterChannel(channelName); + } + + 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 packet on a specified channel name. + * + * @param channelName the channel name + * @return 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(ResourceLocation channelName) throws IllegalArgumentException { + // You cant send without a client player, so this is fine + if (Minecraft.getInstance().getConnection() != null) { + return ClientNetworkingImpl.getAddon(Minecraft.getInstance().getConnection()).getSendableChannels().contains(channelName); + } + + return false; + } + + /** + * Creates a packet which may be sent to a the connected server. + * + * @param channelName the channel name + * @param buf the packet byte buf which represents the payload of the packet + * @return a new packet + */ + public static Packet createC2SPacket(ResourceLocation channelName, FriendlyByteBuf buf) { + Objects.requireNonNull(channelName, "Channel name cannot be null"); + Objects.requireNonNull(buf, "Buf cannot be null"); + + return ClientNetworkingImpl.createPlayC2SPacket(channelName, buf); + } + + /** + * 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 { + // You cant send without a client player, so this is fine + if (Minecraft.getInstance().getConnection() != null) { + return ClientNetworkingImpl.getAddon(Minecraft.getInstance().getConnection()); + } + + throw new IllegalStateException("Cannot get packet sender when not in game!"); + } + + /** + * Sends a packet to the connected server. + * + * @param channelName the channel of the packet + * @param buf the payload of the packet + * @throws IllegalStateException if the client is not connected to a server + */ + public static void send(ResourceLocation channelName, FriendlyByteBuf buf) throws IllegalStateException { + // You cant send without a client player, so this is fine + if (Minecraft.getInstance().getConnection() != null) { + Minecraft.getInstance().getConnection().send(createC2SPacket(channelName, buf)); + return; + } + + throw new IllegalStateException("Cannot send packets when not in game!"); + } + + private ClientPlayNetworking() { + } + + @FunctionalInterface + public interface PlayChannelHandler { + /** + * Handles an incoming packet. + * + *

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.BlockableEventLoop#submit(Runnable) scheduled} using the provided Minecraft client instance. + * + *

An example usage of this is to display an overlay message: + *

{@code
+		 * ClientPlayNetworking.registerReceiver(new Identifier("mymod", "overlay"), (client, handler, buf, responseSender) -&rt; {
+		 * 	String message = buf.readString(32767);
+		 *
+		 * 	// All operations on the server or world must be executed on the server thread
+		 * 	client.execute(() -&rt; {
+		 * 		client.inGameHud.setOverlayMessage(message, true);
+		 * 	});
+		 * });
+		 * }
+ * @param client the client + * @param handler the network handler that received this packet + * @param buf the payload of the packet + * @param responseSender the packet sender + */ + void receive(Minecraft client, ClientPacketListener handler, FriendlyByteBuf buf, PacketSender responseSender); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/package-info.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/package-info.java new file mode 100644 index 000000000..903beb181 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/client/networking/v1/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Networking API (client side), version 1. + * + *

For login stage networking see {@link net.fabricmc.fabric.api.client.networking.v1.ClientLoginNetworking}. + * For play stage networking see {@link net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking}. + * + *

For events related to connection to a server see {@link net.fabricmc.fabric.api.client.networking.v1.ClientLoginConnectionEvents} for login stage + * or {@link net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents} for play stage. + * + *

For events related to the ability of a server to receive packets on a channel of a specific name see {@link net.fabricmc.fabric.api.client.networking.v1.C2SPlayChannelEvents}. + */ + +package com.seibel.lod.forge.fabric.api.client.networking.v1; diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/AutoInvokingEvent.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/AutoInvokingEvent.java new file mode 100644 index 000000000..c06415af8 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/AutoInvokingEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.event; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that this {@link Event} is auto-invoking: + * it calls the event callback implemented by a context parameter type automatically and without registration. + * + *

This means that this event can be listened to in two ways: + *

    + *
  • If the consumer is the context parameter and it implements the callback, it will be automatically invoked, don't register manually. + *
  • Otherwise, there is no invocation and the listener needs manual registration as usual. + *
+ * + *

Do note that there may be more than one context parameter. + * + *

A typical use case is feature augmentation, for example to expose raw clicks to slots. + * The event callback has a slot parameter - the context parameter - and the event itself is carrying this annotation. + * All the slot needs to receive slot clicks is to implement {@code SlotClickCallback} on itself. + * It shouldn't do any explicit event registration like {@code SLOT_CLICK_EVENT.register(this::onSlotClick)}, + * otherwise it will see extraneous callback invocations. + * + *

In general, an auto-invoking event bridges the gap between the flexibility of an event with global reach, + * and the convenience of implementing an interface that gets detected automatically. + * + *

This is a documentation-only annotation, the event factory has to implement the functionality explicitly by checking the parameter type and invoking it. + * On top of adding this annotation, the event field or method should document which parameters are context parameters, + * and under which circumstances they are invoked. + */ + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD }) +public @interface AutoInvokingEvent { +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/Event.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/Event.java new file mode 100644 index 000000000..bc6dc704d --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/Event.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.event; + +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.resources.ResourceLocation; + +/** + * Base class for Fabric's event implementations. + * + * @param The listener type. + * @see EventFactory + */ +@ApiStatus.NonExtendable // Should only be extended by fabric API. +public abstract class Event { + /** + * The invoker field. This should be updated by the implementation to + * always refer to an instance containing all code that should be + * executed upon event emission. + */ + protected volatile T invoker; + + /** + * Returns the invoker instance. + * + *

An "invoker" is an object which hides multiple registered + * listeners of type T under one instance of type T, executing + * them and leaving early as necessary. + * + * @return The invoker instance. + */ + public final T invoker() { + return invoker; + } + + /** + * Register a listener to the event, in the default phase. + * Have a look at {@link #addPhaseOrdering} for an explanation of event phases. + * + * @param listener The desired listener. + */ + public abstract void register(T listener); + + /** + * The ResourceLocation of the default phase. + * Have a look at {@link EventFactory#createWithPhases} for an explanation of event phases. + */ + public static final ResourceLocation DEFAULT_PHASE = new ResourceLocation("fabric", "default"); + + /** + * Register a listener to the event for the specified phase. + * Have a look at {@link EventFactory#createWithPhases} for an explanation of event phases. + * + * @param phase ResourceLocation of the phase this listener should be registered for. It will be created if it didn't exist yet. + * @param listener The desired listener. + */ + public void register(ResourceLocation phase, T listener) { + // This is done to keep compatibility with existing Event subclasses, but they should really not be subclassing Event. + register(listener); + } + + /** + * Request that listeners registered for one phase be executed before listeners registered for another phase. + * Relying on the default phases supplied to {@link EventFactory#createWithPhases} should be preferred over manually + * registering phase ordering dependencies. + * + *

Incompatible ordering constraints such as cycles will lead to inconsistent behavior: + * some constraints will be respected and some will be ignored. If this happens, a warning will be logged. + * + * @param firstPhase The ResourceLocation of the phase that should run before the other. It will be created if it didn't exist yet. + * @param secondPhase The ResourceLocation of the phase that should run after the other. It will be created if it didn't exist yet. + */ + public void addPhaseOrdering(ResourceLocation firstPhase, ResourceLocation secondPhase) { + // This is not abstract to avoid breaking existing Event subclasses, but they should really not be subclassing Event. + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/EventFactory.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/EventFactory.java new file mode 100644 index 000000000..97808e030 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/event/EventFactory.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.event; + +import java.util.function.Function; + +import net.minecraft.resources.ResourceLocation; + +import com.seibel.lod.forge.fabric.impl.base.event.EventFactoryImpl; + +/** + * Helper for creating {@link Event} classes. + */ +public final class EventFactory { + private static boolean profilingEnabled = true; + + private EventFactory() { } + + /** + * @return True if events are supposed to be profiled. + */ + public static boolean isProfilingEnabled() { + return profilingEnabled; + } + + /** + * Invalidate and re-create all existing "invoker" instances across + * events created by this EventFactory. Use this if, for instance, + * the profilingEnabled field changes. + */ + // TODO: Turn this into an event? + public static void invalidate() { + EventFactoryImpl.invalidate(); + } + + /** + * Create an "array-backed" Event instance. + * + *

If your factory simply delegates to the listeners without adding custom behavior, + * consider using {@linkplain #createArrayBacked(Class, Object, Function) the other overload} + * if performance of this event is critical. + * + * @param type The listener class type. + * @param invokerFactory The invoker factory, combining multiple listeners into one instance. + * @param The listener type. + * @return The Event instance. + */ + public static Event createArrayBacked(Class type, Function invokerFactory) { + return EventFactoryImpl.createArrayBacked(type, invokerFactory); + } + + /** + * Create an "array-backed" Event instance with a custom empty invoker, + * for an event whose {@code invokerFactory} only delegates to the listeners. + *

    + *
  • If there is no listener, the custom empty invoker will be used.
  • + *
  • If there is only one listener, that one will be used as the invoker + * and the factory will not be called.
  • + *
  • Only when there are at least two listeners will the factory be used.
  • + *
+ * + *

Having a custom empty invoker (of type (...) -> {}) increases performance + * relative to iterating over an empty array; however, it only really matters + * if the event is executed thousands of times a second. + * + * @param type The listener class type. + * @param emptyInvoker The custom empty invoker. + * @param invokerFactory The invoker factory, combining multiple listeners into one instance. + * @param The listener type. + * @return The Event instance. + */ + public static Event createArrayBacked(Class type, T emptyInvoker, Function invokerFactory) { + return createArrayBacked(type, listeners -> { + if (listeners.length == 0) { + return emptyInvoker; + } else if (listeners.length == 1) { + return listeners[0]; + } else { + return invokerFactory.apply(listeners); + } + }); + } + + /** + * Create an array-backed event with a list of default phases that get invoked in order. + * Exposing the ResourceLocations of the default phases as {@code public static final} constants is encouraged. + * + *

An event phase is a named group of listeners, which may be ordered before or after other groups of listeners. + * This allows some listeners to take priority over other listeners. + * Adding separate events should be considered before making use of multiple event phases. + * + *

Phases may be freely added to events created with any of the factory functions, + * however using this function is preferred for widely used event phases. + * If more phases are necessary, discussion with the author of the Event is encouraged. + * + *

Refer to {@link Event#addPhaseOrdering} for an explanation of event phases. + * + * @param type The listener class type. + * @param invokerFactory The invoker factory, combining multiple listeners into one instance. + * @param defaultPhases The default phases of this event, in the correct order. Must contain {@link Event#DEFAULT_PHASE}. + * @param The listener type. + * @return The Event instance. + */ + public static Event createWithPhases(Class type, Function invokerFactory, ResourceLocation... defaultPhases) { + EventFactoryImpl.ensureContainsDefault(defaultPhases); + EventFactoryImpl.ensureNoDuplicates(defaultPhases); + + Event event = createArrayBacked(type, invokerFactory); + + for (int i = 1; i < defaultPhases.length; ++i) { + event.addPhaseOrdering(defaultPhases[i-1], defaultPhases[i]); + } + + return event; + } + + /** + * Get the listener object name. This can be used in debugging/profiling + * scenarios. + * + * @param handler The listener object. + * @return The listener name. + */ + public static String getHandlerName(Object handler) { + return handler.getClass().getName(); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/EntityTrackingEvents.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/EntityTrackingEvents.java new file mode 100644 index 000000000..c369b64d8 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/EntityTrackingEvents.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import com.seibel.lod.forge.fabric.api.event.Event; +import com.seibel.lod.forge.fabric.api.event.EventFactory; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +/** + * Events related to a tracking entities within a player's view distance. + */ +public final class EntityTrackingEvents { + /** + * An event that is called before player starts tracking an entity. + * Typically this occurs when an entity enters a client's view distance. + * This event is called before the player's client is sent the entity's {@link Entity#getAddEntityPacket() spawn packet}. + */ + public static final Event START_TRACKING = EventFactory.createArrayBacked(StartTracking.class, callbacks -> (trackedEntity, player) -> { + for (StartTracking callback : callbacks) { + callback.onStartTracking(trackedEntity, player); + } + }); + + /** + * An event that is called after a player has stopped tracking an entity. + * The client at this point was sent a packet to {@link net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket destroy} the entity on the client. + * The entity still exists on the server. + */ + public static final Event STOP_TRACKING = EventFactory.createArrayBacked(StopTracking.class, callbacks -> (trackedEntity, player) -> { + for (StopTracking callback : callbacks) { + callback.onStopTracking(trackedEntity, player); + } + }); + + @FunctionalInterface + public interface StartTracking { + /** + * Called before an entity starts getting tracked by a player. + * + * @param trackedEntity the entity that will be tracked + * @param player the player that will track the entity + */ + void onStartTracking(Entity trackedEntity, ServerPlayer player); + } + + @FunctionalInterface + public interface StopTracking { + /** + * Called after an entity stops getting tracked by a player. + * + * @param trackedEntity the entity that is no longer being tracked + * @param player the player that is no longer tracking the entity + */ + void onStopTracking(Entity trackedEntity, ServerPlayer player); + } + + private EntityTrackingEvents() { + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/FutureListeners.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/FutureListeners.java new file mode 100644 index 000000000..88fee7bd1 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/FutureListeners.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import java.util.Objects; +import net.minecraft.network.FriendlyByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.GenericFutureListener; + +/** + * Utilities for working with netty's future listeners. + * @see FutureListener + * @see ChannelFutureListener + */ +public final class FutureListeners { + /** + * Returns a future listener that releases a packet byte buf when the buffer has been sent to a remote connection. + * + * @param buf the buffer + * @return the future listener + */ + public static ChannelFutureListener free(FriendlyByteBuf buf) { + Objects.requireNonNull(buf, "PacketByteBuf cannot be null"); + + return (future) -> { + if (!isLocalChannel(future.channel())) { + buf.release(); + } + }; + } + + /** + * Returns whether a netty channel performs local transportation, or if the message objects in the channel are directly passed than written to and read from a byte buf. + * + * @param channel the channel to check + * @return whether the channel is local + */ + public static boolean isLocalChannel(Channel channel) { + return channel instanceof LocalServerChannel || channel instanceof LocalChannel; + } + + /** + * Combines two future listeners. + * + * @param first the first future listener + * @param second the second future listener + * @param the future type of the first listener, used for casting + * @param the future type of the second listener, used for casting + * @return the combined future listener. + */ + // A, B exist just to allow casting + @SuppressWarnings("unchecked") + public static , B extends Future> GenericFutureListener> union(GenericFutureListener first, GenericFutureListener second) { + // Return an empty future listener in the case of both parameters somehow being null + if (first == null && second == null) { + return future -> { }; + } + + if (first == null) { + return second; + } + + if (second == null) { + return first; + } + + return future -> { + first.operationComplete((A) future); + second.operationComplete((B) future); + }; + } + + private FutureListeners() { + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/PacketByteBufs.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/PacketByteBufs.java new file mode 100644 index 000000000..dcec0210e --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/PacketByteBufs.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import java.util.Objects; +import net.minecraft.network.FriendlyByteBuf; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * Helper methods for working with and creating {@link FriendlyByteBuf}s. + */ +public final class PacketByteBufs { + private static final FriendlyByteBuf EMPTY_PACKET_BYTE_BUF = new FriendlyByteBuf(Unpooled.EMPTY_BUFFER); + + /** + * Returns an empty instance of packet byte buf. + * + * @return an empty buf + */ + public static FriendlyByteBuf empty() { + return EMPTY_PACKET_BYTE_BUF; + } + + /** + * Returns a new heap memory-backed instance of packet byte buf. + * + * @return a new buf + */ + public static FriendlyByteBuf create() { + return new FriendlyByteBuf(Unpooled.buffer()); + } + + // Convenience methods for byte buf methods that return a new byte buf + + /** + * Wraps the newly created buf from {@code buf.readBytes} in a packet byte buf. + * + * @param buf the original buf + * @param length the number of bytes to transfer + * @return the transferred bytes + * @see ByteBuf#readBytes(int) + */ + public static FriendlyByteBuf readBytes(ByteBuf buf, int length) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.readBytes(length)); + } + + /** + * Wraps the newly created buf from {@code buf.readSlice} in a packet byte buf. + * + * @param buf the original buf + * @param length the size of the new slice + * @return the newly created slice + * @see ByteBuf#readSlice(int) + */ + public static FriendlyByteBuf readSlice(ByteBuf buf, int length) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.readSlice(length)); + } + + /** + * Wraps the newly created buf from {@code buf.readRetainedSlice} in a packet byte buf. + * + * @param buf the original buf + * @param length the size of the new slice + * @return the newly created slice + * @see ByteBuf#readRetainedSlice(int) + */ + public static FriendlyByteBuf readRetainedSlice(ByteBuf buf, int length) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.readRetainedSlice(length)); + } + + /** + * Wraps the newly created buf from {@code buf.copy} in a packet byte buf. + * + * @param buf the original buf + * @return a copy of the buf + * @see ByteBuf#copy() + */ + public static FriendlyByteBuf copy(ByteBuf buf) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.copy()); + } + + /** + * Wraps the newly created buf from {@code buf.copy} in a packet byte buf. + * + * @param buf the original buf + * @param index the starting index + * @param length the size of the copy + * @return a copy of the buf + * @see ByteBuf#copy(int, int) + */ + public static FriendlyByteBuf copy(ByteBuf buf, int index, int length) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.copy(index, length)); + } + + /** + * Wraps the newly created buf from {@code buf.slice} in a packet byte buf. + * + * @param buf the original buf + * @return a slice of the buf + * @see ByteBuf#slice() + */ + public static FriendlyByteBuf slice(ByteBuf buf) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.slice()); + } + + /** + * Wraps the newly created buf from {@code buf.retainedSlice} in a packet byte buf. + * + * @param buf the original buf + * @return a slice of the buf + * @see ByteBuf#retainedSlice() + */ + public static FriendlyByteBuf retainedSlice(ByteBuf buf) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.retainedSlice()); + } + + /** + * Wraps the newly created buf from {@code buf.slice} in a packet byte buf. + * + * @param buf the original buf + * @param index the starting index + * @param length the size of the copy + * @return a slice of the buf + * @see ByteBuf#slice(int, int) + */ + public static FriendlyByteBuf slice(ByteBuf buf, int index, int length) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.slice(index, length)); + } + + /** + * Wraps the newly created buf from {@code buf.retainedSlice} in a packet byte buf. + * + * @param buf the original buf + * @param index the starting index + * @param length the size of the copy + * @return a slice of the buf + * @see ByteBuf#retainedSlice(int, int) + */ + public static FriendlyByteBuf retainedSlice(ByteBuf buf, int index, int length) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.retainedSlice(index, length)); + } + + /** + * Wraps the newly created buf from {@code buf.duplicate} in a packet byte buf. + * + * @param buf the original buf + * @return a duplicate of the buf + * @see ByteBuf#duplicate() + */ + public static FriendlyByteBuf duplicate(ByteBuf buf) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.duplicate()); + } + + /** + * Wraps the newly created buf from {@code buf.retainedDuplicate} in a packet byte buf. + * + * @param buf the original buf + * @return a duplicate of the buf + * @see ByteBuf#retainedDuplicate() + */ + public static FriendlyByteBuf retainedDuplicate(ByteBuf buf) { + Objects.requireNonNull(buf, "ByteBuf cannot be null"); + + return new FriendlyByteBuf(buf.retainedDuplicate()); + } + + private PacketByteBufs() { + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/PacketSender.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/PacketSender.java new file mode 100644 index 000000000..28b23c1d5 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/PacketSender.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import java.util.Objects; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.resources.ResourceLocation; +import io.netty.channel.ChannelFutureListener; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.jetbrains.annotations.Nullable; + +/** + * Represents something that supports sending packets to channels. + * @see PacketByteBufs + */ +public interface PacketSender { + /** + * Makes a packet for a channel. + * + * @param channelName the id of the channel + * @param buf the content of the packet + */ + Packet createPacket(ResourceLocation channelName, FriendlyByteBuf buf); + + /** + * Sends a packet. + * + * @param packet the packet + */ + void sendPacket(Packet packet); + + /** + * Sends a packet. + * + * @param packet the packet + * @param callback an optional callback to execute after the packet is sent, may be {@code null}. The callback may also accept a {@link ChannelFutureListener}. + */ + void sendPacket(Packet packet, @Nullable GenericFutureListener> callback); + + /** + * Sends a packet to a channel. + * + * @param channel the id of the channel + * @param buf the content of the packet + */ + default void sendPacket(ResourceLocation channel, FriendlyByteBuf 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} + */ + // the generic future listener can accept ChannelFutureListener + default void sendPacket(ResourceLocation channel, FriendlyByteBuf buf, @Nullable GenericFutureListener> 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/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/S2CPlayChannelEvents.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/S2CPlayChannelEvents.java new file mode 100644 index 000000000..9d17c21cb --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/S2CPlayChannelEvents.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import java.util.List; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import com.seibel.lod.forge.fabric.api.event.Event; +import com.seibel.lod.forge.fabric.api.event.EventFactory; + +/** + * Offers access to events related to the indication of a connected client's ability to receive packets in certain channels. + */ +public final class S2CPlayChannelEvents { + /** + * 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 = EventFactory.createArrayBacked(Register.class, callbacks -> (handler, sender, server, channels) -> { + for (Register callback : callbacks) { + 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 = EventFactory.createArrayBacked(Unregister.class, callbacks -> (handler, sender, server, channels) -> { + for (Unregister callback : callbacks) { + callback.onChannelUnregister(handler, sender, server, channels); + } + }); + + private S2CPlayChannelEvents() { + } + + /** + * @see S2CPlayChannelEvents#REGISTER + */ + @FunctionalInterface + public interface Register { + void onChannelRegister(ServerGamePacketListenerImpl handler, PacketSender sender, MinecraftServer server, List channels); + } + + /** + * @see S2CPlayChannelEvents#UNREGISTER + */ + @FunctionalInterface + public interface Unregister { + void onChannelUnregister(ServerGamePacketListenerImpl handler, PacketSender sender, MinecraftServer server, List channels); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerLoginConnectionEvents.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerLoginConnectionEvents.java new file mode 100644 index 000000000..20683c8fa --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerLoginConnectionEvents.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import com.seibel.lod.forge.fabric.api.event.Event; +import com.seibel.lod.forge.fabric.api.event.EventFactory; + +/** + * Offers access to events related to the connection to a client on a logical server while a client is logging in. + */ +public final class ServerLoginConnectionEvents { + /** + * Event indicating a connection entered the LOGIN state, ready for registering query response handlers. + * + * @see ServerLoginNetworking#registerReceiver(ServerLoginPacketListenerImpl, ResourceLocation, ServerLoginNetworking.LoginQueryResponseHandler) + */ + public static final Event INIT = EventFactory.createArrayBacked(Init.class, callbacks -> (handler, server) -> { + for (Init callback : callbacks) { + 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(ServerLoginPacketListenerImpl, ResourceLocation, 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 PacketSender}. + */ + public static final Event QUERY_START = EventFactory.createArrayBacked(QueryStart.class, callbacks -> (handler, server, sender, synchronizer) -> { + for (QueryStart callback : callbacks) { + 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 = EventFactory.createArrayBacked(Disconnect.class, callbacks -> (handler, server) -> { + for (Disconnect callback : callbacks) { + callback.onLoginDisconnect(handler, server); + } + }); + + private ServerLoginConnectionEvents() { + } + + /** + * @see ServerLoginConnectionEvents#INIT + */ + @FunctionalInterface + public interface Init { + void onLoginInit(ServerLoginPacketListenerImpl handler, MinecraftServer server); + } + + /** + * @see ServerLoginConnectionEvents#QUERY_START + */ + @FunctionalInterface + public interface QueryStart { + void onLoginStart(ServerLoginPacketListenerImpl handler, MinecraftServer server, PacketSender sender, ServerLoginNetworking.LoginSynchronizer synchronizer); + } + + /** + * @see ServerLoginConnectionEvents#DISCONNECT + */ + @FunctionalInterface + public interface Disconnect { + void onLoginDisconnect(ServerLoginPacketListenerImpl handler, MinecraftServer server); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerLoginNetworking.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerLoginNetworking.java new file mode 100644 index 000000000..8ca4a24ed --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerLoginNetworking.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Future; + +import org.jetbrains.annotations.Nullable; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientLoginNetworking; +import com.seibel.lod.forge.fabric.impl.networking.server.ServerNetworkingImpl; +import com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor.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 ClientLoginNetworking + */ +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(ResourceLocation)} 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(ResourceLocation) + * @see ServerLoginNetworking#registerReceiver(ServerLoginPacketListenerImpl, ResourceLocation, LoginQueryResponseHandler) + */ + public static boolean registerGlobalReceiver(ResourceLocation 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(ResourceLocation, LoginQueryResponseHandler) + * @see ServerLoginNetworking#unregisterReceiver(ServerLoginPacketListenerImpl, ResourceLocation) + */ + @Nullable + public static ServerLoginNetworking.LoginQueryResponseHandler unregisterGlobalReceiver(ResourceLocation 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(ServerLoginPacketListenerImpl, ResourceLocation)} 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(ServerLoginPacketListenerImpl networkHandler, ResourceLocation 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(ServerLoginPacketListenerImpl networkHandler, ResourceLocation 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(ServerLoginPacketListenerImpl handler) { + Objects.requireNonNull(handler, "Network handler cannot be null"); + + return ((ServerLoginNetworkHandlerAccessor) handler).getServer(); + } + + private ServerLoginNetworking() { + } + + @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.BlockableEventLoop#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, ServerLoginPacketListenerImpl handler, boolean understood, FriendlyByteBuf buf, LoginSynchronizer synchronizer, PacketSender responseSender); + } + + /** + * Allows blocking client log-in until all all futures passed into {@link LoginSynchronizer#waitFor(Future)} are completed. + * + * @apiNote this interface is not intended to be implemented by users of api. + */ + @FunctionalInterface + 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(new LiteralText("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(new LiteralText("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.BlockableEventLoop#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/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerPlayConnectionEvents.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerPlayConnectionEvents.java new file mode 100644 index 000000000..ad2134761 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerPlayConnectionEvents.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import com.seibel.lod.forge.fabric.api.event.Event; +import com.seibel.lod.forge.fabric.api.event.EventFactory; + +/** + * Offers access to events related to the connection to a client on a logical server while a client is in game. + */ +public final class ServerPlayConnectionEvents { + /** + * Event indicating a connection entered the PLAY state, ready for registering channel handlers. + * + * @see ServerPlayNetworking#registerReceiver(ServerGamePacketListenerImpl, ResourceLocation, ServerPlayNetworking.PlayChannelHandler) + */ + public static final Event INIT = EventFactory.createArrayBacked(Init.class, callbacks -> (handler, server) -> { + for (Init callback : callbacks) { + 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 = EventFactory.createArrayBacked(Join.class, callbacks -> (handler, sender, server) -> { + for (Join callback : callbacks) { + 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 = EventFactory.createArrayBacked(Disconnect.class, callbacks -> (handler, server) -> { + for (Disconnect callback : callbacks) { + callback.onPlayDisconnect(handler, server); + } + }); + + private ServerPlayConnectionEvents() { + } + + @FunctionalInterface + public interface Init { + void onPlayInit(ServerGamePacketListenerImpl handler, MinecraftServer server); + } + + @FunctionalInterface + public interface Join { + void onPlayReady(ServerGamePacketListenerImpl handler, PacketSender sender, MinecraftServer server); + } + + @FunctionalInterface + public interface Disconnect { + void onPlayDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerPlayNetworking.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerPlayNetworking.java new file mode 100644 index 000000000..5e27f260d --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/ServerPlayNetworking.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; + +import java.util.Objects; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientPlayNetworking; +import com.seibel.lod.forge.fabric.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. + * + *

This class should be only used for the logical server. + * + * @see ServerLoginNetworking + * @see ClientPlayNetworking + */ +public final class ServerPlayNetworking { + /** + * Registers a handler to a 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 #unregisterReceiver(ServerGamePacketListenerImpl, ResourceLocation)} 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 ServerPlayNetworking#unregisterGlobalReceiver(ResourceLocation) + * @see ServerPlayNetworking#registerReceiver(ServerGamePacketListenerImpl, ResourceLocation, PlayChannelHandler) + */ + public static boolean registerGlobalReceiver(ResourceLocation channelName, PlayChannelHandler channelHandler) { + return ServerNetworkingImpl.PLAY.registerGlobalReceiver(channelName, channelHandler); + } + + /** + * Removes the handler of a 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 ServerPlayNetworking#registerGlobalReceiver(ResourceLocation, PlayChannelHandler) + * @see ServerPlayNetworking#unregisterReceiver(ServerGamePacketListenerImpl, ResourceLocation) + */ + @Nullable + public static PlayChannelHandler unregisterGlobalReceiver(ResourceLocation channelName) { + return ServerNetworkingImpl.PLAY.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.PLAY.getChannels(); + } + + /** + * Registers a handler to a channel. + * This method differs from {@link ServerPlayNetworking#registerGlobalReceiver(ResourceLocation, PlayChannelHandler)} since + * the channel handler will only be applied to the player represented by the {@link ServerGamePacketListenerImpl}. + * + *

For example, if you only register a receiver using this method when a {@linkplain ServerLoginNetworking#registerGlobalReceiver(ResourceLocation, ServerLoginNetworking.LoginQueryResponseHandler)} + * login response has been received, you should use {@link ServerPlayConnectionEvents#INIT} to register the channel handler. + * + *

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(ServerGamePacketListenerImpl, ResourceLocation)} to unregister the existing handler. + * + * @param networkHandler the handler + * @param channelName the id of the channel + * @param channelHandler the handler + * @return false if a handler is already registered to the channel name + * @see ServerPlayConnectionEvents#INIT + */ + public static boolean registerReceiver(ServerGamePacketListenerImpl networkHandler, ResourceLocation channelName, PlayChannelHandler channelHandler) { + Objects.requireNonNull(networkHandler, "Network handler cannot be null"); + + return ServerNetworkingImpl.getAddon(networkHandler).registerChannel(channelName, channelHandler); + } + + /** + * Removes the handler of a 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 PlayChannelHandler unregisterReceiver(ServerGamePacketListenerImpl networkHandler, ResourceLocation channelName) { + Objects.requireNonNull(networkHandler, "Network handler cannot be null"); + + return ServerNetworkingImpl.getAddon(networkHandler).unregisterChannel(channelName); + } + + /** + * 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(ServerPlayer player) { + Objects.requireNonNull(player, "Server player entity cannot be null"); + + return getReceived(player.connection); + } + + /** + * 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(ServerGamePacketListenerImpl 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(ServerPlayer player) { + Objects.requireNonNull(player, "Server player entity cannot be null"); + + return getSendable(player.connection); + } + + /** + * Gets all channel names that a the connected client declared the ability to receive a packets on. + * + * @param handler the network handler + * @return True if the connected client has declared the ability to receive a packet on the specified channel + */ + public static Set getSendable(ServerGamePacketListenerImpl 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 True if the connected client has declared the ability to receive a packet on the specified channel + */ + public static boolean canSend(ServerPlayer player, ResourceLocation channelName) { + Objects.requireNonNull(player, "Server player entity cannot be null"); + + return canSend(player.connection, channelName); + } + + /** + * 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 True if the connected client has declared the ability to receive a packet on the specified channel + */ + public static boolean canSend(ServerGamePacketListenerImpl handler, ResourceLocation 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); + } + + /** + * Creates a packet which may be sent to a the connected client. + * + * @param channelName the channel name + * @param buf the packet byte buf which represents the payload of the packet + * @return a new packet + */ + public static Packet createS2CPacket(ResourceLocation channelName, FriendlyByteBuf buf) { + Objects.requireNonNull(channelName, "Channel cannot be null"); + Objects.requireNonNull(buf, "Buf cannot be null"); + + return ServerNetworkingImpl.createPlayC2SPacket(channelName, buf); + } + + /** + * Gets the packet sender which sends packets to the connected client. + * + * @param player the player + * @return the packet sender + */ + public static PacketSender getSender(ServerPlayer player) { + Objects.requireNonNull(player, "Server player entity cannot be null"); + + return getSender(player.connection); + } + + /** + * 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(ServerGamePacketListenerImpl handler) { + Objects.requireNonNull(handler, "Server play network handler cannot be null"); + + return ServerNetworkingImpl.getAddon(handler); + } + + /** + * Sends a packet to a player. + * + * @param player the player to send the packet to + * @param channelName the channel of the packet + * @param buf the payload of the packet. + */ + public static void send(ServerPlayer player, ResourceLocation channelName, FriendlyByteBuf buf) { + Objects.requireNonNull(player, "Server player entity cannot be null"); + Objects.requireNonNull(channelName, "Channel name cannot be null"); + Objects.requireNonNull(buf, "Packet byte buf cannot be null"); + + player.connection.send(createS2CPacket(channelName, buf)); + } + + // Helper methods + + /** + * Returns the Minecraft Server of a server play network handler. + * + * @param handler the server play network handler + */ + public static MinecraftServer getServer(ServerGamePacketListenerImpl handler) { + Objects.requireNonNull(handler, "Network handler cannot be null"); + + return handler.player.server; + } + + private ServerPlayNetworking() { + } + + @FunctionalInterface + public interface PlayChannelHandler { + /** + * Handles an incoming packet. + * + *

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.BlockableEventLoop#submit(Runnable) scheduled} using the provided Minecraft server instance. + * + *

An example usage of this is to create an explosion where the player is looking: + *

{@code
+		 * ServerPlayNetworking.registerReceiver(new Identifier("mymod", "boom"), (server, player, handler, buf, responseSender) -&rt; {
+		 * 	boolean fire = buf.readBoolean();
+		 *
+		 * 	// All operations on the server or world must be executed on the server thread
+		 * 	server.execute(() -&rt; {
+		 * 		ModPacketHandler.createExplosion(player, fire);
+		 * 	});
+		 * });
+		 * }
+ * @param server the server + * @param player the player + * @param handler the network handler that received this packet, representing the player/client who sent the packet + * @param buf the payload of the packet + * @param responseSender the packet sender + */ + void receive(MinecraftServer server, ServerPlayer player, ServerGamePacketListenerImpl handler, FriendlyByteBuf buf, PacketSender responseSender); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/package-info.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/package-info.java new file mode 100644 index 000000000..1746b78ca --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/networking/v1/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Networking API, version 1. + * + *

For login stage networking see {@link net.fabricmc.fabric.api.networking.v1.ServerLoginNetworking}. + * For play stage networking see {@link net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking}. + * + *

For events related to the connection to a client see {@link net.fabricmc.fabric.api.networking.v1.ServerLoginConnectionEvents} for login stage + * or {@link net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents} for play stage. + * + *

For events related to the ability of a client to receive packets on a channel of a specific name see {@link net.fabricmc.fabric.api.networking.v1.S2CPlayChannelEvents}. + */ + +package com.seibel.lod.forge.fabric.api.networking.v1; diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/BooleanFunction.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/BooleanFunction.java new file mode 100644 index 000000000..8e5ebf2d5 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/BooleanFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.util; + +/** + * Represents a function that accepts an boolean-valued argument and produces a result. + * + *

This is the {@code boolean}-consuming primitive specialization for {@link java.util.function.Function}. + */ +@FunctionalInterface +public interface BooleanFunction { + /** + * Applies this function to the given argument. + * + * @param value the function argument + * @return the function result + */ + R apply(boolean value); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/NbtType.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/NbtType.java new file mode 100644 index 000000000..f4a61fb75 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/NbtType.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.util; + +/** + * NBT type ID constants. Useful for filtering by value type in a few cases. + * + *

For the current list of types, check with {@link NbtElement#TYPES}. + * + * @see NbtCompound#contains(String, int) + * @see net.minecraft.nbt.NbtTypes#byId(int) + */ +public final class NbtType { + public static final int END = 0; + public static final int BYTE = 1; + public static final int SHORT = 2; + public static final int INT = 3; + public static final int LONG = 4; + public static final int FLOAT = 5; + public static final int DOUBLE = 6; + public static final int BYTE_ARRAY = 7; + public static final int STRING = 8; + public static final int LIST = 9; + public static final int COMPOUND = 10; + public static final int INT_ARRAY = 11; + public static final int LONG_ARRAY = 12; + + /** + * Any numeric value: byte, short, int, long, float, double. + * + * @see NbtCompound#contains(String, int) + */ + public static final int NUMBER = 99; + + private NbtType() { } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/TriState.java b/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/TriState.java new file mode 100644 index 000000000..4a24f30ea --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/api/util/TriState.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.api.util; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +import org.jetbrains.annotations.Nullable; + +/** + * Represents a boolean value which can be true, false or refer to a default value. + */ +public enum TriState { + /** + * Represents the boolean value of {@code false}. + */ + FALSE, + /** + * Represents a value that refers to a "default" value, often as a fallback. + */ + DEFAULT, + /** + * Represents the boolean value of {@code true}. + */ + TRUE; + + /** + * Gets the corresponding tri-state from a boolean value. + * + * @param bool the boolean value + * @return {@link TriState#TRUE} or {@link TriState#FALSE} depending on the value of the boolean. + */ + public static TriState of(boolean bool) { + return bool ? TRUE : FALSE; + } + + /** + * Gets a tri-state from a nullable boxed boolean. + * + * @param bool the boolean value + * @return {@link TriState#DEFAULT} if {@code null}. + * Otherwise {@link TriState#TRUE} or {@link TriState#FALSE} depending on the value of the boolean. + */ + public static TriState of(@Nullable Boolean bool) { + return bool == null ? DEFAULT : of(bool.booleanValue()); + } + + /** + * Gets the value of the tri-state. + * + * @return true if the tri-state is {@link TriState#TRUE}. + * Otherwise false. + */ + public boolean get() { + return this == TRUE; + } + + /** + * Gets the value of the tri-state as a boxed, nullable boolean. + * + * @return {@code null} if {@link TriState#DEFAULT}. + * Otherwise {@code true} if {@link TriState#TRUE} or {@code false} if {@link TriState#FALSE}. + */ + @Nullable + public Boolean getBoxed() { + return this == DEFAULT ? null : this.get(); + } + + /** + * Gets the value of this tri-state. + * If the value is {@link TriState#DEFAULT} then use the supplied value. + * + * @param value the value to fallback to + * @return the value of the tri-state or the supplied value if {@link TriState#DEFAULT}. + */ + public boolean orElse(boolean value) { + return this == DEFAULT ? value : this.get(); + } + + /** + * Gets the value of this tri-state. + * If the value is {@link TriState#DEFAULT} then use the supplied value. + * + * @param supplier the supplier used to get the value to fallback to + * @return the value of the tri-state or the value of the supplier if the tri-state is {@link TriState#DEFAULT}. + */ + public boolean orElseGet(BooleanSupplier supplier) { + return this == DEFAULT ? supplier.getAsBoolean() : this.get(); + } + + /** + * Maps the boolean value of this tri-state if it is {@link TriState#TRUE} or {@link TriState#FALSE}. + * + * @param mapper the mapper to use + * @param the type of object being supplier by the mapper + * @return an optional containing the mapped value; {@link Optional#empty()} if the tri-state is {@link TriState#DEFAULT} or the value provided by the mapper is {@code null}. + */ + public Optional map(BooleanFunction<@Nullable ? extends T> mapper) { + Objects.requireNonNull(mapper, "Mapper function cannot be null"); + + if (this == DEFAULT) { + return Optional.empty(); + } + + return Optional.ofNullable(mapper.apply(this.get())); + } + + /** + * Gets the value of this tri-state, or throws an exception if this tri-state's value is {@link TriState#DEFAULT}. + * + * @param exceptionSupplier the supplying function that produces an exception to be thrown + * @param Type of the exception to be thrown + * @return the value + * @throws X if the value is {@link TriState#DEFAULT} + */ + public boolean orElseThrow(Supplier exceptionSupplier) throws X { + if (this != DEFAULT) { + return this.get(); + } + + throw exceptionSupplier.get(); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/ArrayBackedEvent.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/ArrayBackedEvent.java new file mode 100644 index 000000000..276112496 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/ArrayBackedEvent.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.base.event; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import net.minecraft.resources.ResourceLocation; + +import com.seibel.lod.forge.fabric.api.event.Event; + +class ArrayBackedEvent extends Event { + static final Logger LOGGER = LoggerFactory.getLogger("fabric-api-base"); + + private final Function invokerFactory; + private final Object lock = new Object(); + private T[] handlers; + /** + * Registered event phases. + */ + private final Map> phases = new LinkedHashMap<>(); + /** + * Phases sorted in the correct dependency order. + */ + private final List> sortedPhases = new ArrayList<>(); + + @SuppressWarnings("unchecked") + ArrayBackedEvent(Class type, Function invokerFactory) { + this.invokerFactory = invokerFactory; + this.handlers = (T[]) Array.newInstance(type, 0); + update(); + } + + void update() { + this.invoker = invokerFactory.apply(handlers); + } + + @Override + public void register(T listener) { + register(DEFAULT_PHASE, listener); + } + + @Override + public void register(ResourceLocation phaseResourceLocation, T listener) { + Objects.requireNonNull(phaseResourceLocation, "Tried to register a listener for a null phase!"); + Objects.requireNonNull(listener, "Tried to register a null listener!"); + + synchronized (lock) { + getOrCreatePhase(phaseResourceLocation, true).addListener(listener); + rebuildInvoker(handlers.length + 1); + } + } + + private EventPhaseData getOrCreatePhase(ResourceLocation id, boolean sortIfCreate) { + EventPhaseData phase = phases.get(id); + + if (phase == null) { + phase = new EventPhaseData<>(id, handlers.getClass().getComponentType()); + phases.put(id, phase); + sortedPhases.add(phase); + + if (sortIfCreate) { + PhaseSorting.sortPhases(sortedPhases); + } + } + + return phase; + } + + private void rebuildInvoker(int newLength) { + // Rebuild handlers. + if (sortedPhases.size() == 1) { + // Special case with a single phase: use the array of the phase directly. + handlers = sortedPhases.get(0).listeners; + } else { + @SuppressWarnings("unchecked") + T[] newHandlers = (T[]) Array.newInstance(handlers.getClass().getComponentType(), newLength); + int newHandlersIndex = 0; + + for (EventPhaseData existingPhase : sortedPhases) { + int length = existingPhase.listeners.length; + System.arraycopy(existingPhase.listeners, 0, newHandlers, newHandlersIndex, length); + newHandlersIndex += length; + } + + handlers = newHandlers; + } + + // Rebuild invoker. + update(); + } + + @Override + public void addPhaseOrdering(ResourceLocation firstPhase, ResourceLocation secondPhase) { + Objects.requireNonNull(firstPhase, "Tried to add an ordering for a null phase."); + Objects.requireNonNull(secondPhase, "Tried to add an ordering for a null phase."); + if (firstPhase.equals(secondPhase)) throw new IllegalArgumentException("Tried to add a phase that depends on itself."); + + synchronized (lock) { + EventPhaseData first = getOrCreatePhase(firstPhase, false); + EventPhaseData second = getOrCreatePhase(secondPhase, false); + first.subsequentPhases.add(second); + second.previousPhases.add(first); + PhaseSorting.sortPhases(this.sortedPhases); + rebuildInvoker(handlers.length); + } + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/EventFactoryImpl.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/EventFactoryImpl.java new file mode 100644 index 000000000..cfe5c86ba --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/EventFactoryImpl.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.base.event; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import net.minecraft.resources.ResourceLocation; + +import com.seibel.lod.forge.fabric.api.event.Event; + +public final class EventFactoryImpl { + private static final List> ARRAY_BACKED_EVENTS = new ArrayList<>(); + + private EventFactoryImpl() { } + + public static void invalidate() { + ARRAY_BACKED_EVENTS.forEach(ArrayBackedEvent::update); + } + + public static Event createArrayBacked(Class type, Function invokerFactory) { + ArrayBackedEvent event = new ArrayBackedEvent<>(type, invokerFactory); + ARRAY_BACKED_EVENTS.add(event); + return event; + } + + public static void ensureContainsDefault(ResourceLocation[] defaultPhases) { + for (ResourceLocation id : defaultPhases) { + if (id.equals(Event.DEFAULT_PHASE)) { + return; + } + } + + throw new IllegalArgumentException("The event phases must contain Event.DEFAULT_PHASE."); + } + + public static void ensureNoDuplicates(ResourceLocation[] defaultPhases) { + for (int i = 0; i < defaultPhases.length; ++i) { + for (int j = i+1; j < defaultPhases.length; ++j) { + if (defaultPhases[i].equals(defaultPhases[j])) { + throw new IllegalArgumentException("Duplicate event phase: " + defaultPhases[i]); + } + } + } + } + + // Code originally by sfPlayer1. + // Unfortunately, it's slightly slower than just passing an empty array in the first place. + private static T buildEmptyInvoker(Class handlerClass, Function invokerSetup) { + // find the functional interface method + Method funcIfMethod = null; + + for (Method m : handlerClass.getMethods()) { + if ((m.getModifiers() & (Modifier.STRICT | Modifier.PRIVATE)) == 0) { + if (funcIfMethod != null) { + throw new IllegalStateException("Multiple virtual methods in " + handlerClass + "; cannot build empty invoker!"); + } + + funcIfMethod = m; + } + } + + if (funcIfMethod == null) { + throw new IllegalStateException("No virtual methods in " + handlerClass + "; cannot build empty invoker!"); + } + + Object defValue = null; + + try { + // concert to mh, determine its type without the "this" reference + MethodHandle target = MethodHandles.lookup().unreflect(funcIfMethod); + MethodType type = target.type().dropParameterTypes(0, 1); + + if (type.returnType() != void.class) { + // determine default return value by invoking invokerSetup.apply(T[0]) with all-jvm-default args (null for refs, false for boolean, etc.) + // explicitCastArguments is being used to cast Object=null to the jvm default value for the correct type + + // construct method desc (TLjava/lang/Object;Ljava/lang/Object;...)R where T = invoker ref ("this"), R = invoker ret type and args 1+ are Object for each non-"this" invoker arg + MethodType objTargetType = MethodType.genericMethodType(type.parameterCount()).changeReturnType(type.returnType()).insertParameterTypes(0, target.type().parameterType(0)); + // explicit cast to translate to the invoker args from Object to their real type, inferring jvm default values + MethodHandle objTarget = MethodHandles.explicitCastArguments(target, objTargetType); + + // build invocation args with 0 = "this", 1+ = null + Object[] args = new Object[target.type().parameterCount()]; + //noinspection unchecked + args[0] = invokerSetup.apply((T[]) Array.newInstance(handlerClass, 0)); + + // retrieve default by invoking invokerSetup.apply(T[0]).targetName(def,def,...) + defValue = objTarget.invokeWithArguments(args); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } + + final Object returnValue = defValue; + //noinspection unchecked + return (T) Proxy.newProxyInstance(EventFactoryImpl.class.getClassLoader(), new Class[]{handlerClass}, + (proxy, method, args) -> returnValue); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/EventPhaseData.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/EventPhaseData.java new file mode 100644 index 000000000..5c2eeeec7 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/EventPhaseData.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.base.event; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import net.minecraft.resources.ResourceLocation; + +/** + * Data of an {@link ArrayBackedEvent} phase. + */ +class EventPhaseData { + final ResourceLocation id; + T[] listeners; + final List> subsequentPhases = new ArrayList<>(); + final List> previousPhases = new ArrayList<>(); + int visitStatus = 0; // 0: not visited, 1: visiting, 2: visited + + @SuppressWarnings("unchecked") + EventPhaseData(ResourceLocation id, Class listenerClass) { + this.id = id; + this.listeners = (T[]) Array.newInstance(listenerClass, 0); + } + + void addListener(T listener) { + int oldLength = listeners.length; + listeners = Arrays.copyOf(listeners, oldLength + 1); + listeners[oldLength] = listener; + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/PhaseSorting.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/PhaseSorting.java new file mode 100644 index 000000000..8cb24fa68 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/base/event/PhaseSorting.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.base.event; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; + +import com.google.common.annotations.VisibleForTesting; +import org.jetbrains.annotations.ApiStatus; + +/** + * Contains phase-sorting logic for {@link ArrayBackedEvent}. + */ +@ApiStatus.Internal +public class PhaseSorting { + @VisibleForTesting + public static boolean ENABLE_CYCLE_WARNING = true; + + /** + * Deterministically sort a list of phases. + * 1) Compute phase SCCs (i.e. cycles). + * 2) Sort phases by id within SCCs. + * 3) Sort SCCs with respect to each other by respecting constraints, and by id in case of a tie. + */ + static void sortPhases(List> sortedPhases) { + // FIRST KOSARAJU SCC VISIT + List> toposort = new ArrayList<>(sortedPhases.size()); + + for (EventPhaseData phase : sortedPhases) { + forwardVisit(phase, null, toposort); + } + + clearStatus(toposort); + Collections.reverse(toposort); + + // SECOND KOSARAJU SCC VISIT + Map, PhaseScc> phaseToScc = new IdentityHashMap<>(); + + for (EventPhaseData phase : toposort) { + if (phase.visitStatus == 0) { + List> sccPhases = new ArrayList<>(); + // Collect phases in SCC. + backwardVisit(phase, sccPhases); + // Sort phases by id. + sccPhases.sort(Comparator.comparing(p -> p.id)); + // Mark phases as belonging to this SCC. + PhaseScc scc = new PhaseScc<>(sccPhases); + + for (EventPhaseData phaseInScc : sccPhases) { + phaseToScc.put(phaseInScc, scc); + } + } + } + + clearStatus(toposort); + + // Build SCC graph + for (PhaseScc scc : phaseToScc.values()) { + for (EventPhaseData phase : scc.phases) { + for (EventPhaseData subsequentPhase : phase.subsequentPhases) { + PhaseScc subsequentScc = phaseToScc.get(subsequentPhase); + + if (subsequentScc != scc) { + scc.subsequentSccs.add(subsequentScc); + subsequentScc.inDegree++; + } + } + } + } + + // Order SCCs according to priorities. When there is a choice, use the SCC with the lowest id. + // The priority queue contains all SCCs that currently have 0 in-degree. + PriorityQueue> pq = new PriorityQueue<>(Comparator.comparing(scc -> scc.phases.get(0).id)); + sortedPhases.clear(); + + for (PhaseScc scc : phaseToScc.values()) { + if (scc.inDegree == 0) { + pq.add(scc); + // Prevent adding the same SCC multiple times, as phaseToScc may contain the same value multiple times. + scc.inDegree = -1; + } + } + + while (!pq.isEmpty()) { + PhaseScc scc = pq.poll(); + sortedPhases.addAll(scc.phases); + + for (PhaseScc subsequentScc : scc.subsequentSccs) { + subsequentScc.inDegree--; + + if (subsequentScc.inDegree == 0) { + pq.add(subsequentScc); + } + } + } + } + + private static void forwardVisit(EventPhaseData phase, EventPhaseData parent, List> toposort) { + if (phase.visitStatus == 0) { + // Not yet visited. + phase.visitStatus = 1; + + for (EventPhaseData data : phase.subsequentPhases) { + forwardVisit(data, phase, toposort); + } + + toposort.add(phase); + phase.visitStatus = 2; + } else if (phase.visitStatus == 1 && ENABLE_CYCLE_WARNING) { + // Already visiting, so we have found a cycle. + ArrayBackedEvent.LOGGER.warn(String.format( + "Event phase ordering conflict detected.%nEvent phase %s is ordered both before and after event phase %s.", + phase.id, + parent.id + )); + } + } + + private static void clearStatus(List> phases) { + for (EventPhaseData phase : phases) { + phase.visitStatus = 0; + } + } + + private static void backwardVisit(EventPhaseData phase, List> sccPhases) { + if (phase.visitStatus == 0) { + phase.visitStatus = 1; + sccPhases.add(phase); + + for (EventPhaseData data : phase.previousPhases) { + backwardVisit(data, sccPhases); + } + } + } + + private static class PhaseScc { + final List> phases; + final List> subsequentSccs = new ArrayList<>(); + int inDegree = 0; + + private PhaseScc(List> phases) { + this.phases = phases; + } + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/AbstractChanneledNetworkAddon.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/AbstractChanneledNetworkAddon.java new file mode 100644 index 000000000..9c3ba39f7 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/AbstractChanneledNetworkAddon.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking; + +import java.nio.charset.StandardCharsets; +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 io.netty.util.AsciiString; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.jetbrains.annotations.Nullable; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketByteBufs; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketSender; +import net.minecraft.ResourceLocationException; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.resources.ResourceLocation; + +/** + * 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 { + protected final Connection connection; + protected final GlobalReceiverRegistry receiver; + protected final Set sendableChannels; + protected final Set sendableChannelsView; + + protected AbstractChanneledNetworkAddon(GlobalReceiverRegistry receiver, Connection connection, String description) { + this(receiver, connection, new HashSet<>(), description); + } + + protected AbstractChanneledNetworkAddon(GlobalReceiverRegistry receiver, Connection connection, Set sendableChannels, String description) { + super(receiver, description); + this.connection = connection; + this.receiver = receiver; + this.sendableChannels = sendableChannels; + this.sendableChannelsView = Collections.unmodifiableSet(sendableChannels); + } + + public abstract void lateInit(); + + protected void registerPendingChannels(ChannelInfoHolder holder) { + final Collection pending = holder.getPendingChannelsNames(); + + if (!pending.isEmpty()) { + register(new ArrayList<>(pending)); + pending.clear(); + } + } + + // always supposed to handle async! + protected boolean handle(ResourceLocation channelName, FriendlyByteBuf originalBuf) { + this.logger.debug("Handling inbound packet from channel with name \"{}\"", channelName); + + // Handle reserved packets + if (NetworkingImpl.REGISTER_CHANNEL.equals(channelName)) { + this.receiveRegistration(true, PacketByteBufs.slice(originalBuf)); + return true; + } + + if (NetworkingImpl.UNREGISTER_CHANNEL.equals(channelName)) { + this.receiveRegistration(false, PacketByteBufs.slice(originalBuf)); + return true; + } + + @Nullable H handler = this.getHandler(channelName); + + if (handler == null) { + return false; + } + + FriendlyByteBuf buf = PacketByteBufs.slice(originalBuf); + + try { + this.receive(handler, buf); + } 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, FriendlyByteBuf buf); + + protected void sendInitialChannelRegistrationPacket() { + final FriendlyByteBuf buf = this.createRegistrationPacket(this.getReceivableChannels()); + + if (buf != null) { + this.sendPacket(NetworkingImpl.REGISTER_CHANNEL, buf); + } + } + + @Nullable + protected FriendlyByteBuf createRegistrationPacket(Collection channels) { + if (channels.isEmpty()) { + return null; + } + + FriendlyByteBuf buf = PacketByteBufs.create(); + boolean first = true; + + for (ResourceLocation channel : channels) { + if (first) { + first = false; + } else { + buf.writeByte(0); + } + + buf.writeBytes(channel.toString().getBytes(StandardCharsets.US_ASCII)); + } + + return buf; + } + + // wrap in try with res (buf) + protected void receiveRegistration(boolean register, FriendlyByteBuf 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 { + this.addId(ids, active); + active = new StringBuilder(); + } + } + + this.addId(ids, active); + this.schedule(register ? () -> register(ids) : () -> unregister(ids)); + } + + void register(List ids) { + this.sendableChannels.addAll(ids); + this.invokeRegisterEvent(ids); + } + + void unregister(List ids) { + this.sendableChannels.removeAll(ids); + this.invokeUnregisterEvent(ids); + } + + @Override + public void sendPacket(Packet packet) { + Objects.requireNonNull(packet, "Packet cannot be null"); + + this.connection.send(packet); + } + + @Override + public void sendPacket(Packet packet, GenericFutureListener> callback) { + Objects.requireNonNull(packet, "Packet cannot be null"); + + this.connection.send(packet, callback); + } + + /** + * 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); + + private void addId(List ids, StringBuilder sb) { + String literal = sb.toString(); + + try { + ids.add(new ResourceLocation(literal)); + } catch (ResourceLocationException ex) { + this.logger.warn("Received invalid channel identifier \"{}\" from connection {}", literal, this.connection); + } + } + + public Set getSendableChannels() { + return this.sendableChannelsView; + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/AbstractNetworkAddon.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/AbstractNetworkAddon.java new file mode 100644 index 000000000..b07029a7a --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/AbstractNetworkAddon.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import net.minecraft.resources.ResourceLocation; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; +import org.jetbrains.annotations.Nullable; + +/** + * 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); + } + + @Nullable + public H getHandler(ResourceLocation channel) { + Lock lock = this.lock.readLock(); + lock.lock(); + + try { + return this.handlers.get(channel); + } finally { + lock.unlock(); + } + } + + public boolean registerChannel(ResourceLocation channelName, H handler) { + Objects.requireNonNull(channelName, "Channel name cannot be null"); + Objects.requireNonNull(handler, "Packet handler cannot be null"); + + if (this.isReservedChannel(channelName)) { + throw new IllegalArgumentException(String.format("Cannot register handler for reserved channel with name \"%s\"", 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(ResourceLocation channelName) { + Objects.requireNonNull(channelName, "Channel name cannot be null"); + + if (this.isReservedChannel(channelName)) { + throw new IllegalArgumentException(String.format("Cannot register 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 Set getReceivableChannels() { + Lock lock = this.lock.readLock(); + lock.lock(); + + try { + return new HashSet<>(this.handlers.keySet()); + } finally { + lock.unlock(); + } + } + + protected abstract void handleRegistration(ResourceLocation channelName); + + protected abstract void handleUnregistration(ResourceLocation channelName); + + public final void handleDisconnect() { + if (disconnected.compareAndSet(false, true)) { + invokeDisconnectEvent(); + } + } + + 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(ResourceLocation channelName); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/ChannelInfoHolder.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/ChannelInfoHolder.java new file mode 100644 index 000000000..e713fe157 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/ChannelInfoHolder.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking; + +import java.util.Collection; +import net.minecraft.resources.ResourceLocation; + +public interface ChannelInfoHolder { + /** + * @return Channels which are declared as receivable by the other side but have not been declared yet. + */ + Collection getPendingChannelsNames(); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/DisconnectPacketSource.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/DisconnectPacketSource.java new file mode 100644 index 000000000..be14224b1 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/DisconnectPacketSource.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; + +public interface DisconnectPacketSource { + Packet createDisconnectPacket(Component message); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/GlobalReceiverRegistry.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/GlobalReceiverRegistry.java new file mode 100644 index 000000000..4454385a4 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/GlobalReceiverRegistry.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +public final class GlobalReceiverRegistry { + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final Map handlers; + private final Set> trackedAddons = new HashSet<>(); + + public GlobalReceiverRegistry() { + this(new HashMap<>()); // sync map should be fine as there is little read write competitions + } + + public GlobalReceiverRegistry(Map map) { + this.handlers = map; + } + + @Nullable + public H getHandler(ResourceLocation channelName) { + Lock lock = this.lock.readLock(); + lock.lock(); + + try { + return this.handlers.get(channelName); + } finally { + lock.unlock(); + } + } + + public boolean registerGlobalReceiver(ResourceLocation channelName, H handler) { + Objects.requireNonNull(channelName, "Channel name cannot be null"); + Objects.requireNonNull(handler, "Channel handler cannot be null"); + + if (NetworkingImpl.isReservedPlayChannel(channelName)) { + throw new IllegalArgumentException(String.format("Cannot register handler for reserved channel with name \"%s\"", 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(); + } + } + + public H unregisterGlobalReceiver(ResourceLocation channelName) { + Objects.requireNonNull(channelName, "Channel name cannot be null"); + + if (NetworkingImpl.isReservedPlayChannel(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 { + this.trackedAddons.add(addon); + } finally { + lock.unlock(); + } + } + + public void endSession(AbstractNetworkAddon addon) { + Lock lock = this.lock.writeLock(); + lock.lock(); + + try { + this.trackedAddons.remove(addon); + } finally { + lock.unlock(); + } + } + + private void handleRegistration(ResourceLocation channelName, H handler) { + Lock lock = this.lock.writeLock(); + lock.lock(); + + try { + for (AbstractNetworkAddon addon : this.trackedAddons) { + addon.registerChannel(channelName, handler); + } + } finally { + lock.unlock(); + } + } + + private void handleUnregistration(ResourceLocation channelName) { + Lock lock = this.lock.writeLock(); + lock.lock(); + + try { + for (AbstractNetworkAddon addon : this.trackedAddons) { + addon.unregisterChannel(channelName); + } + } finally { + lock.unlock(); + } + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/NetworkHandlerExtensions.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/NetworkHandlerExtensions.java new file mode 100644 index 000000000..ec2919a03 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/NetworkHandlerExtensions.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking; + +public interface NetworkHandlerExtensions { + AbstractNetworkAddon getAddon(); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/NetworkingImpl.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/NetworkingImpl.java new file mode 100644 index 000000000..01edeef2d --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/NetworkingImpl.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import net.minecraft.network.FriendlyByteBuf; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketByteBufs; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerLoginConnectionEvents; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerLoginNetworking; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.resources.ResourceLocation; + +public final class NetworkingImpl { + public static final String MOD_ID = "fabric-networking-api-v1"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + /** + * Id of packet used to register supported channels. + */ + public static final ResourceLocation REGISTER_CHANNEL = new ResourceLocation("minecraft", "register"); + /** + * Id of packet used to unregister supported channels. + */ + public static final ResourceLocation UNREGISTER_CHANNEL = new ResourceLocation("minecraft", "unregister"); + /** + * Id of the packet used to declare all currently supported channels. + * Dynamic registration of supported channels is still allowed using {@link NetworkingImpl#REGISTER_CHANNEL} and {@link NetworkingImpl#UNREGISTER_CHANNEL}. + */ + public static final ResourceLocation EARLY_REGISTRATION_CHANNEL = new ResourceLocation(MOD_ID, "early_registration"); + + public static void init() { + // Login setup + ServerLoginConnectionEvents.QUERY_START.register((handler, server, sender, synchronizer) -> { + // Send early registration packet + FriendlyByteBuf buf = PacketByteBufs.create(); + Collection channelsNames = ServerPlayNetworking.getGlobalReceivers(); + buf.writeVarInt(channelsNames.size()); + + for (ResourceLocation id : channelsNames) { + buf.writeResourceLocation(id); + } + + sender.sendPacket(EARLY_REGISTRATION_CHANNEL, buf); + NetworkingImpl.LOGGER.debug("Sent accepted channels to the client for \"{}\"", handler.getUserName()); + }); + + ServerLoginNetworking.registerGlobalReceiver(EARLY_REGISTRATION_CHANNEL, (server, handler, understood, buf, synchronizer, sender) -> { + if (!understood) { + // The client is likely a vanilla client. + return; + } + + int n = buf.readVarInt(); + List ids = new ArrayList<>(n); + + for (int i = 0; i < n; i++) { + ids.add(buf.readResourceLocation()); + } + + ((ChannelInfoHolder) handler.getConnection()).getPendingChannelsNames().addAll(ids); + NetworkingImpl.LOGGER.debug("Received accepted channels from the client for \"{}\"", handler.getUserName()); + }); + } + + public static boolean isReservedPlayChannel(ResourceLocation channelName) { + return channelName.equals(REGISTER_CHANNEL) || channelName.equals(UNREGISTER_CHANNEL); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/PacketCallbackListener.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/PacketCallbackListener.java new file mode 100644 index 000000000..e7e2ecefc --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/PacketCallbackListener.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking; + +import net.minecraft.network.protocol.Packet; + +public interface PacketCallbackListener { + /** + * Called after a packet has been sent. + * + * @param packet the packet + */ + void sent(Packet packet); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientLoginNetworkAddon.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientLoginNetworkAddon.java new file mode 100644 index 000000000..88f8bb8a3 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientLoginNetworkAddon.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.jetbrains.annotations.Nullable; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientLoginConnectionEvents; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientLoginNetworking; +import com.seibel.lod.forge.fabric.api.networking.v1.FutureListeners; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketByteBufs; +import com.seibel.lod.forge.fabric.impl.networking.AbstractNetworkAddon; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.login.ClientboundCustomQueryPacket; +import net.minecraft.network.protocol.login.ServerboundCustomQueryPacket; +import net.minecraft.resources.ResourceLocation; + +public final class ClientLoginNetworkAddon extends AbstractNetworkAddon { + private final ClientHandshakePacketListenerImpl handler; + private final Minecraft client; + private boolean firstResponse = true; + + public ClientLoginNetworkAddon(ClientHandshakePacketListenerImpl handler, Minecraft client) { + super(ClientNetworkingImpl.LOGIN, "ClientLoginNetworkAddon for Client"); + this.handler = handler; + this.client = client; + + ClientLoginConnectionEvents.INIT.invoker().onLoginStart(this.handler, this.client); + this.receiver.startSession(this); + } + + public boolean handlePacket(ClientboundCustomQueryPacket packet) { + #if MC_1_16_5 + return handlePacket(packet.getTransactionId(), packet.getName(), packet.getInternalData()); + #else + return handlePacket(packet.getTransactionId(), packet.getIdentifier(), packet.getData()); + #endif + } + + private boolean handlePacket(int queryId, ResourceLocation channelName, FriendlyByteBuf originalBuf) { + this.logger.debug("Handling inbound login response with id {} and channel with name {}", queryId, channelName); + + if (this.firstResponse) { + // Register global handlers + for (Map.Entry entry : ClientNetworkingImpl.LOGIN.getHandlers().entrySet()) { + ClientLoginNetworking.registerReceiver(entry.getKey(), entry.getValue()); + } + + ClientLoginConnectionEvents.QUERY_START.invoker().onLoginQueryStart(this.handler, this.client); + this.firstResponse = false; + } + + @Nullable ClientLoginNetworking.LoginQueryRequestHandler handler = this.getHandler(channelName); + + if (handler == null) { + return false; + } + + FriendlyByteBuf buf = PacketByteBufs.slice(originalBuf); + List>> futureListeners = new ArrayList<>(); + + try { + CompletableFuture<@Nullable FriendlyByteBuf> future = handler.receive(this.client, this.handler, buf, futureListeners::add); + future.thenAccept(result -> { + ServerboundCustomQueryPacket packet = new ServerboundCustomQueryPacket(queryId, result); + GenericFutureListener> listener = null; + + for (GenericFutureListener> each : futureListeners) { + listener = FutureListeners.union(listener, each); + } + + this.handler.getConnection().send(packet, listener); + }); + } catch (Throwable ex) { + this.logger.error("Encountered exception while handling in channel with name \"{}\"", channelName, ex); + throw ex; + } + + return true; + } + + @Override + protected void handleRegistration(ResourceLocation channelName) { + } + + @Override + protected void handleUnregistration(ResourceLocation channelName) { + } + + @Override + protected void invokeDisconnectEvent() { + ClientLoginConnectionEvents.DISCONNECT.invoker().onLoginDisconnect(this.handler, this.client); + this.receiver.endSession(this); + } + + public void handlePlayTransition() { + this.receiver.endSession(this); + } + + @Override + protected boolean isReservedChannel(ResourceLocation channelName) { + return false; + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientNetworkingImpl.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientNetworkingImpl.java new file mode 100644 index 000000000..e2576d1cd --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientNetworkingImpl.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking.client; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.jetbrains.annotations.Nullable; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientLoginNetworking; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientPlayNetworking; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketByteBufs; +import com.seibel.lod.forge.fabric.impl.networking.ChannelInfoHolder; +import com.seibel.lod.forge.fabric.impl.networking.GlobalReceiverRegistry; +import com.seibel.lod.forge.fabric.impl.networking.NetworkHandlerExtensions; +import com.seibel.lod.forge.fabric.impl.networking.NetworkingImpl; +import com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor.ConnectScreenAccessor; +import com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor.MinecraftClientAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConnectScreen; +import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; + +public final class ClientNetworkingImpl { + public static final GlobalReceiverRegistry LOGIN = new GlobalReceiverRegistry<>(); + public static final GlobalReceiverRegistry PLAY = new GlobalReceiverRegistry<>(); + private static ClientPlayNetworkAddon currentPlayAddon; + + public static ClientPlayNetworkAddon getAddon(ClientPacketListener handler) { + return (ClientPlayNetworkAddon) ((NetworkHandlerExtensions) handler).getAddon(); + } + + public static ClientLoginNetworkAddon getAddon(ClientHandshakePacketListenerImpl handler) { + return (ClientLoginNetworkAddon) ((NetworkHandlerExtensions) handler).getAddon(); + } + + public static Packet createPlayC2SPacket(ResourceLocation channelName, FriendlyByteBuf buf) { + return new ServerboundCustomPayloadPacket(channelName, buf); + } + + /** + * Due to the way logging into a integrated or remote dedicated server will differ, we need to obtain the login client connection differently. + */ + @Nullable + public static Connection getLoginConnection() { + final Connection connection = ((MinecraftClientAccessor) Minecraft.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 (Minecraft.getInstance().screen instanceof ConnectScreen) { + return ((ConnectScreenAccessor) Minecraft.getInstance().screen).getConnection(); + } + } + + // We are not connected to a server at all. + return null; + } + + @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 (Minecraft.getInstance().getConnection() != null) { + currentPlayAddon = null; // Shouldn't need this anymore + return ClientNetworkingImpl.getAddon(Minecraft.getInstance().getConnection()); + } + + // 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) { + currentPlayAddon = addon; + } + + public static void clientInit() { + // Reference cleanup for the locally stored addon if we are disconnected + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> { + currentPlayAddon = null; + }); + + // Register a login query handler for early channel registration. + ClientLoginNetworking.registerGlobalReceiver(NetworkingImpl.EARLY_REGISTRATION_CHANNEL, (client, handler, buf, listenerAdder) -> { + int n = buf.readVarInt(); + List ids = new ArrayList<>(n); + + for (int i = 0; i < n; i++) { + ids.add(buf.readResourceLocation()); + } + + ((ChannelInfoHolder) handler.getConnection()).getPendingChannelsNames().addAll(ids); + NetworkingImpl.LOGGER.debug("Received accepted channels from the server"); + + FriendlyByteBuf response = PacketByteBufs.create(); + Collection channels = ClientPlayNetworking.getGlobalReceivers(); + response.writeVarInt(channels.size()); + + for (ResourceLocation id : channels) { + response.writeResourceLocation(id); + } + + NetworkingImpl.LOGGER.debug("Sent accepted channels to the server"); + return CompletableFuture.completedFuture(response); + }); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientPlayNetworkAddon.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientPlayNetworkAddon.java new file mode 100644 index 000000000..001136712 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/client/ClientPlayNetworkAddon.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking.client; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import com.seibel.lod.forge.fabric.api.client.networking.v1.C2SPlayChannelEvents; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientPlayNetworking; +import com.seibel.lod.forge.fabric.impl.networking.AbstractChanneledNetworkAddon; +import com.seibel.lod.forge.fabric.impl.networking.ChannelInfoHolder; +import com.seibel.lod.forge.fabric.impl.networking.NetworkingImpl; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; + +public final class ClientPlayNetworkAddon extends AbstractChanneledNetworkAddon { + private final ClientPacketListener handler; + private final Minecraft client; + private boolean sentInitialRegisterPacket; + + public ClientPlayNetworkAddon(ClientPacketListener handler, Minecraft client) { + super(ClientNetworkingImpl.PLAY, handler.getConnection(), "ClientPlayNetworkAddon for " + handler.getLocalGameProfile().getName()); + this.handler = handler; + this.client = client; + + // Must register pending channels via lateinit + this.registerPendingChannels((ChannelInfoHolder) this.connection); + + // Register global receivers and attach to session + this.receiver.startSession(this); + } + + @Override + public void lateInit() { + for (Map.Entry entry : this.receiver.getHandlers().entrySet()) { + this.registerChannel(entry.getKey(), entry.getValue()); + } + + ClientPlayConnectionEvents.INIT.invoker().onPlayInit(this.handler, this.client); + } + + public void onServerReady() { + ClientPlayConnectionEvents.JOIN.invoker().onPlayReady(this.handler, this, this.client); + + // The client cannot send any packets, including `minecraft:register` until after GameJoinS2CPacket is received. + this.sendInitialChannelRegistrationPacket(); + this.sentInitialRegisterPacket = true; + } + + /** + * Handles an incoming packet. + * + * @param packet the packet to handle + * @return true if the packet has been handled + */ + public boolean handle(ClientboundCustomPayloadPacket packet) { + // Do not handle the packet on game thread + if (this.client.isSameThread()) { + return false; + } + + FriendlyByteBuf buf = packet.getData(); + + try { + return this.handle(packet.getIdentifier(), buf); + } finally { + buf.release(); + } + } + + @Override + protected void receive(ClientPlayNetworking.PlayChannelHandler handler, FriendlyByteBuf buf) { + handler.receive(this.client, this.handler, buf, this); + } + + // impl details + + @Override + protected void schedule(Runnable task) { + Minecraft.getInstance().execute(task); + } + + @Override + public Packet createPacket(ResourceLocation channelName, FriendlyByteBuf buf) { + return ClientPlayNetworking.createC2SPacket(channelName, buf); + } + + @Override + protected void invokeRegisterEvent(List ids) { + C2SPlayChannelEvents.REGISTER.invoker().onChannelRegister(this.handler, this, this.client, ids); + } + + @Override + protected void invokeUnregisterEvent(List ids) { + C2SPlayChannelEvents.UNREGISTER.invoker().onChannelUnregister(this.handler, this, this.client, ids); + } + + @Override + protected void handleRegistration(ResourceLocation channelName) { + // If we can already send packets, immediately send the register packet for this channel + if (this.sentInitialRegisterPacket) { + final FriendlyByteBuf buf = this.createRegistrationPacket(Collections.singleton(channelName)); + + if (buf != null) { + this.sendPacket(NetworkingImpl.REGISTER_CHANNEL, buf); + } + } + } + + @Override + protected void handleUnregistration(ResourceLocation channelName) { + // If we can already send packets, immediately send the unregister packet for this channel + if (this.sentInitialRegisterPacket) { + final FriendlyByteBuf buf = this.createRegistrationPacket(Collections.singleton(channelName)); + + if (buf != null) { + this.sendPacket(NetworkingImpl.UNREGISTER_CHANNEL, buf); + } + } + } + + @Override + protected void invokeDisconnectEvent() { + ClientPlayConnectionEvents.DISCONNECT.invoker().onPlayDisconnect(this.handler, this.client); + this.receiver.endSession(this); + } + + @Override + protected boolean isReservedChannel(ResourceLocation channelName) { + return NetworkingImpl.isReservedPlayChannel(channelName); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/QueryIdFactory.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/QueryIdFactory.java new file mode 100644 index 000000000..99d09272e --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/QueryIdFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.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/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerLoginNetworkAddon.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerLoginNetworkAddon.java new file mode 100644 index 000000000..90b6248a0 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerLoginNetworkAddon.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.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 com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor.ClientboundCustomQueryPacketAccessor; +import io.netty.util.concurrent.GenericFutureListener; +import org.jetbrains.annotations.Nullable; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.login.ClientboundCustomQueryPacket; +import net.minecraft.network.protocol.login.ClientboundLoginCompressionPacket; +import net.minecraft.network.protocol.login.ServerboundCustomQueryPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketByteBufs; +import com.seibel.lod.forge.fabric.api.networking.v1.PacketSender; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerLoginConnectionEvents; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerLoginNetworking; +import com.seibel.lod.forge.fabric.impl.networking.AbstractNetworkAddon; +import com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor.LoginQueryResponseC2SPacketAccessor; +import com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor.ServerLoginNetworkHandlerAccessor; + +public final class ServerLoginNetworkAddon extends AbstractNetworkAddon implements PacketSender { + private final Connection connection; + private final ServerLoginPacketListenerImpl 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(ServerLoginPacketListenerImpl handler) { + super(ServerNetworkingImpl.LOGIN, "ServerLoginNetworkAddon for " + handler.getUserName()); + this.connection = handler.connection; + this.handler = handler; + this.server = ((ServerLoginNetworkHandlerAccessor) handler).getServer(); + this.queryIdFactory = QueryIdFactory.create(); + + ServerLoginConnectionEvents.INIT.invoker().onLoginInit(handler, this.server); + this.receiver.startSession(this); + } + + // 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(); + + // Register global receivers. + for (Map.Entry entry : ServerNetworkingImpl.LOGIN.getHandlers().entrySet()) { + ServerLoginNetworking.registerReceiver(this.handler, entry.getKey(), entry.getValue()); + } + + ServerLoginConnectionEvents.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.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) { + this.connection.send(new ClientboundLoginCompressionPacket(this.server.getCompressionThreshold()), (channelFuture) -> + #if MC_1_16_5 + this.connection.setupCompression(this.server.getCompressionThreshold()) + #else + this.connection.setupCompression(this.server.getCompressionThreshold(), true) + #endif + ); + } + } + + /** + * Handles an incoming query response during login. + * + * @param packet the packet to handle + * @return true if the packet was handled + */ + public boolean handle(ServerboundCustomQueryPacket packet) { + LoginQueryResponseC2SPacketAccessor access = (LoginQueryResponseC2SPacketAccessor) packet; + return handle(access.getTransactionId(), access.getData()); + } + + private boolean handle(int queryId, @Nullable FriendlyByteBuf originalBuf) { + this.logger.debug("Handling inbound login query with id {}", queryId); + ResourceLocation 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 = ServerNetworkingImpl.LOGIN.getHandler(channel); + + if (handler == null) { + return false; + } + + FriendlyByteBuf buf = understood ? PacketByteBufs.slice(originalBuf) : PacketByteBufs.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(ResourceLocation channelName, FriendlyByteBuf buf) { + int queryId = this.queryIdFactory.nextId(); + ClientboundCustomQueryPacket ret; + + #if MC_1_16_5 + ret = new ClientboundCustomQueryPacket(); + ClientboundCustomQueryPacketAccessor access = (ClientboundCustomQueryPacketAccessor) ret; + access.setIdentifier(channelName); + access.setTransactionId(queryId); + access.setData(buf); + #else + ret = new ClientboundCustomQueryPacket(queryId, channelName, buf); + #endif + return ret; + } + + @Override + public void sendPacket(Packet packet) { + Objects.requireNonNull(packet, "Packet cannot be null"); + + this.connection.send(packet); + } + + @Override + public void sendPacket(Packet packet, GenericFutureListener> callback) { + Objects.requireNonNull(packet, "Packet cannot be null"); + + this.connection.send(packet, callback); + } + + public void registerOutgoingPacket(ClientboundCustomQueryPacket packet) { + this.channels.put(packet.getTransactionId(), #if MC_1_16_5 packet.getName() #else packet.getIdentifier() #endif); + } + + @Override + protected void handleRegistration(ResourceLocation channelName) { + } + + @Override + protected void handleUnregistration(ResourceLocation channelName) { + } + + @Override + protected void invokeDisconnectEvent() { + ServerLoginConnectionEvents.DISCONNECT.invoker().onLoginDisconnect(this.handler, this.server); + this.receiver.endSession(this); + } + + public void handlePlayTransition() { + this.receiver.endSession(this); + } + + @Override + protected boolean isReservedChannel(ResourceLocation channelName) { + return false; + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerNetworkingImpl.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerNetworkingImpl.java new file mode 100644 index 000000000..11dfaf284 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerNetworkingImpl.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking.server; + +import com.seibel.lod.forge.fabric.api.networking.v1.ServerLoginNetworking; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerPlayNetworking; +import com.seibel.lod.forge.fabric.impl.networking.GlobalReceiverRegistry; +import com.seibel.lod.forge.fabric.impl.networking.NetworkHandlerExtensions; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; + +public final class ServerNetworkingImpl { + public static final GlobalReceiverRegistry LOGIN = new GlobalReceiverRegistry<>(); + public static final GlobalReceiverRegistry PLAY = new GlobalReceiverRegistry<>(); + + public static ServerPlayNetworkAddon getAddon(ServerGamePacketListenerImpl handler) { + return (ServerPlayNetworkAddon) ((NetworkHandlerExtensions) handler).getAddon(); + } + + public static ServerLoginNetworkAddon getAddon(ServerLoginPacketListenerImpl handler) { + return (ServerLoginNetworkAddon) ((NetworkHandlerExtensions) handler).getAddon(); + } + + public static Packet createPlayC2SPacket(ResourceLocation channel, FriendlyByteBuf buf) { + return new ClientboundCustomPayloadPacket(channel, buf); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerPlayNetworkAddon.java b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerPlayNetworkAddon.java new file mode 100644 index 000000000..df419e63d --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/fabric/impl/networking/server/ServerPlayNetworkAddon.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.fabric.impl.networking.server; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import com.seibel.lod.forge.fabric.api.networking.v1.S2CPlayChannelEvents; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerPlayConnectionEvents; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerPlayNetworking; +import com.seibel.lod.forge.fabric.impl.networking.AbstractChanneledNetworkAddon; +import com.seibel.lod.forge.fabric.impl.networking.ChannelInfoHolder; +import com.seibel.lod.forge.fabric.impl.networking.NetworkingImpl; +import com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor.CustomPayloadC2SPacketAccessor; + +public final class ServerPlayNetworkAddon extends AbstractChanneledNetworkAddon { + private final ServerGamePacketListenerImpl handler; + private final MinecraftServer server; + private boolean sentInitialRegisterPacket; + + public ServerPlayNetworkAddon(ServerGamePacketListenerImpl handler, MinecraftServer server) { + super(ServerNetworkingImpl.PLAY, handler.getConnection(), "ServerPlayNetworkAddon for " + handler.player.getScoreboardName()); + this.handler = handler; + this.server = server; + + // Must register pending channels via lateinit + this.registerPendingChannels((ChannelInfoHolder) this.connection); + + // Register global receivers and attach to session + this.receiver.startSession(this); + } + + @Override + public void lateInit() { + for (Map.Entry entry : this.receiver.getHandlers().entrySet()) { + this.registerChannel(entry.getKey(), entry.getValue()); + } + + ServerPlayConnectionEvents.INIT.invoker().onPlayInit(this.handler, this.server); + } + + public void onClientReady() { + ServerPlayConnectionEvents.JOIN.invoker().onPlayReady(this.handler, this, this.server); + + this.sendInitialChannelRegistrationPacket(); + this.sentInitialRegisterPacket = true; + } + + /** + * Handles an incoming packet. + * + * @param packet the packet to handle + * @return true if the packet has been handled + */ + public boolean handle(ServerboundCustomPayloadPacket packet) { + // Do not handle the packet on game thread + if (this.server.isSameThread()) { + return false; + } + + CustomPayloadC2SPacketAccessor access = (CustomPayloadC2SPacketAccessor) packet; + return this.handle(access.getIdentifier(), access.getData()); + } + + @Override + protected void receive(ServerPlayNetworking.PlayChannelHandler handler, FriendlyByteBuf buf) { + handler.receive(this.server, this.handler.player, this.handler, buf, this); + } + + // impl details + + @Override + protected void schedule(Runnable task) { + this.handler.player.server.execute(task); + } + + @Override + public Packet createPacket(ResourceLocation channelName, FriendlyByteBuf buf) { + return ServerPlayNetworking.createS2CPacket(channelName, buf); + } + + @Override + protected void invokeRegisterEvent(List ids) { + S2CPlayChannelEvents.REGISTER.invoker().onChannelRegister(this.handler, this, this.server, ids); + } + + @Override + protected void invokeUnregisterEvent(List ids) { + S2CPlayChannelEvents.UNREGISTER.invoker().onChannelUnregister(this.handler, this, this.server, ids); + } + + @Override + protected void handleRegistration(ResourceLocation channelName) { + // If we can already send packets, immediately send the register packet for this channel + if (this.sentInitialRegisterPacket) { + final FriendlyByteBuf buf = this.createRegistrationPacket(Collections.singleton(channelName)); + + if (buf != null) { + this.sendPacket(NetworkingImpl.REGISTER_CHANNEL, buf); + } + } + } + + @Override + protected void handleUnregistration(ResourceLocation channelName) { + // If we can already send packets, immediately send the unregister packet for this channel + if (this.sentInitialRegisterPacket) { + final FriendlyByteBuf buf = this.createRegistrationPacket(Collections.singleton(channelName)); + + if (buf != null) { + this.sendPacket(NetworkingImpl.UNREGISTER_CHANNEL, buf); + } + } + } + + @Override + protected void invokeDisconnectEvent() { + ServerPlayConnectionEvents.DISCONNECT.invoker().onPlayDisconnect(this.handler, this.server); + this.receiver.endSession(this); + } + + @Override + protected boolean isReservedChannel(ResourceLocation channelName) { + return NetworkingImpl.isReservedPlayChannel(channelName); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking.zip b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking.zip deleted file mode 100644 index 0c4c6af3b..000000000 Binary files a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking.zip and /dev/null differ diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ClientConnectionMixin.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ClientConnectionMixin.java new file mode 100644 index 000000000..5f2d74b1d --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ClientConnectionMixin.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ConcurrentHashMap; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +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 com.seibel.lod.forge.fabric.impl.networking.ChannelInfoHolder; +import com.seibel.lod.forge.fabric.impl.networking.DisconnectPacketSource; +import com.seibel.lod.forge.fabric.impl.networking.NetworkHandlerExtensions; +import com.seibel.lod.forge.fabric.impl.networking.PacketCallbackListener; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.resources.ResourceLocation; + +@Mixin(Connection.class) +abstract class ClientConnectionMixin implements ChannelInfoHolder { + @Shadow + private PacketListener packetListener; + + @Shadow + public abstract void send(Packet packet, GenericFutureListener> callback); + + @Shadow + public abstract void disconnect(Component disconnectReason); + + @Unique + private Collection playChannels; + + @Inject(method = "", at = @At("RETURN")) + private void initAddedFields(PacketFlow side, CallbackInfo ci) { + this.playChannels = Collections.newSetFromMap(new ConcurrentHashMap<>()); + } + + // Must be fully qualified due to mixin not working in production without it + @SuppressWarnings("UnnecessaryQualifiedMemberReference") + @Redirect(method = "Lnet/minecraft/network/Connection;exceptionCaught(Lio/netty/channel/ChannelHandlerContext;Ljava/lang/Throwable;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;send(Lnet/minecraft/network/protocol/Packet;Lio/netty/util/concurrent/GenericFutureListener;)V")) + private void resendOnExceptionCaught(Connection self, Packet packet, GenericFutureListener> listener) { + PacketListener handler = this.packetListener; + + if (handler instanceof DisconnectPacketSource) { + this.send(((DisconnectPacketSource) handler).createDisconnectPacket(new TranslatableComponent("disconnect.genericReason")), listener); + } else { + this.disconnect(new TranslatableComponent("disconnect.genericReason")); // Don't send packet if we cannot send proper packets + } + } + + @Inject(method = "sendPacket", at = @At(value = "FIELD", target = "Lnet/minecraft/network/Connection;sentPackets:I")) + private void checkPacket(Packet packet, GenericFutureListener> callback, CallbackInfo ci) { + if (this.packetListener instanceof PacketCallbackListener) { + ((PacketCallbackListener) this.packetListener).sent(packet); + } + } + + @Inject(method = "channelInactive", at = @At("HEAD")) + private void handleDisconnect(ChannelHandlerContext channelHandlerContext, CallbackInfo ci) throws Exception { + if (packetListener instanceof NetworkHandlerExtensions) { // not the case for client/server query + ((NetworkHandlerExtensions) packetListener).getAddon().handleDisconnect(); + } + } + + @Override + public Collection getPendingChannelsNames() { + return this.playChannels; + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/EntityTrackerEntryMixin.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/EntityTrackerEntryMixin.java new file mode 100644 index 000000000..458bd9594 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/EntityTrackerEntryMixin.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking; + +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 com.seibel.lod.forge.fabric.api.networking.v1.EntityTrackingEvents; +import net.minecraft.server.level.ServerEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +@Mixin(ServerEntity.class) +abstract class EntityTrackerEntryMixin { + @Shadow + @Final + private Entity entity; + + @Inject(method = "addPairing", at = @At("HEAD")) + private void onStartTracking(ServerPlayer player, CallbackInfo ci) { + EntityTrackingEvents.START_TRACKING.invoker().onStartTracking(this.entity, player); + } + + @Inject(method = "removePairing", at = @At("TAIL")) + private void onStopTracking(ServerPlayer player, CallbackInfo ci) { + EntityTrackingEvents.STOP_TRACKING.invoker().onStopTracking(this.entity, player); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/PlayerManagerMixin.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/PlayerManagerMixin.java new file mode 100644 index 000000000..439001bf7 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/PlayerManagerMixin.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking; + +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 com.seibel.lod.forge.fabric.impl.networking.server.ServerNetworkingImpl; +import net.minecraft.network.Connection; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; + +@Mixin(PlayerList.class) +abstract class PlayerManagerMixin { + @Inject(method = "placeNewPlayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/protocol/game/ClientboundCustomPayloadPacket;(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/network/FriendlyByteBuf;)V")) + private void handlePlayerConnection(Connection connection, ServerPlayer player, CallbackInfo ci) { + ServerNetworkingImpl.getAddon(player.connection).onClientReady(); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ServerLoginNetworkHandlerMixin.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ServerLoginNetworkHandlerMixin.java new file mode 100644 index 000000000..d7cf946ad --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ServerLoginNetworkHandlerMixin.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking; + +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.login.ClientboundCustomQueryPacket; +import net.minecraft.network.protocol.login.ClientboundLoginDisconnectPacket; +import net.minecraft.network.protocol.login.ServerboundCustomQueryPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import com.seibel.lod.forge.fabric.impl.networking.DisconnectPacketSource; +import com.seibel.lod.forge.fabric.impl.networking.NetworkHandlerExtensions; +import com.seibel.lod.forge.fabric.impl.networking.PacketCallbackListener; +import com.seibel.lod.forge.fabric.impl.networking.server.ServerLoginNetworkAddon; + +@Mixin(ServerLoginPacketListenerImpl.class) +abstract class ServerLoginNetworkHandlerMixin implements NetworkHandlerExtensions, DisconnectPacketSource, PacketCallbackListener { + @Shadow + @Final + private MinecraftServer server; + + @Shadow + public abstract void handleAcceptedLogin(); + + @Unique + private ServerLoginNetworkAddon addon; + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.addon = new ServerLoginNetworkAddon((ServerLoginPacketListenerImpl) (Object) this); + } + + @Redirect(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerLoginPacketListenerImpl;handleAcceptedLogin()V")) + private void handlePlayerJoin(ServerLoginPacketListenerImpl handler) { + // Do not accept the player, thereby moving into play stage until all login futures being waited on are completed + if (this.addon.queryTick()) { + this.handleAcceptedLogin(); + } + } + + @Inject(method = "handleCustomQueryPacket", at = @At("HEAD"), cancellable = true) + private void handleCustomPayloadReceivedAsync(ServerboundCustomQueryPacket packet, CallbackInfo ci) { + // Handle queries + if (this.addon.handle(packet)) { + ci.cancel(); + } + } + + @Redirect(method = "handleAcceptedLogin", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getCompressionThreshold()I", ordinal = 0)) + private int removeLateCompressionPacketSending(MinecraftServer server) { + return -1; + } + + @Inject(method = "onDisconnect", at = @At("HEAD")) + private void handleDisconnection(Component reason, CallbackInfo ci) { + this.addon.handleDisconnect(); + } + + #if MC_1_16_5 + @Inject(method = "handleAcceptedLogin", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/players/PlayerList;placeNewPlayer(Lnet/minecraft/network/Connection;Lnet/minecraft/server/level/ServerPlayer;)V")) + private void handlePlayTransitionNormal(CallbackInfo ci) { + this.addon.handlePlayTransition(); + } + + @Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/players/PlayerList;placeNewPlayer(Lnet/minecraft/network/Connection;Lnet/minecraft/server/level/ServerPlayer;)V")) + private void handlePlayTransitionDelayed(CallbackInfo ci) { + this.addon.handlePlayTransition(); + } + #else + @Inject(method = "placeNewPlayer", at = @At("HEAD")) + private void handlePlayTransitionNormal(ServerPlayer player, CallbackInfo ci) { + this.addon.handlePlayTransition(); + } + #endif + + @Override + public void sent(Packet packet) { + if (packet instanceof ClientboundCustomQueryPacket) { + this.addon.registerOutgoingPacket((ClientboundCustomQueryPacket) packet); + } + } + + @Override + public ServerLoginNetworkAddon getAddon() { + return this.addon; + } + + @Override + public Packet createDisconnectPacket(Component message) { + return new ClientboundLoginDisconnectPacket(message); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ServerPlayNetworkHandlerMixin.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 000000000..bfaa0507a --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking; + +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.Connection; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundDisconnectPacket; +import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import com.seibel.lod.forge.fabric.impl.networking.DisconnectPacketSource; +import com.seibel.lod.forge.fabric.impl.networking.NetworkHandlerExtensions; +import com.seibel.lod.forge.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 = ServerGamePacketListenerImpl.class, priority = 999) +abstract class ServerPlayNetworkHandlerMixin implements NetworkHandlerExtensions, DisconnectPacketSource { + @Shadow + @Final + private MinecraftServer server; + @Shadow + @Final + public Connection connection; + + @Unique + private ServerPlayNetworkAddon addon; + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.addon = new ServerPlayNetworkAddon((ServerGamePacketListenerImpl) (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.addon.lateInit(); + } + + @Inject(method = "handleCustomPayload", at = @At("HEAD"), cancellable = true) + private void handleCustomPayloadReceivedAsync(ServerboundCustomPayloadPacket packet, CallbackInfo ci) { + if (this.addon.handle(packet)) { + ci.cancel(); + } + } + + @Inject(method = "onDisconnect", at = @At("HEAD")) + private void handleDisconnection(Component reason, CallbackInfo ci) { + this.addon.handleDisconnect(); + } + + @Override + public ServerPlayNetworkAddon getAddon() { + return this.addon; + } + + @Override + public Packet createDisconnectPacket(Component message) { + return new ClientboundDisconnectPacket(message); + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ClientboundCustomQueryPacketAccessor.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ClientboundCustomQueryPacketAccessor.java new file mode 100644 index 000000000..07da90fff --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ClientboundCustomQueryPacketAccessor.java @@ -0,0 +1,21 @@ +package com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.login.ClientboundCustomQueryPacket; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ClientboundCustomQueryPacket.class) +public interface ClientboundCustomQueryPacketAccessor { + #if MC_1_16_5 + @Accessor + void setTransactionId(int transactionId); + + @Accessor + void setIdentifier(ResourceLocation identifier); + + @Accessor + void setData(FriendlyByteBuf data); + #endif +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ConnectScreenAccessor.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ConnectScreenAccessor.java new file mode 100644 index 000000000..3df191246 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ConnectScreenAccessor.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import net.minecraft.client.gui.screens.ConnectScreen; +import net.minecraft.network.Connection; + +@Mixin(ConnectScreen.class) +public interface ConnectScreenAccessor { + @Accessor + Connection getConnection(); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/CustomPayloadC2SPacketAccessor.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/CustomPayloadC2SPacketAccessor.java new file mode 100644 index 000000000..e5547daba --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/CustomPayloadC2SPacketAccessor.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ServerboundCustomPayloadPacket.class) +public interface CustomPayloadC2SPacketAccessor { + @Accessor + ResourceLocation getIdentifier(); + + @Accessor + FriendlyByteBuf getData(); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/LoginQueryResponseC2SPacketAccessor.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/LoginQueryResponseC2SPacketAccessor.java new file mode 100644 index 000000000..438abc5f9 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/LoginQueryResponseC2SPacketAccessor.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.login.ServerboundCustomQueryPacket; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ServerboundCustomQueryPacket.class) +public interface LoginQueryResponseC2SPacketAccessor { + @Accessor + int getTransactionId(); + + @Nullable + @Accessor + FriendlyByteBuf getData(); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/MinecraftClientAccessor.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/MinecraftClientAccessor.java new file mode 100644 index 000000000..3ee7920c2 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/MinecraftClientAccessor.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor; + +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import net.minecraft.client.Minecraft; +import net.minecraft.network.Connection; + +@Mixin(Minecraft.class) +public interface MinecraftClientAccessor { + @Nullable + @Accessor("pendingConnection") + Connection getConnection(); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ServerLoginNetworkHandlerAccessor.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ServerLoginNetworkHandlerAccessor.java new file mode 100644 index 000000000..d6fe32d51 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/accessor/ServerLoginNetworkHandlerAccessor.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking.accessor; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; + +@Mixin(ServerLoginPacketListenerImpl.class) +public interface ServerLoginNetworkHandlerAccessor { + @Accessor + MinecraftServer getServer(); +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/client/ClientLoginNetworkHandlerMixin.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/client/ClientLoginNetworkHandlerMixin.java new file mode 100644 index 000000000..9fd4dc959 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/client/ClientLoginNetworkHandlerMixin.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking.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 com.seibel.lod.forge.fabric.impl.networking.NetworkHandlerExtensions; +import com.seibel.lod.forge.fabric.impl.networking.client.ClientLoginNetworkAddon; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.login.ClientboundCustomQueryPacket; + +@Mixin(ClientHandshakePacketListenerImpl.class) +abstract class ClientLoginNetworkHandlerMixin implements NetworkHandlerExtensions { + @Shadow + @Final + private Minecraft minecraft; + + @Unique + private ClientLoginNetworkAddon addon; + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.addon = new ClientLoginNetworkAddon((ClientHandshakePacketListenerImpl) (Object) this, this.minecraft); + } + + @Inject(method = "handleCustomQuery", 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(ClientboundCustomQueryPacket packet, CallbackInfo ci) { + if (this.addon.handlePacket(packet)) { + ci.cancel(); + } + } + + @Inject(method = "onDisconnect", at = @At("HEAD")) + private void invokeLoginDisconnectEvent(Component reason, CallbackInfo ci) { + this.addon.handleDisconnect(); + } + + @Inject(method = "handleGameProfile", at = @At("HEAD")) + private void handlePlayTransition(CallbackInfo ci) { + addon.handlePlayTransition(); + } + + @Override + public ClientLoginNetworkAddon getAddon() { + return this.addon; + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/client/ClientPlayNetworkHandlerMixin.java b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/client/ClientPlayNetworkHandlerMixin.java new file mode 100644 index 000000000..e399e6807 --- /dev/null +++ b/forge/src/main/java/com/seibel/lod/forge/mixins/fabric/mixin/networking/client/ClientPlayNetworkHandlerMixin.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.seibel.lod.forge.mixins.fabric.mixin.networking.client; + +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 com.seibel.lod.forge.fabric.impl.networking.NetworkHandlerExtensions; +import com.seibel.lod.forge.fabric.impl.networking.client.ClientNetworkingImpl; +import com.seibel.lod.forge.fabric.impl.networking.client.ClientPlayNetworkAddon; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.game.ClientboundLoginPacket; + +// We want to apply a bit earlier than other mods which may not use us in order to prevent refCount issues + +@Mixin(value = ClientPacketListener.class, priority = 999) +abstract class ClientPlayNetworkHandlerMixin implements NetworkHandlerExtensions { + @Shadow + private Minecraft minecraft; + + @Unique + private ClientPlayNetworkAddon addon; + + @Inject(method = "", at = @At("RETURN")) + private void initAddon(CallbackInfo ci) { + this.addon = new ClientPlayNetworkAddon((ClientPacketListener) (Object) this, this.minecraft); + // 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.addon); + this.addon.lateInit(); + } + + @Inject(method = "handleLogin", at = @At("RETURN")) + private void handleServerPlayReady(ClientboundLoginPacket packet, CallbackInfo ci) { + this.addon.onServerReady(); + } + + @Inject(method = "handleCustomPayload", at = @At("HEAD"), cancellable = true) + private void handleCustomPayload(ClientboundCustomPayloadPacket packet, CallbackInfo ci) { + if (this.addon.handle(packet)) { + ci.cancel(); + } + } + + @Inject(method = "onDisconnect", at = @At("HEAD")) + private void handleDisconnection(Component reason, CallbackInfo ci) { + this.addon.handleDisconnect(); + } + + @Override + public ClientPlayNetworkAddon getAddon() { + return this.addon; + } +} diff --git a/forge/src/main/java/com/seibel/lod/forge/networking/NetworkHandler.java b/forge/src/main/java/com/seibel/lod/forge/networking/NetworkHandler.java index 6d7ce4043..2f8a1063d 100644 --- a/forge/src/main/java/com/seibel/lod/forge/networking/NetworkHandler.java +++ b/forge/src/main/java/com/seibel/lod/forge/networking/NetworkHandler.java @@ -21,8 +21,8 @@ package com.seibel.lod.forge.networking; import com.seibel.lod.common.networking.NetworkInterface; import com.seibel.lod.common.networking.Networking; -//import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientPlayNetworking; -//import com.seibel.lod.forge.fabric.api.networking.v1.ServerPlayNetworking; +import com.seibel.lod.forge.fabric.api.client.networking.v1.ClientPlayNetworking; +import com.seibel.lod.forge.fabric.api.networking.v1.ServerPlayNetworking; /** * @author Ran @@ -30,15 +30,15 @@ import com.seibel.lod.common.networking.Networking; public class NetworkHandler implements NetworkInterface { @Override public void register_Client() { -// ClientPlayNetworking.registerGlobalReceiver(Networking.resourceLocation_meow, (client, handler, buf, responseSender) -> { -// com.seibel.lod.common.networking.NetworkHandler.receivePacketClient(client, handler, buf); -// }); + ClientPlayNetworking.registerGlobalReceiver(Networking.resourceLocation_meow, (client, handler, buf, responseSender) -> { + com.seibel.lod.common.networking.NetworkHandler.receivePacketClient(client, handler, buf); + }); } @Override public void register_Server() { -// ServerPlayNetworking.registerGlobalReceiver(Networking.resourceLocation_meow, (server, player, handler, buf, responseSender) -> { -// com.seibel.lod.common.networking.NetworkHandler.receivePacketServer(server, player, handler, buf); -// }); + ServerPlayNetworking.registerGlobalReceiver(Networking.resourceLocation_meow, (server, player, handler, buf, responseSender) -> { + com.seibel.lod.common.networking.NetworkHandler.receivePacketServer(server, player, handler, buf); + }); } } diff --git a/forge/src/main/resources/lod.mixins.json b/forge/src/main/resources/lod.mixins.json index 589806557..c86ecfd58 100644 --- a/forge/src/main/resources/lod.mixins.json +++ b/forge/src/main/resources/lod.mixins.json @@ -3,7 +3,16 @@ "minVersion": "0.8", "package": "com.seibel.lod.forge.mixins", "mixins": [ - "MixinUtilBackgroudThread" + "MixinUtilBackgroudThread", + "fabric.mixin.networking.ClientConnectionMixin", + "fabric.mixin.networking.EntityTrackerEntryMixin", + "fabric.mixin.networking.PlayerManagerMixin", + "fabric.mixin.networking.ServerLoginNetworkHandlerMixin", + "fabric.mixin.networking.ServerPlayNetworkHandlerMixin", + "fabric.mixin.networking.accessor.ServerLoginNetworkHandlerAccessor", + "fabric.mixin.networking.accessor.CustomPayloadC2SPacketAccessor", + "fabric.mixin.networking.accessor.LoginQueryResponseC2SPacketAccessor", + "fabric.mixin.networking.accessor.ClientboundCustomQueryPacketAccessor" ], "client": [ "MixinOptionsScreen", @@ -11,7 +20,11 @@ "MixinFogRenderer", "MixinChunkGenerator", "MixinLightmap", - "MixinTFChunkGenerator" + "MixinTFChunkGenerator", + "fabric.mixin.networking.accessor.ConnectScreenAccessor", + "fabric.mixin.networking.accessor.MinecraftClientAccessor", + "fabric.mixin.networking.client.ClientLoginNetworkHandlerMixin", + "fabric.mixin.networking.client.ClientPlayNetworkHandlerMixin" ], "server": [] }