Skip to content

Commit

Permalink
Networking: introduce packet-object based API
Browse files Browse the repository at this point in the history
Signed-off-by: modmuss50 <[email protected]>
  • Loading branch information
apple502j authored and modmuss50 committed Apr 4, 2023
1 parent 88da797 commit a6f3ccf
Show file tree
Hide file tree
Showing 8 changed files with 647 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@

import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.Packet;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.listener.ServerPlayPacketListener;
import net.minecraft.network.packet.Packet;
import net.minecraft.util.Identifier;
import net.minecraft.util.thread.ThreadExecutor;

import net.fabricmc.fabric.api.networking.v1.FabricPacket;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.PacketType;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.fabricmc.fabric.impl.networking.client.ClientNetworkingImpl;
import net.fabricmc.fabric.impl.networking.client.ClientPlayNetworkAddon;
Expand All @@ -41,6 +46,9 @@
*
* <p>This class should be only used on the physical client and for the logical client.
*
* <p>See {@link ServerPlayNetworking} for information on how to use the packet
* object-based API.
*
* @see ClientLoginNetworking
* @see ServerPlayNetworking
*/
Expand All @@ -49,9 +57,15 @@ public final class ClientPlayNetworking {
* Registers a handler to a channel.
* A global receiver is registered to all connections, in the present and future.
*
* <p>The handler runs on the network thread. After reading the buffer there, access to game state
* must be performed in the render thread by calling {@link ThreadExecutor#execute(Runnable)}.
*
* <p>If a handler is already registered to the {@code channel}, this method will return {@code false}, and no change will be made.
* Use {@link #unregisterGlobalReceiver(Identifier)} to unregister the existing handler.
*
* <p>For new code, {@link #registerGlobalReceiver(PacketType, PlayPacketHandler)}
* is preferred, as it is designed in a way that prevents thread safety issues.
*
* @param channelName the id of the channel
* @param channelHandler the handler
* @return false if a handler is already registered to the channel
Expand All @@ -62,6 +76,45 @@ public static boolean registerGlobalReceiver(Identifier channelName, PlayChannel
return ClientNetworkingImpl.PLAY.registerGlobalReceiver(channelName, channelHandler);
}

/**
* Registers a handler for a packet type.
* A global receiver is registered to all connections, in the present and future.
*
* <p>If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
* Use {@link #unregisterGlobalReceiver(PacketType)} to unregister the existing handler.
*
* @param type the packet type
* @param handler the handler
* @return false if a handler is already registered to the channel
* @see ClientPlayNetworking#unregisterGlobalReceiver(PacketType)
* @see ClientPlayNetworking#registerReceiver(PacketType, PlayPacketHandler)
*/
public static <T extends FabricPacket> boolean registerGlobalReceiver(PacketType<T> type, PlayPacketHandler<T> handler) {
return registerGlobalReceiver(type.getId(), new PlayChannelHandlerProxy<T>() {
@Override
public PlayPacketHandler<T> getOriginalHandler() {
return handler;
}

@Override
public void receive(MinecraftClient client, ClientPlayNetworkHandler networkHandler, PacketByteBuf buf, PacketSender sender) {
T packet = type.read(buf);

if (client.isOnThread()) {
// Do not submit to the render thread if we're already running there.
// Normally, packets are handled on the network IO thread - though it is
// not guaranteed (for example, with 1.19.4 S2C packet bundling)
// Since we're handling it right now, connection check is redundant.
handler.receive(packet, client.player, sender);
} else {
client.execute(() -> {
if (networkHandler.getConnection().isOpen()) handler.receive(packet, client.player, sender);
});
}
}
});
}

/**
* Removes the handler of a channel.
* A global receiver is registered to all connections, in the present and future.
Expand All @@ -78,6 +131,25 @@ public static PlayChannelHandler unregisterGlobalReceiver(Identifier channelName
return ClientNetworkingImpl.PLAY.unregisterGlobalReceiver(channelName);
}

/**
* Removes the handler for a packet type.
* A global receiver is registered to all connections, in the present and future.
*
* <p>The {@code type} is guaranteed not to have an associated handler after this call.
*
* @param type the packet type
* @return the previous handler, or {@code null} if no handler was bound to the channel,
* or it was not registered using {@link #registerGlobalReceiver(PacketType, PlayPacketHandler)}
* @see ClientPlayNetworking#registerGlobalReceiver(PacketType, PlayPacketHandler)
* @see ClientPlayNetworking#unregisterReceiver(PacketType)
*/
@Nullable
@SuppressWarnings("unchecked")
public static <T extends FabricPacket> PlayPacketHandler<T> unregisterGlobalReceiver(PacketType<T> type) {
PlayChannelHandler handler = ClientNetworkingImpl.PLAY.unregisterGlobalReceiver(type.getId());
return handler instanceof PlayChannelHandlerProxy<?> proxy ? (PlayPacketHandler<T>) proxy.getOriginalHandler() : null;
}

/**
* Gets all channel names which global receivers are registered for.
* A global receiver is registered to all connections, in the present and future.
Expand All @@ -97,6 +169,9 @@ public static Set<Identifier> getGlobalReceivers() {
* <p>For example, if you only register a receiver using this method when a {@linkplain ClientLoginNetworking#registerGlobalReceiver(Identifier, ClientLoginNetworking.LoginQueryRequestHandler)}
* login query has been received, you should use {@link ClientPlayConnectionEvents#INIT} to register the channel handler.
*
* <p>For new code, {@link #registerReceiver(PacketType, PlayPacketHandler)}
* is preferred, as it is designed in a way that prevents thread safety issues.
*
* @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
Expand All @@ -112,6 +187,47 @@ public static boolean registerReceiver(Identifier channelName, PlayChannelHandle
throw new IllegalStateException("Cannot register receiver while not in game!");
}

/**
* Registers a handler for a packet type.
*
* <p>If a handler is already registered for the {@code type}, this method will return {@code false}, and no change will be made.
* Use {@link #unregisterReceiver(PacketType)} to unregister the existing handler.
*
* <p>For example, if you only register a receiver using this method when a {@linkplain ClientLoginNetworking#registerGlobalReceiver(Identifier, ClientLoginNetworking.LoginQueryRequestHandler)}
* login query has been received, you should use {@link ClientPlayConnectionEvents#INIT} to register the channel handler.
*
* @param type the packet type
* @param handler the handler
* @return {@code false} if a handler is already registered for the type
* @throws IllegalStateException if the client is not connected to a server
* @see ClientPlayConnectionEvents#INIT
*/
public static <T extends FabricPacket> boolean registerReceiver(PacketType<T> type, PlayPacketHandler<T> handler) {
return registerReceiver(type.getId(), new PlayChannelHandlerProxy<T>() {
@Override
public PlayPacketHandler<T> getOriginalHandler() {
return handler;
}

@Override
public void receive(MinecraftClient client, ClientPlayNetworkHandler networkHandler, PacketByteBuf buf, PacketSender sender) {
T packet = type.read(buf);

if (client.isOnThread()) {
// Do not submit to the render thread if we're already running there.
// Normally, packets are handled on the network IO thread - though it is
// not guaranteed (for example, with 1.19.4 S2C packet bundling)
// Since we're handling it right now, connection check is redundant.
handler.receive(packet, client.player, sender);
} else {
client.execute(() -> {
if (networkHandler.getConnection().isOpen()) handler.receive(packet, client.player, sender);
});
}
}
});
}

/**
* Removes the handler of a channel.
*
Expand All @@ -132,6 +248,23 @@ public static PlayChannelHandler unregisterReceiver(Identifier channelName) thro
throw new IllegalStateException("Cannot unregister receiver while not in game!");
}

/**
* Removes the handler for a packet type.
*
* <p>The {@code type} is guaranteed not to have an associated handler after this call.
*
* @param type the packet type
* @return the previous handler, or {@code null} if no handler was bound to the channel,
* or it was not registered using {@link #registerReceiver(PacketType, PlayPacketHandler)}
* @throws IllegalStateException if the client is not connected to a server
*/
@Nullable
@SuppressWarnings("unchecked")
public static <T extends FabricPacket> PlayPacketHandler<T> unregisterReceiver(PacketType<T> type) {
PlayChannelHandler handler = unregisterReceiver(type.getId());
return handler instanceof PlayChannelHandlerProxy<?> proxy ? (PlayPacketHandler<T>) proxy.getOriginalHandler() : null;
}

/**
* Gets all the channel names that the client can receive packets on.
*
Expand Down Expand Up @@ -168,7 +301,7 @@ public static Set<Identifier> getSendable() throws IllegalStateException {
* 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.
* @return {@code true} if the connected server has declared the ability to receive a packet on the specified channel.
* False if the client is not in game.
*/
public static boolean canSend(Identifier channelName) throws IllegalArgumentException {
Expand All @@ -180,6 +313,17 @@ public static boolean canSend(Identifier channelName) throws IllegalArgumentExce
return false;
}

/**
* Checks if the connected server declared the ability to receive a packet on a specified channel name.
* This returns {@code false} if the client is not in game.
*
* @param type the packet type
* @return {@code true} if the connected server has declared the ability to receive a packet on the specified channel
*/
public static boolean canSend(PacketType<?> type) {
return canSend(type.getId());
}

/**
* Creates a packet which may be sent to the connected server.
*
Expand Down Expand Up @@ -226,6 +370,21 @@ public static void send(Identifier channelName, PacketByteBuf buf) throws Illega
throw new IllegalStateException("Cannot send packets when not in game!");
}

/**
* Sends a packet to the connected server.
*
* @param packet the packet
* @throws IllegalStateException if the client is not connected to a server
*/
public static <T extends FabricPacket> void send(T packet) {
Objects.requireNonNull(packet, "Packet cannot be null");
Objects.requireNonNull(packet.getType(), "Packet#getType cannot return null");

PacketByteBuf buf = PacketByteBufs.create();
packet.write(buf);
send(packet.getType().getId(), buf);
}

private ClientPlayNetworking() {
}

Expand All @@ -239,11 +398,11 @@ public interface PlayChannelHandler {
*
* <p>An example usage of this is to display an overlay message:
* <pre>{@code
* ClientPlayNetworking.registerReceiver(new Identifier("mymod", "overlay"), (client, handler, buf, responseSender) -&rt; {
* ClientPlayNetworking.registerReceiver(new Identifier("mymod", "overlay"), (client, handler, buf, responseSender) -> {
* String message = buf.readString(32767);
*
* // All operations on the server or world must be executed on the server thread
* client.execute(() -&rt; {
* client.execute(() -> {
* client.inGameHud.setOverlayMessage(message, true);
* });
* });
Expand All @@ -255,4 +414,40 @@ public interface PlayChannelHandler {
*/
void receive(MinecraftClient client, ClientPlayNetworkHandler handler, PacketByteBuf buf, PacketSender responseSender);
}

/**
* An internal packet handler that works as a proxy between old and new API.
* @param <T> the type of the packet
*/
private interface PlayChannelHandlerProxy<T extends FabricPacket> extends PlayChannelHandler {
PlayPacketHandler<T> getOriginalHandler();
}

/**
* A thread-safe packet handler utilizing {@link FabricPacket}.
* @param <T> the type of the packet
*/
@FunctionalInterface
public interface PlayPacketHandler<T extends FabricPacket> {
/**
* Handles the incoming packet. This is called on the render thread, and can safely
* call client methods.
*
* <p>An example usage of this is to display an overlay message:
* <pre>{@code
* // See FabricPacket for creating the packet
* ClientPlayNetworking.registerReceiver(OVERLAY_PACKET_TYPE, (player, packet, responseSender) -> {
* MinecraftClient.getInstance().inGameHud.setOverlayMessage(packet.message(), true);
* });
* }</pre>
*
* <p>The network handler can be accessed via {@link ClientPlayerEntity#networkHandler}.
*
* @param packet the packet
* @param player the player that received the packet
* @param responseSender the packet sender
* @see FabricPacket
*/
void receive(T packet, ClientPlayerEntity player, PacketSender responseSender);
}
}
Original file line number Diff line number Diff line change
@@ -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 net.fabricmc.fabric.api.networking.v1;

import net.minecraft.network.PacketByteBuf;
import net.minecraft.server.network.ServerPlayerEntity;

/**
* A packet to be sent using Networking API. An instance of this class is created
* each time the packet is sent. This can be used on both the client and the server.
*
* <p>Implementations should have fields of values sent over the network.
* For example, a packet consisting of two integers should have two {@code int}
* fields with appropriate name. This is written to the buffer in {@link #write}.
* The packet should have two constructors: one that creates a packet on the sender,
* which initializes the fields to be written, and one that takes a {@link PacketByteBuf}
* and reads the packet.
*
* <p>For each packet class, a corresponding {@link PacketType} instance should be created.
* The type should be stored in a {@code static final} field, and {@link #getType} should
* return that type.
*
* <p>Example of a packet:
* <pre>{@code
* public record BoomPacket(boolean fire) implements FabricPacket {
* public static final PacketType<BoomPacket> TYPE = PacketType.create(new Identifier("example:boom"), BoomPacket::new);
*
* public BoomPacket(PacketByteBuf buf) {
* this(buf.readBoolean());
* }
*
* @Override
* public void write(PacketByteBuf buf) {
* buf.writeBoolean(this.fire);
* }
*
* @Override
* public PacketType<?> getType() {
* return TYPE;
* }
* }
* }</pre>
*
* @see ServerPlayNetworking#registerGlobalReceiver(PacketType, ServerPlayNetworking.PlayPacketHandler)
* @see ServerPlayNetworking#send(ServerPlayerEntity, PacketType, FabricPacket)
* @see PacketSender#sendPacket(PacketType, FabricPacket)
*/
public interface FabricPacket {
/**
* Writes the contents of this packet to the buffer.
* @param buf the output buffer
*/
void write(PacketByteBuf buf);

/**
* Returns the packet type of this packet.
*
* <p>Implementations should store the packet type instance in a {@code static final}
* field and return that here, instead of creating a new instance.
*
* @return the type of this packet
*/
PacketType<?> getType();
}
Loading

0 comments on commit a6f3ccf

Please sign in to comment.