From 974ede5f87ffb243ec1790849980dbe45b22b859 Mon Sep 17 00:00:00 2001 From: hayanesuru Date: Wed, 7 May 2025 00:37:31 +0900 Subject: [PATCH] Do A Barrel Roll Protocol (#315) * Do A Barrel Roll Protocol * cleanup * [ci skip] cleanup * [ci skip] cleanup * [ci skip] rename patch --- .../features/0165-Protocol-Core.patch | 84 +++++ .../modules/network/ProtocolSupport.java | 30 ++ .../leaf/protocol/DoABarrelRollPackets.java | 148 +++++++++ .../leaf/protocol/DoABarrelRollProtocol.java | 298 ++++++++++++++++++ .../leaf/protocol/LeafCustomPayload.java | 10 + .../org/dreeam/leaf/protocol/Protocol.java | 19 ++ .../org/dreeam/leaf/protocol/Protocols.java | 97 ++++++ 7 files changed, 686 insertions(+) create mode 100644 leaf-server/minecraft-patches/features/0165-Protocol-Core.patch create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/protocol/DoABarrelRollPackets.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/protocol/DoABarrelRollProtocol.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/protocol/LeafCustomPayload.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/protocol/Protocol.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/protocol/Protocols.java diff --git a/leaf-server/minecraft-patches/features/0165-Protocol-Core.patch b/leaf-server/minecraft-patches/features/0165-Protocol-Core.patch new file mode 100644 index 00000000..4a11921d --- /dev/null +++ b/leaf-server/minecraft-patches/features/0165-Protocol-Core.patch @@ -0,0 +1,84 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: hayanesuru +Date: Tue, 6 May 2025 17:44:16 +0900 +Subject: [PATCH] Protocol Core + + +diff --git a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java +index 7e19dfe90a63ff26f03b95891dacb7360bba5a3c..d20ffa172227f85b9fd6ac5e2766f6ebd2d07638 100644 +--- a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java ++++ b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java +@@ -47,6 +47,12 @@ public interface CustomPacketPayload { + return; + } + // Leaves end - protocol core ++ // Leaf start - protocol ++ if (value instanceof org.dreeam.leaf.protocol.LeafCustomPayload payload) { ++ org.dreeam.leaf.protocol.Protocols.write(buffer, payload); ++ return; ++ } ++ // Leaf end - protocol + this.writeCap(buffer, value.type(), value); + } + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 98af1ad020a003db66d7319f33d43deec315aec5..e04a6db55d936277f2a852374f11d483d79a90ed 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1839,6 +1839,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop LeafCustomPayload.@NotNull Type createType(String path) { + return new LeafCustomPayload.Type<>(ResourceLocation.fromNamespaceAndPath(DoABarrelRollProtocol.NAMESPACE, path)); + } + + public record ConfigResponseC2SPacket(int protocolVersion, boolean success) implements LeafCustomPayload { + public static final Type TYPE = createType("config_response"); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, ConfigResponseC2SPacket::protocolVersion, + ByteBufCodecs.BOOL, ConfigResponseC2SPacket::success, + ConfigResponseC2SPacket::new + ); + + @Override + public @NotNull Type type() { + return TYPE; + } + } + + public record ConfigSyncS2CPacket(int protocolVersion, + LimitedModConfigServer applicableConfig, + boolean isLimited, + ModConfigServer fullConfig + ) implements LeafCustomPayload { + public static final Type TYPE = createType("config_sync"); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, ConfigSyncS2CPacket::protocolVersion, + LimitedModConfigServer.getCodec(), ConfigSyncS2CPacket::applicableConfig, + ByteBufCodecs.BOOL, ConfigSyncS2CPacket::isLimited, + ModConfigServer.PACKET_CODEC, ConfigSyncS2CPacket::fullConfig, + ConfigSyncS2CPacket::new + ); + + @Override + public @NotNull Type type() { + return TYPE; + } + } + + public record ConfigUpdateAckS2CPacket(int protocolVersion, boolean success) implements LeafCustomPayload { + public static final Type TYPE = createType("config_update_ack"); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, ConfigUpdateAckS2CPacket::protocolVersion, + ByteBufCodecs.BOOL, ConfigUpdateAckS2CPacket::success, + ConfigUpdateAckS2CPacket::new + ); + + @Override + public @NotNull Type type() { + return TYPE; + } + } + + public record ConfigUpdateC2SPacket(int protocolVersion, ModConfigServer config) implements LeafCustomPayload { + public static final Type TYPE = createType("config_update"); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, ConfigUpdateC2SPacket::protocolVersion, + ModConfigServer.PACKET_CODEC, ConfigUpdateC2SPacket::config, + ConfigUpdateC2SPacket::new + ); + + @Override + public @NotNull Type type() { + return TYPE; + } + } + + public record RollSyncC2SPacket(boolean rolling, float roll) implements LeafCustomPayload { + public static final Type TYPE = createType("roll_sync"); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, RollSyncC2SPacket::rolling, + ByteBufCodecs.FLOAT, RollSyncC2SPacket::roll, + RollSyncC2SPacket::new + ); + + @Override + public @NotNull Type type() { + return TYPE; + } + } + + public record RollSyncS2CPacket(int entityId, boolean rolling, float roll) implements LeafCustomPayload { + public static final Type TYPE = createType("roll_sync"); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, RollSyncS2CPacket::entityId, + ByteBufCodecs.BOOL, RollSyncS2CPacket::rolling, + ByteBufCodecs.FLOAT, RollSyncS2CPacket::roll, + RollSyncS2CPacket::new + ); + + @Override + public @NotNull Type type() { + return TYPE; + } + } + + public interface LimitedModConfigServer { + boolean allowThrusting(); + + boolean forceEnabled(); + + static StreamCodec getCodec() { + return StreamCodec.composite( + ByteBufCodecs.BOOL, LimitedModConfigServer::allowThrusting, + ByteBufCodecs.BOOL, LimitedModConfigServer::forceEnabled, + Impl::new + ); + } + + record Impl(boolean allowThrusting, boolean forceEnabled) implements LimitedModConfigServer { + } + } + + public record ModConfigServer(boolean allowThrusting, + boolean forceEnabled, + boolean forceInstalled, + int installedTimeout, + KineticDamage kineticDamage + ) implements LimitedModConfigServer { + public static final StreamCodec PACKET_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, ModConfigServer::allowThrusting, + ByteBufCodecs.BOOL, ModConfigServer::forceEnabled, + ByteBufCodecs.BOOL, ModConfigServer::forceInstalled, + ByteBufCodecs.INT, ModConfigServer::installedTimeout, + KineticDamage.CODEC, ModConfigServer::kineticDamage, + ModConfigServer::new + ); + } + + public enum KineticDamage { + VANILLA, + HIGH_SPEED, + NONE, + INSTANT_KILL; + + public static final StreamCodec CODEC = + ByteBufCodecs.STRING_UTF8.map(KineticDamage::valueOf, KineticDamage::name); + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/protocol/DoABarrelRollProtocol.java b/leaf-server/src/main/java/org/dreeam/leaf/protocol/DoABarrelRollProtocol.java new file mode 100644 index 00000000..dca31a84 --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/protocol/DoABarrelRollProtocol.java @@ -0,0 +1,298 @@ +package org.dreeam.leaf.protocol; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.*; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.network.ServerPlayerConnection; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bukkit.event.player.PlayerKickEvent; +import org.dreeam.leaf.protocol.DoABarrelRollPackets.*; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.OptionalInt; + +public class DoABarrelRollProtocol implements Protocol { + protected static final String NAMESPACE = "do_a_barrel_roll"; + private static final Logger LOGGER = LogManager.getLogger(NAMESPACE); + private static final int PROTOCOL_VERSION = 4; + private static final ModConfigServer DEFAULT = new ModConfigServer(false, false, false, 40, KineticDamage.VANILLA); + private static final Component SYNC_TIMEOUT_MESSAGE = Component.literal("Please install Do a Barrel Roll 2.4.0 or later to play on this server."); + private static DoABarrelRollProtocol INSTANCE = null; + + private final List> c2s = ImmutableList.of( + new Protocols.TypeAndCodec<>(ConfigUpdateC2SPacket.TYPE, ConfigUpdateC2SPacket.STREAM_CODEC), + new Protocols.TypeAndCodec<>(ConfigResponseC2SPacket.TYPE, ConfigResponseC2SPacket.STREAM_CODEC), + new Protocols.TypeAndCodec<>(RollSyncC2SPacket.TYPE, RollSyncC2SPacket.STREAM_CODEC)); + + private final List> s2c = ImmutableList.of( + new Protocols.TypeAndCodec<>(ConfigUpdateAckS2CPacket.TYPE, ConfigUpdateAckS2CPacket.STREAM_CODEC), + new Protocols.TypeAndCodec<>(ConfigSyncS2CPacket.TYPE, ConfigSyncS2CPacket.STREAM_CODEC), + new Protocols.TypeAndCodec<>(RollSyncS2CPacket.TYPE, RollSyncS2CPacket.STREAM_CODEC) + ); + + private ModConfigServer config = DEFAULT; + private boolean configUpdated = false; + + private final Reference2ReferenceMap syncStates = new Reference2ReferenceOpenHashMap<>(); + private final Reference2ReferenceMap scheduledKicks = new Reference2ReferenceOpenHashMap<>(); + public final Reference2BooleanMap isRollingMap = Reference2BooleanMaps.synchronize(new Reference2BooleanOpenHashMap<>()); + public final Reference2FloatMap rollMap = Reference2FloatMaps.synchronize(new Reference2FloatOpenHashMap<>()); + public final Reference2BooleanMap lastIsRollingMap = Reference2BooleanMaps.synchronize(new Reference2BooleanOpenHashMap<>()); + public final Reference2FloatMap lastRollMap = Reference2FloatMaps.synchronize(new Reference2FloatOpenHashMap<>()); + + public static void deinit() { + if (INSTANCE != null) { + INSTANCE = null; + Protocols.unregister(INSTANCE); + } + } + + public static void init( + boolean allowThrusting, + boolean forceEnabled, + boolean forceInstalled, + int installedTimeout, + KineticDamage kineticDamage + ) { + if (INSTANCE == null) { + INSTANCE = new DoABarrelRollProtocol(); + Protocols.register(INSTANCE); + } + INSTANCE.config = new ModConfigServer(allowThrusting, forceEnabled, forceInstalled, installedTimeout, kineticDamage); + INSTANCE.configUpdated = true; + } + + @Override + public String namespace() { + return NAMESPACE; + } + + @Override + public List> c2s() { + return c2s; + } + + @Override + public List> s2c() { + return s2c; + } + + @Override + public void handle(ServerPlayer player, @NotNull LeafCustomPayload payload) { + switch (payload) { + case ConfigUpdateC2SPacket ignored -> + player.connection.send(Protocols.createPacket(new ConfigUpdateAckS2CPacket(PROTOCOL_VERSION, false))); + case ConfigResponseC2SPacket configResponseC2SPacket -> { + var reply = clientReplied(player.connection, configResponseC2SPacket); + if (reply == HandshakeState.RESEND) { + sendHandshake(player); + } + } + case RollSyncC2SPacket rollSyncC2SPacket -> { + var state = getHandshakeState(player.connection); + if (state.state != HandshakeState.ACCEPTED) { + return; + } + var rolling = rollSyncC2SPacket.rolling(); + var roll = rollSyncC2SPacket.roll(); + isRollingMap.put(player.connection, rolling); + if (Float.isInfinite(roll)) { + roll = 0.0F; + } + rollMap.put(player.connection, roll); + } + default -> { + } + } + } + + @Override + public void disconnected(ServerPlayer player) { + final var handler = player.connection; + syncStates.remove(handler); + isRollingMap.removeBoolean(handler); + rollMap.removeFloat(handler); + lastIsRollingMap.removeBoolean(handler); + lastRollMap.removeFloat(handler); + } + + @Override + public void tickTracker(ServerPlayer player) { + if (!isRollingMap.containsKey(player.connection)) { + return; + } + + var isRolling = isRollingMap.getBoolean(player.connection); + var roll = rollMap.getFloat(player.connection); + var lastIsRolling = lastIsRollingMap.getBoolean(player.connection); + var lastRoll = lastRollMap.getFloat(player.connection); + if (isRolling == lastIsRolling && roll == lastRoll) { + return; + } + var payload = new RollSyncS2CPacket(player.getId(), isRolling, roll); + var packet = Protocols.createPacket(payload); + for (ServerPlayerConnection seenBy : player.moonrise$getTrackedEntity().seenBy.toArray(new ServerPlayerConnection[0])) { + if (seenBy instanceof ServerGamePacketListenerImpl conn + && getHandshakeState(conn).state == HandshakeState.ACCEPTED) { + seenBy.send(packet); + } + } + lastIsRollingMap.put(player.connection, isRolling); + lastRollMap.put(player.connection, roll); + } + + @Override + public void tickPlayer(ServerPlayer player) { + if (getHandshakeState(player.connection).state == HandshakeState.NOT_SENT) { + sendHandshake(player); + } + if (!isRollingMap.containsKey(player.connection)) { + return; + } + if (!isRollingMap.getBoolean(player.connection)) { + rollMap.put(player.connection, 0.0F); + } + } + + @Override + public void tickServer(MinecraftServer server) { + var it = scheduledKicks.entrySet().iterator(); + while (it.hasNext()) { + var entry = it.next(); + if (entry.getValue().isDone()) { + it.remove(); + } else { + entry.getValue().tick(); + } + } + + if (configUpdated) { + configUpdated = false; + for (ServerPlayer player : server.getPlayerList().players) { + sendHandshake(player); + } + } + } + + private OptionalInt getSyncTimeout(ModConfigServer config) { + return config.forceInstalled() ? OptionalInt.of(config.installedTimeout()) : OptionalInt.empty(); + } + + private void sendHandshake(ServerPlayer player) { + player.connection.send(Protocols.createPacket(initiateConfigSync(player.connection))); + configSentToClient(player.connection); + } + + private void configSentToClient(ServerGamePacketListenerImpl handler) { + getHandshakeState(handler).state = HandshakeState.SENT; + + OptionalInt timeout = getSyncTimeout(config); + if (timeout.isEmpty()) { + return; + } + scheduledKicks.put(handler, new DelayedRunnable(timeout.getAsInt(), () -> { + if (getHandshakeState(handler).state != HandshakeState.ACCEPTED) { + LOGGER.warn( + "{} did not accept config syncing, config indicates we kick them.", + handler.getPlayer().getName().getString() + ); + handler.disconnect(SYNC_TIMEOUT_MESSAGE, PlayerKickEvent.Cause.PLUGIN); + } + })); + } + + private HandshakeState clientReplied(ServerGamePacketListenerImpl handler, ConfigResponseC2SPacket packet) { + var info = getHandshakeState(handler); + var player = handler.getPlayer(); + + if (info.state == HandshakeState.SENT) { + var protocolVersion = packet.protocolVersion(); + if (protocolVersion < 1 || protocolVersion > PROTOCOL_VERSION) { + LOGGER.warn( + "{} sent unknown protocol version, expected range 1-{}, got {}. Will attempt to proceed anyway.", + player.getName().getString(), + PROTOCOL_VERSION, + protocolVersion + ); + } + + if (protocolVersion == 2 && info.protocolVersion != 2) { + LOGGER.info("{} is using an older protocol version, resending.", player.getName().getString()); + info.state = HandshakeState.RESEND; + } else if (packet.success()) { + LOGGER.info("{} accepted server config.", player.getName().getString()); + info.state = HandshakeState.ACCEPTED; + } else { + LOGGER.warn( + "{} failed to process server config, check client logs find what went wrong.", + player.getName().getString()); + info.state = HandshakeState.FAILED; + } + info.protocolVersion = protocolVersion; + } + + return info.state; + } + + private boolean isLimited(ServerGamePacketListenerImpl net) { + return true; + // return net.getPlayer().getBukkitEntity().hasPermission(DoABarrelRoll.MODID + ".configure"); + } + + private ClientInfo getHandshakeState(ServerGamePacketListenerImpl handler) { + return syncStates.computeIfAbsent(handler, key -> new ClientInfo(HandshakeState.NOT_SENT, PROTOCOL_VERSION, true)); + } + + private ConfigSyncS2CPacket initiateConfigSync(ServerGamePacketListenerImpl handler) { + var isLimited = isLimited(handler); + getHandshakeState(handler).isLimited = isLimited; + return new ConfigSyncS2CPacket(PROTOCOL_VERSION, config, isLimited, isLimited ? DEFAULT : config); + } + + private static class ClientInfo { + private HandshakeState state; + private int protocolVersion; + private boolean isLimited; + + private ClientInfo(HandshakeState state, int protocolVersion, boolean isLimited) { + this.state = state; + this.protocolVersion = protocolVersion; + this.isLimited = isLimited; + } + } + + private static class DelayedRunnable { + private final Runnable runnable; + private final int delay; + private int ticks = 0; + + private DelayedRunnable(int delay, Runnable runnable) { + this.runnable = runnable; + this.delay = delay; + } + + private void tick() { + if (++ticks >= delay) { + runnable.run(); + } + } + + private boolean isDone() { + return ticks >= delay; + } + } + + private enum HandshakeState { + NOT_SENT, + SENT, + ACCEPTED, + FAILED, + RESEND + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/protocol/LeafCustomPayload.java b/leaf-server/src/main/java/org/dreeam/leaf/protocol/LeafCustomPayload.java new file mode 100644 index 00000000..54ca72eb --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/protocol/LeafCustomPayload.java @@ -0,0 +1,10 @@ +package org.dreeam.leaf.protocol; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public interface LeafCustomPayload extends CustomPacketPayload { + @NotNull + @Override + Type type(); +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/protocol/Protocol.java b/leaf-server/src/main/java/org/dreeam/leaf/protocol/Protocol.java new file mode 100644 index 00000000..ac069be5 --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/protocol/Protocol.java @@ -0,0 +1,19 @@ +package org.dreeam.leaf.protocol; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +interface Protocol { + String namespace(); + List> c2s(); + List> s2c(); + void tickServer(MinecraftServer server); + void tickPlayer(ServerPlayer player); + void tickTracker(ServerPlayer player); + void disconnected(ServerPlayer conn); + void handle(ServerPlayer player, @NotNull LeafCustomPayload payload); +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/protocol/Protocols.java b/leaf-server/src/main/java/org/dreeam/leaf/protocol/Protocols.java new file mode 100644 index 00000000..0af1e3d1 --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/protocol/Protocols.java @@ -0,0 +1,97 @@ +package org.dreeam.leaf.protocol; + +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.common.custom.DiscardedPayload; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class Protocols { + private static final ObjectArrayList PROTOCOLS = new ObjectArrayList<>(); + + static void register(Protocol protocol) { + PROTOCOLS.add(protocol); + } + + static void unregister(Protocol protocol) { + PROTOCOLS.remove(protocol); + } + + public record TypeAndCodec(LeafCustomPayload.Type type, StreamCodec codec) {} + + public static void write(B byteBuf, LeafCustomPayload payload) { + for (Protocol protocol : PROTOCOLS) { + if (protocol.namespace().equals(payload.type().id().getNamespace())) { + encode(byteBuf, payload, protocol); + return; + } + } + } + + public static void handle(ServerPlayer player, @NotNull DiscardedPayload payload) { + for (Protocol protocol : PROTOCOLS) { + if (payload.type().id().getNamespace().equals(protocol.namespace())) { + var leafCustomPayload = decode(protocol, payload); + if (leafCustomPayload != null) { + protocol.handle(player, leafCustomPayload); + } + return; + } + } + } + + public static void tickServer(MinecraftServer server) { + for (Protocol protocol : PROTOCOLS) { + protocol.tickServer(server); + } + } + + public static void tickPlayer(ServerPlayer player) { + for (Protocol protocol : PROTOCOLS) { + protocol.tickPlayer(player); + } + } + + public static void tickTracker(ServerPlayer player) { + for (Protocol protocol : PROTOCOLS) { + protocol.tickTracker(player); + } + } + + public static void disconnected(ServerPlayer conn) { + for (Protocol protocol : PROTOCOLS) { + protocol.disconnected(conn); + } + } + + @Contract("_ -> new") + public static @NotNull ClientboundCustomPayloadPacket createPacket(LeafCustomPayload payload) { + return new ClientboundCustomPayloadPacket(payload); + } + + private static void encode(B byteBuf, LeafCustomPayload payload, Protocol protocol) { + for (var codec : protocol.s2c()) { + if (codec.type().id().equals(payload.type().id())) { + byteBuf.writeResourceLocation(payload.type().id()); + //noinspection unchecked,rawtypes + ((StreamCodec) codec.codec()).encode(byteBuf, payload); + return; + } + } + } + + private static @Nullable LeafCustomPayload decode(Protocol protocol, DiscardedPayload payload) { + for (var packet : protocol.c2s()) { + if (packet.type().id().equals(payload.type().id())) { + return packet.codec().decode(new FriendlyByteBuf(Unpooled.wrappedBuffer(payload.data()))); + } + } + return null; + } +}