diff --git a/src/main/java/turniplabs/halplibe/helper/EnvironmentHelper.java b/src/main/java/turniplabs/halplibe/helper/EnvironmentHelper.java new file mode 100644 index 0000000..324696c --- /dev/null +++ b/src/main/java/turniplabs/halplibe/helper/EnvironmentHelper.java @@ -0,0 +1,23 @@ +package turniplabs.halplibe.helper; + +import net.minecraft.client.Minecraft; +import net.minecraft.core.Global; + +public class EnvironmentHelper { + public static boolean isServerEnvironment() { + return Global.isServer; + } + + public static boolean isSinglePlayer() { + if (Global.isServer) { + return false; + } + + return !Minecraft.getMinecraft().isMultiplayerWorld(); + } + + public static boolean isClientWorld() { + return !isSinglePlayer() && !isServerEnvironment(); + } + +} diff --git a/src/main/java/turniplabs/halplibe/helper/network/NetworkHandler.java b/src/main/java/turniplabs/halplibe/helper/network/NetworkHandler.java new file mode 100644 index 0000000..bedb045 --- /dev/null +++ b/src/main/java/turniplabs/halplibe/helper/network/NetworkHandler.java @@ -0,0 +1,275 @@ +package turniplabs.halplibe.helper.network; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.Minecraft; +import net.minecraft.core.entity.player.Player; +import net.minecraft.core.net.packet.Packet; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.entity.player.PlayerServer; +import org.jetbrains.annotations.NotNull; +import turniplabs.halplibe.helper.EnvironmentHelper; + +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public final class NetworkHandler +{ + private static final List> messagesToRegisterForServer = new LinkedList<>(Collections.singletonList( + MessageIdsNetworkMessage::new + )); + + private static final Map> packetReaders = new HashMap<>(); + private static final Map, Short> packetIds = new HashMap<>(); + + private NetworkHandler() + { + } + + public static void setup() + { + Packet.addMapping (88, true, true, UniversalPacket.class ); + + register(); + } + + public static void register() + { + packetReaders.clear(); + packetIds.clear(); + + for (Supplier networkMessage : messagesToRegisterForServer) { + addNetworkMessage(networkMessage); + } + } + + public static void receiveUniversalPacket(NetworkMessage.NetworkContext context, UniversalPacket buffer ) + { + short type = buffer.readShort(); + + if (!packetReaders.containsKey(type)) { + return; + } + + packetReaders.get( type ) + .accept( context, buffer ); + } + + /** + * Register a NetworkMessage, and a thread-unsafe handler for it. + * + * @param factory The factory for this type of message. + */ + @SuppressWarnings({"unused"}) + public static void registerNetworkMessage( Supplier factory ) + { + messagesToRegisterForServer.add(factory); + } + + /** + * Register a NetworkMessage, and a thread-unsafe handler for it. + * + * @param The type of the NetworkMessage to send. + * @param factory The factory for this type of message. + */ + @SuppressWarnings({"unused"}) + public static void addNetworkMessage( Supplier factory ) + { + registerNetworkMessage((short) packetIds.size(), factory); + } + + /** + * Register a NetworkMessage, and a thread-unsafe handler for it. + * + * @param The type of the NetworkMessage to send. + * @param id The identifier for this message type + * @param factory The factory for this type of message. + */ + @SuppressWarnings({"unused"}) + private static void registerNetworkMessage( short id, Supplier factory ) + { + registerNetworkMessage( id, getType( factory ), buf -> { + T instance = factory.get(); + instance.decodeFromUniversalPacket( buf ); + return instance; + } ); + } + + /** + * Register a NetworkMessage, and a thread-unsafe handler for it. + * + * @param The type of the NetworkMessage to send. + * @param type The class of the type of message to send. + * @param id The identifier for this message type + * @param decoder The factory for this type of message. + */ + private static void registerNetworkMessage( short id, Class type, Function decoder ) + { + packetIds.put( type, id ); + packetReaders.put( id, ( context, buf ) -> { + T result = decoder.apply( buf ); + result.handle(context); + } ); + } + + @SuppressWarnings( "unchecked" ) + private static Class getType( Supplier supplier ) + { + return (Class) supplier.get() + .getClass(); + } + + private static UniversalPacket encode(NetworkMessage message ) + { + UniversalPacket buf = new UniversalPacket(); + buf.writeShort( packetIds.get( message.getClass() ) ); + message.encodeToUniversalPacket( buf ); + return buf; + } + + @Environment(EnvType.CLIENT) + private static void sendToPlayerLocal(NetworkMessage message) + { + message.handle(new NetworkMessage.NetworkContext(Minecraft.getMinecraft().thePlayer)); + } + + @Environment(EnvType.SERVER) + private static void sendToPlayerServer(Player player, NetworkMessage message) + { + ((PlayerServer)player).playerNetServerHandler.sendPacket(encode(message)); + } + + @Environment(EnvType.SERVER) + public static void sendToPlayerMessagesConfiguration(Player player) + { + ((PlayerServer)player).playerNetServerHandler.sendPacket(encode(new MessageIdsNetworkMessage(packetIds))); + } + + /** + * Send a NetworkMessage to a specific Player from the server + * If we are in SinglePlayer this will skip encoding and directly call the message handle + */ + @SuppressWarnings({"unused"}) + public static void sendToPlayer(Player player, NetworkMessage message ) + { + if (!EnvironmentHelper.isServerEnvironment()){ + sendToPlayerLocal(message); + return; + } + sendToPlayerServer(player, message); + } + + /** + * Send a NetworkMessage to all Players from the server + * If we are in SinglePlayer this will skip encoding and directly call the message handle + */ + @SuppressWarnings({"unused"}) + public static void sendToAllPlayers( NetworkMessage message ) + { + if (!EnvironmentHelper.isServerEnvironment()){ + sendToPlayerLocal(message); + return; + } + MinecraftServer.getInstance().playerList.sendPacketToAllPlayers(encode(message)); + } + + /** + * Send a NetworkMessage to the Server from the player + * If we are in SinglePlayer this will skip encoding and directly call the message handle + */ + @SuppressWarnings({"unused"}) + @Environment( EnvType.CLIENT ) + public static void sendToServer( NetworkMessage message ) + { + if (EnvironmentHelper.isSinglePlayer()){ + sendToPlayerLocal(message); + return; + } + Minecraft.getMinecraft().getSendQueue().addToSendQueue(encode(message)); + } + + /** + * Send a NetworkMessage to all Players around a block from the server + * If we are in SinglePlayer this will skip encoding and directly call the message handle + */ + @SuppressWarnings({"unused"}) + public static void sendToAllAround(double x, double y, double z, double radius, int dimension, NetworkMessage message ) + { + if (!EnvironmentHelper.isServerEnvironment()){ + sendToPlayerLocal(message); + return; + } + MinecraftServer.getInstance().playerList.sendPacketToPlayersAroundPoint(x, y, z, radius, dimension, encode(message)); + } + + private static class MessageIdsNetworkMessage implements NetworkMessage{ + Map, Short> packetIds; + + public MessageIdsNetworkMessage() {} + + public MessageIdsNetworkMessage(Map, Short> packetIds) { + this.packetIds = packetIds; + } + + @Override + public void encodeToUniversalPacket(@NotNull UniversalPacket packet) { + packet.writeShort((short) packetIds.size()); + + for (Map.Entry, Short> entry : packetIds.entrySet()) { + packet.writeShort(entry.getValue()); + packet.writeString(entry.getKey().getName()); + } + } + + @Override + public void decodeFromUniversalPacket(@NotNull UniversalPacket packet) { + this.packetIds = new HashMap<>(); + + final short size = packet.readShort(); + + try { + for (int i = 0; i < size; i++) { + final short id = packet.readShort(); + final Class messageClass = Class.forName(packet.readString()); + + this.packetIds.put(messageClass, id); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Override + public void handle(NetworkContext context) { + if (EnvironmentHelper.isServerEnvironment()) { + return; + } + + try { + NetworkHandler.packetReaders.clear(); + NetworkHandler.packetIds.clear(); + + for (Map.Entry, Short> entry : packetIds.entrySet()) { + Class klass = entry.getKey(); + if (NetworkMessage.class.isAssignableFrom(klass)) { + Supplier supplier = () -> { + try { + return (NetworkMessage) klass.getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + }; + NetworkHandler.registerNetworkMessage(entry.getValue(), supplier); + } else { + throw new IllegalArgumentException("Class " + klass.getName() + " does not extend NetworkMessage"); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/turniplabs/halplibe/helper/network/NetworkMessage.java b/src/main/java/turniplabs/halplibe/helper/network/NetworkMessage.java new file mode 100644 index 0000000..b53524d --- /dev/null +++ b/src/main/java/turniplabs/halplibe/helper/network/NetworkMessage.java @@ -0,0 +1,42 @@ +package turniplabs.halplibe.helper.network; + +import net.minecraft.core.entity.player.Player; + +import javax.annotation.Nonnull; + +public interface NetworkMessage { + /** + * Encode the UniversalPacket into your NetworkMessage. + * This may be called on any thread, so this should be a pure operation. + * + * @param packet The packet to write data to. + */ + void encodeToUniversalPacket(@Nonnull UniversalPacket packet ); + + /** + * Decode the UniversalPacket into your NetworkMessage. + * This may be called on any thread, so this should be a pure operation. + * + * @param packet The packet to read data from. + */ + void decodeFromUniversalPacket(@Nonnull UniversalPacket packet ); + + /** + * Handle this {@link NetworkMessage}. + * + * @param context An intermediary representation of Packet handler common on both Client and Server environment. + */ + void handle(NetworkContext context); + + class NetworkContext { + /** + * The player that send the NetworkPacket to the handle + */ + public Player player; + + public NetworkContext(Player player) { + this.player = player; + } + } + +} diff --git a/src/main/java/turniplabs/halplibe/helper/network/UniversalPacket.java b/src/main/java/turniplabs/halplibe/helper/network/UniversalPacket.java new file mode 100644 index 0000000..62c2656 --- /dev/null +++ b/src/main/java/turniplabs/halplibe/helper/network/UniversalPacket.java @@ -0,0 +1,298 @@ +package turniplabs.halplibe.helper.network; + +import com.mojang.nbt.NbtIo; +import com.mojang.nbt.tags.CompoundTag; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.Minecraft; +import net.minecraft.core.net.handler.PacketHandler; +import net.minecraft.core.net.packet.Packet; +import org.jetbrains.annotations.NotNull; +import turniplabs.halplibe.helper.EnvironmentHelper; +import turniplabs.halplibe.mixin.accessors.PacketHandlerServerAccessor; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * UniversalPacket is a general purpose packet made to transport multiple message into the same PacketType + * This work similar as DataInput/DataOutput, netty ByteBuf or modern Minecraft PacketByteBuf + */ +public class UniversalPacket extends Packet { + private byte[] buffer; + private int writeIndex; + private int readIndex; + + public UniversalPacket() { + this.buffer = new byte[0]; + this.writeIndex = 0; + this.readIndex = 0; + } + + @Deprecated + public void read(DataInputStream dis) throws IOException { + final int length = dis.readInt(); + buffer = new byte[length]; + writeIndex = dis.read(buffer, 0, length); + } + + /** + * If you want to write the UniversalPacket content to a DataOutputStream, use rawWrite instead + * since this method add an extra 4 bytes to every packet + */ + @Deprecated + public void write(DataOutputStream dos) throws IOException { + dos.writeInt(this.buffer.length); + dos.write(this.buffer); + } + + @SuppressWarnings("unused") + public void rawWrite(DataOutputStream dos) throws IOException { + dos.write(this.buffer); + } + + public void handlePacket(PacketHandler packetHandler) { + if (EnvironmentHelper.isServerEnvironment()) { + handlePacketServer(packetHandler); + return; + } + handlePacketClient(); + } + + @Environment(EnvType.SERVER) + private void handlePacketServer(PacketHandler packetHandler) { + NetworkHandler.receiveUniversalPacket(new NetworkMessage.NetworkContext(( + (PacketHandlerServerAccessor)packetHandler).getPlayerEntity() + ), this); + } + + @Environment(EnvType.CLIENT) + private void handlePacketClient() { + NetworkHandler.receiveUniversalPacket(new NetworkMessage.NetworkContext( + Minecraft.getMinecraft().thePlayer + ), this); + } + + public int getEstimatedSize() { + return buffer.length; + } + + @SuppressWarnings("unused") + public void writeByte(byte value) { + ensureCapacity(1); + buffer[writeIndex++] = value; + } + + @SuppressWarnings("unused") + public void writeByte(int value) { + writeByte((byte) value); + } + + @SuppressWarnings("unused") + public byte readByte() { + ensureReadable(1); + return buffer[readIndex++]; + } + + @SuppressWarnings("unused") + public void writeBytes(int... values) { + ensureCapacity(values.length); + for (int value : values) { + buffer[writeIndex++] = (byte) value; + } + } + + @SuppressWarnings("unused") + public void writeBytes(byte... values) { + ensureCapacity(values.length); + for (int value : values) { + buffer[writeIndex++] = (byte) value; + } + } + + @SuppressWarnings("unused") + public void readBytes(byte[] destination, int length) { + if (length > destination.length) { + throw new IllegalArgumentException(""); + } + ensureReadable(length); + System.arraycopy(buffer, readIndex, destination, 0, length); + readIndex += length; + } + + @SuppressWarnings("unused") + public void writeInt(int value) { + ensureCapacity(4); + buffer[writeIndex++] = (byte) (value >> 24); + buffer[writeIndex++] = (byte) (value >> 16); + buffer[writeIndex++] = (byte) (value >> 8); + buffer[writeIndex++] = (byte) value; + } + + @SuppressWarnings("unused") + public int readInt() { + ensureReadable(4); + return ((buffer[readIndex++] & 0xFF) << 24) | + ((buffer[readIndex++] & 0xFF) << 16) | + ((buffer[readIndex++] & 0xFF) << 8) | + (buffer[readIndex++] & 0xFF); + } + + @SuppressWarnings("unused") + public void writeShort(short value) { + ensureCapacity(2); + buffer[writeIndex++] = (byte) (value >> 8); + buffer[writeIndex++] = (byte) value; + } + + @SuppressWarnings("unused") + public short readShort() { + ensureReadable(2); + return (short) (((buffer[readIndex++] & 0xFF) << 8) | + (buffer[readIndex++] & 0xFF)); + } + + @SuppressWarnings("unused") + public void writeString(String value) { + byte[] stringBytes = value.getBytes(StandardCharsets.UTF_8); + writeInt(stringBytes.length); + ensureCapacity(stringBytes.length); + System.arraycopy(stringBytes, 0, buffer, writeIndex, stringBytes.length); + writeIndex += stringBytes.length; + } + + @SuppressWarnings("unused") + public String readString() { + int length = readInt(); + ensureReadable(length); + String value = new String(buffer, readIndex, length, StandardCharsets.UTF_8); + readIndex += length; + return value; + } + + @SuppressWarnings("unused") + public void writeBoolean(boolean value) { + ensureCapacity(1); + buffer[writeIndex++] = (byte) (value ? 1 : 0); + } + + @SuppressWarnings("unused") + public boolean readBoolean() { + ensureReadable(1); + return buffer[readIndex++] != 0; + } + + @SuppressWarnings("unused") + public void writeDouble(double value) { + long bits = Double.doubleToLongBits(value); + writeLong(bits); + } + + @SuppressWarnings("unused") + public double readDouble() { + long bits = readLong(); + return Double.longBitsToDouble(bits); + } + + @SuppressWarnings("unused") + public void writeLong(long value) { + ensureCapacity(8); + for (int i = 7; i >= 0; i--) { + buffer[writeIndex++] = (byte) (value >> (i * 8)); + } + } + + @SuppressWarnings("unused") + public long readLong() { + ensureReadable(8); + long value = 0; + for (int i = 0; i < 8; i++) { + value = (value << 8) | (buffer[readIndex++] & 0xFF); + } + return value; + } + + @SuppressWarnings("unused") + public void writeEnumConstant(Enum instance) { + int ordinal = instance.ordinal(); + this.writeByte(ordinal); + } + + @SuppressWarnings("unused") + public > T readEnumConstant(Class enumClass) { + int ordinal = this.readByte(); + T[] enumConstants = enumClass.getEnumConstants(); + return enumConstants[ordinal]; + } + + @SuppressWarnings("unused") + public void writeCompoundTag(CompoundTag tag) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + NbtIo.writeCompressed(tag, baos); + } catch (IOException e) { + throw new RuntimeException(e); + } + byte[] buffer = baos.toByteArray(); + writeShort((short)buffer.length); + writeBytes(buffer); + } + + @SuppressWarnings("unused") + public CompoundTag readCompoundTag() { + int length = Short.toUnsignedInt(readShort()); + if (length == 0) { + return null; + } else { + byte[] data = new byte[length]; + readBytes(data, length); + try { + return NbtIo.readCompressed(new ByteArrayInputStream(data)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @SuppressWarnings("unused") + public InputStream readBytesAsStream(int length) { + ensureReadable(length); + return new InputStream() { + private int remaining = length; + + @Override + public int read() { + if (remaining <= 0) { + return -1; + } + remaining--; + return buffer[readIndex++] & 0xFF; + } + + @Override + public int read(byte @NotNull [] b, int off, int len) { + if (remaining <= 0) { + return -1; + } + int toRead = Math.min(len, remaining); + System.arraycopy(buffer, readIndex, b, off, toRead); + readIndex += toRead; + remaining -= toRead; + return toRead; + } + }; + } + + private void ensureCapacity(int length) { + if (writeIndex + length > buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length + length + 64); + } + } + + private void ensureReadable(int length) { + if (readIndex + length > writeIndex) { + throw new IndexOutOfBoundsException("Not enough data to read."); + } + } +} \ No newline at end of file diff --git a/src/main/java/turniplabs/halplibe/mixin/MinecraftMixin.java b/src/main/java/turniplabs/halplibe/mixin/MinecraftMixin.java index 090eddb..86e5fa4 100644 --- a/src/main/java/turniplabs/halplibe/mixin/MinecraftMixin.java +++ b/src/main/java/turniplabs/halplibe/mixin/MinecraftMixin.java @@ -7,6 +7,7 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import turniplabs.halplibe.helper.network.NetworkHandler; import turniplabs.halplibe.util.BlockInitEntrypoint; import turniplabs.halplibe.util.ClientStartEntrypoint; @@ -35,6 +36,7 @@ public void beforeGameStartEntrypoint(CallbackInfo ci){ @Inject(method = "startGame", at = @At("TAIL")) public void afterGameStartEntrypoint(CallbackInfo ci){ + NetworkHandler.setup(); FabricLoader.getInstance().getEntrypoints("afterGameStart", GameStartEntrypoint.class).forEach(GameStartEntrypoint::afterGameStart); FabricLoader.getInstance().getEntrypoints("afterClientStart", ClientStartEntrypoint.class).forEach(ClientStartEntrypoint::afterClientStart); } diff --git a/src/main/java/turniplabs/halplibe/mixin/MinecraftServerMixin.java b/src/main/java/turniplabs/halplibe/mixin/MinecraftServerMixin.java index e8159f7..52ec383 100644 --- a/src/main/java/turniplabs/halplibe/mixin/MinecraftServerMixin.java +++ b/src/main/java/turniplabs/halplibe/mixin/MinecraftServerMixin.java @@ -8,6 +8,7 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import turniplabs.halplibe.helper.network.NetworkHandler; import turniplabs.halplibe.util.GameStartEntrypoint; import turniplabs.halplibe.util.RecipeEntrypoint; @@ -24,6 +25,7 @@ public void recipeEntrypoint(CallbackInfoReturnable cir){ public void beforeGameStartEntrypoint(CallbackInfoReturnable cir){ instance = (MinecraftServer)(Object)this; Global.isServer = true; + NetworkHandler.setup(); FabricLoader.getInstance().getEntrypoints("beforeGameStart", GameStartEntrypoint.class).forEach(GameStartEntrypoint::beforeGameStart); } diff --git a/src/main/java/turniplabs/halplibe/mixin/PacketHandlerLoginMixin.java b/src/main/java/turniplabs/halplibe/mixin/PacketHandlerLoginMixin.java new file mode 100644 index 0000000..4b6a757 --- /dev/null +++ b/src/main/java/turniplabs/halplibe/mixin/PacketHandlerLoginMixin.java @@ -0,0 +1,23 @@ +package turniplabs.halplibe.mixin; + +import net.minecraft.core.net.packet.PacketLogin; +import net.minecraft.server.entity.player.PlayerServer; +import net.minecraft.server.net.handler.PacketHandlerLogin; +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 org.spongepowered.asm.mixin.injection.callback.LocalCapture; +import turniplabs.halplibe.helper.network.NetworkHandler; + +@Mixin(value = PacketHandlerLogin.class, remap = false) +public class PacketHandlerLoginMixin { + @Inject(method = "doLogin(Lnet/minecraft/core/net/packet/PacketLogin;)V", at = @At(value = "INVOKE", + target = "Lnet/minecraft/server/net/handler/PacketHandlerServer;sendPacket(Lnet/minecraft/core/net/packet/Packet;)V", + ordinal = 0, shift = At.Shift.AFTER), + locals = LocalCapture.CAPTURE_FAILHARD + ) + public void doLogin(PacketLogin packetlogin, CallbackInfo ci, PlayerServer player) { + NetworkHandler.sendToPlayerMessagesConfiguration(player); + } + } diff --git a/src/main/java/turniplabs/halplibe/mixin/accessors/PacketHandlerServerAccessor.java b/src/main/java/turniplabs/halplibe/mixin/accessors/PacketHandlerServerAccessor.java new file mode 100644 index 0000000..26ca5b9 --- /dev/null +++ b/src/main/java/turniplabs/halplibe/mixin/accessors/PacketHandlerServerAccessor.java @@ -0,0 +1,12 @@ +package turniplabs.halplibe.mixin.accessors; + +import net.minecraft.server.entity.player.PlayerServer; +import net.minecraft.server.net.handler.PacketHandlerServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(value = PacketHandlerServer.class, remap = false) +public interface PacketHandlerServerAccessor { + @Accessor + PlayerServer getPlayerEntity(); +} diff --git a/src/main/resources/halplibe.mixins.json b/src/main/resources/halplibe.mixins.json index 3a061b4..3d576d9 100644 --- a/src/main/resources/halplibe.mixins.json +++ b/src/main/resources/halplibe.mixins.json @@ -14,7 +14,8 @@ "accessors.EntityFXAccessor", "accessors.LanguageAccessor", "accessors.WeightedRandomBagAccessor", - "accessors.WeightedRandomBagEntryAccessor" + "accessors.WeightedRandomBagEntryAccessor", + "accessors.PacketHandlerServerAccessor" ], "client": [ "PacketHandlerClientMixin", @@ -25,6 +26,7 @@ "models.TileEntityRendererDispatcherMixin" ], "server": [ + "PacketHandlerLoginMixin" ], "injectors": { "defaultRequire": 1