Browse Source

Add support for PacketTransformer and a SplitPacketTransformer (#142)

* Add support for PacketTransformer and a SplitPacketTransformer

* Add testmod and make it work

* Add experimental, Remove generics, they are pointless
shedaniel 3 năm trước cách đây
mục cha
commit
6b2506d1fd

+ 1 - 1
build.gradle

@@ -1,5 +1,5 @@
 plugins {
-    id "architectury-plugin" version "3.3-SNAPSHOT"
+    id "architectury-plugin" version "3.4-SNAPSHOT"
     id "dev.architectury.loom" version "0.7.3-SNAPSHOT" apply false
     id "org.cadixdev.licenser" version "0.5.0"
     id "com.matthewprenger.cursegradle" version "1.4.0" apply false

+ 33 - 14
common/src/main/java/me/shedaniel/architectury/networking/NetworkManager.java

@@ -20,48 +20,67 @@
 package me.shedaniel.architectury.networking;
 
 import dev.architectury.injectables.annotations.ExpectPlatform;
+import me.shedaniel.architectury.networking.transformers.PacketCollector;
+import me.shedaniel.architectury.networking.transformers.PacketSink;
+import me.shedaniel.architectury.networking.transformers.PacketTransformer;
+import me.shedaniel.architectury.networking.transformers.SinglePacketCollector;
 import me.shedaniel.architectury.utils.Env;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
-import net.minecraft.client.Minecraft;
 import net.minecraft.network.FriendlyByteBuf;
 import net.minecraft.network.protocol.Packet;
 import net.minecraft.resources.ResourceLocation;
 import net.minecraft.server.level.ServerPlayer;
 import net.minecraft.world.entity.Entity;
 import net.minecraft.world.entity.player.Player;
+import org.jetbrains.annotations.ApiStatus;
 
-import java.util.Objects;
+import java.util.Collections;
+import java.util.List;
 
 public final class NetworkManager {
-    @ExpectPlatform
     public static void registerReceiver(Side side, ResourceLocation id, NetworkReceiver receiver) {
-        throw new AssertionError();
+        registerReceiver(side, id, Collections.emptyList(), receiver);
     }
     
     @ExpectPlatform
+    @ApiStatus.Experimental
+    public static void registerReceiver(Side side, ResourceLocation id, List<PacketTransformer> packetTransformers, NetworkReceiver receiver) {
+        throw new AssertionError();
+    }
+    
+    @Deprecated
+    @ApiStatus.ScheduledForRemoval
     public static Packet<?> toPacket(Side side, ResourceLocation id, FriendlyByteBuf buf) {
+        SinglePacketCollector sink = new SinglePacketCollector(null);
+        collectPackets(sink, side, id, buf);
+        return sink.getPacket();
+    }
+    
+    @Deprecated
+    @ApiStatus.ScheduledForRemoval
+    public static List<Packet<?>> toPackets(Side side, ResourceLocation id, FriendlyByteBuf buf) {
+        PacketCollector sink = new PacketCollector(null);
+        collectPackets(sink, side, id, buf);
+        return sink.collect();
+    }
+    
+    @ExpectPlatform
+    public static void collectPackets(PacketSink sink, Side side, ResourceLocation id, FriendlyByteBuf buf) {
         throw new AssertionError();
     }
     
     public static void sendToPlayer(ServerPlayer player, ResourceLocation id, FriendlyByteBuf buf) {
-        Objects.requireNonNull(player, "Unable to send packet to a 'null' player!").connection.send(toPacket(serverToClient(), id, buf));
+        collectPackets(PacketSink.ofPlayer(player), serverToClient(), id, buf);
     }
     
     public static void sendToPlayers(Iterable<ServerPlayer> players, ResourceLocation id, FriendlyByteBuf buf) {
-        Packet<?> packet = toPacket(serverToClient(), id, buf);
-        for (ServerPlayer player : players) {
-            Objects.requireNonNull(player, "Unable to send packet to a 'null' player!").connection.send(packet);
-        }
+        collectPackets(PacketSink.ofPlayers(players), serverToClient(), id, buf);
     }
     
     @Environment(EnvType.CLIENT)
     public static void sendToServer(ResourceLocation id, FriendlyByteBuf buf) {
-        if (Minecraft.getInstance().getConnection() != null) {
-            Minecraft.getInstance().getConnection().send(toPacket(clientToServer(), id, buf));
-        } else {
-            throw new IllegalStateException("Unable to send packet to the server while not in game!");
-        }
+        collectPackets(PacketSink.client(), clientToServer(), id, buf);
     }
     
     @Environment(EnvType.CLIENT)

+ 49 - 0
common/src/main/java/me/shedaniel/architectury/networking/transformers/PacketCollector.java

@@ -0,0 +1,49 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.networking.transformers;
+
+import net.minecraft.network.protocol.Packet;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class PacketCollector implements PacketSink {
+    @Nullable
+    private final Consumer<Packet<?>> consumer;
+    private final List<Packet<?>> packets = new ArrayList<>();
+    
+    public PacketCollector(@Nullable Consumer<Packet<?>> consumer) {
+        this.consumer = consumer;
+    }
+    
+    @Override
+    public void accept(Packet<?> packet) {
+        packets.add(packet);
+        if (this.consumer != null) {
+            this.consumer.accept(packet);
+        }
+    }
+    
+    public List<Packet<?>> collect() {
+        return packets;
+    }
+}

+ 56 - 0
common/src/main/java/me/shedaniel/architectury/networking/transformers/PacketSink.java

@@ -0,0 +1,56 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.networking.transformers;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.Minecraft;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.server.level.ServerPlayer;
+
+import java.util.Objects;
+
+@FunctionalInterface
+public interface PacketSink {
+    static PacketSink ofPlayer(ServerPlayer player) {
+        return packet -> Objects.requireNonNull(player, "Unable to send packet to a 'null' player!").connection.send(packet);
+    }
+    
+    static PacketSink ofPlayers(Iterable<? extends ServerPlayer> players) {
+        return packet -> {
+            for (ServerPlayer player : players) {
+                Objects.requireNonNull(player, "Unable to send packet to a 'null' player!").connection.send(packet);
+            }
+        };
+    }
+    
+    @Environment(EnvType.CLIENT)
+    static PacketSink client() {
+        return packet -> {
+            if (Minecraft.getInstance().getConnection() != null) {
+                Minecraft.getInstance().getConnection().send(packet);
+            } else {
+                throw new IllegalStateException("Unable to send packet to the server while not in game!");
+            }
+        };
+    }
+    
+    void accept(Packet<?> packet);
+}

+ 111 - 0
common/src/main/java/me/shedaniel/architectury/networking/transformers/PacketTransformer.java

@@ -0,0 +1,111 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.networking.transformers;
+
+import me.shedaniel.architectury.networking.NetworkManager;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.resources.ResourceLocation;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+@ApiStatus.Experimental
+public interface PacketTransformer {
+    void inbound(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, NetworkManager.PacketContext context, TransformationSink sink);
+    
+    void outbound(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, TransformationSink sink);
+    
+    @FunctionalInterface
+    interface TransformationSink {
+        void accept(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf);
+    }
+    
+    static PacketTransformer none() {
+        return new PacketTransformer() {
+            @Override
+            public void inbound(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, NetworkManager.PacketContext context, TransformationSink sink) {
+                sink.accept(side, id, buf);
+            }
+            
+            @Override
+            public void outbound(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, TransformationSink sink) {
+                sink.accept(side, id, buf);
+            }
+        };
+    }
+    
+    static PacketTransformer concat(Iterable<? extends PacketTransformer> transformers) {
+        if (transformers instanceof Collection && ((Collection<? extends PacketTransformer>) transformers).isEmpty()) {
+            return PacketTransformer.none();
+        } else if (transformers instanceof Collection && ((Collection<? extends PacketTransformer>) transformers).size() == 1) {
+            return transformers.iterator().next();
+        }
+        return new PacketTransformer() {
+            @Override
+            public void inbound(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, NetworkManager.PacketContext context, TransformationSink sink) {
+                traverse(side, id, buf, context, sink, true, 0);
+            }
+            
+            @Override
+            public void outbound(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, TransformationSink sink) {
+                traverse(side, id, buf, null, sink, false, 0);
+            }
+            
+            private void traverse(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, @Nullable NetworkManager.PacketContext context, TransformationSink outerSink, boolean inbound, int index) {
+                if (transformers instanceof List) {
+                    if (((List<? extends PacketTransformer>) transformers).size() > index) {
+                        PacketTransformer transformer = ((List<? extends PacketTransformer>) transformers).get(index);
+                        TransformationSink sink = (side1, id1, buf1) -> {
+                            traverse(side1, id1, buf1, context, outerSink, inbound, index + 1);
+                        };
+                        if (inbound) {
+                            transformer.inbound(side, id, buf, context, sink);
+                        } else {
+                            transformer.outbound(side, id, buf, sink);
+                        }
+                    } else {
+                        outerSink.accept(side, id, buf);
+                    }
+                } else {
+                    Iterator<? extends PacketTransformer> iterator = transformers.iterator();
+                    for (int i = 0; i < index; i++) {
+                        iterator.next();
+                    }
+                    PacketTransformer transformer = iterator.hasNext() ? iterator.next() : PacketTransformer.none();
+                    TransformationSink sink = (side1, id1, buf1) -> {
+                        if (iterator.hasNext()) {
+                            traverse(side1, id1, buf1, context, outerSink, inbound, index + 1);
+                        } else {
+                            outerSink.accept(side1, id1, buf1);
+                        }
+                    };
+                    if (inbound) {
+                        transformer.inbound(side, id, buf, context, sink);
+                    } else {
+                        transformer.outbound(side, id, buf, sink);
+                    }
+                }
+            }
+        };
+    }
+}

+ 51 - 0
common/src/main/java/me/shedaniel/architectury/networking/transformers/SinglePacketCollector.java

@@ -0,0 +1,51 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.networking.transformers;
+
+import net.minecraft.network.protocol.Packet;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.Consumer;
+
+public class SinglePacketCollector implements PacketSink {
+    @Nullable
+    private final Consumer<Packet<?>> consumer;
+    private Packet<?> packet;
+    
+    public SinglePacketCollector(@Nullable Consumer<Packet<?>> consumer) {
+        this.consumer = consumer;
+    }
+    
+    @Override
+    public void accept(Packet<?> packet) {
+        if (this.packet == null) {
+            this.packet = packet;
+            if (this.consumer != null) {
+                this.consumer.accept(packet);
+            }
+        } else {
+            throw new IllegalStateException("Already accepted one packet!");
+        }
+    }
+    
+    public Packet<?> getPacket() {
+        return packet;
+    }
+}

+ 210 - 0
common/src/main/java/me/shedaniel/architectury/networking/transformers/SplitPacketTransformer.java

@@ -0,0 +1,210 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.networking.transformers;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import me.shedaniel.architectury.event.events.PlayerEvent;
+import me.shedaniel.architectury.event.events.client.ClientPlayerEvent;
+import me.shedaniel.architectury.networking.NetworkManager;
+import me.shedaniel.architectury.utils.Env;
+import me.shedaniel.architectury.utils.EnvExecutor;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.resources.ResourceLocation;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+@ApiStatus.Experimental
+public class SplitPacketTransformer implements PacketTransformer {
+    private static final Logger LOGGER = LogManager.getLogger(SplitPacketTransformer.class);
+    private static final byte START = 0x0;
+    private static final byte PART = 0x1;
+    private static final byte END = 0x2;
+    private static final byte ONLY = 0x3;
+    
+    private static class PartKey {
+        private final NetworkManager.Side side;
+        @Nullable
+        private final UUID playerUUID;
+        
+        public PartKey(NetworkManager.Side side, @Nullable UUID playerUUID) {
+            this.side = side;
+            this.playerUUID = playerUUID;
+        }
+        
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof PartKey)) return false;
+            PartKey key = (PartKey) o;
+            return side == key.side && Objects.equals(playerUUID, key.playerUUID);
+        }
+        
+        @Override
+        public int hashCode() {
+            return Objects.hash(side, playerUUID);
+        }
+        
+        @Override
+        public String toString() {
+            return "PartKey{" +
+                    "side=" + side +
+                    ", playerUUID=" + playerUUID +
+                    '}';
+        }
+    }
+    
+    private static class PartData {
+        private final ResourceLocation id;
+        private final int partsExpected;
+        private final List<FriendlyByteBuf> parts;
+        
+        public PartData(ResourceLocation id, int partsExpected) {
+            this.id = id;
+            this.partsExpected = partsExpected;
+            this.parts = new ArrayList<>();
+        }
+    }
+    
+    private final Map<PartKey, PartData> cache = Collections.synchronizedMap(new HashMap<>());
+    
+    public SplitPacketTransformer() {
+        PlayerEvent.PLAYER_QUIT.register(player -> {
+            cache.keySet().removeIf(key -> Objects.equals(key.playerUUID, player.getUUID()));
+        });
+        EnvExecutor.runInEnv(Env.CLIENT, () -> new Client()::init);
+    }
+    
+    private class Client {
+        @Environment(EnvType.CLIENT)
+        private void init() {
+            ClientPlayerEvent.CLIENT_PLAYER_QUIT.register(player -> {
+                cache.keySet().removeIf(key -> key.side == NetworkManager.Side.S2C);
+            });
+        }
+    }
+    
+    @Override
+    public void inbound(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, NetworkManager.PacketContext context, TransformationSink sink) {
+        PartKey key = side == NetworkManager.Side.S2C ? new PartKey(side, null) : new PartKey(side, context.getPlayer().getUUID());
+        PartData data;
+        switch (buf.readByte()) {
+            case START:
+                data = new PartData(id, buf.readInt());
+                if (cache.put(key, data) != null) {
+                    LOGGER.warn("Received invalid START packet for SplitPacketTransformer with packet id " + id + " for side " + side);
+                }
+                buf.retain();
+                data.parts.add(buf);
+                break;
+            case PART:
+                if ((data = cache.get(key)) == null) {
+                    LOGGER.warn("Received invalid PART packet for SplitPacketTransformer with packet id " + id + " for side " + side);
+                    buf.release();
+                } else if (!data.id.equals(id)) {
+                    LOGGER.warn("Received invalid PART packet for SplitPacketTransformer with packet id " + id + " for side " + side + ", id in cache is " + data.id);
+                    buf.release();
+                    for (FriendlyByteBuf part : data.parts) {
+                        if (part != buf) {
+                            part.release();
+                        }
+                    }
+                    cache.remove(key);
+                } else {
+                    buf.retain();
+                    data.parts.add(buf);
+                }
+                break;
+            case END:
+                if ((data = cache.get(key)) == null) {
+                    LOGGER.warn("Received invalid END packet for SplitPacketTransformer with packet id " + id + " for side " + side);
+                    buf.release();
+                } else if (!data.id.equals(id)) {
+                    LOGGER.warn("Received invalid END packet for SplitPacketTransformer with packet id " + id + " for side " + side + ", id in cache is " + data.id);
+                    buf.release();
+                    for (FriendlyByteBuf part : data.parts) {
+                        if (part != buf) {
+                            part.release();
+                        }
+                    }
+                    cache.remove(key);
+                } else {
+                    buf.retain();
+                    data.parts.add(buf);
+                }
+                if (data.parts.size() != data.partsExpected) {
+                    LOGGER.warn("Received invalid END packet for SplitPacketTransformer with packet id " + id + " for side " + side + " with size " + data.parts + ", parts expected is " + data.partsExpected);
+                    for (FriendlyByteBuf part : data.parts) {
+                        if (part != buf) {
+                            part.release();
+                        }
+                    }
+                } else {
+                    FriendlyByteBuf byteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(data.parts.toArray(new ByteBuf[0])));
+                    sink.accept(side, data.id, byteBuf);
+                    byteBuf.release();
+                }
+                cache.remove(key);
+                break;
+            case ONLY:
+                sink.accept(side, id, buf);
+                break;
+            default:
+                throw new IllegalStateException("Illegal split packet header!");
+        }
+    }
+    
+    @Override
+    public void outbound(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf, TransformationSink sink) {
+        int maxSize = (side == NetworkManager.Side.C2S ? 32767 : 1048576) - 1 - 10;
+        if (buf.readableBytes() <= maxSize) {
+            ByteBuf stateBuf = Unpooled.buffer(1);
+            stateBuf.writeByte(ONLY);
+            FriendlyByteBuf packetBuffer = new FriendlyByteBuf(Unpooled.wrappedBuffer(stateBuf, buf));
+            sink.accept(side, id, packetBuffer);
+        } else {
+            int partSize = maxSize - 4;
+            int parts = Math.round(buf.readableBytes() / (float) partSize);
+            for (int i = 0; i < parts; i++) {
+                FriendlyByteBuf packetBuffer = new FriendlyByteBuf(Unpooled.buffer());
+                if (i == 0) {
+                    packetBuffer.writeByte(START);
+                    packetBuffer.writeInt(parts);
+                } else if (i == parts - 1) {
+                    packetBuffer.writeByte(END);
+                } else {
+                    packetBuffer.writeByte(PART);
+                }
+                int next = Math.min(buf.readableBytes(), partSize);
+                packetBuffer.writeBytes(buf.retainedSlice(buf.readerIndex(), next));
+                buf.skipBytes(next);
+                sink.accept(side, id, packetBuffer);
+            }
+            
+        }
+        buf.release();
+    }
+}

