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
This commit is contained in:
shedaniel
2021-10-23 18:27:28 +08:00
committed by GitHub
parent f636b1ad96
commit a6a361e5e1
12 changed files with 661 additions and 35 deletions

View File

@@ -20,48 +20,67 @@
package dev.architectury.networking;
import dev.architectury.injectables.annotations.ExpectPlatform;
import dev.architectury.networking.transformers.PacketCollector;
import dev.architectury.networking.transformers.PacketSink;
import dev.architectury.networking.transformers.PacketTransformer;
import dev.architectury.networking.transformers.SinglePacketCollector;
import dev.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) {
var packet = toPacket(serverToClient(), id, buf);
for (var 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)

View File

@@ -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 dev.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;
}
}

View File

@@ -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 dev.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 (var 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);
}

View File

@@ -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 dev.architectury.networking.transformers;
import dev.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);
}
}
}
};
}
}

View File

@@ -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 dev.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;
}
}

View File

@@ -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 dev.architectury.networking.transformers;
import dev.architectury.event.events.client.ClientPlayerEvent;
import dev.architectury.event.events.common.PlayerEvent;
import dev.architectury.networking.NetworkManager;
import dev.architectury.utils.Env;
import dev.architectury.utils.EnvExecutor;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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();
}
}