From 37ec0d8999c5ef0bd0f624960270c569d870026e Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Fri, 6 Feb 2026 19:54:46 +0700 Subject: [PATCH 1/9] Initial work on refactoring, use channel handler for reading packet instead. --- build.gradle | 2 +- .../anticheat/check/api/impl/PacketCheck.java | 2 +- .../check/impl/badpackets/BadPacketA.java | 2 +- .../check/impl/badpackets/BadPacketB.java | 2 +- .../anticheat/check/impl/reach/Reach.java | 2 +- .../packets/input/AuthInputPackets.java | 11 ++- .../packets/input/PostAuthInputPackets.java | 4 +- .../packets/other/NetworkLatencyPackets.java | 4 +- .../packets/other/PacketCheckRunner.java | 8 +- .../packets/other/VehiclePackets.java | 8 +- .../packets/player/PlayerEffectPackets.java | 8 +- .../player/PlayerInventoryPackets.java | 16 ++-- .../packets/player/PlayerVelocityPackets.java | 10 +- .../packets/server/ServerChunkPackets.java | 13 ++- .../packets/server/ServerDataPackets.java | 38 +++++--- .../packets/server/ServerEntityPackets.java | 8 +- .../ac/boar/anticheat/player/BoarPlayer.java | 8 +- .../player/manager/BoarPlayerManager.java | 2 +- .../boar/anticheat/teleport/TeleportUtil.java | 6 +- .../inventory/ItemTransactionValidator.java | 2 +- .../java/ac/boar/geyser/util/GeyserUtil.java | 31 ++---- .../ac/boar/protocol/BoarHandlerAdaptor.java | 74 +++++++++++++++ .../java/ac/boar/protocol/PacketEvents.java | 2 +- .../{event => api}/CloudburstPacketEvent.java | 2 +- .../ac/boar/protocol/api/PacketListener.java | 9 ++ .../protocol/listener/PacketListener.java | 11 --- .../mitm/CloudburstReceiveListener.java | 42 -------- .../protocol/mitm/CloudburstSendListener.java | 95 ------------------- 28 files changed, 178 insertions(+), 244 deletions(-) create mode 100644 src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java rename src/main/java/ac/boar/protocol/{event => api}/CloudburstPacketEvent.java (92%) create mode 100644 src/main/java/ac/boar/protocol/api/PacketListener.java delete mode 100644 src/main/java/ac/boar/protocol/listener/PacketListener.java delete mode 100644 src/main/java/ac/boar/protocol/mitm/CloudburstReceiveListener.java delete mode 100644 src/main/java/ac/boar/protocol/mitm/CloudburstSendListener.java diff --git a/build.gradle b/build.gradle index cdc6aaa3..f3e7f679 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } dependencies { - compileOnly("org.geysermc.geyser:core:2.9.0-SNAPSHOT") { + compileOnly("org.geysermc.geyser:core:2.9.2-SNAPSHOT") { exclude group: "com.google.code.gson", module: "gson" } compileOnly("it.unimi.dsi:fastutil:8.5.15") diff --git a/src/main/java/ac/boar/anticheat/check/api/impl/PacketCheck.java b/src/main/java/ac/boar/anticheat/check/api/impl/PacketCheck.java index 56bfb225..623f6ae1 100644 --- a/src/main/java/ac/boar/anticheat/check/api/impl/PacketCheck.java +++ b/src/main/java/ac/boar/anticheat/check/api/impl/PacketCheck.java @@ -2,7 +2,7 @@ import ac.boar.anticheat.check.api.Check; import ac.boar.anticheat.player.BoarPlayer; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.PacketListener; public class PacketCheck extends Check implements PacketListener { public PacketCheck(BoarPlayer player) { diff --git a/src/main/java/ac/boar/anticheat/check/impl/badpackets/BadPacketA.java b/src/main/java/ac/boar/anticheat/check/impl/badpackets/BadPacketA.java index 6ad37cf2..984c7b0d 100644 --- a/src/main/java/ac/boar/anticheat/check/impl/badpackets/BadPacketA.java +++ b/src/main/java/ac/boar/anticheat/check/impl/badpackets/BadPacketA.java @@ -4,7 +4,7 @@ import ac.boar.anticheat.check.api.impl.PacketCheck; import ac.boar.anticheat.player.BoarPlayer; import ac.boar.anticheat.util.MathUtil; -import ac.boar.protocol.event.CloudburstPacketEvent; +import ac.boar.protocol.api.CloudburstPacketEvent; import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; @CheckInfo(name = "Bad Packet", type = "A") diff --git a/src/main/java/ac/boar/anticheat/check/impl/badpackets/BadPacketB.java b/src/main/java/ac/boar/anticheat/check/impl/badpackets/BadPacketB.java index ded72829..c26b94c1 100644 --- a/src/main/java/ac/boar/anticheat/check/impl/badpackets/BadPacketB.java +++ b/src/main/java/ac/boar/anticheat/check/impl/badpackets/BadPacketB.java @@ -4,7 +4,7 @@ import ac.boar.anticheat.check.api.impl.PacketCheck; import ac.boar.anticheat.player.BoarPlayer; import ac.boar.anticheat.util.MathUtil; -import ac.boar.protocol.event.CloudburstPacketEvent; +import ac.boar.protocol.api.CloudburstPacketEvent; import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; import org.geysermc.geyser.util.MathUtils; diff --git a/src/main/java/ac/boar/anticheat/check/impl/reach/Reach.java b/src/main/java/ac/boar/anticheat/check/impl/reach/Reach.java index d90e2014..9cdf98e6 100644 --- a/src/main/java/ac/boar/anticheat/check/impl/reach/Reach.java +++ b/src/main/java/ac/boar/anticheat/check/impl/reach/Reach.java @@ -10,7 +10,7 @@ import ac.boar.anticheat.util.Pair; import ac.boar.anticheat.util.math.ReachUtil; import ac.boar.anticheat.util.math.Vec3; -import ac.boar.protocol.event.CloudburstPacketEvent; +import ac.boar.protocol.api.CloudburstPacketEvent; import org.cloudburstmc.protocol.bedrock.data.GameType; import org.cloudburstmc.protocol.bedrock.data.InputMode; diff --git a/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java b/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java index eb7780bd..a08d6094 100644 --- a/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java @@ -12,8 +12,8 @@ import ac.boar.anticheat.teleport.data.TeleportCache; import ac.boar.anticheat.util.DimensionUtil; import ac.boar.anticheat.util.math.Vec3; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData; import org.cloudburstmc.protocol.bedrock.packet.*; import org.geysermc.geyser.entity.EntityDefinitions; @@ -132,14 +132,14 @@ public void onPacketReceived(final CloudburstPacketEvent event) { } @Override - public void onPacketSend(CloudburstPacketEvent event, boolean immediate) { + public void onPacketSend(CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); if (event.getPacket() instanceof ChangeDimensionPacket packet) { int dimensionId = packet.getDimension(); final BedrockDimension dimension = DimensionUtil.dimensionFromId(dimensionId); - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getTeleportUtil().getQueuedTeleports().add(new TeleportCache.DimensionSwitch(player.sentStackId.get(), new Vec3(packet.getPosition().up(EntityDefinitions.PLAYER.offset())))); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { if (player.compensatedWorld.getDimension() != dimension) { @@ -149,6 +149,7 @@ public void onPacketSend(CloudburstPacketEvent event, boolean immediate) { player.compensatedWorld.getChunks().clear(); player.compensatedWorld.setDimension(dimension); + System.out.println("Set dimension!"); player.getFlagTracker().clear(); player.getFlagTracker().flying(false); @@ -170,7 +171,7 @@ public void onPacketSend(CloudburstPacketEvent event, boolean immediate) { packet.setMode(MovePlayerPacket.Mode.TELEPORT); } - player.getTeleportUtil().queueTeleport(new Vec3(packet.getPosition()), immediate); + player.getTeleportUtil().queueTeleport(new Vec3(packet.getPosition())); } } } diff --git a/src/main/java/ac/boar/anticheat/packets/input/PostAuthInputPackets.java b/src/main/java/ac/boar/anticheat/packets/input/PostAuthInputPackets.java index aef272c2..7a5c938e 100644 --- a/src/main/java/ac/boar/anticheat/packets/input/PostAuthInputPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/input/PostAuthInputPackets.java @@ -3,8 +3,8 @@ import ac.boar.anticheat.data.input.TickData; import ac.boar.anticheat.packets.input.legacy.LegacyAuthInputPackets; import ac.boar.anticheat.player.BoarPlayer; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; public class PostAuthInputPackets implements PacketListener { diff --git a/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java b/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java index c904ba11..0a01efe2 100644 --- a/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java @@ -1,8 +1,8 @@ package ac.boar.anticheat.packets.other; import ac.boar.geyser.util.GeyserUtil; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.protocol.bedrock.packet.NetworkStackLatencyPacket; import org.geysermc.api.util.BedrockPlatform; import org.geysermc.geyser.session.GeyserSession; diff --git a/src/main/java/ac/boar/anticheat/packets/other/PacketCheckRunner.java b/src/main/java/ac/boar/anticheat/packets/other/PacketCheckRunner.java index ca32f810..f61a9fe8 100644 --- a/src/main/java/ac/boar/anticheat/packets/other/PacketCheckRunner.java +++ b/src/main/java/ac/boar/anticheat/packets/other/PacketCheckRunner.java @@ -2,18 +2,18 @@ import ac.boar.anticheat.check.api.Check; import ac.boar.anticheat.check.api.impl.PacketCheck; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; public class PacketCheckRunner implements PacketListener { @Override - public void onPacketSend(final CloudburstPacketEvent event, final boolean immediate) { + public void onPacketSend(final CloudburstPacketEvent event) { for (final Check check : event.getPlayer().getCheckHolder().values()) { if (!(check instanceof PacketCheck packetCheck)) { continue; } - packetCheck.onPacketSend(event, immediate); + packetCheck.onPacketSend(event); } } diff --git a/src/main/java/ac/boar/anticheat/packets/other/VehiclePackets.java b/src/main/java/ac/boar/anticheat/packets/other/VehiclePackets.java index 2479d2e6..96266a3d 100644 --- a/src/main/java/ac/boar/anticheat/packets/other/VehiclePackets.java +++ b/src/main/java/ac/boar/anticheat/packets/other/VehiclePackets.java @@ -3,8 +3,8 @@ import ac.boar.anticheat.compensated.cache.entity.EntityCache; import ac.boar.anticheat.player.BoarPlayer; import ac.boar.anticheat.player.data.VehicleData; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData; import org.cloudburstmc.protocol.bedrock.packet.InteractPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket; @@ -26,7 +26,7 @@ public void onPacketReceived(CloudburstPacketEvent event) { } @Override - public void onPacketSend(CloudburstPacketEvent event, boolean immediate) { + public void onPacketSend(CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); if (event.getPacket() instanceof SetEntityLinkPacket packet) { final EntityLinkData link = packet.getEntityLink(); @@ -56,7 +56,7 @@ public void onPacketSend(CloudburstPacketEvent event, boolean immediate) { // Yep. player.getTeleportUtil().getQueuedTeleports().clear(); - player.sendLatencyStack(immediate); + player.sendLatencyStack(); if (link.getType() == EntityLinkData.Type.REMOVE) { player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.vehicleData = null); return; diff --git a/src/main/java/ac/boar/anticheat/packets/player/PlayerEffectPackets.java b/src/main/java/ac/boar/anticheat/packets/player/PlayerEffectPackets.java index b20274f6..96d99345 100644 --- a/src/main/java/ac/boar/anticheat/packets/player/PlayerEffectPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/player/PlayerEffectPackets.java @@ -3,14 +3,14 @@ import ac.boar.anticheat.data.vanilla.StatusEffect; import ac.boar.anticheat.player.BoarPlayer; import ac.boar.anticheat.util.EntityUtil; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.protocol.bedrock.packet.MobEffectPacket; import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; public class PlayerEffectPackets implements PacketListener { @Override - public void onPacketSend(final CloudburstPacketEvent event, final boolean immediate) { + public void onPacketSend(final CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); if (event.getPacket() instanceof MobEffectPacket packet) { @@ -23,7 +23,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi return; } - player.sendLatencyStack(immediate); + player.sendLatencyStack(); if (packet.getEvent() == MobEffectPacket.Event.ADD || packet.getEvent() == MobEffectPacket.Event.MODIFY) { player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.getActiveEffects().put(effect, new StatusEffect(effect, packet.getAmplifier(), packet.getDuration() + 1))); diff --git a/src/main/java/ac/boar/anticheat/packets/player/PlayerInventoryPackets.java b/src/main/java/ac/boar/anticheat/packets/player/PlayerInventoryPackets.java index a814bdeb..00ed028f 100644 --- a/src/main/java/ac/boar/anticheat/packets/player/PlayerInventoryPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/player/PlayerInventoryPackets.java @@ -5,8 +5,8 @@ import ac.boar.anticheat.compensated.cache.container.impl.TradeContainerCache; import ac.boar.anticheat.data.inventory.ItemCache; import ac.boar.anticheat.player.BoarPlayer; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; @@ -74,7 +74,7 @@ public void onPacketReceived(final CloudburstPacketEvent event) { } @Override - public void onPacketSend(final CloudburstPacketEvent event, final boolean immediate) { + public void onPacketSend(final CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); final CompensatedInventory inventory = player.compensatedInventory; @@ -126,7 +126,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi if (event.getPacket() instanceof ContainerOpenPacket packet) { // System.out.println(packet); - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { final ContainerCache container = inventory.getContainer(packet.getId()); inventory.openContainer = Objects.requireNonNullElseGet(container, () -> new ContainerCache(inventory, packet.getId(), packet.getType(), packet.getBlockPosition(), packet.getUniqueEntityId())); @@ -135,7 +135,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi // if (event.getPacket() instanceof UpdateEquipPacket packet) { // System.out.println(packet); -// player.sendLatencyStack(immediate); +// player.sendLatencyStack(); // player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { try { // inventory.openContainer = new ContainerCache((byte) packet.getWindowId(), // ContainerType.from(packet.getWindowType()), Vector3i.ZERO, packet.getUniqueEntityId()); @@ -147,7 +147,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi return; } - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { try { inventory.openContainer = new TradeContainerCache(inventory, packet.getOffers(), (byte) packet.getContainerId(), packet.getContainerType(), Vector3i.ZERO, packet.getTraderUniqueEntityId()); @@ -155,7 +155,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi } if (event.getPacket() instanceof InventorySlotPacket packet) { - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { // Bundle should be handled separately. if (packet.getContainerId() == 125) { @@ -188,7 +188,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi } if (event.getPacket() instanceof InventoryContentPacket packet) { - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { // Bundle should be handled separately. if (packet.getContainerId() == 125) { diff --git a/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java b/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java index 9e103d3d..fc64632c 100644 --- a/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java @@ -3,15 +3,15 @@ import ac.boar.anticheat.data.input.VelocityData; import ac.boar.anticheat.player.BoarPlayer; import ac.boar.anticheat.util.math.Vec3; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.protocol.bedrock.data.MovementEffectType; import org.cloudburstmc.protocol.bedrock.packet.MovementEffectPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; public class PlayerVelocityPackets implements PacketListener { @Override - public void onPacketSend(final CloudburstPacketEvent event, final boolean immediate) { + public void onPacketSend(final CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); // Yes only this, there no packet for explosion (for bedrock), geyser translate explosion directly to SetEntityMotionPacket @@ -23,7 +23,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi // I think there is some rewind like behaviour when there is ehm the tick is not 0, so just default back to 0 till I figure it out. packet.setTick(0); - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.queuedVelocities.put(player.sentStackId.get() + 1, new VelocityData(player.sentStackId.get() + 1, player.tick, new Vec3(packet.getMotion()))); event.getPostTasks().add(player::sendLatencyStack); } @@ -37,7 +37,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi // Well anyway.... if you just send a valid tick id or send an invalid id it works fine :D packet.setTick(Integer.MIN_VALUE); - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { if (player.glideBoostTicks == 0 && packet.getDuration() == 0 || packet.getDuration() == Integer.MAX_VALUE) { player.glideBoostTicks = 1; diff --git a/src/main/java/ac/boar/anticheat/packets/server/ServerChunkPackets.java b/src/main/java/ac/boar/anticheat/packets/server/ServerChunkPackets.java index a1783804..c74d3de8 100644 --- a/src/main/java/ac/boar/anticheat/packets/server/ServerChunkPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/server/ServerChunkPackets.java @@ -8,10 +8,9 @@ import ac.boar.anticheat.util.geyser.BoarChunk; import ac.boar.anticheat.util.geyser.BoarChunkSection; import ac.boar.anticheat.util.math.Vec3; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufInputStream; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; import org.cloudburstmc.math.GenericMath; @@ -29,12 +28,12 @@ public class ServerChunkPackets implements PacketListener { @Override - public void onPacketSend(CloudburstPacketEvent event, boolean immediate) { + public void onPacketSend(CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); final CompensatedWorld world = player.compensatedWorld; if (event.getPacket() instanceof NetworkChunkPublisherUpdatePacket packet) { - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { world.setCenterX(packet.getPosition().getX() >> 4); @@ -57,7 +56,7 @@ public void onPacketSend(CloudburstPacketEvent event, boolean immediate) { final int x = packet.getChunkX() << 4, z = packet.getChunkZ() << 4; // Avoid spamming latency if possible, unless the player is seriously lagging then this shouldn't false. if (Math.abs(player.position.x - x) <= 16 || Math.abs(player.position.z - z) <= 16) { - player.sendLatencyStack(immediate); + player.sendLatencyStack(); } final BedrockDimension dimension = DimensionUtil.dimensionFromId(packet.getDimension()); @@ -114,7 +113,7 @@ public void onPacketSend(CloudburstPacketEvent event, boolean immediate) { // Avoid spamming latency if possible, unless the player is seriously lagging then this shouldn't false. boolean send = player.position.distanceTo(new Vec3(packet.getBlockPosition())) <= 16; if (send) { - player.sendLatencyStack(immediate); + player.sendLatencyStack(); } player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> world.updateBlock(packet.getBlockPosition(), packet.getDataLayer(), packet.getDefinition().getRuntimeId())); diff --git a/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java b/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java index 3d6dd143..a82ee9c7 100644 --- a/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java @@ -1,16 +1,16 @@ package ac.boar.anticheat.packets.server; +import ac.boar.anticheat.Boar; import ac.boar.anticheat.compensated.cache.container.ContainerCache; import ac.boar.anticheat.compensated.cache.entity.EntityCache; import ac.boar.anticheat.data.EntityDimensions; import ac.boar.anticheat.data.vanilla.AttributeInstance; import ac.boar.anticheat.player.BoarPlayer; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; -import org.cloudburstmc.protocol.bedrock.data.Ability; -import org.cloudburstmc.protocol.bedrock.data.AbilityLayer; -import org.cloudburstmc.protocol.bedrock.data.AttributeData; -import org.cloudburstmc.protocol.bedrock.data.GameType; +import ac.boar.anticheat.util.DimensionUtil; +import ac.boar.anticheat.validator.blockbreak.ServerBreakBlockValidator; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; +import org.cloudburstmc.protocol.bedrock.data.*; import org.cloudburstmc.protocol.bedrock.data.attribute.AttributeModifierData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; @@ -24,9 +24,25 @@ public class ServerDataPackets implements PacketListener { @Override - public void onPacketSend(final CloudburstPacketEvent event, final boolean immediate) { + public void onPacketSend(final CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); + if (event.getPacket() instanceof StartGamePacket start) { + player.runtimeEntityId = start.getRuntimeEntityId(); + + player.compensatedWorld.setDimension(DimensionUtil.dimensionFromId(start.getDimensionId())); + player.currentLoadingScreen = null; + player.inLoadingScreen = true; + + // We need this to do rewind teleport. + start.setAuthoritativeMovementMode(AuthoritativeMovementMode.SERVER_WITH_REWIND); + start.setRewindHistorySize(Boar.getConfig().rewindHistory()); + player.serverBreakBlockValidator = new ServerBreakBlockValidator(player); + + player.sendLatencyStack(); + player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.gameType = start.getPlayerGameType()); + } + if (event.getPacket() instanceof SetPlayerGameTypePacket packet) { player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.gameType = GameType.from(packet.getGamemode())); @@ -37,7 +53,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi return; } - event.getPostTasks().add(() -> player.sendLatencyStack(immediate)); + event.getPostTasks().add(() -> player.sendLatencyStack()); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get() + 1, () -> { player.abilities.clear(); for (AbilityLayer layer : packet.getAbilityLayers()) { @@ -86,7 +102,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi flagsCopy = null; } - player.sendLatencyStack(immediate); + player.sendLatencyStack(); final long id = player.sentStackId.get(); player.desyncedFlag.set(flagsCopy != null ? id : -1); @@ -133,7 +149,7 @@ public void onPacketSend(final CloudburstPacketEvent event, final boolean immedi return; } - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { if (player.vehicleData != null) { return; @@ -170,7 +186,7 @@ public void onPacketReceived(final CloudburstPacketEvent event) { final SessionPlayerEntity entity = player.getSession().getPlayerEntity(); UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); - attributesPacket.setRuntimeEntityId(entity.getGeyserId()); + attributesPacket.setRuntimeEntityId(entity.geyserId()); attributesPacket.getAttributes().addAll(entity.getAttributes().values()); player.getSession().sendUpstreamPacket(attributesPacket); diff --git a/src/main/java/ac/boar/anticheat/packets/server/ServerEntityPackets.java b/src/main/java/ac/boar/anticheat/packets/server/ServerEntityPackets.java index 48698dc8..9b1d59ab 100644 --- a/src/main/java/ac/boar/anticheat/packets/server/ServerEntityPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/server/ServerEntityPackets.java @@ -3,8 +3,8 @@ import ac.boar.anticheat.compensated.cache.entity.EntityCache; import ac.boar.anticheat.player.BoarPlayer; import ac.boar.anticheat.util.math.Vec3; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.packet.*; @@ -12,10 +12,10 @@ public class ServerEntityPackets implements PacketListener { @Override - public void onPacketSend(final CloudburstPacketEvent event, final boolean immediate) { + public void onPacketSend(final CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); if (event.getPacket() instanceof RemoveEntityPacket packet) { - player.sendLatencyStack(immediate); + player.sendLatencyStack(); player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { if (player.vehicleData != null && player.vehicleData.vehicleRuntimeId == packet.getUniqueEntityId()) { player.vehicleData = null; diff --git a/src/main/java/ac/boar/anticheat/player/BoarPlayer.java b/src/main/java/ac/boar/anticheat/player/BoarPlayer.java index 62c31870..f1106136 100644 --- a/src/main/java/ac/boar/anticheat/player/BoarPlayer.java +++ b/src/main/java/ac/boar/anticheat/player/BoarPlayer.java @@ -13,7 +13,6 @@ import ac.boar.anticheat.validator.blockbreak.ServerBreakBlockValidator; import ac.boar.geyser.util.GeyserUtil; import ac.boar.mappings.BlockMappings; -import ac.boar.protocol.mitm.CloudburstReceiveListener; import lombok.Getter; import ac.boar.anticheat.check.api.holder.CheckHolder; @@ -22,7 +21,6 @@ import ac.boar.anticheat.util.math.Box; import ac.boar.anticheat.util.math.Mutable; import ac.boar.anticheat.validator.inventory.ItemTransactionValidator; -import ac.boar.protocol.mitm.CloudburstSendListener; import ac.boar.anticheat.player.data.PlayerData; import lombok.Setter; import org.cloudburstmc.math.GenericMath; @@ -55,11 +53,9 @@ public final class BoarPlayer extends PlayerData { @Getter @Setter private BedrockServerSession cloudburstDownstream; - public CloudburstSendListener cloudburstUpstream; - public CloudburstReceiveListener downstreamPacketHandler; public RakSessionCodec rakSessionCodec; - public long runtimeEntityId, javaEntityId; + public long runtimeEntityId; @Getter private final TeleportUtil teleportUtil = new TeleportUtil(this); @@ -97,7 +93,7 @@ public BoarPlayer(GeyserSession session) { AIR_IDS.add(BEDROCK_AIR); AIR_IDS.add(mappings.getBedrockBlockId(Blocks.CAVE_AIR.defaultBlockState().javaId())); AIR_IDS.add(mappings.getBedrockBlockId(Blocks.VOID_AIR.defaultBlockState().javaId())); -// + for (GeyserAttributeType type : GeyserAttributeType.values()) { final String identifier = type.getBedrockIdentifier(); if (identifier == null || this.attributes.containsKey(type.getBedrockIdentifier())) { diff --git a/src/main/java/ac/boar/anticheat/player/manager/BoarPlayerManager.java b/src/main/java/ac/boar/anticheat/player/manager/BoarPlayerManager.java index 7e17e8f9..608e1570 100644 --- a/src/main/java/ac/boar/anticheat/player/manager/BoarPlayerManager.java +++ b/src/main/java/ac/boar/anticheat/player/manager/BoarPlayerManager.java @@ -15,7 +15,7 @@ public BoarPlayer add(GeyserConnection connection) { } final BoarPlayer player = new BoarPlayer((GeyserSession) connection); - GeyserUtil.hookIntoCloudburstMC(player); + GeyserUtil.hook(player); this.put(connection, player); return player; } diff --git a/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java b/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java index a9f2b6a9..16439033 100644 --- a/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java +++ b/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java @@ -56,12 +56,12 @@ public void teleportTo(final TeleportCache cache) { packet.setMode(MovePlayerPacket.Mode.TELEPORT); packet.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); - this.queueTeleport(teleport.getPosition(), false); + this.queueTeleport(teleport.getPosition()); this.player.getCloudburstDownstream().sendPacket(packet); } - public void queueTeleport(final Vec3 position, boolean immediate) { - player.sendLatencyStack(immediate); + public void queueTeleport(final Vec3 position) { + player.sendLatencyStack(); this.queuedTeleports.add(new TeleportCache.Normal(player.sentStackId.get(), position)); this.lastKnowValid = position.toVector3f(); } diff --git a/src/main/java/ac/boar/anticheat/validator/inventory/ItemTransactionValidator.java b/src/main/java/ac/boar/anticheat/validator/inventory/ItemTransactionValidator.java index 9768f5ef..bb4acda5 100644 --- a/src/main/java/ac/boar/anticheat/validator/inventory/ItemTransactionValidator.java +++ b/src/main/java/ac/boar/anticheat/validator/inventory/ItemTransactionValidator.java @@ -87,7 +87,7 @@ public boolean handle(final InventoryTransactionPacket packet) { slotPacket.setItem(ItemData.AIR); slotPacket.setContainerId(ContainerId.INVENTORY); slotPacket.setSlot(slot); - player.cloudburstUpstream.sendPacket(slotPacket); +// player.cloudburstUpstream.sendPacket(slotPacket); } if (dropCounts == slotData.getCount()) { diff --git a/src/main/java/ac/boar/geyser/util/GeyserUtil.java b/src/main/java/ac/boar/geyser/util/GeyserUtil.java index d93689f6..ee59eac9 100644 --- a/src/main/java/ac/boar/geyser/util/GeyserUtil.java +++ b/src/main/java/ac/boar/geyser/util/GeyserUtil.java @@ -1,10 +1,10 @@ package ac.boar.geyser.util; import ac.boar.anticheat.player.BoarPlayer; -import ac.boar.protocol.mitm.CloudburstReceiveListener; -import ac.boar.protocol.mitm.CloudburstSendListener; +import ac.boar.protocol.BoarHandlerAdaptor; +import io.netty.channel.Channel; import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacketHandler; +import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.UpstreamSession; @@ -14,30 +14,17 @@ public class GeyserUtil { public final static long MAGIC_FORM_IMAGE_HACK_TIMESTAMP = -1234567890L; public static final long MAGIC_VIRTUAL_INVENTORY_HACK = -9876543210L; - public static void hookIntoCloudburstMC(final BoarPlayer player) { + public static void hook(final BoarPlayer player) { try { - player.setCloudburstDownstream(findCloudburstSession(player.getSession())); - - injectCloudburstUpstream(player); - injectCloudburstDownstream(player); + BedrockServerSession session = findCloudburstSession(player.getSession()); + player.setCloudburstDownstream(session); + final Channel channel = session.getPeer().getChannel(); + channel.pipeline().addAfter(BedrockPacketCodec.NAME, BoarHandlerAdaptor.NAME, new BoarHandlerAdaptor(player, (BedrockPacketCodec) channel.pipeline().get(BedrockPacketCodec.NAME))); } catch (Exception ignored) { - player.kick("Failed to hook into cloudburst session!"); + player.kick("Failed to hook into bedrock channel pipeline!"); } } - private static void injectCloudburstDownstream(final BoarPlayer player) { - final BedrockServerSession session = player.getCloudburstDownstream(); - final BedrockPacketHandler handler = session.getPacketHandler(); - session.setPacketHandler(player.downstreamPacketHandler = new CloudburstReceiveListener(player, handler)); - } - - private static void injectCloudburstUpstream(final BoarPlayer player) throws Exception { - final BedrockServerSession session = player.getCloudburstDownstream(); - final Field upstream = GeyserSession.class.getDeclaredField("upstream"); - upstream.setAccessible(true); - upstream.set(player.getSession(), player.cloudburstUpstream = new CloudburstSendListener(player, session, (UpstreamSession) upstream.get(player.getSession()))); - } - private static BedrockServerSession findCloudburstSession(final GeyserSession connection) throws Exception { final Field upstream = GeyserSession.class.getDeclaredField("upstream"); upstream.setAccessible(true); diff --git a/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java b/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java new file mode 100644 index 00000000..d7326f12 --- /dev/null +++ b/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java @@ -0,0 +1,74 @@ +package ac.boar.protocol; + +import ac.boar.anticheat.player.BoarPlayer; +import ac.boar.protocol.api.CloudburstPacketEvent; +import ac.boar.protocol.api.PacketListener; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; +import lombok.RequiredArgsConstructor; +import org.cloudburstmc.protocol.bedrock.netty.BedrockPacketWrapper; +import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; + +import java.util.List; + +@RequiredArgsConstructor +public class BoarHandlerAdaptor extends MessageToMessageCodec { + private final BoarPlayer player; + private final BedrockPacketCodec codec; + + public static final String NAME = "boar-packet-handler"; + + @Override + protected void encode(ChannelHandlerContext ctx, BedrockPacketWrapper msg, List out) { + final CloudburstPacketEvent event = new CloudburstPacketEvent(this.player, msg.getPacket()); + try { + for (final PacketListener listener : PacketEvents.getApi().getListeners()) { + listener.onPacketSend(event); + } + } catch (Exception ignored) { + } + + if (event.isCancelled()) { + return; + } + + msg.setPacketBuffer(null); + + ByteBuf buf = ctx.alloc().buffer(128); + try { + BedrockPacket packet = msg.getPacket(); + msg.setPacketId(this.codec.getPacketId(packet)); + this.codec.encodeHeader(buf, msg); + this.codec.getCodec().tryEncode(this.codec.getHelper(), buf, packet); + + msg.setPacketBuffer(buf.retain()); + out.add(msg.retain()); + } catch (Exception ignored) { + } finally { + buf.release(); + } + +// System.out.println(event.getPacket().getPacketType()); + } + + @Override + protected void decode(ChannelHandlerContext ctx, BedrockPacketWrapper msg, List out) { + final CloudburstPacketEvent event = new CloudburstPacketEvent(this.player, msg.getPacket()); + try { + for (final PacketListener listener : PacketEvents.getApi().getListeners()) { + listener.onPacketReceived(event); + } + } catch (Exception ignored) { + } + + if (event.isCancelled()) { + return; + } + + msg.setPacket(event.getPacket()); + out.add(msg.retain()); + } + +} diff --git a/src/main/java/ac/boar/protocol/PacketEvents.java b/src/main/java/ac/boar/protocol/PacketEvents.java index cdaf517f..10cec198 100644 --- a/src/main/java/ac/boar/protocol/PacketEvents.java +++ b/src/main/java/ac/boar/protocol/PacketEvents.java @@ -1,6 +1,6 @@ package ac.boar.protocol; -import ac.boar.protocol.listener.PacketListener; +import ac.boar.protocol.api.PacketListener; import lombok.Getter; import java.util.ArrayList; diff --git a/src/main/java/ac/boar/protocol/event/CloudburstPacketEvent.java b/src/main/java/ac/boar/protocol/api/CloudburstPacketEvent.java similarity index 92% rename from src/main/java/ac/boar/protocol/event/CloudburstPacketEvent.java rename to src/main/java/ac/boar/protocol/api/CloudburstPacketEvent.java index 49ba1152..4f31c79f 100644 --- a/src/main/java/ac/boar/protocol/event/CloudburstPacketEvent.java +++ b/src/main/java/ac/boar/protocol/api/CloudburstPacketEvent.java @@ -1,4 +1,4 @@ -package ac.boar.protocol.event; +package ac.boar.protocol.api; import lombok.Data; diff --git a/src/main/java/ac/boar/protocol/api/PacketListener.java b/src/main/java/ac/boar/protocol/api/PacketListener.java new file mode 100644 index 00000000..58e32450 --- /dev/null +++ b/src/main/java/ac/boar/protocol/api/PacketListener.java @@ -0,0 +1,9 @@ +package ac.boar.protocol.api; + +public interface PacketListener { + default void onPacketSend(final CloudburstPacketEvent event) { + } + + default void onPacketReceived(final CloudburstPacketEvent event) { + } +} \ No newline at end of file diff --git a/src/main/java/ac/boar/protocol/listener/PacketListener.java b/src/main/java/ac/boar/protocol/listener/PacketListener.java deleted file mode 100644 index a9625137..00000000 --- a/src/main/java/ac/boar/protocol/listener/PacketListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package ac.boar.protocol.listener; - -import ac.boar.protocol.event.CloudburstPacketEvent; - -public interface PacketListener { - default void onPacketSend(final CloudburstPacketEvent event, final boolean immediate) { - } - - default void onPacketReceived(final CloudburstPacketEvent event) { - } -} \ No newline at end of file diff --git a/src/main/java/ac/boar/protocol/mitm/CloudburstReceiveListener.java b/src/main/java/ac/boar/protocol/mitm/CloudburstReceiveListener.java deleted file mode 100644 index 686b7cd2..00000000 --- a/src/main/java/ac/boar/protocol/mitm/CloudburstReceiveListener.java +++ /dev/null @@ -1,42 +0,0 @@ -package ac.boar.protocol.mitm; - -import ac.boar.protocol.PacketEvents; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; -import lombok.Getter; - -import ac.boar.anticheat.player.BoarPlayer; -import lombok.RequiredArgsConstructor; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacketHandler; -import org.cloudburstmc.protocol.common.PacketSignal; - -@RequiredArgsConstructor -@Getter -public final class CloudburstReceiveListener implements BedrockPacketHandler { - private final BoarPlayer player; - private final BedrockPacketHandler oldHandler; - - @Override - public PacketSignal handlePacket(BedrockPacket packet) { - if (player.isClosed()) { - return PacketSignal.HANDLED; - } - - final CloudburstPacketEvent event = new CloudburstPacketEvent(this.player, packet); - for (final PacketListener listener : PacketEvents.getApi().getListeners()) { - listener.onPacketReceived(event); - } - - if (event.isCancelled()) { - return PacketSignal.HANDLED; - } - - return oldHandler.handlePacket(event.getPacket()); - } - - @Override - public void onDisconnect(CharSequence reason) { - this.oldHandler.onDisconnect(reason); - } -} \ No newline at end of file diff --git a/src/main/java/ac/boar/protocol/mitm/CloudburstSendListener.java b/src/main/java/ac/boar/protocol/mitm/CloudburstSendListener.java deleted file mode 100644 index e9c60c34..00000000 --- a/src/main/java/ac/boar/protocol/mitm/CloudburstSendListener.java +++ /dev/null @@ -1,95 +0,0 @@ -package ac.boar.protocol.mitm; - -import ac.boar.anticheat.Boar; -import ac.boar.anticheat.util.DimensionUtil; -import ac.boar.anticheat.validator.blockbreak.ServerBreakBlockValidator; -import ac.boar.protocol.PacketEvents; -import ac.boar.protocol.event.CloudburstPacketEvent; -import ac.boar.protocol.listener.PacketListener; -import lombok.NonNull; - -import ac.boar.anticheat.player.BoarPlayer; -import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.data.AuthoritativeMovementMode; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.cloudburstmc.protocol.bedrock.packet.StartGamePacket; -import org.cloudburstmc.protocol.bedrock.packet.UpdateClientInputLocksPacket; -import org.geysermc.geyser.session.UpstreamSession; - -public final class CloudburstSendListener extends UpstreamSession { - private final BoarPlayer player; - private final UpstreamSession oldSession; - - public CloudburstSendListener(BoarPlayer player, BedrockServerSession session, UpstreamSession oldSession) { - super(session); - this.player = player; - this.oldSession = oldSession; - } - - @Override - public void disconnect(String reason) { - oldSession.disconnect(reason); - } - - @Override - public void sendPacket(@NonNull BedrockPacket packet) { - if (player.isClosed()) { - return; - } - - if (packet instanceof UpdateClientInputLocksPacket) { - // Nope, don't, pain in the ass to support this. - return; - } - - final CloudburstPacketEvent event = new CloudburstPacketEvent(this.player, packet); - for (final PacketListener listener : PacketEvents.getApi().getListeners()) { - listener.onPacketSend(event, false); - } - - if (event.isCancelled()) { - return; - } - - if (event.getPacket() instanceof StartGamePacket start) { - player.runtimeEntityId = start.getRuntimeEntityId(); - player.javaEntityId = player.getSession().getPlayerEntity().getEntityId(); - - player.compensatedWorld.setDimension(DimensionUtil.dimensionFromId(start.getDimensionId())); - player.currentLoadingScreen = null; - player.inLoadingScreen = true; - - // We need this to do rewind teleport. - start.setAuthoritativeMovementMode(AuthoritativeMovementMode.SERVER_WITH_REWIND); - start.setRewindHistorySize(Boar.getConfig().rewindHistory()); - player.serverBreakBlockValidator = new ServerBreakBlockValidator(player); - - player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.gameType = start.getPlayerGameType()); - } - - oldSession.sendPacket(event.getPacket()); - event.getPostTasks().forEach(Runnable::run); - event.getPostTasks().clear(); - } - - @Override - public void sendPacketImmediately(@NonNull BedrockPacket packet) { - if (player.isClosed()) { - return; - } - - final CloudburstPacketEvent event = new CloudburstPacketEvent(this.player, packet); - for (final PacketListener listener : PacketEvents.getApi().getListeners()) { - listener.onPacketSend(event, true); - } - - if (event.isCancelled()) { - return; - } - - oldSession.sendPacketImmediately(event.getPacket()); - event.getPostTasks().forEach(Runnable::run); - event.getPostTasks().clear(); - } -} \ No newline at end of file From 1074288f4e1502c44b870227b60d0f8314dfe9a7 Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Sun, 8 Feb 2026 17:24:41 +0700 Subject: [PATCH 2/9] Make this build. --- build.gradle | 4 ++-- .../cache/container/impl/PlayerContainerCache.java | 2 +- .../ac/boar/anticheat/data/inventory/ItemCache.java | 2 +- .../packets/player/PlayerVelocityPackets.java | 2 +- .../validator/inventory/ItemTransactionValidator.java | 10 +++++----- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index f3e7f679..dfca7864 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { group = 'ac.boar' -version = '1.0.4' +version = '2.0' repositories { mavenCentral() @@ -17,7 +17,7 @@ repositories { } dependencies { - compileOnly("org.geysermc.geyser:core:2.9.2-SNAPSHOT") { + compileOnly("org.geysermc.geyser:core:2.9.3-SNAPSHOT") { exclude group: "com.google.code.gson", module: "gson" } compileOnly("it.unimi.dsi:fastutil:8.5.15") diff --git a/src/main/java/ac/boar/anticheat/compensated/cache/container/impl/PlayerContainerCache.java b/src/main/java/ac/boar/anticheat/compensated/cache/container/impl/PlayerContainerCache.java index edbe61a6..274c472a 100644 --- a/src/main/java/ac/boar/anticheat/compensated/cache/container/impl/PlayerContainerCache.java +++ b/src/main/java/ac/boar/anticheat/compensated/cache/container/impl/PlayerContainerCache.java @@ -22,7 +22,7 @@ public ItemData getHeldItemData() { } public GeyserItemStack getHeldItem() { - return GeyserItemStack.from(this.inventory.translate(getHeldItemData())); + return GeyserItemStack.from(inventory.getPlayer().getSession(), this.inventory.translate(getHeldItemData())); } public ItemCache getItemFromSlot(final int slot) { diff --git a/src/main/java/ac/boar/anticheat/data/inventory/ItemCache.java b/src/main/java/ac/boar/anticheat/data/inventory/ItemCache.java index 8fa6e43a..bacf3ed9 100644 --- a/src/main/java/ac/boar/anticheat/data/inventory/ItemCache.java +++ b/src/main/java/ac/boar/anticheat/data/inventory/ItemCache.java @@ -48,7 +48,7 @@ public static ItemCache build(final CompensatedInventory inventory, final ItemDa final ItemCache cache = new ItemCache(data); final ItemStack itemStack = inventory.translate(data); - if (GeyserItemStack.from(itemStack).is(inventory.getPlayer().getSession(), ItemTag.BUNDLES)) { + if (GeyserItemStack.from(inventory.getPlayer().getSession(), itemStack).is(inventory.getPlayer().getSession(), ItemTag.BUNDLES)) { int id = -1; try { diff --git a/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java b/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java index fc64632c..00d8bda7 100644 --- a/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java @@ -20,7 +20,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { return; } - // I think there is some rewind like behaviour when there is ehm the tick is not 0, so just default back to 0 till I figure it out. + // I think there is some rewind like behavior when there is ehm the tick is not 0, so just default back to 0 till I figure it out. packet.setTick(0); player.sendLatencyStack(); diff --git a/src/main/java/ac/boar/anticheat/validator/inventory/ItemTransactionValidator.java b/src/main/java/ac/boar/anticheat/validator/inventory/ItemTransactionValidator.java index bb4acda5..470e74ae 100644 --- a/src/main/java/ac/boar/anticheat/validator/inventory/ItemTransactionValidator.java +++ b/src/main/java/ac/boar/anticheat/validator/inventory/ItemTransactionValidator.java @@ -171,7 +171,7 @@ public boolean handle(final InventoryTransactionPacket packet) { } ItemCache heldItem = inventory.inventoryContainer.getHeldItemCache(); - GeyserItemStack geyserItemStack = GeyserItemStack.from(inventory.translate(heldItem.getData())); + GeyserItemStack geyserItemStack = GeyserItemStack.from(player.getSession(), inventory.translate(heldItem.getData())); Item item = geyserItemStack.asItem(); boolean heldItemExist = !heldItem.isEmpty(); @@ -301,18 +301,18 @@ public boolean handle(final InventoryTransactionPacket packet) { if (item.javaId() == Items.WATER_BUCKET.javaId()) { player.compensatedWorld.updateBlock(newBlockPos, 0, player.getSession().getBlockMappings().getBedrockWater().getRuntimeId()); - GeyserItemStack stack = GeyserItemStack.of(Items.BUCKET.javaId(), 1); + GeyserItemStack stack = GeyserItemStack.of(player.getSession(), Items.BUCKET.javaId(), 1); inventory.inventoryContainer.set(inventory.heldItemSlot, inventory.translate(stack.getItemStack())); } else if (item.javaId() == Items.LAVA_BUCKET.javaId()) { player.compensatedWorld.updateBlock(newBlockPos, 0, player.getSession().getBlockMappings().getBedrockBlockId(Blocks.LAVA. defaultBlockState().javaId())); - GeyserItemStack stack = GeyserItemStack.of(Items.BUCKET.javaId(), 1); + GeyserItemStack stack = GeyserItemStack.of(player.getSession(), Items.BUCKET.javaId(), 1); inventory.inventoryContainer.set(inventory.heldItemSlot, inventory.translate(stack.getItemStack())); } else if (item.javaId() == Items.POWDER_SNOW_BUCKET.javaId()) { player.compensatedWorld.updateBlock(newBlockPos, 0, player.getSession().getBlockMappings().getBedrockBlockId(Blocks.POWDER_SNOW.defaultBlockState().javaId())); - GeyserItemStack stack = GeyserItemStack.of(Items.BUCKET.javaId(), 1); + GeyserItemStack stack = GeyserItemStack.of(player.getSession(), Items.BUCKET.javaId(), 1); inventory.inventoryContainer.set(inventory.heldItemSlot, inventory.translate(stack.getItemStack())); } else if (item.javaId() == Items.BUCKET.javaId()) { int javaId = -1, layer = 0; @@ -333,7 +333,7 @@ public boolean handle(final InventoryTransactionPacket packet) { player.compensatedWorld.updateBlock(newBlockPos, layer, player.getSession().getBlockMappings().getBedrockAir().getRuntimeId()); - GeyserItemStack stack = GeyserItemStack.of(javaId, 1); + GeyserItemStack stack = GeyserItemStack.of(player.getSession(), javaId, 1); inventory.inventoryContainer.set(inventory.heldItemSlot, inventory.translate(stack.getItemStack())); } if (item instanceof BlockItem blockItem) { // Handle block item after bucket. Block mappedBlock = BlockMappings.getItemToBlock().getOrDefault(blockItem, Blocks.AIR); From 37fd777721d8dc35efed008f8cc2ce169d5dd90d Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Sun, 8 Feb 2026 18:35:03 +0700 Subject: [PATCH 3/9] Initial work on refactoring latency handling. --- DIFFERENCES_WIKI.md | 251 ------------------ README.md | 7 +- .../anticheat/acks/BoarAcknowledgement.java | 42 +-- .../check/api/impl/PingBasedCheck.java | 4 +- .../anticheat/check/impl/timer/Timer.java | 6 +- .../packets/input/AuthInputPackets.java | 2 +- .../packets/other/NetworkLatencyPackets.java | 41 ++- .../packets/other/VehiclePackets.java | 4 +- .../packets/player/PlayerEffectPackets.java | 14 +- .../player/PlayerInventoryPackets.java | 24 +- .../packets/player/PlayerVelocityPackets.java | 2 +- .../packets/server/ServerChunkPackets.java | 10 +- .../packets/server/ServerDataPackets.java | 18 +- .../packets/server/ServerEntityPackets.java | 16 +- .../ac/boar/anticheat/player/BoarPlayer.java | 56 +--- .../boar/anticheat/teleport/TeleportUtil.java | 4 +- .../ac/boar/anticheat/util/LatencyUtil.java | 194 +++----------- .../java/ac/boar/geyser/util/GeyserUtil.java | 2 +- .../ac/boar/protocol/BoarHandlerAdaptor.java | 4 +- 19 files changed, 141 insertions(+), 560 deletions(-) delete mode 100644 DIFFERENCES_WIKI.md diff --git a/DIFFERENCES_WIKI.md b/DIFFERENCES_WIKI.md deleted file mode 100644 index 99efa347..00000000 --- a/DIFFERENCES_WIKI.md +++ /dev/null @@ -1,251 +0,0 @@ - -# Bedrock - Java differences. -So I can keep track of differences between Java - Bedrock in general and also so other developer that works on Projects related to Java-Bedrock can have an easier time :) - -- Most of the information here is rather a combination of reverse enginerring BDS (1.21.10) and a lot of debugging, and also from [bedrock-protocol-docs](https://github.com/Mojang/bedrock-protocol-docs) and other that I found when working on Boar. - -## Attribute -### Packet -- On Java Edition, the attribute packet consist of 2 thing, baseValue and attribute modifiers, when the client received this, it will clear the modidifer, update the base value, and add new modifiers into the attribute. - -- On Bedrock Edition, the attribute packet consist of minium, maximum, defaultMinimum, defaultMaximum, baseValue, **value**, attribute modifiers. Minium, maximum, and baseValue is pretty self explanatory, meanwhile the **value** is the calculated value (after applying attribute modifiers to the base value or could be any value, depends on the server), and attribute modifiers act the same as on Java. Bedrock client (likely) handle this in this order: clearModifier, baseValue, min/max, value, newAttributes, I'm not certain where newAttributes is placed but likely after clear or after value. - -### Class -- On Java Edition, everytime an attribute is added/removed or when baseValue is updated, a value called "**dirty**" will be set to true, and this will make sures that the value will be updated when you called the value. -```java -private void addModifier(AttributeModifier attributeModifier) { - AttributeModifier attributeModifier2 = this.modifierById.putIfAbsent(attributeModifier.id(), attributeModifier); - if (attributeModifier2 != null) { - throw new IllegalArgumentException("Modifier is already applied on this attribute!"); - } - this.getModifiers(attributeModifier.operation()).put(attributeModifier.id(), attributeModifier); - this.setDirty(); -} - -protected void setDirty() { - this.dirty = true; - this.onDirty.accept(this); -} - -public double getValue() { - if (this.dirty) { - this.cachedValue = this.calculateValue(); - this.dirty = false; - } - return this.cachedValue; -} -``` - -- Howerver, on Bedrock there is a few *slight* differences. First of all the "**dirty**" value will only be updated to TRUE **IF THE baseValue IS UPDATED**, in other case like updating min, max, clearing attribute, adding/removing attribute, the value going to be **updated right away**. Looking at the example below *(This is only a recreation of what I see in BDS code)* - -```java -public void setBaseValue(float baseValue) { - if (this.baseValue == baseValue) { - return; - } - - this.baseValue = value; - this.setDirty(); -} - -private void addModifier(AttributeModifierData modifier) { - final AttributeModifierData lv = this.modifiers.putIfAbsent(modifier.getId(), modifier); - if (lv == null) { - this.update(); - } -} - -protected void update() { - this.value = this.computeValue(); -} - -public float getValue() { - if (this.dirty) { - this.value = this.computeValue(); - this.dirty = false; - } - - return this.value; -} -``` - -#### Note for Geyser (and why it is so important). -### READ THIS, IT'S IMPORTANT AND IT WILL BE THE REASON WHY 90% OF ANTICHEATS WILL FALSE FLAG FOR THIS. - -- As listed above, the differences in packet and class, you might notice that the Bedrock packet can change the value directly and how Bedrock updated the value instantly. The problem is Geyser **DO NOT** translate attribute modifiers *(don't blame them they have a good reason for doing so)*, so when player *START* sprinting, the server send back attribute and the only thing Geyser translate is the calculated **value** and nothing else, baseValue, min, max, and the rest remains the same. The client received this, cleared the modifiers, update to the new value (which is this case is 0.13) and no new modifiers to add! Now the real problem is when the player *STOP* sprinting, player will attempt to remove the sprinting modifier attribute, but the problem is, *there is none to begin which* since it is already cleared from before, therefore nothing to update, the attribute will remains the same value 0.13, therefore the player keep on sprinting even though they already stopped and only will stop when the server send back the value 0.1. -#### FAQ (For the sections above). -##### Why don't Geyser translate the attribute modifier? -- They simply can't, UUID, values, and differences in each possible attribute modifier that CAN BE CHANGE between update is not worth it to translate, if they fucked anything up, the attribute value is wrong all the time which is the bad thing. -##### Ok, how IS the *Class* section is any relevant to the *Note for Geyser* section. -- Well if you try to handle it normally with the Java version of the class. It will set dirty to true, the value doesn't get updated instantly, but because of the attribute modifiers is now empty, even tho the value is set to 0.13, the next time you call *getValue()* it will try to update, poof now your value is back to 0.1 which is wrong. - -## Block Friction, Jump Factor -### getBlockPosBelowThatAffectsMyMovement() -- On Java Edition, this value try to get the block that 0.5 blocks below you meanwhile on Bedrock it's 0.1 -### Block Friction -- Only that honey block now have the same block friction as slime (0.8) -### Jump Factor -- Only honey block have this behaviour, jump factor will now be 0.6 instead of 0.5. - -## Bouncy Block -- Only bed block act differently, you can look at the differences instantly here. -```java -// Java Edition. -private void bounceUp(Entity entity) { - Vec3 vec3 = entity.getDeltaMovement(); - if (vec3.y < 0.0) { - double d = entity instanceof LivingEntity ? 1.0 : 0.8; - entity.setDeltaMovement(vec3.x, -vec3.y * (double)0.66f * d, vec3.z); - } -} -``` - -```java -// Bedrock Edition (from Boar code). -if (cache.is(BlockTag.BEDS, state.block()) && player.velocity.y < 0.0 && !player.sneaking) { - final float d = living ? 1.0F : 0.8F; - player.velocity.y = -player.velocity.y * 0.75F * d; - if (player.velocity.y > 0.75) { - player.velocity.y = 0.75F; - } - - return; -} -``` -## Stepping on a block. -### Honey block. -- On Java Edition, stepping on a honey block will get handle by *speedFactor* multiplier, while on Bedrock it will be handle by the *stepOn* method you normally see on Java. -- On Bedrock Edition, the stepping on thing will act the same as slime block. - -```java -double d = Math.abs(entity.getDeltaMovement().y); -if (d < 0.1 && !entity.isSteppingCarefully()) { - double e = 0.4 + d * 0.2; - entity.setDeltaMovement(entity.getDeltaMovement().multiply(e, 1.0, e)); -} -``` - -## Speed Factor -- Doesn't seems to exist at all on Bedrock. - -## Block Collision -- Look at [BedrockCollision](https://github.com/Oryxel/Boar/blob/new-engine/src/main/java/ac/boar/anticheat/collision/BedrockCollision.java) class in Boar anticheat (incomplete). - -## World Layer -- This behaviour only exist on Bedrock edition, a block could have multiple *layer*. - -## Climbing ladder -- On Java Edition, when you're climbing a ladder, a value called 0.2 will be set to your y motion after player done their movement for that tick and will be affect by tick end (0.2 - 0.08) * 0.98 resulted in climbing only 0.1176 blocks per tick. -```java -// Java Edition -private Vec3 handleRelativeFrictionAndCalculateMovement(Vec3 vec3, float f) { - this.moveRelative(this.getFrictionInfluencedSpeed(f), vec3); - this.setDeltaMovement(this.handleOnClimbable(this.getDeltaMovement())); - this.move(MoverType.SELF, this.getDeltaMovement()); // Move player to the calculated position. - Vec3 vec32 = this.getDeltaMovement(); - - // Finally handle climbing. - if ((this.horizontalCollision || this.jumping) && (this.onClimbable() || this.wasInPowderSnow && PowderSnowBlock.canEntityWalkOnPowderSnow(this))) { - vec32 = new Vec3(vec32.x, 0.2, vec32.z); - } - return vec32; -} - -private void travelInAir(Vec3 vec3) { - BlockPos blockPos = this.getBlockPosBelowThatAffectsMyMovement(); - float f = this.onGround() ? this.level().getBlockState(blockPos).getBlock().getFriction() : 1.0f; - float g = f * 0.91f; - Vec3 vec32 = this.handleRelativeFrictionAndCalculateMovement(vec3, f); - double d = vec32.y; - - // Tick End act the same, climbing won't affect this. - MobEffectInstance mobEffectInstance = this.getEffect(MobEffects.LEVITATION); - d = mobEffectInstance != null ? (d += (0.05 * (double)(mobEffectInstance.getAmplifier() + 1) - vec32.y) * 0.2) : (!this.level().isClientSide || this.level().hasChunkAt(blockPos) ? (d -= this.getEffectiveGravity()) : (this.getY() > (double)this.level().getMinY() ? -0.1 : 0.0)); - if (this.shouldDiscardFriction()) { - this.setDeltaMovement(vec32.x, d, vec32.z); - } else { - float h = this instanceof FlyingAnimal ? g : 0.98f; - this.setDeltaMovement(vec32.x * (double)g, d * (double)h, vec32.z * (double)g); - } -} -``` - -- However on Bedrock,Climbing part in *handleRelativeFrictionAndCalculateMovement* will be the move to before move method, climbing will also affect tick end if player is having horizontal collision. -```java -// Bedrock Edition (Recreation of BDS code) -private Vec3 handleRelativeFrictionAndCalculateMovement(Vec3 vec3, float f) { - this.moveRelative(this.getFrictionInfluencedSpeed(f), vec3); - this.setDeltaMovement(this.handleOnClimbable(this.getDeltaMovement())); - Vec3 vec32 = this.getDeltaMovement(); - if ((this.horizontalCollision || this.jumping) && (this.onClimbable() || this.wasInPowderSnow && PowderSnowBlock.canEntityWalkOnPowderSnow(this))) { - vec32 = new Vec3(vec32.x, 0.2, vec32.z); - } - this.move(MoverType.SELF, vec32); // Move player to the calculated position. - return vec32; -} - -public void finalizeMovement() { - final StatusEffect effect = player.getEffect(Effect.LEVITATION); - if (effect != null) { - player.velocity.y += (0.05f * (effect.getAmplifier() + 1) - player.velocity.y) * 0.2f; - } else if (player.compensatedWorld.isChunkLoaded((int) player.position.x, (int) player.position.z)) { - player.velocity.y -= player.getEffectiveGravity(); - } else { - // Seems to be 0 all the times, not -0.1 depends on your y, or well I don't know? - player.velocity.y = 0; - } - - player.velocity.y *= 0.98F; - - if (player.horizontalCollision && (player.onClimbable() || player.getInBlockState().is(Blocks.POWDER_SNOW) && PowderSnowBlock.canEntityWalkOnPowderSnow(player))) { - player.velocity.y = 0.2F; - } - - float g = this.prevSlipperiness * 0.91F; - player.velocity = player.velocity.multiply(g, 1, g); -} -``` - -## Movement "inside" block. -### Stuck Speed Multiplier (Cobwebs, Sweet Berry, ....) -- On Java Edition, the client loop through a list of blocks that near the player, and then update the stuck multiplier, which means if the order for example is in. -``` -Sweet Berry -Cobweb -Sweet Berry -Power Snow <- Ta da, the client use the latest one! -``` -- On Bedrock Edition howerver, it will choose the speed multiplier that going slow down player the most -``` -Sweet Berry -Cobweb <- Client now use this! -Sweet Berry -Power Snow -``` -- You can take a deeper look inside [Boar code](https://github.com/Oryxel/Boar/blob/8fc9e55743690c5382d25530f17a2832d3d547dd/src/main/java/ac/boar/anticheat/data/BoarBlockState.java#L49) to see how it works. -```java -// Crap code don't mind it. -final boolean xLargerThanThreshold = Math.abs(player.stuckSpeedMultiplier.x) >= 1.0E-7; -final boolean yLargerThanThreshold = Math.abs(player.stuckSpeedMultiplier.y) >= 1.0E-7; -final boolean zLargerThanThreshold = Math.abs(player.stuckSpeedMultiplier.z) >= 1.0E-7; -if (xLargerThanThreshold || yLargerThanThreshold || zLargerThanThreshold) { - player.stuckSpeedMultiplier.x = Math.min(player.stuckSpeedMultiplier.x, movementMultiplier.x); - player.stuckSpeedMultiplier.y = Math.min(player.stuckSpeedMultiplier.y, movementMultiplier.y); - player.stuckSpeedMultiplier.z = Math.min(player.stuckSpeedMultiplier.z, movementMultiplier.z); -} else { - player.stuckSpeedMultiplier = movementMultiplier; -} -``` - -### Honey block -- Simply take look at [HoneyBlockState](https://github.com/Oryxel/Boar/blob/new-engine/src/main/java/ac/boar/anticheat/data/block/impl/HoneyBlockState.java). - -## Input -- On Bedrock Edition, the client can entirely control how much they move (in range of -1 to 1) and is not limited like Java Edition, however this is only the case with analog move vector (joystick thingy). -- The input will also be normalized **only if** by normalizing the input the player won't gain any advantage before/after the input. -- Input multiplier for using item is different from Java (0.122499995). -- You can take a look at how Boar handle Input [here](https://github.com/Oryxel/Boar/blob/new-engine/src/main/java/ac/boar/anticheat/util/InputUtil.java) - -## Minimal motion -- On Java Edtion, if entity motion is smaller than 0.003 or 0.005 depends on the version the motion going to set to 0. -- On Bedrock Edition, this value seems to be around the range 1.0E-8 -> 1.0E-9 before the value is set to 0. \ No newline at end of file diff --git a/README.md b/README.md index 68b926af..1bcc99c0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ -# BOAR IS DISCONTINUED, GO USE THEMIS OR BUY GRIM V3 INSTEAD, I WILL NO LONGER MAINTAIN THIS. - # Boar Boar is a POC project that allows you to enable [server-auth-with-rewind](https://github.com/Mojang/bedrock-protocol-docs/blob/main/additional_docs/ConfiguringAntiCheat.md) for [GeyserMC](https://github.com/GeyserMC/Geyser) project with a few more checks and improvements compare to BDS. -* *Note: I'm getting more and more tired maintaining this project, so this project won't be frequently update. However this project is not abandoned, I will still maintain -it, update and push fixes from time to time if needed. If there are bugs and bypasses, you can still can contact to me about it and I wil get to it when I can.* - ### Documented differences between Java - Bedrock [here](https://github.com/oryxel1/Boar/blob/master/DIFFERENCES_WIKI.md) with detailed explainations. ### ⚠️ WARNING: THIS ONLY FOR BEDROCK PLAYER NOT JAVA PLAYER! YOU WILL NEED TO PAIR THIS WITH ANOTHER JAVA ANTICHEAT! @@ -16,7 +11,7 @@ A dedicated (proof of concept) anti cheat for GeyserMC project. ### Features - I will keep this short: lag compensation, movement simulation (prediction), smooth rewind setback. -- Also this anticheat is actually a Geyser extension! +- Also, this anticheat is actually a Geyser extension! ### Current detections list #### Almost every single movement-related cheats (except vehicle aka boat/horse), including - but not limited to: diff --git a/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java b/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java index 8b67d2f5..e5a72831 100644 --- a/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java +++ b/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java @@ -14,26 +14,26 @@ public class BoarAcknowledgement { private static final Map rakSessionToPlayer = new HashMap<>(); public static void handle(final RakSessionCodec codec, final RakDatagramPacket datagram) { - BoarPlayer player = rakSessionToPlayer.get(codec); - if (player == null) { - return; - } - - if (player.isClosed()) { - return; - } - - if (player.receivedStackId.get() == player.sentStackId.get()) { - return; - } - - long lastLatency = player.getLatencyUtil().getLastSentTime().ms(); - - long distance = datagram.getSendTime() - lastLatency; - if (distance <= Boar.getConfig().maxAcknowledgementTime() || lastLatency == -1 || player.inLoadingScreen || player.sinceLoadingScreen < 5) { - return; - } - - player.getLatencyUtil().confirmByTime(datagram.getSendTime()); +// BoarPlayer player = rakSessionToPlayer.get(codec); +// if (player == null) { +// return; +// } +// +// if (player.isClosed()) { +// return; +// } +// +// if (player.receivedStackId.get() == player.sentStackId.get()) { +// return; +// } +// +// long lastLatency = player.getLatencyUtil().getLastSentTime().ms(); +// +// long distance = datagram.getSendTime() - lastLatency; +// if (distance <= Boar.getConfig().maxAcknowledgementTime() || lastLatency == -1 || player.inLoadingScreen || player.sinceLoadingScreen < 5) { +// return; +// } +// +// player.getLatencyUtil().confirmByTime(datagram.getSendTime()); } } diff --git a/src/main/java/ac/boar/anticheat/check/api/impl/PingBasedCheck.java b/src/main/java/ac/boar/anticheat/check/api/impl/PingBasedCheck.java index fc445923..f260b8d5 100644 --- a/src/main/java/ac/boar/anticheat/check/api/impl/PingBasedCheck.java +++ b/src/main/java/ac/boar/anticheat/check/api/impl/PingBasedCheck.java @@ -9,6 +9,6 @@ public PingBasedCheck(BoarPlayer player) { super(player); } - public void onLatencySend(long id) {} - public void onLatencyAccepted(long id, LatencyUtil.Time time) {} +// public void onLatencySend(long id) {} + public void onLatencyAccepted(LatencyUtil.Latency latency) {} } diff --git a/src/main/java/ac/boar/anticheat/check/impl/timer/Timer.java b/src/main/java/ac/boar/anticheat/check/impl/timer/Timer.java index c2c5aa2e..93eaa383 100644 --- a/src/main/java/ac/boar/anticheat/check/impl/timer/Timer.java +++ b/src/main/java/ac/boar/anticheat/check/impl/timer/Timer.java @@ -21,14 +21,14 @@ public Timer(final BoarPlayer player) { } @Override - public void onLatencyAccepted(long id, LatencyUtil.Time time) { + public void onLatencyAccepted(LatencyUtil.Latency latency) { if (!this.beforeAuthInput) { return; } this.beforeAuthInput = false; - if (time.ns() > System.nanoTime() + this.balance) { - long distance = (time.ns() - (System.nanoTime() + this.balance)) - (AVERAGE_DISTANCE / 2); + if (latency.ns() > System.nanoTime() + this.balance) { + long distance = (latency.ns() - (System.nanoTime() + this.balance)) - (AVERAGE_DISTANCE / 2); this.balance += distance; this.loseBalance = Math.max(0, this.loseBalance - distance); diff --git a/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java b/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java index a08d6094..63647d96 100644 --- a/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java @@ -141,7 +141,7 @@ public void onPacketSend(CloudburstPacketEvent event) { player.sendLatencyStack(); player.getTeleportUtil().getQueuedTeleports().add(new TeleportCache.DimensionSwitch(player.sentStackId.get(), new Vec3(packet.getPosition().up(EntityDefinitions.PLAYER.offset())))); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.getLatencyUtil().queue(() -> { if (player.compensatedWorld.getDimension() != dimension) { player.currentLoadingScreen = packet.getLoadingScreenId(); player.inLoadingScreen = true; diff --git a/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java b/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java index 0a01efe2..383e744f 100644 --- a/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java @@ -1,48 +1,45 @@ package ac.boar.anticheat.packets.other; -import ac.boar.geyser.util.GeyserUtil; +import ac.boar.anticheat.player.BoarPlayer; +import ac.boar.anticheat.util.LatencyUtil; import ac.boar.protocol.api.CloudburstPacketEvent; import ac.boar.protocol.api.PacketListener; import org.cloudburstmc.protocol.bedrock.packet.NetworkStackLatencyPacket; import org.geysermc.api.util.BedrockPlatform; -import org.geysermc.geyser.session.GeyserSession; public class NetworkLatencyPackets implements PacketListener { public final static long LATENCY_MAGNITUDE = 1000000L; public final static long PS5_LATENCY_MAGNITUDE = 1000L; @Override - public void onPacketReceived(final CloudburstPacketEvent event) { + public void onPacketSend(final CloudburstPacketEvent event) { if (!(event.getPacket() instanceof NetworkStackLatencyPacket packet)) { return; } - final GeyserSession session = event.getPlayer().getSession(); - // Bedrock player have different latency magnitude depending on the platform, however this is the only one we know about - // TODO: Figure out what the magnitude for PS4 is, currently we only know PS5 (BedrockPlatform.PS4 is an misleading name.) - long id = packet.getTimestamp() / (session.platform() == BedrockPlatform.PS4 ? PS5_LATENCY_MAGNITUDE : LATENCY_MAGNITUDE); + event.getPlayer().getLatencyUtil().queue(packet.getTimestamp(), false); + } - // Positive id is for keep alive passthrough hack, and there also only 2 other negative id that we just need to check for. - // This implementation could be a problem later on considering that Networking API will soon become a thing but oh welp. - // Let's hope there isn't anything else that will conflict with our anticheat latency system. - if (id >= 0 || id == GeyserUtil.MAGIC_FORM_IMAGE_HACK_TIMESTAMP) { + @Override + public void onPacketReceived(final CloudburstPacketEvent event) { + if (!(event.getPacket() instanceof NetworkStackLatencyPacket packet)) { return; } + final BoarPlayer player = event.getPlayer(); + final LatencyUtil.Latency poll = player.getLatencyUtil().sentQueue().poll(); - // There is this weird bug with virtual inventory hack, so uhhh, this is the work around for it - // TODO: Properly fix Boar to actually resolve this (https://github.com/oryxel1/Boar/issues/29). - if (id == GeyserUtil.MAGIC_VIRTUAL_INVENTORY_HACK) { - if (session.getPendingOrCurrentBedrockInventoryId() == -1) { // There is no hack to be done here. - return; - } + // Bedrock player have different latency magnitude depending on the platform, however this is the only one we know about + // TODO: Figure out what the magnitude for PS4 is, currently we only know PS5 (BedrockPlatform.PS4 is an misleading name.) + long id = packet.getTimestamp() / (player.getSession().platform() == BedrockPlatform.PS4 ? PS5_LATENCY_MAGNITUDE : LATENCY_MAGNITUDE); - if (session.getInventoryHolder() != null) { - session.getInventoryHolder().pending(true); // Yep. - } + if (poll == null || poll.id() != id) { + player.kick("Invalid latency id, expected=" + (poll == null ? "none" : poll.id()) + ", actual=" + id); return; } - event.getPlayer().getLatencyUtil().confirmStackId(Math.abs(id)); - event.setCancelled(true); + poll.run(); + + player.getLatencyUtil().onLatencyAccepted(poll); + event.setCancelled(poll.ours()); } } diff --git a/src/main/java/ac/boar/anticheat/packets/other/VehiclePackets.java b/src/main/java/ac/boar/anticheat/packets/other/VehiclePackets.java index 96266a3d..bdfeb246 100644 --- a/src/main/java/ac/boar/anticheat/packets/other/VehiclePackets.java +++ b/src/main/java/ac/boar/anticheat/packets/other/VehiclePackets.java @@ -58,11 +58,11 @@ public void onPacketSend(CloudburstPacketEvent event) { player.sendLatencyStack(); if (link.getType() == EntityLinkData.Type.REMOVE) { - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.vehicleData = null); + player.getLatencyUtil().queue(() -> player.vehicleData = null); return; } - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.getLatencyUtil().queue(() -> { player.vehicleData = new VehicleData(); // player.vehicleData.canWeControlThisVehicle = link.getType() == EntityLinkData.Type.RIDER; player.vehicleData.vehicleRuntimeId = entityId; diff --git a/src/main/java/ac/boar/anticheat/packets/player/PlayerEffectPackets.java b/src/main/java/ac/boar/anticheat/packets/player/PlayerEffectPackets.java index 96d99345..49307324 100644 --- a/src/main/java/ac/boar/anticheat/packets/player/PlayerEffectPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/player/PlayerEffectPackets.java @@ -23,13 +23,13 @@ public void onPacketSend(final CloudburstPacketEvent event) { return; } - player.sendLatencyStack(); - - if (packet.getEvent() == MobEffectPacket.Event.ADD || packet.getEvent() == MobEffectPacket.Event.MODIFY) { - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.getActiveEffects().put(effect, new StatusEffect(effect, packet.getAmplifier(), packet.getDuration() + 1))); - } else if (packet.getEvent() == MobEffectPacket.Event.REMOVE) { - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.getActiveEffects().remove(effect)); - } + player.sendLatencyStack(() -> { + if (packet.getEvent() == MobEffectPacket.Event.ADD || packet.getEvent() == MobEffectPacket.Event.MODIFY) { + player.getActiveEffects().put(effect, new StatusEffect(effect, packet.getAmplifier(), packet.getDuration() + 1)); + } else if (packet.getEvent() == MobEffectPacket.Event.REMOVE) { + player.getActiveEffects().remove(effect); + } + }); } } } \ No newline at end of file diff --git a/src/main/java/ac/boar/anticheat/packets/player/PlayerInventoryPackets.java b/src/main/java/ac/boar/anticheat/packets/player/PlayerInventoryPackets.java index 00ed028f..cd2fd733 100644 --- a/src/main/java/ac/boar/anticheat/packets/player/PlayerInventoryPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/player/PlayerInventoryPackets.java @@ -79,7 +79,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { final CompensatedInventory inventory = player.compensatedInventory; if (event.getPacket() instanceof CreativeContentPacket packet) { - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.getLatencyUtil().queue(() -> { inventory.getCreativeData().clear(); for (final CreativeItemData data : packet.getContents()) { @@ -89,7 +89,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { } if (event.getPacket() instanceof CraftingDataPacket packet) { - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.getLatencyUtil().queue(() -> { inventory.getCraftingData().clear(); for (final RecipeData data : packet.getCraftingData()) { @@ -127,7 +127,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { if (event.getPacket() instanceof ContainerOpenPacket packet) { // System.out.println(packet); player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.getLatencyUtil().queue(() -> { final ContainerCache container = inventory.getContainer(packet.getId()); inventory.openContainer = Objects.requireNonNullElseGet(container, () -> new ContainerCache(inventory, packet.getId(), packet.getType(), packet.getBlockPosition(), packet.getUniqueEntityId())); }); @@ -147,16 +147,17 @@ public void onPacketSend(final CloudburstPacketEvent event) { return; } - player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { try { - inventory.openContainer = new TradeContainerCache(inventory, packet.getOffers(), + player.sendLatencyStack(() -> { + try { + inventory.openContainer = new TradeContainerCache(inventory, packet.getOffers(), (byte) packet.getContainerId(), packet.getContainerType(), Vector3i.ZERO, packet.getTraderUniqueEntityId()); - } catch (Exception ignored) {}}); + } catch (Exception ignored) { + } + }); } if (event.getPacket() instanceof InventorySlotPacket packet) { - player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.sendLatencyStack(() -> { // Bundle should be handled separately. if (packet.getContainerId() == 125) { final ItemCache cache; @@ -189,7 +190,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { if (event.getPacket() instanceof InventoryContentPacket packet) { player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.sendLatencyStack(() -> { // Bundle should be handled separately. if (packet.getContainerId() == 125) { final ItemCache cache; @@ -234,8 +235,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { final int slot = packet.getSelectedHotbarSlot(); if (slot >= 0 && slot < 9) { - player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> inventory.heldItemSlot = slot); + player.sendLatencyStack(() -> inventory.heldItemSlot = slot); } } } diff --git a/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java b/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java index 00d8bda7..9c6c63d8 100644 --- a/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java @@ -38,7 +38,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { packet.setTick(Integer.MIN_VALUE); player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.sendLatencyStack(() -> { if (player.glideBoostTicks == 0 && packet.getDuration() == 0 || packet.getDuration() == Integer.MAX_VALUE) { player.glideBoostTicks = 1; return; diff --git a/src/main/java/ac/boar/anticheat/packets/server/ServerChunkPackets.java b/src/main/java/ac/boar/anticheat/packets/server/ServerChunkPackets.java index c74d3de8..ae8a4ca7 100644 --- a/src/main/java/ac/boar/anticheat/packets/server/ServerChunkPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/server/ServerChunkPackets.java @@ -33,9 +33,7 @@ public void onPacketSend(CloudburstPacketEvent event) { final CompensatedWorld world = player.compensatedWorld; if (event.getPacket() instanceof NetworkChunkPublisherUpdatePacket packet) { - player.sendLatencyStack(); - - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.sendLatencyStack(() -> { world.setCenterX(packet.getPosition().getX() >> 4); world.setCenterZ(packet.getPosition().getZ() >> 4); world.setRadius(packet.getRadius()); @@ -85,7 +83,7 @@ public void onPacketSend(CloudburstPacketEvent event) { buf.release(); } - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.getLatencyUtil().queue(() -> { if (!player.compensatedWorld.isInLoadDistance(packet.getChunkX(), packet.getChunkZ()) || dimension != player.compensatedWorld.getDimension()) { // System.out.println("Out of distance..."); return; @@ -116,12 +114,12 @@ public void onPacketSend(CloudburstPacketEvent event) { player.sendLatencyStack(); } - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> world.updateBlock(packet.getBlockPosition(), packet.getDataLayer(), packet.getDefinition().getRuntimeId())); + player.getLatencyUtil().queue(() -> world.updateBlock(packet.getBlockPosition(), packet.getDataLayer(), packet.getDefinition().getRuntimeId())); } if (event.getPacket() instanceof BlockEntityDataPacket packet) { player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.getLatencyUtil().queue(() -> { final BoarChunk chunk = player.compensatedWorld.getChunk(packet.getBlockPosition().getX() >> 4, packet.getBlockPosition().getZ() >> 4); if (chunk == null) { return; diff --git a/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java b/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java index a82ee9c7..98574a26 100644 --- a/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java @@ -39,13 +39,11 @@ public void onPacketSend(final CloudburstPacketEvent event) { start.setRewindHistorySize(Boar.getConfig().rewindHistory()); player.serverBreakBlockValidator = new ServerBreakBlockValidator(player); - player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.gameType = start.getPlayerGameType()); + player.sendLatencyStack(() -> player.gameType = start.getPlayerGameType()); } if (event.getPacket() instanceof SetPlayerGameTypePacket packet) { - player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> player.gameType = GameType.from(packet.getGamemode())); + player.sendLatencyStack(() -> player.gameType = GameType.from(packet.getGamemode())); } if (event.getPacket() instanceof UpdateAbilitiesPacket packet) { @@ -53,15 +51,14 @@ public void onPacketSend(final CloudburstPacketEvent event) { return; } - event.getPostTasks().add(() -> player.sendLatencyStack()); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get() + 1, () -> { + event.getPostTasks().add(() -> player.sendLatencyStack(() -> { player.abilities.clear(); for (AbilityLayer layer : packet.getAbilityLayers()) { player.abilities.addAll(layer.getAbilityValues()); } player.getFlagTracker().setFlying(player.abilities.contains(Ability.FLYING) || player.abilities.contains(Ability.MAY_FLY) && player.getFlagTracker().isFlying()); - }); + })); } if (event.getPacket() instanceof SetEntityDataPacket packet) { @@ -73,7 +70,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { // No need to send latency, we only use a few's metadata values from them and most of them almost never actually changed so we should be good, // for eg: (COLLIDEABLE flag is always true for certain entity regardless of what). - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> cache.setMetadata(packet.getMetadata())); + player.getLatencyUtil().queue(() -> cache.setMetadata(packet.getMetadata())); return; } @@ -106,7 +103,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { final long id = player.sentStackId.get(); player.desyncedFlag.set(flagsCopy != null ? id : -1); - player.getLatencyUtil().addTaskToQueue(id, () -> { + player.getLatencyUtil().queue(() -> { if (flagsCopy != null) { player.getFlagTracker().set(player, flagsCopy); } @@ -149,8 +146,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { return; } - player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.sendLatencyStack(() -> { if (player.vehicleData != null) { return; } diff --git a/src/main/java/ac/boar/anticheat/packets/server/ServerEntityPackets.java b/src/main/java/ac/boar/anticheat/packets/server/ServerEntityPackets.java index 9b1d59ab..c34ddb37 100644 --- a/src/main/java/ac/boar/anticheat/packets/server/ServerEntityPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/server/ServerEntityPackets.java @@ -16,7 +16,7 @@ public void onPacketSend(final CloudburstPacketEvent event) { final BoarPlayer player = event.getPlayer(); if (event.getPacket() instanceof RemoveEntityPacket packet) { player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { + player.sendLatencyStack(() -> { if (player.vehicleData != null && player.vehicleData.vehicleRuntimeId == packet.getUniqueEntityId()) { player.vehicleData = null; } @@ -119,17 +119,7 @@ private void queuePositionUpdate(final CloudburstPacketEvent event, final Entity // But if player respond to the transaction AFTER the position packet they 100% already receive the packet. player.sendLatencyStack(); - final long id = player.sentStackId.get(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> { - entity.interpolate(position, lerp && distance < 4096); - // Bukkit.broadcastMessage("Player received position=" + position + ", id=" + id); - }); - - // Bukkit.broadcastMessage("New position=" + position + ", id=" + player.sentStackId.get()); - - event.getPostTasks().add(() -> { - player.sendLatencyStack(); - player.getLatencyUtil().addTaskToQueue(player.sentStackId.get(), () -> entity.setPast(null)); - }); + player.getLatencyUtil().queue(() -> entity.interpolate(position, lerp && distance < 4096)); + event.getPostTasks().add(() -> player.sendLatencyStack(() -> entity.setPast(null))); } } \ No newline at end of file diff --git a/src/main/java/ac/boar/anticheat/player/BoarPlayer.java b/src/main/java/ac/boar/anticheat/player/BoarPlayer.java index f1106136..53073835 100644 --- a/src/main/java/ac/boar/anticheat/player/BoarPlayer.java +++ b/src/main/java/ac/boar/anticheat/player/BoarPlayer.java @@ -13,6 +13,7 @@ import ac.boar.anticheat.validator.blockbreak.ServerBreakBlockValidator; import ac.boar.geyser.util.GeyserUtil; import ac.boar.mappings.BlockMappings; +import ac.boar.protocol.BoarHandlerAdaptor; import lombok.Getter; import ac.boar.anticheat.check.api.holder.CheckHolder; @@ -31,6 +32,7 @@ import org.cloudburstmc.protocol.bedrock.data.Ability; import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.netty.BedrockPacketWrapper; import org.cloudburstmc.protocol.bedrock.packet.NetworkStackLatencyPacket; import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.entity.EntityDefinitions; @@ -45,6 +47,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicLong; public final class BoarPlayer extends PlayerData { @@ -52,7 +55,7 @@ public final class BoarPlayer extends PlayerData { private final GeyserSession session; @Getter @Setter - private BedrockServerSession cloudburstDownstream; + private BedrockServerSession bedrockSession; public RakSessionCodec rakSessionCodec; public long runtimeEntityId; @@ -65,7 +68,6 @@ public final class BoarPlayer extends PlayerData { @Getter private final LatencyUtil latencyUtil = new LatencyUtil(this); - public final AtomicLong receivedStackId = new AtomicLong(-1), sentStackId = new AtomicLong(0); // Lag compensation public final CompensatedWorldImpl compensatedWorld = new CompensatedWorldImpl(this); @@ -108,54 +110,24 @@ public boolean isClosed() { return this.session.isClosed() || this.session.getUpstream().isClosed(); } - public void sendLatencyStack() { - this.sendLatencyStack(false); + public void sendLatencyStack(Runnable runnable) { + sendLatencyStack(); + this.latencyUtil.queue(runnable); } - public void sendLatencyStack(boolean immediate) { - if (doTimeOut()) { - this.kick("Timed out."); - return; - } -// System.out.println("Send latency: " + System.currentTimeMillis()); - - long id = this.sentStackId.incrementAndGet(); - if (id == -GeyserUtil.MAGIC_FORM_IMAGE_HACK_TIMESTAMP || id == -GeyserUtil.MAGIC_VIRTUAL_INVENTORY_HACK) { - id = this.sentStackId.incrementAndGet(); - } + public void sendLatencyStack() { + long id = ThreadLocalRandom.current().nextLong(-5000000L, 5000000L); // We have to send negative values since geyser translate positive one. final NetworkStackLatencyPacket latencyPacket = new NetworkStackLatencyPacket(); - latencyPacket.setTimestamp(-id); + latencyPacket.setTimestamp(id); latencyPacket.setFromServer(true); - this.latencyUtil.addLatencyToQueue(id); - - if (immediate) { - this.getSession().sendUpstreamPacketImmediately(latencyPacket); - } else { - this.getSession().sendUpstreamPacket(latencyPacket); - } - -// System.out.println("Sent: " + System.currentTimeMillis()); - } - - private boolean doTimeOut() { - if (this.sentStackId.get() - this.receivedStackId.get() < 5) { - return false; - } - - if (this.latencyUtil.getNextSentTime() == this.latencyUtil.getLastSentTime()) { -// System.out.println("The same, skip!"); - return false; - } - - long latencyFault = Math.max(0, this.latencyUtil.getNextSentTime().ms() - this.latencyUtil.getLastSentTime().ms()); - long distance = System.currentTimeMillis() - this.latencyUtil.getLastRespondTime(); - distance -= latencyFault; + this.bedrockSession.getPeer().getChannel().pipeline().context(BoarHandlerAdaptor.NAME).writeAndFlush( + BedrockPacketWrapper.create(0, 0, 0, latencyPacket, null) + ); -// System.out.println("Dist=" + distance + ", sentDis=" + latencyFault); - return distance >= Boar.getConfig().maxLatencyWait(); + this.latencyUtil.queue(id, true); } public boolean isMovementExempted() { diff --git a/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java b/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java index 16439033..c980a19a 100644 --- a/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java +++ b/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java @@ -57,7 +57,7 @@ public void teleportTo(final TeleportCache cache) { packet.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); this.queueTeleport(teleport.getPosition()); - this.player.getCloudburstDownstream().sendPacket(packet); + this.player.getBedrockSession().sendPacket(packet); } public void queueTeleport(final Vec3 position) { @@ -94,7 +94,7 @@ public void rewind(final RewindData rewind) { player.sendLatencyStack(); this.queuedTeleports.add(new TeleportCache.Rewind(player.sentStackId.get(), tick, new Vec3(packet.getPosition()), new Vec3(packet.getDelta()), onGround)); - this.player.getCloudburstDownstream().sendPacket(packet); + this.player.getBedrockSession().sendPacket(packet); } public void cachePosition(long tick, Vector3f position) { diff --git a/src/main/java/ac/boar/anticheat/util/LatencyUtil.java b/src/main/java/ac/boar/anticheat/util/LatencyUtil.java index 523e538c..169f6cd0 100644 --- a/src/main/java/ac/boar/anticheat/util/LatencyUtil.java +++ b/src/main/java/ac/boar/anticheat/util/LatencyUtil.java @@ -1,193 +1,75 @@ package ac.boar.anticheat.util; -import ac.boar.anticheat.Boar; import ac.boar.anticheat.check.api.Check; import ac.boar.anticheat.check.api.impl.PingBasedCheck; import ac.boar.anticheat.player.BoarPlayer; -import lombok.Getter; +import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; +import lombok.ToString; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedDeque; @RequiredArgsConstructor public final class LatencyUtil { private final BoarPlayer player; - private final Queue sentQueue = new ConcurrentLinkedQueue<>(); - private final Map idToSentTime = new ConcurrentHashMap<>(); - private final Map> idToTasks = new ConcurrentHashMap<>(); - // Give the player a bit of a leniency for their first latency. - @Getter - private long lastRespondTime = System.currentTimeMillis() + Boar.getConfig().maxLatencyWait(); - @Getter - private long prevSentTime = System.currentTimeMillis(); - private Time prevReceivedSentTime = new Time(-1, -1), nextRecivedSentTime = new Time(-1, -1); - - public Time getLastSentTime() { - return this.prevReceivedSentTime; - } - public Time getNextSentTime() { - return this.nextRecivedSentTime; - } - - public boolean hasId(long id) { - return this.sentQueue.contains(id); + private final Deque sentQueue = new ConcurrentLinkedDeque<>(); + public Deque sentQueue() { + return this.sentQueue; } - public void addLatencyToQueue(long id) { - final Time time = new Time(System.currentTimeMillis(), System.nanoTime()); - this.sentQueue.add(id); - this.idToSentTime.put(id, time); - onLatencySend(); - - this.prevSentTime = System.currentTimeMillis(); - if (this.prevReceivedSentTime == this.nextRecivedSentTime) { - this.nextRecivedSentTime = time; - } + public void queue(long id, boolean ours) { + this.sentQueue.add(new Latency(id, System.currentTimeMillis(), System.nanoTime(), ours, ours ? new ArrayList<>() : null)); } - public void addTaskToQueue(long id, Runnable runnable) { - if (id <= player.receivedStackId.get()) { + public void queue(Runnable runnable) { + if (this.sentQueue.isEmpty()) { runnable.run(); return; } - this.idToTasks.computeIfAbsent(id, k -> new ArrayList<>()).add(runnable); - } - - public void confirmByTime(long time) { - if (time < this.prevReceivedSentTime.ms()) { - return; - } - - long lastId = -1; - while (true) { - Long next = this.sentQueue.peek(); - if (next == null) { - break; - } - - final Time sentTime = this.idToSentTime.get(next); - if (sentTime.ms() > time) { - break; - } - - this.idToSentTime.remove(next); - if (sentTime.ms() > this.prevReceivedSentTime.ms()) { - this.prevReceivedSentTime = sentTime; - } - - final List tasks = this.idToTasks.remove(next); - if (tasks != null) { - tasks.forEach(Runnable::run); - } - - onLatencyAccepted(next, sentTime); - - this.sentQueue.poll(); - lastId = next; - } - - if (lastId == -1 || lastId < player.receivedStackId.get()) { - return; - } - - this.nextRecivedSentTime = this.prevReceivedSentTime; - - final Long next = this.sentQueue.peek(); - if (next != null) { - final Time sentTime = this.idToSentTime.get(next); - if (sentTime != null) { - this.nextRecivedSentTime = sentTime; - } - } - - // This prevents cheaters from answering to ONE latency every X seconds (depends on the config) to prevent timed out. - // Instead, this will force the cheater to answer to the latest latency that have sent time distance to the current time by X seconds - long distance = System.currentTimeMillis() - this.prevReceivedSentTime.ms(); - if (distance >= Boar.getConfig().maxLatencyWait()) { - player.kick("Timed out."); - return; + if (this.sentQueue.getLast().tasks == null) { + this.sentQueue.getLast().tasks = new ArrayList<>(); } - this.lastRespondTime = System.currentTimeMillis(); - player.receivedStackId.set(lastId); + this.sentQueue.getLast().tasks.add(runnable); } - public void confirmStackId(long id) { - if (!hasId(id) || id <= player.receivedStackId.get()) { - return; - } - - while (true) { - Long next = this.sentQueue.peek(); - if (next == null || next > id) { - break; - } - - final List tasks = this.idToTasks.remove(next); - if (tasks != null) { - tasks.forEach(Runnable::run); - } - - final Time sentTime = this.idToSentTime.remove(next); - if (sentTime != null) { - if (sentTime.ms() > this.prevReceivedSentTime.ms()) { - this.prevReceivedSentTime = sentTime; - } - - onLatencyAccepted(next, sentTime); + public void onLatencyAccepted(Latency latency) { + for (final Check check : this.player.getCheckHolder().values()) { + if (!(check instanceof PingBasedCheck pingBasedCheck)) { + continue; } - this.sentQueue.poll(); + pingBasedCheck.onLatencyAccepted(latency); } + } - this.nextRecivedSentTime = this.prevReceivedSentTime; - - final Long next = this.sentQueue.peek(); - if (next != null) { - final Time sentTime = this.idToSentTime.get(next); - if (sentTime != null) { - this.nextRecivedSentTime = sentTime; - } + @ToString + @AllArgsConstructor + public static final class Latency { + private final long id; + private final long ms; + private final long ns; + private boolean ours; + private List tasks; + + public boolean ours() { + return this.ours; } - // This prevents cheaters from answering to ONE latency every X seconds (depends on the config) to prevent timed out. - // Instead, this will force the cheater to answer to the latest latency that have sent time distance to the current time by X seconds - long distance = System.currentTimeMillis() - this.prevReceivedSentTime.ms(); - if (distance >= Boar.getConfig().maxLatencyWait()) { - player.kick("Timed out."); - return; + public long id() { + return this.id; } - this.lastRespondTime = System.currentTimeMillis(); - player.receivedStackId.set(id); - } - - private void onLatencySend() { - for (final Check check : this.player.getCheckHolder().values()) { - if (!(check instanceof PingBasedCheck pingBasedCheck)) { - continue; - } - - pingBasedCheck.onLatencySend(player.sentStackId.get()); + public long ns() { + return this.ns; } - } - private void onLatencyAccepted(long id, Time time) { - for (final Check check : this.player.getCheckHolder().values()) { - if (!(check instanceof PingBasedCheck pingBasedCheck)) { - continue; + public void run() { + if (this.tasks != null) { + this.tasks.forEach(Runnable::run); } - - pingBasedCheck.onLatencyAccepted(id, time); } } - - public record Time(long ms, long ns) { - } } diff --git a/src/main/java/ac/boar/geyser/util/GeyserUtil.java b/src/main/java/ac/boar/geyser/util/GeyserUtil.java index ee59eac9..df5561cb 100644 --- a/src/main/java/ac/boar/geyser/util/GeyserUtil.java +++ b/src/main/java/ac/boar/geyser/util/GeyserUtil.java @@ -17,7 +17,7 @@ public class GeyserUtil { public static void hook(final BoarPlayer player) { try { BedrockServerSession session = findCloudburstSession(player.getSession()); - player.setCloudburstDownstream(session); + player.setBedrockSession(session); final Channel channel = session.getPeer().getChannel(); channel.pipeline().addAfter(BedrockPacketCodec.NAME, BoarHandlerAdaptor.NAME, new BoarHandlerAdaptor(player, (BedrockPacketCodec) channel.pipeline().get(BedrockPacketCodec.NAME))); } catch (Exception ignored) { diff --git a/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java b/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java index d7326f12..80df2fc2 100644 --- a/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java +++ b/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java @@ -50,7 +50,7 @@ protected void encode(ChannelHandlerContext ctx, BedrockPacketWrapper msg, List< buf.release(); } -// System.out.println(event.getPacket().getPacketType()); + event.getPostTasks().forEach(Runnable::run); } @Override @@ -69,6 +69,8 @@ protected void decode(ChannelHandlerContext ctx, BedrockPacketWrapper msg, List< msg.setPacket(event.getPacket()); out.add(msg.retain()); + + event.getPostTasks().forEach(Runnable::run); } } From 8c94178966e5b7a66a4a07f9ca3bbfed7a404560 Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Sun, 8 Feb 2026 18:49:18 +0700 Subject: [PATCH 4/9] Change the way we handle uncertain velocity. --- .../anticheat/data/input/VelocityData.java | 6 -- .../packets/input/AuthInputPackets.java | 13 +---- .../input/legacy/LegacyAuthInputPackets.java | 15 ----- .../packets/player/PlayerVelocityPackets.java | 13 +++-- .../anticheat/player/data/PlayerData.java | 4 +- .../prediction/PredictionRunner.java | 56 +++++-------------- .../prediction/engine/data/Vector.java | 2 + 7 files changed, 29 insertions(+), 80 deletions(-) delete mode 100644 src/main/java/ac/boar/anticheat/data/input/VelocityData.java diff --git a/src/main/java/ac/boar/anticheat/data/input/VelocityData.java b/src/main/java/ac/boar/anticheat/data/input/VelocityData.java deleted file mode 100644 index 31203618..00000000 --- a/src/main/java/ac/boar/anticheat/data/input/VelocityData.java +++ /dev/null @@ -1,6 +0,0 @@ -package ac.boar.anticheat.data.input; - -import ac.boar.anticheat.util.math.Vec3; - -public record VelocityData(long stackId, long tick, Vec3 velocity) { -} diff --git a/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java b/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java index 63647d96..e4e3c6d4 100644 --- a/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java @@ -3,7 +3,6 @@ import ac.boar.anticheat.check.impl.reach.Reach; import ac.boar.anticheat.check.impl.timer.Timer; import ac.boar.anticheat.data.input.PredictionData; -import ac.boar.anticheat.data.input.VelocityData; import ac.boar.anticheat.packets.input.legacy.LegacyAuthInputPackets; import ac.boar.anticheat.packets.input.teleport.TeleportHandler; import ac.boar.anticheat.player.BoarPlayer; @@ -92,16 +91,7 @@ public void onPacketReceived(final CloudburstPacketEvent event) { player.setPos(player.unvalidatedPosition); // Clear velocity out manually since we haven't handled em. - Iterator> iterator = player.queuedVelocities.entrySet().iterator(); - - Map.Entry entry; - while (iterator.hasNext() && (entry = iterator.next()) != null) { - if (entry.getKey() >= player.receivedStackId.get()) { - break; - } else { - iterator.remove(); - } - } + player.certainVelocity = null; // This is fine, we only need tick end and use before and after to calculate ground. player.predictionResult = new PredictionData(Vec3.ZERO, player.velocity.y < 0 && player.getInputData().contains(PlayerAuthInputData.VERTICAL_COLLISION) ? new Vec3(0, 1, 0) : Vec3.ZERO, player.unvalidatedTickEnd); @@ -149,7 +139,6 @@ public void onPacketSend(CloudburstPacketEvent event) { player.compensatedWorld.getChunks().clear(); player.compensatedWorld.setDimension(dimension); - System.out.println("Set dimension!"); player.getFlagTracker().clear(); player.getFlagTracker().flying(false); diff --git a/src/main/java/ac/boar/anticheat/packets/input/legacy/LegacyAuthInputPackets.java b/src/main/java/ac/boar/anticheat/packets/input/legacy/LegacyAuthInputPackets.java index cca6f9c4..0c4abb9e 100644 --- a/src/main/java/ac/boar/anticheat/packets/input/legacy/LegacyAuthInputPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/input/legacy/LegacyAuthInputPackets.java @@ -4,7 +4,6 @@ import ac.boar.anticheat.check.api.impl.OffsetHandlerCheck; import ac.boar.anticheat.compensated.cache.container.ContainerCache; import ac.boar.anticheat.data.ItemUseTracker; -import ac.boar.anticheat.data.input.VelocityData; import ac.boar.anticheat.player.BoarPlayer; import ac.boar.anticheat.prediction.UncertainRunner; import ac.boar.anticheat.prediction.engine.data.VectorType; @@ -59,20 +58,6 @@ public static void doPostPrediction(final BoarPlayer player, final PlayerAuthInp player.setPos(player.unvalidatedPosition.clone(), false); } - // Also clear out old velocity. - if (player.bestPossibility.getType() == VectorType.VELOCITY) { - Iterator> iterator = player.queuedVelocities.entrySet().iterator(); - - Map.Entry entry; - while (iterator.hasNext() && (entry = iterator.next()) != null) { - if (entry.getKey() > player.bestPossibility.getStackId()) { - break; - } else { - iterator.remove(); - } - } - } - player.prevPosition = player.position; } diff --git a/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java b/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java index 9c6c63d8..9b15f210 100644 --- a/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/player/PlayerVelocityPackets.java @@ -1,7 +1,8 @@ package ac.boar.anticheat.packets.player; -import ac.boar.anticheat.data.input.VelocityData; import ac.boar.anticheat.player.BoarPlayer; +import ac.boar.anticheat.prediction.engine.data.Vector; +import ac.boar.anticheat.prediction.engine.data.VectorType; import ac.boar.anticheat.util.math.Vec3; import ac.boar.protocol.api.CloudburstPacketEvent; import ac.boar.protocol.api.PacketListener; @@ -23,9 +24,13 @@ public void onPacketSend(final CloudburstPacketEvent event) { // I think there is some rewind like behavior when there is ehm the tick is not 0, so just default back to 0 till I figure it out. packet.setTick(0); - player.sendLatencyStack(); - player.queuedVelocities.put(player.sentStackId.get() + 1, new VelocityData(player.sentStackId.get() + 1, player.tick, new Vec3(packet.getMotion()))); - event.getPostTasks().add(player::sendLatencyStack); + player.sendLatencyStack(() -> player.uncertainVelocity = new Vector(VectorType.VELOCITY, new Vec3(packet.getMotion()))); + event.getPostTasks().add(() -> player.sendLatencyStack(() -> { + if (player.uncertainVelocity != null) { + player.certainVelocity = player.uncertainVelocity; + } + player.uncertainVelocity = null; + })); } if (event.getPacket() instanceof MovementEffectPacket packet) { diff --git a/src/main/java/ac/boar/anticheat/player/data/PlayerData.java b/src/main/java/ac/boar/anticheat/player/data/PlayerData.java index 4c35dda0..44f81fba 100644 --- a/src/main/java/ac/boar/anticheat/player/data/PlayerData.java +++ b/src/main/java/ac/boar/anticheat/player/data/PlayerData.java @@ -4,7 +4,6 @@ import ac.boar.anticheat.compensated.CompensatedInventory; import ac.boar.anticheat.data.*; import ac.boar.anticheat.data.input.PredictionData; -import ac.boar.anticheat.data.input.VelocityData; import ac.boar.anticheat.data.vanilla.AttributeInstance; import ac.boar.anticheat.data.vanilla.StatusEffect; import ac.boar.anticheat.player.data.tracker.FlagTracker; @@ -94,7 +93,8 @@ public StatusEffect getEffect(final Effect effect) { // Movement related, (movement input, player EOT, ...) public Vec3 input = Vec3.ZERO; public Vec3 unvalidatedTickEnd = Vec3.ZERO; - public final Map queuedVelocities = Collections.synchronizedMap(new TreeMap<>()); + + public Vector uncertainVelocity, certainVelocity; // Attribute related, abilities public final Map attributes = new HashMap<>(); diff --git a/src/main/java/ac/boar/anticheat/prediction/PredictionRunner.java b/src/main/java/ac/boar/anticheat/prediction/PredictionRunner.java index e5611193..0137359a 100644 --- a/src/main/java/ac/boar/anticheat/prediction/PredictionRunner.java +++ b/src/main/java/ac/boar/anticheat/prediction/PredictionRunner.java @@ -2,7 +2,6 @@ import ac.boar.anticheat.collision.Collider; import ac.boar.anticheat.data.input.PredictionData; -import ac.boar.anticheat.data.input.VelocityData; import ac.boar.anticheat.player.BoarPlayer; import ac.boar.anticheat.prediction.engine.base.PredictionEngine; import ac.boar.anticheat.prediction.engine.data.Vector; @@ -20,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; @RequiredArgsConstructor public class PredictionRunner { @@ -50,46 +50,15 @@ private boolean findBestTickStartVelocity() { final List possibleVelocities = new ArrayList<>(); - boolean forceVelocity = false; - - VelocityData forcedVelocity = null; - for (final VelocityData data : player.queuedVelocities.values()) { - if (data.stackId() > player.receivedStackId.get()) { - break; - } - - forcedVelocity = data; - forceVelocity = true; - } - - // TODO: Figure out if old velocity affect rewind or not. - if (forceVelocity) { - // Player already accepted the second latency stack, player HAVE to accept this velocity. - possibleVelocities.add(new Vector(VectorType.VELOCITY, forcedVelocity.velocity(), forcedVelocity.stackId())); - } else { - possibleVelocities.add(new Vector(VectorType.NORMAL, player.velocity.clone())); - - // So here is the thing, this implementation is wrong, this could false in cases where the player - // velocity and tick end result in the same motion. Now this is actually quite easy to check for, but - // I'm too lazy to check for that now sooooo, ignore for now, if this were to be implemented, just allow - // velocity to be taken twice if the velocity have the same offset as tick end, not actually an advantage. - - // Find the nearest velocity that player already accept the first latency stack. - VelocityData nearestVelocity = null; - for (final VelocityData data : player.queuedVelocities.values()) { - if ((data.stackId() - 1) > player.receivedStackId.get()) { - break; - } - - // This should only be ONE result, player cannot accept 2 velocity at once since velocity is wrapped between 2 latency stack. - nearestVelocity = data; - } - - if (nearestVelocity != null) { - possibleVelocities.add(new Vector(VectorType.VELOCITY, nearestVelocity.velocity(), nearestVelocity.stackId())); - // System.out.println("nearest velocity!"); + // It is possible to have both "certain" and uncertain velocity. + if (player.uncertainVelocity != null) { + possibleVelocities.add(player.uncertainVelocity); + if (player.uncertainVelocity.isTwice()) { + player.uncertainVelocity = null; } } + possibleVelocities.add(Objects.requireNonNullElseGet(player.certainVelocity, () -> new Vector(VectorType.NORMAL, player.velocity.clone()))); + player.certainVelocity = null; float closetDistance = Float.MAX_VALUE; @@ -124,10 +93,11 @@ private boolean findBestTickStartVelocity() { distance += 1.0E-6f; } - // Do <= to priority velocity over normal last tick in case if both have the same velocity result. - if (distance <= closetDistance) { + if (distance < closetDistance) { closetDistance = distance; player.bestPossibility = possibility; + } else if (distance == closetDistance && possibility == player.uncertainVelocity) { + player.uncertainVelocity.setTwice(true); } } @@ -138,6 +108,10 @@ private boolean findBestTickStartVelocity() { return false; } + if (player.bestPossibility == player.uncertainVelocity && !player.uncertainVelocity.isTwice()) { + player.uncertainVelocity = null; + } + // We can start the ACTUAL prediction now. player.velocity = player.bestPossibility.getVelocity(); return true; diff --git a/src/main/java/ac/boar/anticheat/prediction/engine/data/Vector.java b/src/main/java/ac/boar/anticheat/prediction/engine/data/Vector.java index 6a1704dc..50a98980 100644 --- a/src/main/java/ac/boar/anticheat/prediction/engine/data/Vector.java +++ b/src/main/java/ac/boar/anticheat/prediction/engine/data/Vector.java @@ -13,6 +13,8 @@ public class Vector { private Vec3 velocity; private VectorType type; + private boolean ignored; + private boolean twice; public Vector(final VectorType type, final Vec3 vec3) { this.type = type; From c160a6e5160f8efbb073879a8b2e9ea22be0665e Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Fri, 13 Mar 2026 19:30:14 +0700 Subject: [PATCH 5/9] Fixed stuff, make this build. --- build.gradle | 2 +- .../anticheat/acks/BoarAcknowledgement.java | 50 +++++++++++-------- .../ac/boar/anticheat/check/api/Check.java | 2 +- .../anticheat/check/impl/reach/Reach.java | 6 +-- .../compensated/cache/entity/EntityCache.java | 2 +- .../world/base/CompensatedWorld.java | 2 +- .../boar/anticheat/data/ItemUseTracker.java | 5 ++ .../packets/input/AuthInputPackets.java | 28 ++++++----- .../input/teleport/TeleportHandler.java | 14 ++++-- .../packets/other/NetworkLatencyPackets.java | 2 + .../packets/server/ServerDataPackets.java | 10 ++-- .../prediction/ticker/impl/PlayerTicker.java | 2 +- .../boar/anticheat/teleport/TeleportUtil.java | 16 +++--- .../teleport/data/TeleportCache.java | 18 ++++--- .../ac/boar/anticheat/util/LatencyUtil.java | 6 +++ .../boar/anticheat/util/math/ReachUtil.java | 2 +- .../inventory/ItemTransactionValidator.java | 2 +- src/main/resources/extension.yml | 4 +- src/main/resources/plugin.yml | 1 - src/main/resources/velocity-plugin.json | 0 20 files changed, 101 insertions(+), 73 deletions(-) delete mode 100644 src/main/resources/plugin.yml delete mode 100644 src/main/resources/velocity-plugin.json diff --git a/build.gradle b/build.gradle index dfca7864..84573a52 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } dependencies { - compileOnly("org.geysermc.geyser:core:2.9.3-SNAPSHOT") { + compileOnly("org.geysermc.geyser:core:2.9.4-SNAPSHOT") { exclude group: "com.google.code.gson", module: "gson" } compileOnly("it.unimi.dsi:fastutil:8.5.15") diff --git a/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java b/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java index e5a72831..1151a0cc 100644 --- a/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java +++ b/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java @@ -2,11 +2,13 @@ import ac.boar.anticheat.Boar; import ac.boar.anticheat.player.BoarPlayer; +import ac.boar.anticheat.util.LatencyUtil; import lombok.Getter; import org.cloudburstmc.netty.channel.raknet.packet.RakDatagramPacket; import org.cloudburstmc.netty.handler.codec.raknet.common.RakSessionCodec; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; public class BoarAcknowledgement { @@ -14,26 +16,32 @@ public class BoarAcknowledgement { private static final Map rakSessionToPlayer = new HashMap<>(); public static void handle(final RakSessionCodec codec, final RakDatagramPacket datagram) { -// BoarPlayer player = rakSessionToPlayer.get(codec); -// if (player == null) { -// return; -// } -// -// if (player.isClosed()) { -// return; -// } -// -// if (player.receivedStackId.get() == player.sentStackId.get()) { -// return; -// } -// -// long lastLatency = player.getLatencyUtil().getLastSentTime().ms(); -// -// long distance = datagram.getSendTime() - lastLatency; -// if (distance <= Boar.getConfig().maxAcknowledgementTime() || lastLatency == -1 || player.inLoadingScreen || player.sinceLoadingScreen < 5) { -// return; -// } -// -// player.getLatencyUtil().confirmByTime(datagram.getSendTime()); + BoarPlayer player = rakSessionToPlayer.get(codec); + if (player == null) { + return; + } + + if (player.isClosed()) { + return; + } + + if (player.getLatencyUtil().sentQueue().isEmpty() || player.getLatencyUtil().prevAcceptedLatency == null) { + return; + } + + long lastLatency = player.getLatencyUtil().prevAcceptedLatency.ms(); + + long distance = datagram.getSendTime() - lastLatency; + if (distance <= Boar.getConfig().maxAcknowledgementTime() || lastLatency == -1 || player.inLoadingScreen || player.sinceLoadingScreen < 5) { + return; + } + + for (LatencyUtil.Latency next : player.getLatencyUtil().sentQueue()) { + if (next.ms() > datagram.getSendTime()) { + break; + } + + next.run(); + } } } diff --git a/src/main/java/ac/boar/anticheat/check/api/Check.java b/src/main/java/ac/boar/anticheat/check/api/Check.java index 43a229b3..668ceaf2 100644 --- a/src/main/java/ac/boar/anticheat/check/api/Check.java +++ b/src/main/java/ac/boar/anticheat/check/api/Check.java @@ -47,6 +47,6 @@ public void fail(String verbose) { } protected final String getDisplayName() { - return player.getSession().getPlayerEntity().getDisplayName(); + return player.getSession().getPlayerEntity().getDisplayName(true); } } diff --git a/src/main/java/ac/boar/anticheat/check/impl/reach/Reach.java b/src/main/java/ac/boar/anticheat/check/impl/reach/Reach.java index 9cdf98e6..b600c79e 100644 --- a/src/main/java/ac/boar/anticheat/check/impl/reach/Reach.java +++ b/src/main/java/ac/boar/anticheat/check/impl/reach/Reach.java @@ -94,11 +94,7 @@ public void pollQueuedHits() { float hitDistance = 0; for (Map.Entry, EntityCache> entry : this.queuedHitAttacks.entrySet()) { final EntityCache entity = entry.getValue(); - if (entity == null || entity.getType() != EntityType.PLAYER) { - // Nope, other than player no entity reach can be reliably calculate, due to geyser entity position delay (know bug). - // This weirdly only applied to non-player entity too (yay!) so I can at least somewhat accurately check for reach cheat in PVP - // (https://github.com/GeyserMC/Geyser/issues/5034) and (https://github.com/GeyserMC/Geyser/issues/2520). - // We don't want the player to cheat either, so we handle it silently. + if (entity == null) { continue; } diff --git a/src/main/java/ac/boar/anticheat/compensated/cache/entity/EntityCache.java b/src/main/java/ac/boar/anticheat/compensated/cache/entity/EntityCache.java index a6e0e1c5..d09ed98e 100644 --- a/src/main/java/ac/boar/anticheat/compensated/cache/entity/EntityCache.java +++ b/src/main/java/ac/boar/anticheat/compensated/cache/entity/EntityCache.java @@ -22,7 +22,7 @@ public final class EntityCache { private final BoarPlayer player; private final EntityType type; private final EntityDefinition definition; - private final long stackId, runtimeId; + private final long runtimeId; private EntityDimensions dimensions; private Vec3 serverPosition = Vec3.ZERO; diff --git a/src/main/java/ac/boar/anticheat/compensated/world/base/CompensatedWorld.java b/src/main/java/ac/boar/anticheat/compensated/world/base/CompensatedWorld.java index 507aacee..15936afb 100644 --- a/src/main/java/ac/boar/anticheat/compensated/world/base/CompensatedWorld.java +++ b/src/main/java/ac/boar/anticheat/compensated/world/base/CompensatedWorld.java @@ -62,7 +62,7 @@ public EntityCache addToCache(final BoarPlayer player, final long runtimeId, fin boolean affectedByOffset = definition.entityType() == EntityType.PLAYER || definition.identifier().equalsIgnoreCase("minecraft:boat") || definition.identifier().equalsIgnoreCase("minecraft:chest_boat"); player.sendLatencyStack(); - final EntityCache cache = new EntityCache(player, definition.entityType(), definition, player.sentStackId.get(), runtimeId); + final EntityCache cache = new EntityCache(player, definition.entityType(), definition, runtimeId); cache.setAffectedByOffset(affectedByOffset); // Default back to default bounding box if there ain't anything. cache.setDimensions(EntityDimensions.fixed(definition.width(), definition.height())); diff --git a/src/main/java/ac/boar/anticheat/data/ItemUseTracker.java b/src/main/java/ac/boar/anticheat/data/ItemUseTracker.java index 352fa87d..f62cb22d 100644 --- a/src/main/java/ac/boar/anticheat/data/ItemUseTracker.java +++ b/src/main/java/ac/boar/anticheat/data/ItemUseTracker.java @@ -23,6 +23,11 @@ public enum DirtyUsing { METADATA, INVENTORY_TRANSACTION, NONE } + public boolean isUsingSpear() { + return javaItemId == Items.WOODEN_SPEAR.javaId() || javaItemId == Items.STONE_SPEAR.javaId() + || javaItemId == Items.IRON_SPEAR.javaId() || javaItemId == Items.GOLDEN_SPEAR.javaId() || javaItemId == Items.DIAMOND_SPEAR.javaId() || javaItemId == Items.NETHERITE_SPEAR.javaId(); + } + public void preTick() { if (!player.getFlagTracker().has(EntityFlag.USING_ITEM)) { return; diff --git a/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java b/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java index e4e3c6d4..b6e2c55c 100644 --- a/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/input/AuthInputPackets.java @@ -87,22 +87,24 @@ public void onPacketReceived(final CloudburstPacketEvent event) { return; } - if (player.isMovementExempted()) { - player.setPos(player.unvalidatedPosition); + if (!player.getTeleportUtil().isTeleporting()) { + if (player.isMovementExempted()) { + player.setPos(player.unvalidatedPosition); - // Clear velocity out manually since we haven't handled em. - player.certainVelocity = null; + // Clear velocity out manually since we haven't handled em. + player.certainVelocity = null; - // This is fine, we only need tick end and use before and after to calculate ground. - player.predictionResult = new PredictionData(Vec3.ZERO, player.velocity.y < 0 && player.getInputData().contains(PlayerAuthInputData.VERTICAL_COLLISION) ? new Vec3(0, 1, 0) : Vec3.ZERO, player.unvalidatedTickEnd); - player.velocity = player.unvalidatedTickEnd.clone(); + // This is fine, we only need tick end and use before and after to calculate ground. + player.predictionResult = new PredictionData(Vec3.ZERO, player.velocity.y < 0 && player.getInputData().contains(PlayerAuthInputData.VERTICAL_COLLISION) ? new Vec3(0, 1, 0) : Vec3.ZERO, player.unvalidatedTickEnd); + player.velocity = player.unvalidatedTickEnd.clone(); - player.bestPossibility = Vector.NONE; - } else { - if (!player.inLoadingScreen && player.sinceLoadingScreen >= 2 || player.unvalidatedTickEnd.lengthSquared() > 0) { - new PredictionRunner(player).run(); + player.bestPossibility = Vector.NONE; } else { - player.velocity = Vec3.ZERO.clone(); + if (!player.inLoadingScreen && player.sinceLoadingScreen >= 2 || player.unvalidatedTickEnd.lengthSquared() > 0) { + new PredictionRunner(player).run(); + } else { + player.velocity = Vec3.ZERO.clone(); + } } } @@ -130,7 +132,7 @@ public void onPacketSend(CloudburstPacketEvent event) { final BedrockDimension dimension = DimensionUtil.dimensionFromId(dimensionId); player.sendLatencyStack(); - player.getTeleportUtil().getQueuedTeleports().add(new TeleportCache.DimensionSwitch(player.sentStackId.get(), new Vec3(packet.getPosition().up(EntityDefinitions.PLAYER.offset())))); + player.getTeleportUtil().queue(new TeleportCache.DimensionSwitch(new Vec3(packet.getPosition().up(EntityDefinitions.PLAYER.offset())))); player.getLatencyUtil().queue(() -> { if (player.compensatedWorld.getDimension() != dimension) { player.currentLoadingScreen = packet.getLoadingScreenId(); diff --git a/src/main/java/ac/boar/anticheat/packets/input/teleport/TeleportHandler.java b/src/main/java/ac/boar/anticheat/packets/input/teleport/TeleportHandler.java index 295f1110..80202343 100644 --- a/src/main/java/ac/boar/anticheat/packets/input/teleport/TeleportHandler.java +++ b/src/main/java/ac/boar/anticheat/packets/input/teleport/TeleportHandler.java @@ -25,16 +25,16 @@ protected void processQueuedTeleports(final BoarPlayer player, final PlayerAuthI TeleportCache cache; while ((cache = queuedTeleports.peek()) != null) { - if (player.receivedStackId.get() < cache.getStackId()) { + if (!cache.isAccepted()) { break; } queuedTeleports.poll(); - TeleportCache peek = queuedTeleports.peek(); - if (peek != null && player.receivedStackId.get() < peek.getStackId()) { - continue; - } +// TeleportCache peek = queuedTeleports.peek(); +// if (peek != null && peek.isAccepted()) { +// continue; +// } // Bedrock don't reply to teleport individually using a separate tick packet instead it just simply set its position to // the teleported position and then let us know the *next tick*, so we do the same! @@ -90,6 +90,10 @@ private void processTeleport(final BoarPlayer player, final TeleportCache.Normal // we could check for current offset and if it's close then player might possibility HAVEN'T received the rewind yet? // Just ignore the edge cases for now. private void processRewind(final BoarPlayer player, final TeleportCache.Rewind rewind, final PlayerAuthInputPacket packet) { + if (player.getTeleportUtil().isTeleporting()) { + return; // nah. + } + if (player.isMovementExempted()) { // Fully exempted from rewind teleport. return; } diff --git a/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java b/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java index 383e744f..02781b77 100644 --- a/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java @@ -41,5 +41,7 @@ public void onPacketReceived(final CloudburstPacketEvent event) { player.getLatencyUtil().onLatencyAccepted(poll); event.setCancelled(poll.ours()); + + player.getLatencyUtil().prevAcceptedLatency = poll; } } diff --git a/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java b/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java index 98574a26..cc03176a 100644 --- a/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/server/ServerDataPackets.java @@ -101,8 +101,8 @@ public void onPacketSend(final CloudburstPacketEvent event) { player.sendLatencyStack(); - final long id = player.sentStackId.get(); - player.desyncedFlag.set(flagsCopy != null ? id : -1); +// final long id = player.sentStackId.get(); +// player.desyncedFlag.set(flagsCopy != null ? id : -1); player.getLatencyUtil().queue(() -> { if (flagsCopy != null) { player.getFlagTracker().set(player, flagsCopy); @@ -135,9 +135,9 @@ public void onPacketSend(final CloudburstPacketEvent event) { player.dimensions = player.dimensions.hardScaled(scale); } - if (player.desyncedFlag.get() == id) { - player.desyncedFlag.set(-1); - } +// if (player.desyncedFlag.get() == id) { +// player.desyncedFlag.set(-1); +// } }); } diff --git a/src/main/java/ac/boar/anticheat/prediction/ticker/impl/PlayerTicker.java b/src/main/java/ac/boar/anticheat/prediction/ticker/impl/PlayerTicker.java index 5dee581f..779f582d 100644 --- a/src/main/java/ac/boar/anticheat/prediction/ticker/impl/PlayerTicker.java +++ b/src/main/java/ac/boar/anticheat/prediction/ticker/impl/PlayerTicker.java @@ -20,7 +20,7 @@ public void applyInput() { player.input = player.input.multiply(0.3F); } - if (player.getFlagTracker().has(EntityFlag.USING_ITEM)) { + if (player.getFlagTracker().has(EntityFlag.USING_ITEM) && !player.getItemUseTracker().isUsingSpear()) { player.input = player.input.multiply(0.122499995F); } } diff --git a/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java b/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java index c980a19a..03fc606d 100644 --- a/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java +++ b/src/main/java/ac/boar/anticheat/teleport/TeleportUtil.java @@ -35,7 +35,7 @@ public boolean isTeleporting() { } public void teleportTo(final Vector3f position) { - this.teleportTo(new TeleportCache.Normal(0, new Vec3(position))); + this.teleportTo(new TeleportCache.Normal(new Vec3(position))); } public void teleportTo(final TeleportCache cache) { @@ -56,16 +56,21 @@ public void teleportTo(final TeleportCache cache) { packet.setMode(MovePlayerPacket.Mode.TELEPORT); packet.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); - this.queueTeleport(teleport.getPosition()); this.player.getBedrockSession().sendPacket(packet); } public void queueTeleport(final Vec3 position) { - player.sendLatencyStack(); - this.queuedTeleports.add(new TeleportCache.Normal(player.sentStackId.get(), position)); + queue(new TeleportCache.Normal(position)); this.lastKnowValid = position.toVector3f(); } + public void queue(TeleportCache cache) { + this.queuedTeleports.add(cache); + player.sendLatencyStack(() -> { + cache.setAccepted(true); + }); + } + // Rewind teleport part. private final Map authInputHistory = new ConcurrentSkipListMap<>(); private final Map rewindHistory = new ConcurrentSkipListMap<>(); @@ -92,8 +97,7 @@ public void rewind(final RewindData rewind) { packet.setVehicleRotation(Vector2f.ZERO); packet.setPredictionType(player.vehicleData != null ? PredictionType.VEHICLE : PredictionType.PLAYER); - player.sendLatencyStack(); - this.queuedTeleports.add(new TeleportCache.Rewind(player.sentStackId.get(), tick, new Vec3(packet.getPosition()), new Vec3(packet.getDelta()), onGround)); + queue(new TeleportCache.Rewind(tick, new Vec3(packet.getPosition()), new Vec3(packet.getDelta()), onGround)); this.player.getBedrockSession().sendPacket(packet); } diff --git a/src/main/java/ac/boar/anticheat/teleport/data/TeleportCache.java b/src/main/java/ac/boar/anticheat/teleport/data/TeleportCache.java index 35c5c486..68bbee00 100644 --- a/src/main/java/ac/boar/anticheat/teleport/data/TeleportCache.java +++ b/src/main/java/ac/boar/anticheat/teleport/data/TeleportCache.java @@ -3,26 +3,28 @@ import ac.boar.anticheat.util.math.Vec3; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.ToString; -import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket; @RequiredArgsConstructor @ToString @Getter public class TeleportCache { - private final long stackId; private final Vec3 position; + @Setter + private boolean accepted; + @Getter public static class Normal extends TeleportCache { - public Normal(long stackId, Vec3 position) { - super(stackId, position); + public Normal(Vec3 position) { + super(position); } } public static class DimensionSwitch extends TeleportCache { - public DimensionSwitch(long stackId, Vec3 position) { - super(stackId, position); + public DimensionSwitch(Vec3 position) { + super(position); } } @@ -33,8 +35,8 @@ public static class Rewind extends TeleportCache { private final Vec3 tickEnd; private final boolean onGround; - public Rewind(long stackId, long tick, Vec3 position, Vec3 tickEnd, boolean onGround) { - super(stackId, position); + public Rewind(long tick, Vec3 position, Vec3 tickEnd, boolean onGround) { + super(position); this.tick = tick; this.tickEnd = tickEnd; this.onGround = onGround; diff --git a/src/main/java/ac/boar/anticheat/util/LatencyUtil.java b/src/main/java/ac/boar/anticheat/util/LatencyUtil.java index 169f6cd0..34137e04 100644 --- a/src/main/java/ac/boar/anticheat/util/LatencyUtil.java +++ b/src/main/java/ac/boar/anticheat/util/LatencyUtil.java @@ -14,6 +14,8 @@ public final class LatencyUtil { private final BoarPlayer player; private final Deque sentQueue = new ConcurrentLinkedDeque<>(); + public Latency prevAcceptedLatency; + public Deque sentQueue() { return this.sentQueue; } @@ -66,6 +68,10 @@ public long ns() { return this.ns; } + public long ms() { + return this.ms; + } + public void run() { if (this.tasks != null) { this.tasks.forEach(Runnable::run); diff --git a/src/main/java/ac/boar/anticheat/util/math/ReachUtil.java b/src/main/java/ac/boar/anticheat/util/math/ReachUtil.java index b0d8f5c9..603b3da2 100644 --- a/src/main/java/ac/boar/anticheat/util/math/ReachUtil.java +++ b/src/main/java/ac/boar/anticheat/util/math/ReachUtil.java @@ -37,7 +37,7 @@ public static float calculateReach(final BoarPlayer player, final Pair Date: Fri, 13 Mar 2026 19:45:05 +0700 Subject: [PATCH 6/9] Fixed timed out. --- .../packets/other/NetworkLatencyPackets.java | 1 + .../ac/boar/anticheat/player/BoarPlayer.java | 23 +++++++++++++++++-- .../ac/boar/anticheat/util/LatencyUtil.java | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java b/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java index 02781b77..b84ae90d 100644 --- a/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java +++ b/src/main/java/ac/boar/anticheat/packets/other/NetworkLatencyPackets.java @@ -42,6 +42,7 @@ public void onPacketReceived(final CloudburstPacketEvent event) { player.getLatencyUtil().onLatencyAccepted(poll); event.setCancelled(poll.ours()); + player.getLatencyUtil().prevAcceptedTime = System.currentTimeMillis(); player.getLatencyUtil().prevAcceptedLatency = poll; } } diff --git a/src/main/java/ac/boar/anticheat/player/BoarPlayer.java b/src/main/java/ac/boar/anticheat/player/BoarPlayer.java index 53073835..50d26bf0 100644 --- a/src/main/java/ac/boar/anticheat/player/BoarPlayer.java +++ b/src/main/java/ac/boar/anticheat/player/BoarPlayer.java @@ -11,9 +11,9 @@ import ac.boar.anticheat.util.MathUtil; import ac.boar.anticheat.util.math.Vec3; import ac.boar.anticheat.validator.blockbreak.ServerBreakBlockValidator; -import ac.boar.geyser.util.GeyserUtil; import ac.boar.mappings.BlockMappings; import ac.boar.protocol.BoarHandlerAdaptor; +import io.netty.channel.EventLoop; import lombok.Getter; import ac.boar.anticheat.check.api.holder.CheckHolder; @@ -24,6 +24,7 @@ import ac.boar.anticheat.validator.inventory.ItemTransactionValidator; import ac.boar.anticheat.player.data.PlayerData; import lombok.Setter; +import lombok.SneakyThrows; import org.cloudburstmc.math.GenericMath; import org.cloudburstmc.math.TrigMath; import org.cloudburstmc.math.vector.Vector3i; @@ -44,11 +45,12 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +import java.lang.reflect.Field; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.TimeUnit; public final class BoarPlayer extends PlayerData { @Getter @@ -83,6 +85,7 @@ public final class BoarPlayer extends PlayerData { @Getter private final Map trackedDebugPlayers = new ConcurrentHashMap<>(); + @SneakyThrows public BoarPlayer(GeyserSession session) { this.session = session; @@ -104,6 +107,22 @@ public BoarPlayer(GeyserSession session) { this.attributes.put(identifier, new AttributeInstance(type.getDefaultValue())); } + + + final Field field = GeyserSession.class.getDeclaredField("tickEventLoop"); + field.setAccessible(true); + ((EventLoop)field.get(session)).scheduleAtFixedRate(this::serverTick, 50000000, 50000000, TimeUnit.NANOSECONDS); + } + + public void serverTick() { + if (this.getLatencyUtil().sentQueue().isEmpty()) { + sendLatencyStack(); + return; + } + + if (System.currentTimeMillis() - this.getLatencyUtil().prevAcceptedTime > Boar.getConfig().maxLatencyWait()) { + kick("Timed out!"); + } } public boolean isClosed() { diff --git a/src/main/java/ac/boar/anticheat/util/LatencyUtil.java b/src/main/java/ac/boar/anticheat/util/LatencyUtil.java index 34137e04..6b226705 100644 --- a/src/main/java/ac/boar/anticheat/util/LatencyUtil.java +++ b/src/main/java/ac/boar/anticheat/util/LatencyUtil.java @@ -15,6 +15,7 @@ public final class LatencyUtil { private final BoarPlayer player; private final Deque sentQueue = new ConcurrentLinkedDeque<>(); public Latency prevAcceptedLatency; + public long prevAcceptedTime = System.currentTimeMillis(); public Deque sentQueue() { return this.sentQueue; From 9970cf0220a10c6bb3f4041528801560dbe32d3b Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Fri, 13 Mar 2026 20:04:32 +0700 Subject: [PATCH 7/9] Switch from Groovy to Kotlin. --- .github/FUNDING.yml | 1 - .github/workflows/gradle.yml | 40 -- .gitignore | 8 +- LICENSE | 674 ----------------------- README.md | 46 -- build.gradle | 107 ---- build.gradle.kts | 84 +++ gradle.properties | 0 gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 41 +- gradlew.bat | 35 +- settings.gradle | 7 - settings.gradle.kts | 2 + 14 files changed, 120 insertions(+), 930 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/workflows/gradle.yml delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 gradle.properties delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 7c1e00a8..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -ko_fi: oryxel diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index c9d27145..00000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Publish - -on: - workflow_dispatch: - push: - paths-ignore: - - '.gitignore' - - 'LICENSE' - - 'README.md' - - 'DIFFERENCES_WIKI.md' - - 'gradle.yml' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Change wrapper permissions - if: ${{ github.repository == 'oryxel1/Boar' && github.ref_name == 'master' }} - run: chmod +x ./gradlew - - uses: gradle/actions/wrapper-validation@v4 - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: 17 - - name: Build Project - if: ${{ github.repository == 'oryxel1/Boar' && github.ref_name == 'master' }} - run: ./gradlew shadowJar - - name: Upload Artifacts to GitHub - uses: actions/upload-artifact@v4 - if: ${{ success() && github.repository == 'oryxel1/Boar' && github.ref_name == 'master' }} - with: - name: Boar - path: build/libs/ - if-no-files-found: error - - name: Publish to Modrinth - if: ${{ success() && github.repository == 'oryxel1/Boar' && github.ref_name == 'master' }} - env: - MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - run: ./gradlew modrinth diff --git a/.gitignore b/.gitignore index c4c9b416..ee148d22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -/shelf/ -/workspace.xml -/.idea/ -/.gradle/ -/build/ \ No newline at end of file +.gradle/ +build/ +.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e72bfdda..00000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 1bcc99c0..00000000 --- a/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Boar - -Boar is a POC project that allows you to enable [server-auth-with-rewind](https://github.com/Mojang/bedrock-protocol-docs/blob/main/additional_docs/ConfiguringAntiCheat.md) for -[GeyserMC](https://github.com/GeyserMC/Geyser) project with a few more checks and improvements compare to BDS. - -### Documented differences between Java - Bedrock [here](https://github.com/oryxel1/Boar/blob/master/DIFFERENCES_WIKI.md) with detailed explainations. - -### ⚠️ WARNING: THIS ONLY FOR BEDROCK PLAYER NOT JAVA PLAYER! YOU WILL NEED TO PAIR THIS WITH ANOTHER JAVA ANTICHEAT! -A dedicated (proof of concept) anti cheat for GeyserMC project. -- Warning: No guarantee about performance, lag compatibility, or if I will ever finish this. - -### Features -- I will keep this short: lag compensation, movement simulation (prediction), smooth rewind setback. -- Also, this anticheat is actually a Geyser extension! - -### Current detections list -#### Almost every single movement-related cheats (except vehicle aka boat/horse), including - but not limited to: -- Fly, Jesus, Step, Fast Climb, High Jump (Any type of fly cheats) -- Speed (Any type of speed cheats) -- No Fall (Detected using the fly check, impossible to bypass) -- Velocity (99.99%/100.01% velocity - basically any kind of velocity cheat) -- No Slow -- And the list goes on.... -#### And other additions check aside from movements. -- Reach (> 3 blocks - anything greater than 3 blocks of reach, depends on the config) -- Hitbox (any kind of hitbox expansion, including touch (cheater) player) -- Timer (anything greater than 20 ticks) -- PingSpoof (cannot guarantee a 100% detection, but can silently compensate for some) - -### Problems -- A lot of movement differences (and features) is not implemented. - -### Differences from other Geyser anti-cheat. -#### You can take a look at a list of "other anti-cheats" [here](https://geysermc.org/wiki/geyser/anticheat-compatibility/) -- It's free..... and open-source, which is pretty dang good already. -- Boar is **extremly sensitive** and can detect **EXTREMLY** small movement mismatch, designed based off the vanilla movement code making it *mathematically impossible to bypass*. -- Boar can accurately detect and cancel any hit **beyond 3.0 blocks reach** with minimal falses while not affecting legit player but still accurately cancelling cheaters hit. -- Can effectively detect **pingspoofing** by abusing a certain system in RakNet, and silently compenstating for it, making it harder to use lag-based cheats. -- "Perfectly" account for **client lag and latency lag** without relying on tricks, making it harder to false lagging legits player while still effectively catching cheaters. -- Accurately check for any timer-based check, even if the cheaters only move 1 ticks faster (**1.001x - 1.05x game speed**) boar can still catch and detect it, without affecting lagging players. - -### Credits -- https://github.com/GeyserMC/Geyser -- https://github.com/oomph-ac/oomph (fireworks boosting boost code) -- https://github.com/RaphiMC/ViaBedrock -- https://github.com/Mojang/bedrock-protocol-docs diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 84573a52..00000000 --- a/build.gradle +++ /dev/null @@ -1,107 +0,0 @@ -plugins { - id 'java' - id "com.github.johnrengelman.shadow" version "8.1.1" - id "com.modrinth.minotaur" version "2.+" -} - - -group = 'ac.boar' -version = '2.0' - -repositories { - mavenCentral() - maven { - name = "opencollab-dev" - url = "https://repo.opencollab.dev/main/" - } -} - -dependencies { - compileOnly("org.geysermc.geyser:core:2.9.4-SNAPSHOT") { - exclude group: "com.google.code.gson", module: "gson" - } - compileOnly("it.unimi.dsi:fastutil:8.5.15") - compileOnly("org.projectlombok:lombok:1.18.36") - annotationProcessor("org.projectlombok:lombok:1.18.36") - - compileOnly("com.google.code.gson:gson:2.3.1") - testImplementation("com.google.code.gson:gson:2.3.1") - - implementation("it.unimi.dsi:fastutil:8.5.15") - - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2") - - implementation("net.lenni0451.classtransform:core:1.14.1") - implementation("net.lenni0451:Reflect:1.5.0") -} - -shadowJar { - relocate 'net.lenni0451.classtransform', 'ac.boar.shaded.classtransform' - relocate 'org.objectweb.asm', 'ac.boar.shaded.asm' - relocate 'net.lenni0451.reflect', 'ac.boar.shaded.reflect' - relocate 'it.unimi.dsi.fastutil', 'ac.boar.shaded.fastutil' - relocate 'com.fasterxml.jackson', 'ac.boar.shaded.jackson' - relocate 'org.yaml.snakeyaml', 'ac.boar.shaded.snakeyaml' -} - -def targetJavaVersion = 17 -java { - def javaVersion = JavaVersion.toVersion(targetJavaVersion) - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - if (JavaVersion.current() < javaVersion) { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) - } -} - -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' - - if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { - options.release.set(targetJavaVersion) - } -} - -processResources { - def props = [version: version] - inputs.properties props - filteringCharset 'UTF-8' - filesMatching('plugin.yml') { - expand props - } -} - -modrinth { - token = System.getenv("MODRINTH_TOKEN") - versionName = version + "-" + getCommitHash() - versionNumber = version + "-" + getCommitHash() - changelog = getCommitMessage() - projectId = "boar" - versionType = "alpha" - uploadFile = shadowJar - // Don't comment on this :) - gameVersions = ["1.8","1.8.1","1.8.2","1.8.3","1.8.4","1.8.5","1.8.6","1.8.7","1.8.8","1.8.9","1.9","1.9.1","1.9.2","1.9.3","1.9.4","1.10","1.10.1","1.10.2","1.11","1.11.1","1.11.2","1.12","1.12.1","1.12.2","1.13","1.13.1","1.13.2","1.14","1.14.1","1.14.2","1.14.3","1.14.4","1.15","1.15.1","1.15.2","1.16","1.16.1","1.16.2","1.16.3","1.16.4","1.16.5","1.17","1.17.1","1.18","1.18.1","1.18.2","1.19","1.19.1","1.19.2","1.19.3","1.19.4","1.20","1.20.1","1.20.2","1.20.3","1.20.4","1.20.5","1.20.6","1.21","1.21.1","1.21.2","1.21.3","1.21.4","1.21.5","1.21.6","1.21.7","1.21.8","1.21.9", "1.21.10"]; - loaders = ["geyser"] -} - -def getCommitMessage() { - def stdout = new ByteArrayOutputStream() - exec { - commandLine 'git', 'log', '-1', '--pretty=%B' - standardOutput = stdout - } - return stdout.toString().trim() -} - -// Thanks to https://gist.github.com/JonasGroeger/7620911 :tm: -def getCommitHash() { - def gitFolder = "$projectDir/.git/" - def takeFromHash = 7 - def head = new File(gitFolder + "HEAD").text.split(":") - def isCommit = head.length == 1 - - if(isCommit) return head[0].trim().take(takeFromHash) - - def refHead = new File(gitFolder + head[1].trim()) - refHead.text.trim().take takeFromHash -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..9c35a8f3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,84 @@ +import java.io.ByteArrayOutputStream + +plugins { + id("java") + id("com.gradleup.shadow") version "8.3.0" + id("com.modrinth.minotaur") version "2.+" +} + +group = "ac.boar" +version = "2.0.0" + +repositories { + mavenCentral() + maven("https://repo.opencollab.dev/main/") +} + +dependencies { + compileOnly("org.geysermc.geyser:core:2.9.4-SNAPSHOT") { + exclude(group = "com.google.code.gson", module = "gson") + } + + compileOnly("it.unimi.dsi:fastutil:8.5.15") + compileOnly("org.projectlombok:lombok:1.18.36") + annotationProcessor("org.projectlombok:lombok:1.18.36") + + compileOnly("com.google.code.gson:gson:2.3.1") + testImplementation("com.google.code.gson:gson:2.3.1") + + implementation("it.unimi.dsi:fastutil:8.5.15") + + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2") + + implementation("net.lenni0451.classtransform:core:1.14.1") + implementation("net.lenni0451:Reflect:1.5.0") +} + +tasks { + shadowJar { + archiveFileName = "boar.jar" + + relocate("net.lenni0451.classtransform", "ac.boar.shaded.classtransform") + relocate("org.objectweb.asm", "ac.boar.shaded.asm") + relocate("net.lenni0451.reflect", "ac.boar.shaded.reflect") + relocate("it.unimi.dsi.fastutil", "ac.boar.shaded.fastutil") + relocate("com.fasterxml.jackson", "ac.boar.shaded.jackson") + relocate("org.yaml.snakeyaml", "ac.boar.shaded.snakeyaml") + } +} + +modrinth { + token = System.getenv("MODRINTH_TOKEN") + versionName.set(getCommitHash()) + versionNumber.set(project.version.toString() + "-" + getCommitHash()) + changelog = getCommitMessage() + projectId = "boar" + versionType = "alpha" + uploadFile.set(tasks.getByPath("shadowJar")) + + // Don't comment on this :) + gameVersions = listOf("1.8","1.8.1","1.8.2","1.8.3","1.8.4","1.8.5","1.8.6","1.8.7","1.8.8","1.8.9","1.9","1.9.1","1.9.2","1.9.3","1.9.4","1.10","1.10.1","1.10.2","1.11","1.11.1","1.11.2","1.12","1.12.1","1.12.2","1.13","1.13.1","1.13.2","1.14","1.14.1","1.14.2","1.14.3","1.14.4","1.15","1.15.1","1.15.2","1.16","1.16.1","1.16.2","1.16.3","1.16.4","1.16.5","1.17","1.17.1","1.18","1.18.1","1.18.2","1.19","1.19.1","1.19.2","1.19.3","1.19.4","1.20","1.20.1","1.20.2","1.20.3","1.20.4","1.20.5","1.20.6","1.21","1.21.1","1.21.2","1.21.3","1.21.4","1.21.5","1.21.6","1.21.7","1.21.8","1.21.9", "1.21.10", "1.21.11"); + loaders = listOf("geyser") +} + +fun getCommitMessage(): String { + val stdout = ByteArrayOutputStream() + exec { + commandLine("git", "log", "-1", "--pretty=%B") + standardOutput = stdout + } + return stdout.toString().trim() +} + +// Thanks to https://gist.github.com/JonasGroeger/7620911 :tm: +fun getCommitHash(): String { + val gitFolder = "$projectDir/.git/" + val takeFromHash = 7 + val head = File(gitFolder + "HEAD").readText().split(":") + val isCommit = head.size == 1 + + if(isCommit) return head[0].trim().take(takeFromHash) + + val refHead = File(gitFolder + head[1].trim()) + return refHead.readText().trim(); +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch literal 60756 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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

NnX)EH+Nua)3Y(c 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 /dev/null && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -131,29 +133,22 @@ location of your Java installation." fi else JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,15 +193,11 @@ if "$cygwin" || "$msys" ; then done fi - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -214,12 +205,6 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 25da30db..107acd32 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%"=="" @echo off +@if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,8 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused +if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -41,13 +40,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute +if "%ERRORLEVEL%" == "0" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. goto fail @@ -57,11 +56,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. goto fail @@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd +if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index fb76fcfa..00000000 --- a/settings.gradle +++ /dev/null @@ -1,7 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - } -} - -rootProject.name = 'Boar' \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..925ea4ad --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "boar" + From 4ad5d70d6edd95aff553d76409ec84300a8b192b Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Fri, 13 Mar 2026 20:05:12 +0700 Subject: [PATCH 8/9] Added back github actions and funding. --- .github/FUNDING.yml | 1 + .github/workflows/gradle.yml | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/gradle.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..7c1e00a8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: oryxel diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..c9d27145 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,40 @@ +name: Publish + +on: + workflow_dispatch: + push: + paths-ignore: + - '.gitignore' + - 'LICENSE' + - 'README.md' + - 'DIFFERENCES_WIKI.md' + - 'gradle.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Change wrapper permissions + if: ${{ github.repository == 'oryxel1/Boar' && github.ref_name == 'master' }} + run: chmod +x ./gradlew + - uses: gradle/actions/wrapper-validation@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + - name: Build Project + if: ${{ github.repository == 'oryxel1/Boar' && github.ref_name == 'master' }} + run: ./gradlew shadowJar + - name: Upload Artifacts to GitHub + uses: actions/upload-artifact@v4 + if: ${{ success() && github.repository == 'oryxel1/Boar' && github.ref_name == 'master' }} + with: + name: Boar + path: build/libs/ + if-no-files-found: error + - name: Publish to Modrinth + if: ${{ success() && github.repository == 'oryxel1/Boar' && github.ref_name == 'master' }} + env: + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + run: ./gradlew modrinth From 71502463ea2b9190e59671eb694c5c9a75406c73 Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Fri, 13 Mar 2026 20:42:00 +0700 Subject: [PATCH 9/9] Removed acks, and stuff. --- README.md | 42 +++++++++++++++++ build.gradle.kts | 6 --- .../anticheat/acks/BoarAcknowledgement.java | 47 ------------------- .../java/ac/boar/anticheat/config/Config.java | 7 --- .../boar/anticheat/config/ConfigLoader.java | 1 - .../ac/boar/anticheat/player/BoarPlayer.java | 2 - .../prediction/engine/data/Vector.java | 2 + .../ac/boar/anticheat/util/LatencyUtil.java | 1 + src/main/java/ac/boar/geyser/GeyserBoar.java | 11 ----- .../java/ac/boar/injector/BoarInjector.java | 20 -------- .../GeyserExtensionClassProvider.java | 40 ---------------- .../RakSessionCodecTransformer.java | 31 ------------ .../ac/boar/protocol/BoarHandlerAdaptor.java | 8 ++++ src/main/resources/config.yml | 4 -- 14 files changed, 53 insertions(+), 169 deletions(-) create mode 100644 README.md delete mode 100644 src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java delete mode 100644 src/main/java/ac/boar/injector/BoarInjector.java delete mode 100644 src/main/java/ac/boar/injector/provider/GeyserExtensionClassProvider.java delete mode 100644 src/main/java/ac/boar/injector/transformers/RakSessionCodecTransformer.java diff --git a/README.md b/README.md new file mode 100644 index 00000000..5187be98 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Boar + +Boar is a POC project that allows you to enable [server-auth-with-rewind](https://github.com/Mojang/bedrock-protocol-docs/blob/main/additional_docs/ConfiguringAntiCheat.md) for +[GeyserMC](https://github.com/GeyserMC/Geyser) project with a few more checks and improvements compare to BDS. + +### ⚠️ WARNING: THIS ONLY FOR BEDROCK PLAYER NOT JAVA PLAYER! YOU WILL NEED TO PAIR THIS WITH ANOTHER JAVA ANTICHEAT! +A dedicated (proof of concept) anti cheat for GeyserMC project. +- Warning: No guarantee about performance, lag compatibility, or if I will ever finish this. + +### Features +- I will keep this short: lag compensation, movement simulation (prediction), smooth rewind setback. +- Also, this anticheat is actually a Geyser extension! + +### Current detections list +#### Almost every single movement-related cheats (except vehicle aka boat/horse), including - but not limited to: +- Fly, Jesus, Step, Fast Climb, High Jump (Any type of fly cheats) +- Speed (Any type of speed cheats) +- No Fall (Detected using the fly check, impossible to bypass) +- Velocity (99.99%/100.01% velocity - basically any kind of velocity cheat) +- No Slow +- And the list goes on.... +#### And other additions check aside from movements. +- Reach (> 3 blocks - anything greater than 3 blocks of reach, depends on the config) +- Hitbox (any kind of hitbox expansion, including touch (cheater) player) +- Timer (anything greater than 20 ticks) + +### Problems +- A lot of movement differences (and features) is not implemented. + +### Differences from other Geyser anti-cheat. +#### You can take a look at a list of "other anti-cheats" [here](https://geysermc.org/wiki/geyser/anticheat-compatibility/) +- It's free..... and open-source, which is pretty dang good already. +- Boar is **extremly sensitive** and can detect **EXTREMLY** small movement mismatch, designed based off the vanilla movement code making it *mathematically impossible to bypass*. +- Boar can accurately detect and cancel any hit **beyond 3.0 blocks reach** with minimal falses while not affecting legit player but still accurately cancelling cheaters hit. +- "Perfectly" account for **client lag and latency lag** without relying on tricks, making it harder to false lagging legits player while still effectively catching cheaters. +- Accurately check for any timer-based check, even if the cheaters only move 1 ticks faster (**1.001x - 1.05x game speed**) boar can still catch and detect it, without affecting lagging players. + +### Credits +- https://github.com/GeyserMC/Geyser +- https://github.com/oomph-ac/oomph (fireworks boosting boost code) +- https://github.com/RaphiMC/ViaBedrock +- https://github.com/Mojang/bedrock-protocol-docs diff --git a/build.gradle.kts b/build.gradle.kts index 9c35a8f3..43088887 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,18 +29,12 @@ dependencies { implementation("it.unimi.dsi:fastutil:8.5.15") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2") - - implementation("net.lenni0451.classtransform:core:1.14.1") - implementation("net.lenni0451:Reflect:1.5.0") } tasks { shadowJar { archiveFileName = "boar.jar" - relocate("net.lenni0451.classtransform", "ac.boar.shaded.classtransform") - relocate("org.objectweb.asm", "ac.boar.shaded.asm") - relocate("net.lenni0451.reflect", "ac.boar.shaded.reflect") relocate("it.unimi.dsi.fastutil", "ac.boar.shaded.fastutil") relocate("com.fasterxml.jackson", "ac.boar.shaded.jackson") relocate("org.yaml.snakeyaml", "ac.boar.shaded.snakeyaml") diff --git a/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java b/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java deleted file mode 100644 index 1151a0cc..00000000 --- a/src/main/java/ac/boar/anticheat/acks/BoarAcknowledgement.java +++ /dev/null @@ -1,47 +0,0 @@ -package ac.boar.anticheat.acks; - -import ac.boar.anticheat.Boar; -import ac.boar.anticheat.player.BoarPlayer; -import ac.boar.anticheat.util.LatencyUtil; -import lombok.Getter; -import org.cloudburstmc.netty.channel.raknet.packet.RakDatagramPacket; -import org.cloudburstmc.netty.handler.codec.raknet.common.RakSessionCodec; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -public class BoarAcknowledgement { - @Getter - private static final Map rakSessionToPlayer = new HashMap<>(); - - public static void handle(final RakSessionCodec codec, final RakDatagramPacket datagram) { - BoarPlayer player = rakSessionToPlayer.get(codec); - if (player == null) { - return; - } - - if (player.isClosed()) { - return; - } - - if (player.getLatencyUtil().sentQueue().isEmpty() || player.getLatencyUtil().prevAcceptedLatency == null) { - return; - } - - long lastLatency = player.getLatencyUtil().prevAcceptedLatency.ms(); - - long distance = datagram.getSendTime() - lastLatency; - if (distance <= Boar.getConfig().maxAcknowledgementTime() || lastLatency == -1 || player.inLoadingScreen || player.sinceLoadingScreen < 5) { - return; - } - - for (LatencyUtil.Latency next : player.getLatencyUtil().sentQueue()) { - if (next.ms() > datagram.getSendTime()) { - break; - } - - next.run(); - } - } -} diff --git a/src/main/java/ac/boar/anticheat/config/Config.java b/src/main/java/ac/boar/anticheat/config/Config.java index 5ccd4a3c..a16e4daa 100644 --- a/src/main/java/ac/boar/anticheat/config/Config.java +++ b/src/main/java/ac/boar/anticheat/config/Config.java @@ -31,9 +31,6 @@ public final class Config { @JsonProperty("ignore-ghost-block") @JsonSetter(nulls = Nulls.SKIP) private boolean ignoreGhostBlock; - @JsonProperty("max-acknowledgement-time") - @JsonSetter(nulls = Nulls.SKIP) - private long maxAcknowledgementTime = 500L; @JsonProperty("max-latency-wait") @JsonSetter(nulls = Nulls.SKIP) private long maxLatencyWait = 15000L; @@ -68,10 +65,6 @@ public boolean ignoreGhostBlock() { return ignoreGhostBlock; } - public long maxAcknowledgementTime() { - return maxAcknowledgementTime; - } - public long maxLatencyWait() { return maxLatencyWait; } diff --git a/src/main/java/ac/boar/anticheat/config/ConfigLoader.java b/src/main/java/ac/boar/anticheat/config/ConfigLoader.java index eaee9d26..fb35b87b 100644 --- a/src/main/java/ac/boar/anticheat/config/ConfigLoader.java +++ b/src/main/java/ac/boar/anticheat/config/ConfigLoader.java @@ -68,7 +68,6 @@ private static boolean writeConfigFile(File configFile, Class extensionClass, s = s.replace("ignore-ghost-block: false", "ignore-ghost-block: " + config.ignoreGhostBlock()); s = s.replace("differ-till-alert: 0.0", "differ-till-alert: " + config.alertThreshold()); s = s.replace("debug-mode: false", "debug-mode: " + config.debugMode()); - s = s.replace("max-acknowledgement-time: 500", "max-acknowledgement-time: " + config.maxAcknowledgementTime()); s = s.replace("max-latency-wait: 15000", "max-latency-wait: " + config.maxLatencyWait()); } diff --git a/src/main/java/ac/boar/anticheat/player/BoarPlayer.java b/src/main/java/ac/boar/anticheat/player/BoarPlayer.java index 50d26bf0..a6992810 100644 --- a/src/main/java/ac/boar/anticheat/player/BoarPlayer.java +++ b/src/main/java/ac/boar/anticheat/player/BoarPlayer.java @@ -58,7 +58,6 @@ public final class BoarPlayer extends PlayerData { @Getter @Setter private BedrockServerSession bedrockSession; - public RakSessionCodec rakSessionCodec; public long runtimeEntityId; @@ -137,7 +136,6 @@ public void sendLatencyStack(Runnable runnable) { public void sendLatencyStack() { long id = ThreadLocalRandom.current().nextLong(-5000000L, 5000000L); - // We have to send negative values since geyser translate positive one. final NetworkStackLatencyPacket latencyPacket = new NetworkStackLatencyPacket(); latencyPacket.setTimestamp(id); latencyPacket.setFromServer(true); diff --git a/src/main/java/ac/boar/anticheat/prediction/engine/data/Vector.java b/src/main/java/ac/boar/anticheat/prediction/engine/data/Vector.java index 50a98980..cfa843b0 100644 --- a/src/main/java/ac/boar/anticheat/prediction/engine/data/Vector.java +++ b/src/main/java/ac/boar/anticheat/prediction/engine/data/Vector.java @@ -3,7 +3,9 @@ import ac.boar.anticheat.util.math.Vec3; import lombok.Getter; import lombok.Setter; +import lombok.ToString; +@ToString @Getter @Setter public class Vector { diff --git a/src/main/java/ac/boar/anticheat/util/LatencyUtil.java b/src/main/java/ac/boar/anticheat/util/LatencyUtil.java index 6b226705..273fd3b5 100644 --- a/src/main/java/ac/boar/anticheat/util/LatencyUtil.java +++ b/src/main/java/ac/boar/anticheat/util/LatencyUtil.java @@ -77,6 +77,7 @@ public void run() { if (this.tasks != null) { this.tasks.forEach(Runnable::run); } + this.tasks = null; } } } diff --git a/src/main/java/ac/boar/geyser/GeyserBoar.java b/src/main/java/ac/boar/geyser/GeyserBoar.java index 2fc26dba..889222f7 100644 --- a/src/main/java/ac/boar/geyser/GeyserBoar.java +++ b/src/main/java/ac/boar/geyser/GeyserBoar.java @@ -1,15 +1,11 @@ package ac.boar.geyser; import ac.boar.anticheat.Boar; -import ac.boar.anticheat.acks.BoarAcknowledgement; import ac.boar.anticheat.alert.AlertManager; import ac.boar.anticheat.config.Config; import ac.boar.anticheat.config.ConfigLoader; import ac.boar.anticheat.player.BoarPlayer; -import ac.boar.injector.BoarInjector; import lombok.Getter; -import org.cloudburstmc.netty.channel.raknet.RakChildChannel; -import org.cloudburstmc.netty.handler.codec.raknet.common.RakSessionCodec; import org.geysermc.event.subscribe.Subscribe; import org.geysermc.geyser.api.command.Command; import org.geysermc.geyser.api.command.CommandSource; @@ -37,8 +33,6 @@ public void onSessionJoin(SessionLoginEvent event) { return; } - RakSessionCodec rakSessionCodec = ((RakChildChannel) player.getSession().getUpstream().getSession().getPeer().getChannel()).rakPipeline().get(RakSessionCodec.class); - BoarAcknowledgement.getRakSessionToPlayer().put(player.rakSessionCodec = rakSessionCodec, player); nameToSessions.put(event.connection().bedrockUsername(), (GeyserSession) event.connection()); } @@ -49,7 +43,6 @@ public void onSessionLeave(SessionDisconnectEvent event) { return; } - BoarAcknowledgement.getRakSessionToPlayer().remove(player.rakSessionCodec); nameToSessions.remove(event.connection().bedrockUsername()); } @@ -58,10 +51,6 @@ public void onGeyserPostInitializeEvent(GeyserPostInitializeEvent event) { logger = this.logger(); Boar.getInstance().init(this); - - if (Boar.getConfig().maxAcknowledgementTime() != -1) { - BoarInjector.injectToRak(); - } } @Subscribe diff --git a/src/main/java/ac/boar/injector/BoarInjector.java b/src/main/java/ac/boar/injector/BoarInjector.java deleted file mode 100644 index 72a7ce16..00000000 --- a/src/main/java/ac/boar/injector/BoarInjector.java +++ /dev/null @@ -1,20 +0,0 @@ -package ac.boar.injector; - -import ac.boar.injector.provider.GeyserExtensionClassProvider; -import ac.boar.injector.transformers.RakSessionCodecTransformer; -import net.lenni0451.classtransform.TransformerManager; -import net.lenni0451.reflect.Agents; - -import java.io.IOException; - -public class BoarInjector { - public static void injectToRak() { - TransformerManager transformerManager = new TransformerManager(new GeyserExtensionClassProvider()); - transformerManager.addTransformer(RakSessionCodecTransformer.class.getName()); - try { - transformerManager.hookInstrumentation(Agents.getInstrumentation()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/main/java/ac/boar/injector/provider/GeyserExtensionClassProvider.java b/src/main/java/ac/boar/injector/provider/GeyserExtensionClassProvider.java deleted file mode 100644 index a5c71d34..00000000 --- a/src/main/java/ac/boar/injector/provider/GeyserExtensionClassProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -package ac.boar.injector.provider; - -import net.lenni0451.classtransform.utils.tree.IClassProvider; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.extension.GeyserExtensionLoader; -import org.jetbrains.annotations.NotNull; - -import javax.annotation.Nonnull; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.util.Map; -import java.util.Objects; -import java.util.function.Supplier; - -import static net.lenni0451.classtransform.utils.ASMUtils.slash; - -public class GeyserExtensionClassProvider implements IClassProvider { - @Override - @Nonnull - public byte[] getClass(@NotNull String name) throws ClassNotFoundException { - Class klass = ((GeyserExtensionLoader)GeyserImpl.getInstance().getExtensionManager().extensionLoader()).classByName(name); - - try (InputStream is = klass.getClassLoader().getResourceAsStream(slash(name) + ".class")) { - Objects.requireNonNull(is, "Class input stream is null"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buf = new byte[1024]; - int len; - while ((len = is.read(buf)) > 0) baos.write(buf, 0, len); - return baos.toByteArray(); - } catch (Throwable t) { - throw new ClassNotFoundException(name, t); - } - } - - @Override - @Nonnull - public Map> getAllClasses() { - return Map.of(); - } -} diff --git a/src/main/java/ac/boar/injector/transformers/RakSessionCodecTransformer.java b/src/main/java/ac/boar/injector/transformers/RakSessionCodecTransformer.java deleted file mode 100644 index d71e21d8..00000000 --- a/src/main/java/ac/boar/injector/transformers/RakSessionCodecTransformer.java +++ /dev/null @@ -1,31 +0,0 @@ -package ac.boar.injector.transformers; - -import net.lenni0451.classtransform.annotations.CInline; -import net.lenni0451.classtransform.annotations.CTarget; -import net.lenni0451.classtransform.annotations.CTransformer; -import net.lenni0451.classtransform.annotations.injection.CInject; -import org.cloudburstmc.netty.channel.raknet.packet.RakDatagramPacket; -import org.cloudburstmc.netty.handler.codec.raknet.common.RakSessionCodec; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.extension.ExtensionLoader; -import org.geysermc.geyser.extension.GeyserExtensionLoader; - -@SuppressWarnings("ALL") -@CTransformer(RakSessionCodec.class) -public class RakSessionCodecTransformer { - @CInline - @CInject(method = "onIncomingAck", target = @CTarget("HEAD")) - public void onIncomingAck(RakDatagramPacket datagram, long curTime) { - ExtensionLoader extLoader = GeyserImpl.getInstance().getExtensionManager().extensionLoader(); - if (!(extLoader instanceof GeyserExtensionLoader extensionLoader)) { - return; - } - - try { - Class klass = extensionLoader.classByName("ac.boar.anticheat.acks.BoarAcknowledgement"); - klass.getDeclaredMethod("handle", RakSessionCodec.class, RakDatagramPacket.class).invoke(null, ((Object) this), datagram); - } catch (Exception ignored) { - return; - } - } -} diff --git a/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java b/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java index 80df2fc2..e9dc7c13 100644 --- a/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java +++ b/src/main/java/ac/boar/protocol/BoarHandlerAdaptor.java @@ -22,6 +22,10 @@ public class BoarHandlerAdaptor extends MessageToMessageCodec out) { + if (player.isClosed()) { + return; + } + final CloudburstPacketEvent event = new CloudburstPacketEvent(this.player, msg.getPacket()); try { for (final PacketListener listener : PacketEvents.getApi().getListeners()) { @@ -55,6 +59,10 @@ protected void encode(ChannelHandlerContext ctx, BedrockPacketWrapper msg, List< @Override protected void decode(ChannelHandlerContext ctx, BedrockPacketWrapper msg, List out) { + if (player.isClosed()) { + return; + } + final CloudburstPacketEvent event = new CloudburstPacketEvent(this.player, msg.getPacket()); try { for (final PacketListener listener : PacketEvents.getApi().getListeners()) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index da634912..97171d3e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -32,10 +32,6 @@ disabled-checks: [] # NOTE: This will only take effect if player is STANDING on the block. ignore-ghost-block: false -# How long player can accept acknowledgements but not latency packet before we try to correct them. Shall this be disabled, set this to -1. -# Recommends to turn this on and have the value around 200ms-500ms. -max-acknowledgement-time: 500 - # How long do we wait for player to respond to latency confirmation/acknowledgements till we kicked the player for timed out. # This value shouldn't be too low to account for lagging player and shouldn't be too high so cheaters won't be able to stop responding to latency for too long. max-latency-wait: 15000