+ 53 - 7
fabric/src/main/java/me/shedaniel/architectury/networking/fabric/NetworkManagerImpl.java

@@ -21,6 +21,8 @@ package me.shedaniel.architectury.networking.fabric;
 
 import me.shedaniel.architectury.networking.NetworkManager;
 import me.shedaniel.architectury.networking.NetworkManager.NetworkReceiver;
+import me.shedaniel.architectury.networking.transformers.PacketSink;
+import me.shedaniel.architectury.networking.transformers.PacketTransformer;
 import me.shedaniel.architectury.utils.Env;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
@@ -34,22 +36,55 @@ import net.minecraft.server.level.ServerPlayer;
 import net.minecraft.world.entity.Entity;
 import net.minecraft.world.entity.player.Player;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 public class NetworkManagerImpl {
-    public static void registerReceiver(NetworkManager.Side side, ResourceLocation id, NetworkReceiver receiver) {
+    private static final Map<ResourceLocation, NetworkReceiver> C2S_RECEIVER = new HashMap<>();
+    private static final Map<ResourceLocation, NetworkReceiver> S2C_RECEIVER = new HashMap<>();
+    private static final Map<ResourceLocation, PacketTransformer> C2S_TRANSFORMERS = new HashMap<>();
+    private static final Map<ResourceLocation, PacketTransformer> S2C_TRANSFORMERS = new HashMap<>();
+    
+    public static void registerReceiver(NetworkManager.Side side, ResourceLocation id, List<PacketTransformer> packetTransformers, NetworkReceiver receiver) {
         if (side == NetworkManager.Side.C2S) {
-            registerC2SReceiver(id, receiver);
+            registerC2SReceiver(id, packetTransformers, receiver);
         } else if (side == NetworkManager.Side.S2C) {
-            registerS2CReceiver(id, receiver);
+            registerS2CReceiver(id, packetTransformers, receiver);
         }
     }
     
-    private static void registerC2SReceiver(ResourceLocation id, NetworkReceiver receiver) {
-        ServerSidePacketRegistry.INSTANCE.register(id, (packetContext, buf) -> receiver.receive(buf, to(packetContext)));
+    private static void registerC2SReceiver(ResourceLocation id, List<PacketTransformer> packetTransformers, NetworkReceiver receiver) {
+        C2S_RECEIVER.put(id, receiver);
+        PacketTransformer transformer = PacketTransformer.concat(packetTransformers);
+        ServerSidePacketRegistry.INSTANCE.register(id, (packetContext, buf) -> {
+            NetworkManager.PacketContext context = to(packetContext);
+            transformer.inbound(NetworkManager.Side.C2S, id, buf, context, (side, id1, buf1) -> {
+                NetworkReceiver networkReceiver = side == NetworkManager.Side.C2S ? C2S_RECEIVER.get(id1) : S2C_RECEIVER.get(id1);
+                if (networkReceiver == null) {
+                    throw new IllegalArgumentException("Network Receiver not found! " + id1);
+                }
+                networkReceiver.receive(buf1, context);
+            });
+        });
+        C2S_TRANSFORMERS.put(id, transformer);
     }
     
     @Environment(EnvType.CLIENT)
-    private static void registerS2CReceiver(ResourceLocation id, NetworkReceiver receiver) {
-        ClientSidePacketRegistry.INSTANCE.register(id, (packetContext, buf) -> receiver.receive(buf, to(packetContext)));
+    private static void registerS2CReceiver(ResourceLocation id, List<PacketTransformer> packetTransformers, NetworkReceiver receiver) {
+        S2C_RECEIVER.put(id, receiver);
+        PacketTransformer transformer = PacketTransformer.concat(packetTransformers);
+        ClientSidePacketRegistry.INSTANCE.register(id, (packetContext, buf) -> {
+            NetworkManager.PacketContext context = to(packetContext);
+            transformer.inbound(NetworkManager.Side.S2C, id, buf, context, (side, id1, buf1) -> {
+                NetworkReceiver networkReceiver = side == NetworkManager.Side.C2S ? C2S_RECEIVER.get(id1) : S2C_RECEIVER.get(id1);
+                if (networkReceiver == null) {
+                    throw new IllegalArgumentException("Network Receiver not found! " + id1);
+                }
+                networkReceiver.receive(buf1, context);
+            });
+        });
+        S2C_TRANSFORMERS.put(id, transformer);
     }
     
     private static NetworkManager.PacketContext to(PacketContext context) {
@@ -71,6 +106,17 @@ public class NetworkManagerImpl {
         };
     }
     
+    public static void collectPackets(PacketSink sink, NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf) {
+        PacketTransformer transformer = side == NetworkManager.Side.C2S ? C2S_TRANSFORMERS.get(id) : S2C_TRANSFORMERS.get(id);
+        if (transformer != null) {
+            transformer.outbound(side, id, buf, (side1, id1, buf1) -> {
+                sink.accept(toPacket(side1, id1, buf1));
+            });
+        } else {
+            sink.accept(toPacket(side, id, buf));
+        }
+    }
+    
     public static Packet<?> toPacket(NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf) {
         if (side == NetworkManager.Side.C2S) {
             return toC2SPacket(id, buf);

+ 3 - 2
forge/src/main/java/me/shedaniel/architectury/networking/forge/ClientNetworkingManager.java

@@ -30,6 +30,7 @@ import net.minecraftforge.common.MinecraftForge;
 import net.minecraftforge.eventbus.api.SubscribeEvent;
 import net.minecraftforge.fml.network.NetworkEvent;
 
+import java.util.Collections;
 import java.util.Set;
 
 import static me.shedaniel.architectury.networking.forge.NetworkManagerImpl.C2S;
@@ -38,10 +39,10 @@ import static me.shedaniel.architectury.networking.forge.NetworkManagerImpl.SYNC
 @OnlyIn(Dist.CLIENT)
 public class ClientNetworkingManager {
     public static void initClient() {
-        NetworkManagerImpl.CHANNEL.addListener(NetworkManagerImpl.createPacketHandler(NetworkEvent.ServerCustomPayloadEvent.class, NetworkManagerImpl.S2C));
+        NetworkManagerImpl.CHANNEL.addListener(NetworkManagerImpl.createPacketHandler(NetworkEvent.ServerCustomPayloadEvent.class, NetworkManagerImpl.S2C_TRANSFORMERS));
         MinecraftForge.EVENT_BUS.register(ClientNetworkingManager.class);
         
-        NetworkManagerImpl.registerS2CReceiver(SYNC_IDS, (buffer, context) -> {
+        NetworkManagerImpl.registerS2CReceiver(SYNC_IDS, Collections.emptyList(), (buffer, context) -> {
             Set<ResourceLocation> receivables = NetworkManagerImpl.serverReceivables;
             int size = buffer.readInt();
             receivables.clear();

+ 40 - 12
forge/src/main/java/me/shedaniel/architectury/networking/forge/NetworkManagerImpl.java

@@ -25,6 +25,8 @@ import io.netty.buffer.Unpooled;
 import me.shedaniel.architectury.forge.ArchitecturyForge;
 import me.shedaniel.architectury.networking.NetworkManager;
 import me.shedaniel.architectury.networking.NetworkManager.NetworkReceiver;
+import me.shedaniel.architectury.networking.transformers.PacketSink;
+import me.shedaniel.architectury.networking.transformers.PacketTransformer;
 import me.shedaniel.architectury.utils.Env;
 import net.minecraft.network.FriendlyByteBuf;
 import net.minecraft.network.protocol.Packet;
@@ -48,6 +50,7 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -55,11 +58,11 @@ import java.util.function.Consumer;
 
 @Mod.EventBusSubscriber(modid = ArchitecturyForge.MOD_ID)
 public class NetworkManagerImpl {
-    public static void registerReceiver(NetworkManager.Side side, ResourceLocation id, NetworkReceiver receiver) {
+    public static void registerReceiver(NetworkManager.Side side, ResourceLocation id, List<PacketTransformer> packetTransformers, NetworkReceiver receiver) {
         if (side == NetworkManager.Side.C2S) {
-            registerC2SReceiver(id, receiver);
+            registerC2SReceiver(id, packetTransformers, receiver);
         } else if (side == NetworkManager.Side.S2C) {
-            registerS2CReceiver(id, receiver);
+            registerS2CReceiver(id, packetTransformers, receiver);
         }
     }
     
@@ -70,21 +73,34 @@ public class NetworkManagerImpl {
         return (side == NetworkManager.Side.C2S ? NetworkDirection.PLAY_TO_SERVER : NetworkDirection.PLAY_TO_CLIENT).buildPacket(Pair.of(packetBuffer, 0), CHANNEL_ID).getThis();
     }
     
+    public static void collectPackets(PacketSink sink, NetworkManager.Side side, ResourceLocation id, FriendlyByteBuf buf) {
+        PacketTransformer transformer = side == NetworkManager.Side.C2S ? C2S_TRANSFORMERS.get(id) : S2C_TRANSFORMERS.get(id);
+        if (transformer != null) {
+            transformer.outbound(side, id, buf, (side1, id1, buf1) -> {
+                sink.accept(toPacket(side1, id1, buf1));
+            });
+        } else {
+            sink.accept(toPacket(side, id, buf));
+        }
+    }
+    
     private static final Logger LOGGER = LogManager.getLogger();
     private static final ResourceLocation CHANNEL_ID = new ResourceLocation("architectury:network");
     static final ResourceLocation SYNC_IDS = new ResourceLocation("architectury:sync_ids");
     static final EventNetworkChannel CHANNEL = NetworkRegistry.newEventChannel(CHANNEL_ID, () -> "1", version -> true, version -> true);
     static final Map<ResourceLocation, NetworkReceiver> S2C = Maps.newHashMap();
     static final Map<ResourceLocation, NetworkReceiver> C2S = Maps.newHashMap();
+    static final Map<ResourceLocation, PacketTransformer> S2C_TRANSFORMERS = Maps.newHashMap();
+    static final Map<ResourceLocation, PacketTransformer> C2S_TRANSFORMERS = Maps.newHashMap();
     static final Set<ResourceLocation> serverReceivables = Sets.newHashSet();
     private static final Multimap<Player, ResourceLocation> clientReceivables = Multimaps.newMultimap(Maps.newHashMap(), Sets::newHashSet);
     
     static {
-        CHANNEL.addListener(createPacketHandler(NetworkEvent.ClientCustomPayloadEvent.class, C2S));
+        CHANNEL.addListener(createPacketHandler(NetworkEvent.ClientCustomPayloadEvent.class, C2S_TRANSFORMERS));
         
         DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> ClientNetworkingManager::initClient);
         
-        registerC2SReceiver(SYNC_IDS, (buffer, context) -> {
+        registerC2SReceiver(SYNC_IDS, Collections.emptyList(), (buffer, context) -> {
             Set<ResourceLocation> receivables = (Set<ResourceLocation>) clientReceivables.get(context.getPlayer());
             int size = buffer.readInt();
             receivables.clear();
@@ -94,7 +110,7 @@ public class NetworkManagerImpl {
         });
     }
     
-    static <T extends NetworkEvent> Consumer<T> createPacketHandler(Class<T> clazz, Map<ResourceLocation, NetworkReceiver> map) {
+    static <T extends NetworkEvent> Consumer<T> createPacketHandler(Class<T> clazz, Map<ResourceLocation, PacketTransformer> map) {
         return event -> {
             if (event.getClass() != clazz) return;
             NetworkEvent.Context context = event.getSource().get();
@@ -102,10 +118,11 @@ public class NetworkManagerImpl {
             FriendlyByteBuf buffer = event.getPayload();
             if (buffer == null) return;
             ResourceLocation type = buffer.readResourceLocation();
-            NetworkReceiver receiver = map.get(type);
+            PacketTransformer transformer = map.get(type);
             
-            if (receiver != null) {
-                receiver.receive(buffer, new NetworkManager.PacketContext() {
+            if (transformer != null) {
+                NetworkManager.Side side = context.getDirection().getReceptionSide() == LogicalSide.CLIENT ? NetworkManager.Side.S2C : NetworkManager.Side.C2S;
+                NetworkManager.PacketContext packetContext = new NetworkManager.PacketContext() {
                     @Override
                     public Player getPlayer() {
                         return getEnvironment() == Env.CLIENT ? getClientPlayer() : context.getSender();
@@ -124,6 +141,13 @@ public class NetworkManagerImpl {
                     private Player getClientPlayer() {
                         return DistExecutor.unsafeCallWhenOn(Dist.CLIENT, () -> ClientNetworkingManager::getClientPlayer);
                     }
+                };
+                transformer.inbound(side, type, buffer, packetContext, (side1, id1, buf1) -> {
+                    NetworkReceiver networkReceiver = side == NetworkManager.Side.C2S ? C2S.get(id1) : S2C.get(id1);
+                    if (networkReceiver == null) {
+                        throw new IllegalArgumentException("Network Receiver not found! " + id1);
+                    }
+                    networkReceiver.receive(buf1, packetContext);
                 });
             } else {
                 LOGGER.error("Unknown message ID: " + type);
@@ -134,12 +158,16 @@ public class NetworkManagerImpl {
     }
     
     @OnlyIn(Dist.CLIENT)
-    public static void registerS2CReceiver(ResourceLocation id, NetworkReceiver receiver) {
+    public static void registerS2CReceiver(ResourceLocation id, List<PacketTransformer> packetTransformers, NetworkReceiver receiver) {
         S2C.put(id, receiver);
+        PacketTransformer transformer = PacketTransformer.concat(packetTransformers);
+        S2C_TRANSFORMERS.put(id, transformer);
     }
     
-    public static void registerC2SReceiver(ResourceLocation id, NetworkReceiver receiver) {
+    public static void registerC2SReceiver(ResourceLocation id, List<PacketTransformer> packetTransformers, NetworkReceiver receiver) {
         C2S.put(id, receiver);
+        PacketTransformer transformer = PacketTransformer.concat(packetTransformers);
+        C2S_TRANSFORMERS.put(id, transformer);
     }
     
     public static boolean canServerReceive(ResourceLocation id) {
@@ -150,7 +178,7 @@ public class NetworkManagerImpl {
         return clientReceivables.get(player).contains(id);
     }
     
-    public static Packet<?> createAddEntityPacket(Entity entity){
+    public static Packet<?> createAddEntityPacket(Entity entity) {
         return NetworkHooks.getEntitySpawningPacket(entity);
     }
     

+ 1 - 1
gradle.properties

@@ -6,7 +6,7 @@ supported_version=1.16.4/5
 
 archives_base_name=architectury
 archives_base_name_snapshot=architectury-snapshot
-base_version=1.23
+base_version=1.24
 maven_group=me.shedaniel
 
 fabric_loader_version=0.11.1

+ 1 - 0
testmod-common/src/main/java/me/shedaniel/architectury/test/TestMod.java

@@ -53,6 +53,7 @@ public class TestMod {
         TestBlockInteractions.init();
         if (Platform.getEnvironment() == Env.CLIENT) {
             TestKeybinds.initialize();
+            TestModNet.initializeClient();
             EntityRenderers.register(TestEntity.TYPE, MinecartRenderer<TestEntity>::new);
         }
     }

+ 35 - 0
testmod-common/src/main/java/me/shedaniel/architectury/test/networking/TestModNet.java

@@ -19,9 +19,18 @@
 
 package me.shedaniel.architectury.test.networking;
 
+import io.netty.buffer.Unpooled;
+import me.shedaniel.architectury.event.events.client.ClientPlayerEvent;
+import me.shedaniel.architectury.networking.NetworkManager;
 import me.shedaniel.architectury.networking.simple.MessageType;
 import me.shedaniel.architectury.networking.simple.SimpleNetworkManager;
+import me.shedaniel.architectury.networking.transformers.SplitPacketTransformer;
 import me.shedaniel.architectury.test.TestMod;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.resources.ResourceLocation;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collections;
 
 public interface TestModNet {
     SimpleNetworkManager NET = SimpleNetworkManager.create(TestMod.MOD_ID);
@@ -31,7 +40,33 @@ public interface TestModNet {
     
     // An example Server to Client message
     MessageType SYNC_DATA = NET.registerS2C("sync_data", SyncDataMessage::new);
+    ResourceLocation BIG_DATA = new ResourceLocation(TestMod.MOD_ID, "big_data");
+    String BIG_STRING = StringUtils.repeat('a', 60000);
     
     static void initialize() {
+        NetworkManager.registerReceiver(NetworkManager.Side.C2S, BIG_DATA, Collections.singletonList(new SplitPacketTransformer()), (buf, context) -> {
+            String utf = buf.readUtf(Integer.MAX_VALUE / 4);
+            if (utf.equals(BIG_STRING)) {
+                TestMod.SINK.accept("Network Split Packets worked");
+            } else {
+                throw new AssertionError(utf);
+            }
+            utf = buf.readUtf(Integer.MAX_VALUE / 4);
+            if (utf.equals(BIG_STRING)) {
+                TestMod.SINK.accept("Network Split Packets worked");
+            } else {
+                throw new AssertionError(utf);
+            }
+        });
+    }
+    
+    static void initializeClient() {
+        ClientPlayerEvent.CLIENT_PLAYER_JOIN.register(player -> {
+            FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer());
+            buf.writeUtf(BIG_STRING, Integer.MAX_VALUE / 4);
+            // write twice
+            buf.writeUtf(BIG_STRING, Integer.MAX_VALUE / 4);
+            NetworkManager.sendToServer(BIG_DATA, buf);
+        });
     }
 }