From 45c0d0fac266d68fafa29f0e86f2ab7bb301c753 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 17:39:00 +0100 Subject: [PATCH 01/29] feat: change field type "encodedFlags" of Abilities packet to int --- .../sonar/common/fallback/protocol/FallbackPreparer.java | 2 +- .../sonar/common/fallback/protocol/packets/play/Abilities.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index f478d31ec..94dd1cb0a 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -33,7 +33,7 @@ public class FallbackPreparer { // Abilities - public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f); + public final FallbackPacket DEFAULT_ABILITIES = new Abilities(0x00, 0f, 0f); // Chunks public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0); // Finish Configuration diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Abilities.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Abilities.java index 6cb53c769..36768a5f4 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Abilities.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Abilities.java @@ -31,7 +31,7 @@ @NoArgsConstructor @AllArgsConstructor public class Abilities implements FallbackPacket { - private byte encodedFlags; + private int encodedFlags; private float flySpeed, walkSpeed; @Override From 5ba58ac99fdda4dc50654a4a26f4049226747b84 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 20:03:08 +0100 Subject: [PATCH 02/29] feat: add MapData packet feat: add SetSlot packet --- .../sonar/api/config/SonarConfiguration.java | 78 ++++++++++------- .../fallback/FallbackVerificationHandler.java | 8 +- .../protocol/FallbackPacketRegistry.java | 26 ++++++ .../fallback/protocol/FallbackPreparer.java | 5 +- .../common/fallback/protocol/map/MapInfo.java | 32 +++++++ .../protocol/packets/play/MapData.java | 84 ++++++++++++++++++ .../protocol/packets/play/SetSlot.java | 86 +++++++++++++++++++ 7 files changed, 283 insertions(+), 36 deletions(-) create mode 100644 sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java create mode 100644 sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/MapData.java create mode 100644 sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index a58a77e66..2efb10a58 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -98,12 +98,38 @@ public static final class Verification { private Timing timing; @Getter - @RequiredArgsConstructor public enum Timing { ALWAYS, DURING_ATTACK, NEVER } - private boolean checkGravity; + private final Map map = new Map(); + private final Gravity gravity = new Gravity(); + + @Getter + public static final class Map { + private boolean enabled; + } + + @Getter + public static final class Gravity { + private boolean enabled; + private Gamemode gamemode; + private int maxMovementTicks; + private int maxIgnoredTicks; + + @Getter + @RequiredArgsConstructor + public enum Gamemode { + SURVIVAL(0), + CREATIVE(1), + ADVENTURE(2), + // Keep this for backwards compatibility + SPECTATOR(2); + + private final int id; + } + } + private boolean logConnections; private boolean logDuringAttack; private boolean debugXYZPositions; @@ -115,22 +141,7 @@ public enum Timing { private String successLog; private String blacklistLog; - private Gamemode gamemode; - - @Getter - @RequiredArgsConstructor - public enum Gamemode { - SURVIVAL(0), - CREATIVE(1), - ADVENTURE(2), - SPECTATOR(3); - - private final int id; - } - private int maxBrandLength; - private int maxMovementTicks; - private int maxIgnoredTicks; private int maxLoginPackets; private int maxPing; private int readTimeout; @@ -413,17 +424,32 @@ public void load() { + LINE_SEPARATOR + "All predicted motions are precalculated in order to save performance"); generalConfig.getYaml().setComment("verification.checks.gravity.enabled", "Should Sonar check for valid client gravity? (Recommended)"); - verification.checkGravity = generalConfig.getBoolean("verification.checks.gravity.enabled", true); + verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled", true); generalConfig.getYaml().setComment("verification.checks.gravity.max-movement-ticks", "Maximum number of ticks the player has to fall in order to be allowed to hit the platform"); - verification.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks", 8), + verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks", 8), 2, 100); generalConfig.getYaml().setComment("verification.checks.gravity.max-ignored-ticks", "Maximum number of ignored Y movement changes before a player fails verification"); - verification.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), 1, - 128); + verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), + 1, 128); + + generalConfig.getYaml().setComment("verification.checks.gravity.gamemode", + "The gamemode of the player during verification" + + LINE_SEPARATOR + "Possible types: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR" + + LINE_SEPARATOR + "- SURVIVAL: all UI components are visible" + + LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden" + + LINE_SEPARATOR + "- ADVENTURE: all UI components are visible"); + verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf( + generalConfig.getString("verification.checks.gravity.gamemode", Verification.Gravity.Gamemode.ADVENTURE.name()).toUpperCase()); + + generalConfig.getYaml().setComment("verification.checks.map-captcha", + "Make the player type a code from a virtual map in chat after the gravity check"); + generalConfig.getYaml().setComment("verification.checks.map-captcha.enabled", + "Should Sonar make the player pass a captcha?"); + verification.map.enabled = generalConfig.getBoolean("verification.checks.map-captcha.enabled", false); generalConfig.getYaml().setComment("verification.checks.valid-name-regex", "Regex for validating usernames during verification"); @@ -452,16 +478,6 @@ public void load() { "Maximum number of login packets the player has to send in order to be kicked"); verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets", 256), 128, 8192); - generalConfig.getYaml().setComment("verification.gamemode", - "The gamemode of the player during verification" - + LINE_SEPARATOR + "Possible types: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR" - + LINE_SEPARATOR + "- SURVIVAL: all UI components are visible" - + LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden" - + LINE_SEPARATOR + "- ADVENTURE: all UI components are visible" - + LINE_SEPARATOR + "- SPECTATOR: all UI components are hidden (Recommended)"); - verification.gamemode = Verification.Gamemode.valueOf( - generalConfig.getString("verification.gamemode", Verification.Gamemode.SPECTATOR.name()).toUpperCase()); - generalConfig.getYaml().setComment("verification.log-connections", "Should Sonar log new verification attempts?"); verification.logConnections = generalConfig.getBoolean("verification.log-connections", true); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index bbb815bc1..3a2ce777a 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -57,6 +57,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener @Setter private @NotNull State state = State.LOGIN_ACK; private boolean listenForMovements; + private String currentCaptcha; private final SystemTimer login = new SystemTimer(); private static final Random RANDOM = new Random(); @@ -165,7 +166,8 @@ private void sendAbilitiesAndTeleport() { // Teleport the player to the spawn position user.delayedWrite(new PositionLook( SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION, - 0f, 0f, expectedTeleportId, false)); + 0f, -90f, expectedTeleportId, false)); + // Make sure the player escapes the 1.18.2+ "Loading terrain" screen user.delayedWrite(new DefaultSpawnPosition( SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION, 0f)); } @@ -323,7 +325,7 @@ public void handle(final @NotNull FallbackPacket packet) { checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id"); // Checking gravity is disabled, just finish verification - if (!Sonar.get().getConfig().getVerification().isCheckGravity()) { + if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { finish(); return; } @@ -417,7 +419,7 @@ private void handlePositionUpdate(final double x, final double y, final double z // We have to account for this or the player will fail the verification. if (deltaY == 0) { // Check for too many ignored Y ticks - final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getMaxIgnoredTicks(); + final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getGravity().getMaxIgnoredTicks(); checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks"); return; } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java index 74a8cd65e..416e028e9 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java @@ -195,6 +195,32 @@ public enum FallbackPacketRegistry { map(0x4C, MINECRAFT_1_19_3, true), map(0x50, MINECRAFT_1_19_4, true), map(0x52, MINECRAFT_1_20_2, true)); + clientbound.register(MapData.class, MapData::new, + map(0x34, MINECRAFT_1_7_2, true), + map(0x24, MINECRAFT_1_9, true), + map(0x26, MINECRAFT_1_13, true), + map(0x27, MINECRAFT_1_15, true), + map(0x26, MINECRAFT_1_16, true), + map(0x25, MINECRAFT_1_16_2, true), + map(0x27, MINECRAFT_1_17, true), + map(0x24, MINECRAFT_1_19, true), + map(0x26, MINECRAFT_1_19_1, true), + map(0x25, MINECRAFT_1_19_3, true), + map(0x29, MINECRAFT_1_19_4, true), + map(0x2A, MINECRAFT_1_20_2, true)); + clientbound.register(SetSlot.class, SetSlot::new, + map(0x2F, MINECRAFT_1_7_2, true), + map(0x16, MINECRAFT_1_9, true), + map(0x17, MINECRAFT_1_13, true), + map(0x16, MINECRAFT_1_14, true), + map(0x17, MINECRAFT_1_15, true), + map(0x16, MINECRAFT_1_16, true), + map(0x15, MINECRAFT_1_16_2, true), + map(0x16, MINECRAFT_1_17, true), + map(0x13, MINECRAFT_1_19, true), + map(0x12, MINECRAFT_1_19_3, true), + map(0x14, MINECRAFT_1_19_4, true), + map(0x15, MINECRAFT_1_20_2, true)); serverbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index 94dd1cb0a..f189baae2 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -34,6 +34,7 @@ public class FallbackPreparer { // Abilities public final FallbackPacket DEFAULT_ABILITIES = new Abilities(0x00, 0f, 0f); + public final FallbackPacket CAPTCHA_ABILITIES = new Abilities(0x02, 0f, 0f); // Chunks public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0); // Finish Configuration @@ -59,7 +60,7 @@ public class FallbackPreparer { public void prepare() { joinGame = new JoinGame(0, - Sonar.get().getConfig().getVerification().getGamemode().getId(), + Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(), 0, false, 0, @@ -70,7 +71,7 @@ public void prepare() { "minecraft:overworld"); maxFallDistance = 0; - maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks(); + maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks(); preparedCachedYMotions = new double[maxMovementTick + 8]; for (int i = 0; i < preparedCachedYMotions.length; i++) { diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java new file mode 100644 index 000000000..f12475f66 --- /dev/null +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 Sonar Contributors + * + * 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 . + */ + +package xyz.jonesdev.sonar.common.fallback.protocol.map; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public final class MapInfo { + public static final int DIMENSIONS = 128; + public static final int SCALE = DIMENSIONS * DIMENSIONS; + + private final int columns, rows; + private final int x, y; + private final byte[] buffer; +} diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/MapData.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/MapData.java new file mode 100644 index 000000000..a01d443a3 --- /dev/null +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/MapData.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 Sonar Contributors + * + * 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 . + */ + +package xyz.jonesdev.sonar.common.fallback.protocol.packets.play; + +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; +import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion; +import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket; +import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfo; + +import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt; + +@Getter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public final class MapData implements FallbackPacket { + private int scale; + private MapInfo mapInfo; + + @Override + public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) { + writeVarInt(byteBuf, 0); + + final byte[] data = mapInfo.getBuffer(); + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) { + byteBuf.writeShort(data.length + 3); + byteBuf.writeByte(0); + byteBuf.writeByte(mapInfo.getX()); + byteBuf.writeByte(mapInfo.getY()); + + byteBuf.writeBytes(data); + } else { + byteBuf.writeByte(scale); + + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0 + && protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) < 0) { + byteBuf.writeBoolean(false); + } + + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) { + byteBuf.writeBoolean(false); + } + + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) { + byteBuf.writeBoolean(false); + } else { + writeVarInt(byteBuf, 0); + } + + byteBuf.writeByte(mapInfo.getColumns()); + byteBuf.writeByte(mapInfo.getRows()); + byteBuf.writeByte(mapInfo.getX()); + byteBuf.writeByte(mapInfo.getY()); + + writeVarInt(byteBuf, data.length); + byteBuf.writeBytes(data); + } + } + + @Override + public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException(); + } +} diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java new file mode 100644 index 000000000..cdf69d1aa --- /dev/null +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 Sonar Contributors + * + * 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 . + */ + +package xyz.jonesdev.sonar.common.fallback.protocol.packets.play; + +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import net.kyori.adventure.nbt.CompoundBinaryTag; +import org.jetbrains.annotations.NotNull; +import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion; +import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket; + +import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*; +import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag; +import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag; +import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt; + +@Getter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public final class SetSlot implements FallbackPacket { + private int windowId; + private int slot; + private int count; + private int data; + private CompoundBinaryTag compoundBinaryTag; + + @Override + public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException(); + } + + @Override + public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) { + byteBuf.writeByte(windowId); + + if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) { + writeVarInt(byteBuf, 0); + } + + byteBuf.writeShort(slot); + + if (protocolVersion.compareTo(MINECRAFT_1_13_2) >= 0) { + byteBuf.writeBoolean(true); + } + + final int id = 941; + if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) { + byteBuf.writeShort(id); + } else { + writeVarInt(byteBuf, id); + } + byteBuf.writeByte(count); + if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) { + byteBuf.writeShort(data); + } + + if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) { + byteBuf.writeByte(0); //No Nbt + } else { + if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) { + writeNamelessCompoundTag(byteBuf, compoundBinaryTag); + } else { + writeCompoundTag(byteBuf, compoundBinaryTag); + } + } + } +} From 99ce6860f68342122b6bacfa0b443ff543a3ceb4 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 20:25:12 +0100 Subject: [PATCH 03/29] feat: implement map captcha state --- .../fallback/FallbackVerificationHandler.java | 47 +++++++++-- .../common/fallback/protocol/map/MapType.java | 78 +++++++++++++++++++ .../protocol/packets/play/SetSlot.java | 6 +- 3 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapType.java diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 3a2ce777a..7de75d036 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -23,6 +23,7 @@ import lombok.Setter; import lombok.val; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import xyz.jonesdev.sonar.api.Sonar; import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent; import xyz.jonesdev.sonar.api.fallback.FallbackUser; @@ -57,7 +58,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener @Setter private @NotNull State state = State.LOGIN_ACK; private boolean listenForMovements; - private String currentCaptcha; + private @Nullable String currentCaptcha; private final SystemTimer login = new SystemTimer(); private static final Random RANDOM = new Random(); @@ -71,6 +72,8 @@ public enum State { CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION, // PLAY checks TELEPORT, POSITION, + // Captcha + MAP_CAPTCHA, // Done SUCCESS } @@ -183,6 +186,11 @@ private void sendChunkData() { user.delayedWrite(updateSectionBlocks); // Send all packets in one flush user.getChannel().flush(); + // Checking gravity is disabled, just finish verification + if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { + // Switch to captcha state if needed + captchaOrFinish(); + } } private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user, @@ -219,6 +227,8 @@ private static boolean validateClientBrand(final @NotNull FallbackUser use public void handle(final @NotNull FallbackPacket packet) { // The player has already been verified, drop all other packets if (state == State.SUCCESS) return; + // We are expecting a captcha code in chat, drop all other packets + if (state == State.MAP_CAPTCHA) return; // Check if the player is not sending a ton of packets to the server final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick; @@ -324,12 +334,6 @@ public void handle(final @NotNull FallbackPacket packet) { // Check if the transaction ID is valid checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id"); - // Checking gravity is disabled, just finish verification - if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { - finish(); - return; - } - // First, send an Abilities packet to the client to make // sure the player falls even in spectator mode. // Then, teleport the player to the spawn position. @@ -440,7 +444,7 @@ private void handlePositionUpdate(final double x, final double y, final double z // The player is colliding to blocks, finish verification if (ground) { - finish(); + captchaOrFinish(); return; } } else { @@ -479,6 +483,33 @@ private void checkFrame(final boolean condition, final String message) { } } + private void captchaOrFinish() { + if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) { + if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { + // Make sure the player escapes the 1.18.2+ "Loading terrain" screen + user.delayedWrite(new DefaultSpawnPosition( + SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f)); + } + handleMapCaptcha(); + } else { + finish(); + } + } + + private void handleMapCaptcha() { + state = State.MAP_CAPTCHA; + + + // Teleport the player to the position above the platform + user.delayedWrite(new PositionLook( + SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, + 0f, 90f, 0, false)); + // Make sure the player cannot move + user.delayedWrite(CAPTCHA_ABILITIES); + // Send all packets in one flush + user.getChannel().flush(); + } + private void finish() { state = State.SUCCESS; diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapType.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapType.java new file mode 100644 index 000000000..0cc9fd664 --- /dev/null +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapType.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 Sonar Contributors + * + * 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 . + */ + +package xyz.jonesdev.sonar.common.fallback.protocol.map; + +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion; + +import java.util.function.Function; + +@SuppressWarnings("unused") +@RequiredArgsConstructor +public enum MapType { + FILLED_MAP(protocolVersion -> { + // Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/items.json + switch (protocolVersion) { + default: + // 1.7-1.12.2 + return 358; + case MINECRAFT_1_13: + case MINECRAFT_1_13_1: + return 608; + case MINECRAFT_1_13_2: + return 613; + case MINECRAFT_1_14: + case MINECRAFT_1_14_1: + case MINECRAFT_1_14_2: + case MINECRAFT_1_14_3: + case MINECRAFT_1_14_4: + case MINECRAFT_1_15: + case MINECRAFT_1_15_1: + case MINECRAFT_1_15_2: + return 671; + case MINECRAFT_1_16: + case MINECRAFT_1_16_1: + case MINECRAFT_1_16_2: + case MINECRAFT_1_16_3: + case MINECRAFT_1_16_4: + return 733; + case MINECRAFT_1_17: + case MINECRAFT_1_17_1: + case MINECRAFT_1_18: + case MINECRAFT_1_18_2: + return 847; + case MINECRAFT_1_19: + case MINECRAFT_1_19_1: + return 886; + case MINECRAFT_1_19_3: + return 914; + case MINECRAFT_1_19_4: + return 937; + case MINECRAFT_1_20: + case MINECRAFT_1_20_2: + return 941; + } + }); + + private final Function function; + + public int getId(final @NotNull ProtocolVersion protocolVersion) { + return function.apply(protocolVersion); + } +} diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java index cdf69d1aa..143efb6b6 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java @@ -41,6 +41,7 @@ public final class SetSlot implements FallbackPacket { private int slot; private int count; private int data; + private int itemId; private CompoundBinaryTag compoundBinaryTag; @Override @@ -62,11 +63,10 @@ public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersio byteBuf.writeBoolean(true); } - final int id = 941; if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) { - byteBuf.writeShort(id); + byteBuf.writeShort(itemId); } else { - writeVarInt(byteBuf, id); + writeVarInt(byteBuf, itemId); } byteBuf.writeByte(count); if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) { From edf8fd6e77264c7d68ef8fab0bf77824ae7b5965 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 20:39:14 +0100 Subject: [PATCH 04/29] feat: [wip] implement simple map captcha --- .../fallback/FallbackVerificationHandler.java | 15 +++- .../fallback/protocol/FallbackPreparer.java | 22 ++++-- .../common/fallback/protocol/map/MapInfo.java | 1 + .../fallback/protocol/map/MapPreparer.java | 77 +++++++++++++++++++ .../protocol/packets/play/SetSlot.java | 5 ++ 5 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 7de75d036..720db7a0d 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -30,6 +30,9 @@ import xyz.jonesdev.sonar.api.model.VerifiedPlayer; import xyz.jonesdev.sonar.api.timer.SystemTimer; import xyz.jonesdev.sonar.common.fallback.protocol.*; +import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfo; +import xyz.jonesdev.sonar.common.fallback.protocol.map.MapPreparer; +import xyz.jonesdev.sonar.common.fallback.protocol.map.MapType; import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration; import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged; import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*; @@ -58,7 +61,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener @Setter private @NotNull State state = State.LOGIN_ACK; private boolean listenForMovements; - private @Nullable String currentCaptcha; + private @Nullable MapInfo captcha; private final SystemTimer login = new SystemTimer(); private static final Random RANDOM = new Random(); @@ -499,11 +502,15 @@ private void captchaOrFinish() { private void handleMapCaptcha() { state = State.MAP_CAPTCHA; + // Set slot to map + user.delayedWrite(new SetSlot(0, 36, 1, 0, + MapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT)); + + captcha = MapPreparer.getRandomCaptcha(); + user.delayedWrite(new MapData(0, captcha)); // Teleport the player to the position above the platform - user.delayedWrite(new PositionLook( - SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, - 0f, 90f, 0, false)); + user.delayedWrite(CAPTCHA_POSITION); // Make sure the player cannot move user.delayedWrite(CAPTCHA_ABILITIES); // Send all packets in one flush diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index f189baae2..20d5fc41d 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -19,15 +19,14 @@ import lombok.experimental.UtilityClass; import xyz.jonesdev.sonar.api.Sonar; +import xyz.jonesdev.sonar.api.timer.SystemTimer; import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition; import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType; import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock; +import xyz.jonesdev.sonar.common.fallback.protocol.map.MapPreparer; import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration; import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync; -import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities; -import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData; -import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame; -import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks; +import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*; @UtilityClass public class FallbackPreparer { @@ -52,6 +51,10 @@ public class FallbackPreparer { public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed + // Captcha position + public final FallbackPacket CAPTCHA_POSITION = new PositionLook( + SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false); + private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW]; public int maxMovementTick, dynamicSpawnYPosition; @@ -91,13 +94,20 @@ public void prepare() { x + (BLOCKS_PER_ROW / 2), DEFAULT_Y_COLLIDE_POSITION, z + (BLOCKS_PER_ROW / 2), - 0, 0 - ); + 0, 0); CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER); } } // Prepare UpdateSectionBlocks packet updateSectionBlocks = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS); + + // Pre-compute captcha + if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) { + final SystemTimer timer = new SystemTimer(); + Sonar.get().getLogger().info("Pre-computing map captcha answers..."); + MapPreparer.prepare(); + Sonar.get().getLogger().info("Done ({}s)!", timer); + } } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java index f12475f66..427122cb1 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java @@ -26,6 +26,7 @@ public final class MapInfo { public static final int DIMENSIONS = 128; public static final int SCALE = DIMENSIONS * DIMENSIONS; + private final String answer; private final int columns, rows; private final int x, y; private final byte[] buffer; diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java new file mode 100644 index 000000000..0f442da38 --- /dev/null +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 Sonar Contributors + * + * 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 . + */ + +package xyz.jonesdev.sonar.common.fallback.protocol.map; + +import lombok.experimental.UtilityClass; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.Random; + +@UtilityClass +public class MapPreparer { + private final MapInfo[] CACHED = new MapInfo[10000]; + private final Random RANDOM = new Random(); + + public void prepare() { + for (int i = 0; i < CACHED.length; i++) { + // Send map data + final BufferedImage image = new BufferedImage(MapInfo.DIMENSIONS, MapInfo.DIMENSIONS, BufferedImage.TYPE_INT_RGB); + final Graphics2D graphics = image.createGraphics(); + + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setColor(Color.WHITE); + + // Please enter captcha + graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 20)); + graphics.drawString("Your code:", 5, 20); + + // Captcha + graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 43)); + + final Random random = new Random(); + final String possible = "0123456789"; + final char[] captcha = new char[4]; + for (int _i = 0; _i < captcha.length; _i++) { + captcha[_i] = possible.charAt(random.nextInt(possible.length())); + } + final String answer = new String(captcha); + + final FontMetrics fontMetrics = graphics.getFontMetrics(); + final int stringWidth = fontMetrics.stringWidth(answer); + graphics.drawString(answer, image.getWidth() / 2 - stringWidth / 2, image.getHeight() / 2 + 21); + + final byte[] buffer = new byte[MapInfo.SCALE]; + for (int x = 0; x < image.getWidth(); x++) { + for (int y = 0; y < image.getHeight(); y++) { + final int colorIndex = y * image.getWidth() + x; + final int pixel = image.getRGB(x, y); + if (pixel != -16777216) { + buffer[colorIndex] = (byte) 47; + } + } + } + + CACHED[i] = new MapInfo(answer, image.getWidth(), image.getHeight(), 0, 0, buffer); + } + } + + public MapInfo getRandomCaptcha() { + return CACHED[RANDOM.nextInt(CACHED.length)]; + } +} diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java index 143efb6b6..277be6c97 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java @@ -23,6 +23,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; import net.kyori.adventure.nbt.CompoundBinaryTag; +import net.kyori.adventure.nbt.IntBinaryTag; import org.jetbrains.annotations.NotNull; import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion; import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket; @@ -44,6 +45,10 @@ public final class SetSlot implements FallbackPacket { private int itemId; private CompoundBinaryTag compoundBinaryTag; + public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder() + .put("map", IntBinaryTag.intBinaryTag(0)) + .build(); + @Override public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) { throw new UnsupportedOperationException(); From b540cb5f017b254a92bb6837752e34c2512e59f8 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 20:40:35 +0100 Subject: [PATCH 05/29] feat: cache captcha spawn position packet --- .../sonar/common/fallback/FallbackVerificationHandler.java | 3 +-- .../sonar/common/fallback/protocol/FallbackPreparer.java | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 720db7a0d..7fbd9ac55 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -490,8 +490,7 @@ private void captchaOrFinish() { if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) { if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { // Make sure the player escapes the 1.18.2+ "Loading terrain" screen - user.delayedWrite(new DefaultSpawnPosition( - SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f)); + user.delayedWrite(CAPTCHA_SPAWN_POSITION); } handleMapCaptcha(); } else { diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index 20d5fc41d..995d93fe9 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -54,6 +54,8 @@ public class FallbackPreparer { // Captcha position public final FallbackPacket CAPTCHA_POSITION = new PositionLook( SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false); + public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPosition( + SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f); private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW]; From 6412a999a71f18e20f1b8d8f29acaf2533b40213 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 21:08:06 +0100 Subject: [PATCH 06/29] feat: add a chat message when you verify and enter the captcha --- .../sonar/api/config/SonarConfiguration.java | 12 ++++ .../fallback/FallbackVerificationHandler.java | 9 ++- .../protocol/FallbackPacketRegistry.java | 12 ++++ .../fallback/protocol/FallbackPreparer.java | 15 +++- .../fallback/protocol/map/MapPreparer.java | 18 +++-- .../fallback/protocol/packets/play/Chat.java | 71 +++++++++++++++++++ 6 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index 2efb10a58..cc90b05a2 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -108,6 +108,7 @@ public enum Timing { @Getter public static final class Map { private boolean enabled; + private Component enterCodeMessage; } @Getter @@ -116,6 +117,7 @@ public static final class Gravity { private Gamemode gamemode; private int maxMovementTicks; private int maxIgnoredTicks; + private Component youAreBeingChecked; @Getter @RequiredArgsConstructor @@ -899,6 +901,16 @@ public void load() { verification.successLog = formatString(messagesConfig.getString("verification.logs.successful", "%name% has been verified successfully (%time%s!).")); + messagesConfig.getYaml().setComment("verification.welcome", + "Message that is shown to the player when they are being checked for valid gravity"); + verification.gravity.youAreBeingChecked = deserialize(messagesConfig.getString("verification.welcome", + "Please wait a moment for the verification to finish...")); + + messagesConfig.getYaml().setComment("verification.enter-code", + "Message that is shown to the player when they have to enter the answer to the captcha"); + verification.map.enterCodeMessage = deserialize(messagesConfig.getString("verification.enter-code", + "Please enter the code in chat that is displayed on the map:")); + messagesConfig.getYaml().setComment("verification.too-many-players", "Disconnect message that is shown when too many players are verifying at the same time"); verification.tooManyPlayers = deserialize(fromList(messagesConfig.getStringList("verification.too-many-players", diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 7fbd9ac55..59e338013 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -187,12 +187,15 @@ private void sendChunkData() { // Send an UpdateSectionBlocks packet with a platform of blocks // to check if the player collides with the solid platform. user.delayedWrite(updateSectionBlocks); - // Send all packets in one flush - user.getChannel().flush(); // Checking gravity is disabled, just finish verification if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { // Switch to captcha state if needed captchaOrFinish(); + } else { + // Make sure the player knows we are checking them + user.delayedWrite(youAreBeingChecked); + // Send all packets in one flush + user.getChannel().flush(); } } @@ -512,6 +515,8 @@ private void handleMapCaptcha() { user.delayedWrite(CAPTCHA_POSITION); // Make sure the player cannot move user.delayedWrite(CAPTCHA_ABILITIES); + // Make sure the player knows what to do + user.delayedWrite(enterCodeMessage); // Send all packets in one flush user.getChannel().flush(); } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java index 416e028e9..77daa24e7 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java @@ -221,6 +221,18 @@ public enum FallbackPacketRegistry { map(0x12, MINECRAFT_1_19_3, true), map(0x14, MINECRAFT_1_19_4, true), map(0x15, MINECRAFT_1_20_2, true)); + clientbound.register(Chat.class, Chat::new, + map(0x02, MINECRAFT_1_7_2, true), + map(0x0F, MINECRAFT_1_9, true), + map(0x0E, MINECRAFT_1_13, true), + map(0x0F, MINECRAFT_1_15, true), + map(0x0E, MINECRAFT_1_16, true), + map(0x0F, MINECRAFT_1_18_2, true), + map(0x5F, MINECRAFT_1_19, true), + map(0x62, MINECRAFT_1_19_1, true), + map(0x60, MINECRAFT_1_19_3, true), + map(0x64, MINECRAFT_1_19_4, true), + map(0x67, MINECRAFT_1_20_2, true)); serverbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index 995d93fe9..a9abbac8a 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -40,6 +40,9 @@ public class FallbackPreparer { public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration(); // Synchronize Registry public final FallbackPacket REGISTRY_SYNC = new RegistrySync(); + // Chat + public FallbackPacket enterCodeMessage; + public FallbackPacket youAreBeingChecked; // JoinGame public FallbackPacket joinGame; // Update Section Blocks @@ -104,12 +107,20 @@ public void prepare() { // Prepare UpdateSectionBlocks packet updateSectionBlocks = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS); - // Pre-compute captcha + // "You are being checked" message + if (Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { + youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked(), 1); + } + if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) { + // "Enter your code" message + enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCodeMessage(), 1); + final SystemTimer timer = new SystemTimer(); Sonar.get().getLogger().info("Pre-computing map captcha answers..."); + // Pre-compute captcha answers MapPreparer.prepare(); - Sonar.get().getLogger().info("Done ({}s)!", timer); + Sonar.get().getLogger().info("Successfully computed captcha answers in {}!", timer); } } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index 0f442da38..66fb1e084 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -28,6 +28,11 @@ public class MapPreparer { private final MapInfo[] CACHED = new MapInfo[10000]; private final Random RANDOM = new Random(); + private final Font TITLE_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 20); + private final Font CODE_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 43); + + private final String POSSIBLE = "0123456789"; + public void prepare() { for (int i = 0; i < CACHED.length; i++) { // Send map data @@ -38,24 +43,23 @@ public void prepare() { graphics.setColor(Color.WHITE); // Please enter captcha - graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 20)); + graphics.setFont(TITLE_FONT); graphics.drawString("Your code:", 5, 20); // Captcha - graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 43)); + graphics.setFont(CODE_FONT); - final Random random = new Random(); - final String possible = "0123456789"; final char[] captcha = new char[4]; for (int _i = 0; _i < captcha.length; _i++) { - captcha[_i] = possible.charAt(random.nextInt(possible.length())); + captcha[_i] = POSSIBLE.charAt(RANDOM.nextInt(POSSIBLE.length())); } final String answer = new String(captcha); - final FontMetrics fontMetrics = graphics.getFontMetrics(); - final int stringWidth = fontMetrics.stringWidth(answer); + // Center captcha string + final int stringWidth = graphics.getFontMetrics().stringWidth(answer); graphics.drawString(answer, image.getWidth() / 2 - stringWidth / 2, image.getHeight() / 2 + 21); + // Store image in buffer final byte[] buffer = new byte[MapInfo.SCALE]; for (int x = 0; x < image.getWidth(); x++) { for (int y = 0; y < image.getHeight(); y++) { diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java new file mode 100644 index 000000000..b435952f7 --- /dev/null +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 Sonar Contributors + * + * 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 . + */ + +package xyz.jonesdev.sonar.common.fallback.protocol.packets.play; + +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; +import org.jetbrains.annotations.NotNull; +import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion; +import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket; + +import java.util.UUID; + +import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*; +import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString; +import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID; +import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public final class Chat implements FallbackPacket { + private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L); + private Component component; + private int position; + + @Override + public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) { + // Serialized message + final String serialized = JSONComponentSerializer.json().serialize(component); + writeString(byteBuf, serialized); + + // Type + if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) { + byteBuf.writeBoolean(position == 2); + } else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) { + writeVarInt(byteBuf, position); + } else { + byteBuf.writeByte(position); + } + + // Sender + if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0 + && protocolVersion.compareTo(MINECRAFT_1_19) < 0) { + writeUUID(byteBuf, PLACEHOLDER_UUID); + } + } + + @Override + public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException(); + } +} From 14d7922f16d2a3fcef897a5c436fa178834dd7f9 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 21:25:06 +0100 Subject: [PATCH 07/29] fix: Chat packet error on 1.7 --- .../sonar/common/fallback/protocol/packets/play/Chat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java index b435952f7..79595a4ad 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java @@ -53,7 +53,7 @@ public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protoco byteBuf.writeBoolean(position == 2); } else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) { writeVarInt(byteBuf, position); - } else { + } else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) { byteBuf.writeByte(position); } From 8e24a71b995e0d6391d020d88333f69db9914180 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 21:25:20 +0100 Subject: [PATCH 08/29] feat: make map prepared amount and dictionary customizable --- .../sonar/api/config/SonarConfiguration.java | 10 ++++++++++ .../fallback/FallbackVerificationHandler.java | 3 +-- .../fallback/protocol/FallbackPreparer.java | 6 +++--- .../common/fallback/protocol/map/MapPreparer.java | 15 +++++++++------ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index cc90b05a2..861b31083 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -108,6 +108,8 @@ public enum Timing { @Getter public static final class Map { private boolean enabled; + private int precomputeAmount; + private String dictionary; private Component enterCodeMessage; } @@ -453,6 +455,14 @@ public void load() { "Should Sonar make the player pass a captcha?"); verification.map.enabled = generalConfig.getBoolean("verification.checks.map-captcha.enabled", false); + generalConfig.getYaml().setComment("verification.checks.map-captcha.precompute", + "How many answers should Sonar precompute (prepare)?"); + verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 10000); + + generalConfig.getYaml().setComment("verification.checks.map-captcha.dictionary", + "Characters (letters and numbers) that are allowed to appear in the answer to the captcha"); + verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "0123456789"); + generalConfig.getYaml().setComment("verification.checks.valid-name-regex", "Regex for validating usernames during verification"); verification.validNameRegex = Pattern.compile(generalConfig.getString( diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 59e338013..478b2d33d 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -507,10 +507,9 @@ private void handleMapCaptcha() { // Set slot to map user.delayedWrite(new SetSlot(0, 36, 1, 0, MapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT)); - + // Send map data captcha = MapPreparer.getRandomCaptcha(); user.delayedWrite(new MapData(0, captcha)); - // Teleport the player to the position above the platform user.delayedWrite(CAPTCHA_POSITION); // Make sure the player cannot move diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index a9abbac8a..70f009eac 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -117,10 +117,10 @@ public void prepare() { enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCodeMessage(), 1); final SystemTimer timer = new SystemTimer(); - Sonar.get().getLogger().info("Pre-computing map captcha answers..."); - // Pre-compute captcha answers + Sonar.get().getLogger().info("Precomputing map captcha answers..."); + // Precompute captcha answers MapPreparer.prepare(); - Sonar.get().getLogger().info("Successfully computed captcha answers in {}!", timer); + Sonar.get().getLogger().info("Successfully precomputed captcha answers in {}!", timer); } } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index 66fb1e084..ce52d1488 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -18,6 +18,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol.map; import lombok.experimental.UtilityClass; +import xyz.jonesdev.sonar.api.Sonar; import java.awt.*; import java.awt.image.BufferedImage; @@ -25,16 +26,18 @@ @UtilityClass public class MapPreparer { - private final MapInfo[] CACHED = new MapInfo[10000]; private final Random RANDOM = new Random(); private final Font TITLE_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 20); private final Font CODE_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 43); - private final String POSSIBLE = "0123456789"; + private MapInfo[] cached; public void prepare() { - for (int i = 0; i < CACHED.length; i++) { + cached = new MapInfo[Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount()]; + final String dictionary = Sonar.get().getConfig().getVerification().getMap().getDictionary(); + + for (int i = 0; i < cached.length; i++) { // Send map data final BufferedImage image = new BufferedImage(MapInfo.DIMENSIONS, MapInfo.DIMENSIONS, BufferedImage.TYPE_INT_RGB); final Graphics2D graphics = image.createGraphics(); @@ -51,7 +54,7 @@ public void prepare() { final char[] captcha = new char[4]; for (int _i = 0; _i < captcha.length; _i++) { - captcha[_i] = POSSIBLE.charAt(RANDOM.nextInt(POSSIBLE.length())); + captcha[_i] = dictionary.charAt(RANDOM.nextInt(dictionary.length())); } final String answer = new String(captcha); @@ -71,11 +74,11 @@ public void prepare() { } } - CACHED[i] = new MapInfo(answer, image.getWidth(), image.getHeight(), 0, 0, buffer); + cached[i] = new MapInfo(answer, image.getWidth(), image.getHeight(), 0, 0, buffer); } } public MapInfo getRandomCaptcha() { - return CACHED[RANDOM.nextInt(CACHED.length)]; + return cached[RANDOM.nextInt(cached.length)]; } } From 45bbbd520ceccb65b1b241efe772093ad0035bc4 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 21:28:16 +0100 Subject: [PATCH 09/29] fix: SetSlot packet error on 1.7 --- .../common/fallback/protocol/packets/play/SetSlot.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java index 277be6c97..786c7b830 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java @@ -79,7 +79,11 @@ public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersio } if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) { - byteBuf.writeByte(0); //No Nbt + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) { + byteBuf.writeShort(-1); + } else { + byteBuf.writeByte(0); + } } else { if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) { writeNamelessCompoundTag(byteBuf, compoundBinaryTag); From 8e865fd4ba99087c12e84d667939f815f7a908fc Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 21:55:12 +0100 Subject: [PATCH 10/29] fix: make map data work on 1.7 --- .../fallback/FallbackVerificationHandler.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 478b2d33d..5ebaff5c9 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -38,6 +38,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*; import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil; +import java.util.Objects; import java.util.Random; import java.util.UUID; import java.util.regex.Pattern; @@ -507,9 +508,25 @@ private void handleMapCaptcha() { // Set slot to map user.delayedWrite(new SetSlot(0, 36, 1, 0, MapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT)); - // Send map data + // Get random captcha captcha = MapPreparer.getRandomCaptcha(); - user.delayedWrite(new MapData(0, captcha)); + Objects.requireNonNull(captcha); + // Send map data + if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) { + byte[][] grid = new byte[MapInfo.DIMENSIONS][MapInfo.DIMENSIONS]; + for (int i = 0; i < captcha.getBuffer().length; i++) { + final byte buf = captcha.getBuffer()[i]; + grid[i & 127][i >> 7] = buf; + } + + for (int i = 0; i < MapInfo.DIMENSIONS; ++i) { + final MapInfo mapInfo_v1_7 = new MapInfo( + captcha.getAnswer(), MapInfo.DIMENSIONS, MapInfo.DIMENSIONS, i, 0, grid[i]); + user.delayedWrite(new MapData(0, mapInfo_v1_7)); + } + } else { + user.delayedWrite(new MapData(0, captcha)); + } // Teleport the player to the position above the platform user.delayedWrite(CAPTCHA_POSITION); // Make sure the player cannot move From 1b0aca6a31ac0d295f605e982e02df1eec7b5575 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 22:07:21 +0100 Subject: [PATCH 11/29] feat: randomize font size & font style --- .../fallback/protocol/map/MapPreparer.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index ce52d1488..a9a7a4c97 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -28,14 +28,27 @@ public class MapPreparer { private final Random RANDOM = new Random(); - private final Font TITLE_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 20); - private final Font CODE_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 43); + private final String[] FONT_TYPES = new String[] { + Font.DIALOG_INPUT, + Font.DIALOG, + Font.SANS_SERIF, + Font.SERIF + }; + + private final int[] FONT_STYLES = new int[] { + Font.PLAIN, + Font.BOLD, + Font.ITALIC, + Font.ITALIC | Font.BOLD + }; private MapInfo[] cached; public void prepare() { cached = new MapInfo[Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount()]; + final String dictionary = Sonar.get().getConfig().getVerification().getMap().getDictionary(); + final Font titleFont = new Font(Font.SANS_SERIF, Font.BOLD, 16); for (int i = 0; i < cached.length; i++) { // Send map data @@ -45,22 +58,31 @@ public void prepare() { graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setColor(Color.WHITE); + final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)]; + final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)]; + final int fontSize = 32 + RANDOM.nextInt(10); + @SuppressWarnings("all") + final Font answerFont = new Font(fontType, fontStyle, fontSize); + // Please enter captcha - graphics.setFont(TITLE_FONT); + graphics.setFont(titleFont); graphics.drawString("Your code:", 5, 20); // Captcha - graphics.setFont(CODE_FONT); + graphics.setFont(answerFont); final char[] captcha = new char[4]; for (int _i = 0; _i < captcha.length; _i++) { captcha[_i] = dictionary.charAt(RANDOM.nextInt(dictionary.length())); } final String answer = new String(captcha); + System.out.println(answer + " generated /// font size:" + fontSize + " font: "); // Center captcha string final int stringWidth = graphics.getFontMetrics().stringWidth(answer); - graphics.drawString(answer, image.getWidth() / 2 - stringWidth / 2, image.getHeight() / 2 + 21); + graphics.drawString(answer, + image.getWidth() / 2 - stringWidth / 2, + image.getHeight() / 2 + fontSize / 2); // Store image in buffer final byte[] buffer = new byte[MapInfo.SCALE]; From ad7c0d8d328cd38c7a038daa77c0f82122da169a Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 22:28:56 +0100 Subject: [PATCH 12/29] fix: implement KeepAlive during captcha and add timer --- .../sonar/api/config/SonarConfiguration.java | 19 +++++++-- .../fallback/FallbackVerificationHandler.java | 39 +++++++++++++++++-- .../fallback/protocol/FallbackPreparer.java | 4 +- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index 861b31083..43fa8b6e8 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -109,8 +109,10 @@ public enum Timing { public static final class Map { private boolean enabled; private int precomputeAmount; + private int maxDuration; private String dictionary; - private Component enterCodeMessage; + private Component enterCode; + private String enterCodeActionBar; } @Getter @@ -459,6 +461,10 @@ public void load() { "How many answers should Sonar precompute (prepare)?"); verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 10000); + generalConfig.getYaml().setComment("verification.checks.map-captcha.max-duration", + "How long should Sonar wait until the player fails the captcha?"); + verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration", 60000); + generalConfig.getYaml().setComment("verification.checks.map-captcha.dictionary", "Characters (letters and numbers) that are allowed to appear in the answer to the captcha"); verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "0123456789"); @@ -916,10 +922,15 @@ public void load() { verification.gravity.youAreBeingChecked = deserialize(messagesConfig.getString("verification.welcome", "Please wait a moment for the verification to finish...")); - messagesConfig.getYaml().setComment("verification.enter-code", + messagesConfig.getYaml().setComment("verification.captcha.enter-code", "Message that is shown to the player when they have to enter the answer to the captcha"); - verification.map.enterCodeMessage = deserialize(messagesConfig.getString("verification.enter-code", - "Please enter the code in chat that is displayed on the map:")); + verification.map.enterCode = deserialize(messagesConfig.getString("verification.captcha.enter-code", + "Please enter the code in chat that is displayed on the map.")); + messagesConfig.getYaml().setComment("verification.captcha.action-bar", + "Timer that is shown to the player when they have to enter the answer to the captcha" + + LINE_SEPARATOR + "(Set this to '' to disable the action bar message)"); + verification.map.enterCodeActionBar = messagesConfig.getString("verification.captcha.action-bar", + "You have %time-left% seconds left to enter the code in chat"); messagesConfig.getYaml().setComment("verification.too-many-players", "Disconnect message that is shown when too many players are verifying at the same time"); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 5ebaff5c9..26b693218 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -22,6 +22,7 @@ import io.netty.handler.codec.DecoderException; import lombok.Setter; import lombok.val; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import xyz.jonesdev.sonar.api.Sonar; @@ -65,6 +66,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener private @Nullable MapInfo captcha; private final SystemTimer login = new SystemTimer(); + private final SystemTimer keepAlive = new SystemTimer(); + private final SystemTimer actionBar = new SystemTimer(); private static final Random RANDOM = new Random(); public enum State { @@ -234,15 +237,45 @@ private static boolean validateClientBrand(final @NotNull FallbackUser use public void handle(final @NotNull FallbackPacket packet) { // The player has already been verified, drop all other packets if (state == State.SUCCESS) return; + // Check for timeout since the player could be sending packets but not important ones + final long timeout = Sonar.get().getConfig().getVerification().getMaxPing(); // We are expecting a captcha code in chat, drop all other packets - if (state == State.MAP_CAPTCHA) return; + if (state == State.MAP_CAPTCHA) { + // Check if the player took too long to enter the captcha + final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration(); + checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha"); + + // Every second + if (actionBar.elapsed(1000L)) { + final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar(); + // Only send action bar if the message is actually supposed to be sent + if (!actionBarMessage.isEmpty()) { + final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D); + user.write(new Chat(MiniMessage.miniMessage().deserialize( + actionBarMessage.replace("%time-left%", timeLeft)), 2)); + // Make sure to reset the timer + actionBar.reset(); + } + } + + // Every about 10 seconds + if (keepAlive.elapsed(timeout)) { + // Send the message again to remind the player + user.delayedWrite(enterCodeMessage); + // Send a KeepAlive packet to prevent timeout + user.delayedWrite(CAPTCHA_KEEP_ALIVE); + // Send both packets in one flush + user.getChannel().flush(); + // Make sure to reset the timer + keepAlive.reset(); + } + return; + } // Check if the player is not sending a ton of packets to the server final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick; checkFrame(++totalReceivedPackets < maxPackets, "too many packets"); - // Check for timeout since the player could be sending packets but not important ones - final long timeout = Sonar.get().getConfig().getVerification().getMaxPing(); // Check if the time limit has exceeded if (login.elapsed(timeout)) { user.getChannel().close(); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index 70f009eac..dbc0dd64e 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -40,6 +40,8 @@ public class FallbackPreparer { public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration(); // Synchronize Registry public final FallbackPacket REGISTRY_SYNC = new RegistrySync(); + // Keep Alive + public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlive(0L); // Chat public FallbackPacket enterCodeMessage; public FallbackPacket youAreBeingChecked; @@ -114,7 +116,7 @@ public void prepare() { if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) { // "Enter your code" message - enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCodeMessage(), 1); + enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode(), 1); final SystemTimer timer = new SystemTimer(); Sonar.get().getLogger().info("Precomputing map captcha answers..."); From 7bf807e6372668cc402f1e4ebcf3804db465c578 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 22:54:38 +0100 Subject: [PATCH 13/29] feat: add color and x/y randomization to map captcha --- .../sonar/api/config/SonarConfiguration.java | 2 +- .../fallback/protocol/map/MapPreparer.java | 49 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index 43fa8b6e8..fbed5faa1 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -467,7 +467,7 @@ public void load() { generalConfig.getYaml().setComment("verification.checks.map-captcha.dictionary", "Characters (letters and numbers) that are allowed to appear in the answer to the captcha"); - verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "0123456789"); + verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "123456789"); generalConfig.getYaml().setComment("verification.checks.valid-name-regex", "Regex for validating usernames during verification"); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index a9a7a4c97..34a52f56c 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -42,13 +42,26 @@ public class MapPreparer { Font.ITALIC | Font.BOLD }; + private final int[][] COLOR_PALETTE = new int[][] { + new int[] { // Blue + 48, + 49, + 50, + 51 + }, + new int[] { // Gray + 45, + 46, + 47, + } + }; + private MapInfo[] cached; public void prepare() { cached = new MapInfo[Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount()]; final String dictionary = Sonar.get().getConfig().getVerification().getMap().getDictionary(); - final Font titleFont = new Font(Font.SANS_SERIF, Font.BOLD, 16); for (int i = 0; i < cached.length; i++) { // Send map data @@ -63,12 +76,6 @@ public void prepare() { final int fontSize = 32 + RANDOM.nextInt(10); @SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize); - - // Please enter captcha - graphics.setFont(titleFont); - graphics.drawString("Your code:", 5, 20); - - // Captcha graphics.setFont(answerFont); final char[] captcha = new char[4]; @@ -76,23 +83,35 @@ public void prepare() { captcha[_i] = dictionary.charAt(RANDOM.nextInt(dictionary.length())); } final String answer = new String(captcha); - System.out.println(answer + " generated /// font size:" + fontSize + " font: "); - // Center captcha string + // Calculate text position final int stringWidth = graphics.getFontMetrics().stringWidth(answer); - graphics.drawString(answer, - image.getWidth() / 2 - stringWidth / 2, - image.getHeight() / 2 + fontSize / 2); + int _x = image.getWidth() / 2 - stringWidth / 2; + int _y = image.getHeight() / 2 + fontSize / 3; + + // Draw each character one by one + for (final char c : captcha) { + final String character = String.valueOf(c); + final int randomOffsetX = -3 + RANDOM.nextInt(6); + final int randomOffsetY = -7 + RANDOM.nextInt(14); + + graphics.drawString(character, _x, _y); + _x += graphics.getFontMetrics().stringWidth(character); + _x += randomOffsetX; + _y += randomOffsetY; + } + // Select random color palette + final int randomColorPalette = RANDOM.nextInt(COLOR_PALETTE.length); // Store image in buffer final byte[] buffer = new byte[MapInfo.SCALE]; for (int x = 0; x < image.getWidth(); x++) { for (int y = 0; y < image.getHeight(); y++) { final int colorIndex = y * image.getWidth() + x; final int pixel = image.getRGB(x, y); - if (pixel != -16777216) { - buffer[colorIndex] = (byte) 47; - } + if (pixel == -16777216) continue; + final int randomColor = COLOR_PALETTE[randomColorPalette][RANDOM.nextInt(COLOR_PALETTE[randomColorPalette].length)]; + buffer[colorIndex] = (byte) randomColor; } } From 386960834e131f0a8b4126f0971c4cdb57c50cb7 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 23:04:27 +0100 Subject: [PATCH 14/29] feat: add green and red color palette --- .../sonar/api/config/SonarConfiguration.java | 2 +- .../common/fallback/protocol/map/MapPreparer.java | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index fbed5faa1..f9b591a23 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -463,7 +463,7 @@ public void load() { generalConfig.getYaml().setComment("verification.checks.map-captcha.max-duration", "How long should Sonar wait until the player fails the captcha?"); - verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration", 60000); + verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration", 45000); generalConfig.getYaml().setComment("verification.checks.map-captcha.dictionary", "Characters (letters and numbers) that are allowed to appear in the answer to the captcha"); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index 34a52f56c..4f14b1a98 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -53,6 +53,18 @@ public class MapPreparer { 45, 46, 47, + }, + new int[] { // Green + 4, + 5, + 6, + 7, + }, + new int[] { // Red + 16, + 17, + 18, + 19 } }; @@ -73,7 +85,7 @@ public void prepare() { final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)]; final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)]; - final int fontSize = 32 + RANDOM.nextInt(10); + final int fontSize = 33 + RANDOM.nextInt(10); @SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize); graphics.setFont(answerFont); From 1568b7fb7d06176632f66fbcf3d0b8ca97729894 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Tue, 28 Nov 2023 23:41:13 +0100 Subject: [PATCH 15/29] feat: implement captcha validation --- .../sonar/api/config/SonarConfiguration.java | 10 ++ .../fallback/FallbackVerificationHandler.java | 20 +++- .../protocol/FallbackPacketRegistry.java | 33 ++++--- .../fallback/protocol/FallbackPreparer.java | 9 +- .../fallback/protocol/packets/play/Chat.java | 92 +++++++++++++++++-- .../common/utility/protocol/ProtocolUtil.java | 20 ++++ 6 files changed, 161 insertions(+), 23 deletions(-) diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index f9b591a23..accea5bad 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -110,8 +110,10 @@ public static final class Map { private boolean enabled; private int precomputeAmount; private int maxDuration; + private int maxTries; private String dictionary; private Component enterCode; + private Component failedCaptcha; private String enterCodeActionBar; } @@ -465,6 +467,10 @@ public void load() { "How long should Sonar wait until the player fails the captcha?"); verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration", 45000); + generalConfig.getYaml().setComment("verification.checks.map-captcha.max-tries", + "How many times must a player fail the captcha before failing the verification?"); + verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries", 3); + generalConfig.getYaml().setComment("verification.checks.map-captcha.dictionary", "Characters (letters and numbers) that are allowed to appear in the answer to the captcha"); verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "123456789"); @@ -931,6 +937,10 @@ public void load() { + LINE_SEPARATOR + "(Set this to '' to disable the action bar message)"); verification.map.enterCodeActionBar = messagesConfig.getString("verification.captcha.action-bar", "You have %time-left% seconds left to enter the code in chat"); + messagesConfig.getYaml().setComment("verification.captcha.incorrect", + "Message that is shown to the player when they enter the wrong answer in chat"); + verification.map.failedCaptcha = deserialize(messagesConfig.getString("verification.captcha.incorrect", + "You have entered the wrong code. Please try again.")); messagesConfig.getYaml().setComment("verification.too-many-players", "Disconnect message that is shown when too many players are verifying at the same time"); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 26b693218..936f97a35 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -64,6 +64,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener private @NotNull State state = State.LOGIN_ACK; private boolean listenForMovements; private @Nullable MapInfo captcha; + private int captchaTriesLeft; private final SystemTimer login = new SystemTimer(); private final SystemTimer keepAlive = new SystemTimer(); @@ -245,6 +246,20 @@ public void handle(final @NotNull FallbackPacket packet) { final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration(); checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha"); + // Handle incoming chat messages + if (packet instanceof Chat) { + final Chat chat = (Chat) packet; + Objects.requireNonNull(captcha); + if (!chat.getMessage().equals(captcha.getAnswer())) { + // Captcha is incorrect + checkFrame(captchaTriesLeft-- > 0, "failed captcha too often"); + user.write(incorrectCaptcha); + return; + } + // Captcha is correct + finish(); + } + // Every second if (actionBar.elapsed(1000L)) { final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar(); @@ -252,7 +267,7 @@ public void handle(final @NotNull FallbackPacket packet) { if (!actionBarMessage.isEmpty()) { final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D); user.write(new Chat(MiniMessage.miniMessage().deserialize( - actionBarMessage.replace("%time-left%", timeLeft)), 2)); + actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE)); // Make sure to reset the timer actionBar.reset(); } @@ -538,6 +553,9 @@ private void captchaOrFinish() { private void handleMapCaptcha() { state = State.MAP_CAPTCHA; + // Reset max tries + captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries(); + // Set slot to map user.delayedWrite(new SetSlot(0, 36, 1, 0, MapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT)); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java index 77daa24e7..30175572a 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java @@ -222,18 +222,27 @@ public enum FallbackPacketRegistry { map(0x14, MINECRAFT_1_19_4, true), map(0x15, MINECRAFT_1_20_2, true)); clientbound.register(Chat.class, Chat::new, - map(0x02, MINECRAFT_1_7_2, true), - map(0x0F, MINECRAFT_1_9, true), - map(0x0E, MINECRAFT_1_13, true), - map(0x0F, MINECRAFT_1_15, true), - map(0x0E, MINECRAFT_1_16, true), - map(0x0F, MINECRAFT_1_18_2, true), - map(0x5F, MINECRAFT_1_19, true), - map(0x62, MINECRAFT_1_19_1, true), - map(0x60, MINECRAFT_1_19_3, true), - map(0x64, MINECRAFT_1_19_4, true), - map(0x67, MINECRAFT_1_20_2, true)); - + map(0x02, MINECRAFT_1_7_2, false), + map(0x0F, MINECRAFT_1_9, false), + map(0x0E, MINECRAFT_1_13, false), + map(0x0F, MINECRAFT_1_15, false), + map(0x0E, MINECRAFT_1_16, false), + map(0x0F, MINECRAFT_1_18_2, false), + map(0x5F, MINECRAFT_1_19, false), + map(0x62, MINECRAFT_1_19_1, false), + map(0x60, MINECRAFT_1_19_3, false), + map(0x64, MINECRAFT_1_19_4, false), + map(0x67, MINECRAFT_1_20_2, false)); + + serverbound.register(Chat.class, Chat::new, + map(0x01, MINECRAFT_1_7_2, false), + map(0x02, MINECRAFT_1_9, false), + map(0x03, MINECRAFT_1_12, false), + map(0x02, MINECRAFT_1_12_1, false), + map(0x03, MINECRAFT_1_14, false), + map(0x04, MINECRAFT_1_19, false), + map(0x05, MINECRAFT_1_19_1, false), + map(0x05, MINECRAFT_1_19_3, false)); serverbound.register(KeepAlive.class, KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x0B, MINECRAFT_1_9, false), diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index dbc0dd64e..be39ff0c1 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -45,6 +45,7 @@ public class FallbackPreparer { // Chat public FallbackPacket enterCodeMessage; public FallbackPacket youAreBeingChecked; + public FallbackPacket incorrectCaptcha; // JoinGame public FallbackPacket joinGame; // Update Section Blocks @@ -111,18 +112,18 @@ public void prepare() { // "You are being checked" message if (Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { - youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked(), 1); + youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked()); } if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) { - // "Enter your code" message - enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode(), 1); + enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode()); + incorrectCaptcha = new Chat(Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha()); final SystemTimer timer = new SystemTimer(); Sonar.get().getLogger().info("Precomputing map captcha answers..."); // Precompute captcha answers MapPreparer.prepare(); - Sonar.get().getLogger().info("Successfully precomputed captcha answers in {}!", timer); + Sonar.get().getLogger().info("Successfully precomputed captcha answers in {}s!", timer); } } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java index 79595a4ad..1a3e5bda7 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java @@ -18,6 +18,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol.packets.play; import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.CorruptedFrameException; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -27,11 +28,12 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion; import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket; +import java.time.Instant; import java.util.UUID; import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*; -import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString; -import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID; +import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*; +import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt; import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt; @Data @@ -40,7 +42,31 @@ public final class Chat implements FallbackPacket { private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L); private Component component; - private int position; + private String message; + private byte position; + private boolean signedPreview; + private boolean unsigned = false; + private Instant timestamp; + private Instant expiry; + private long salt; + private boolean signed; + private byte[] signature; + private static final int DIV_FLOOR = -Math.floorDiv(-20, 8); + + public static final byte CHAT_TYPE = (byte) 0; + public static final byte SYSTEM_TYPE = (byte) 1; + public static final byte GAME_INFO_TYPE = (byte) 2; + + // Clientbound LegacyChat + public Chat(final Component component) { + this(component, SYSTEM_TYPE); + } + + // Clientbound LegacyChat + public Chat(final Component component, final byte position) { + this(component, null, position, false, false, + null, null, 0L, false, null); + } @Override public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) { @@ -50,7 +76,7 @@ public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protoco // Type if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) { - byteBuf.writeBoolean(position == 2); + byteBuf.writeBoolean(position == GAME_INFO_TYPE); } else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) { writeVarInt(byteBuf, position); } else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) { @@ -65,7 +91,61 @@ public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protoco } @Override - public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) { - throw new UnsupportedOperationException(); + public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) { + message = readString(byteBuf, 256); + + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 + && protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) <= 0) { + final long expiresAt = byteBuf.readLong(); + final long saltLong = byteBuf.readLong(); + final byte[] signatureBytes = readByteArray(byteBuf); + + if (saltLong != 0L && signatureBytes.length > 0) { + signature = signatureBytes; + expiry = Instant.ofEpochMilli(expiresAt); + } else if ((protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0 + || saltLong == 0L) && signatureBytes.length == 0) { + unsigned = true; + } else { + throw new CorruptedFrameException("Invalid signature"); + } + + signedPreview = byteBuf.readBoolean(); + if (signedPreview && unsigned) { + throw new CorruptedFrameException("Signature missing"); + } + + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { + final int size = readVarInt(byteBuf); + if (size < 0 || size > 5) { + throw new CorruptedFrameException("Invalid previous messages"); + } + + for (int i = 0; i < size; i++) { + readUUID(byteBuf); + readByteArray(byteBuf); + } + + if (byteBuf.readBoolean()) { + readUUID(byteBuf); + readByteArray(byteBuf); + } + } + } else { + timestamp = Instant.ofEpochMilli(byteBuf.readLong()); + salt = byteBuf.readLong(); + signed = byteBuf.readBoolean(); + if (signed) { + byte[] sign = new byte[256]; + byteBuf.readBytes(sign); + signature = sign; + } else { + signature = new byte[0]; + } + + readVarInt(byteBuf); + byte[] bytes = new byte[DIV_FLOOR]; + byteBuf.readBytes(bytes); + } } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/utility/protocol/ProtocolUtil.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/utility/protocol/ProtocolUtil.java index 026cd158b..2c910045a 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/utility/protocol/ProtocolUtil.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/utility/protocol/ProtocolUtil.java @@ -35,6 +35,8 @@ import java.util.UUID; import java.util.regex.Pattern; +import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt; + // Taken from // https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @UtilityClass @@ -49,6 +51,24 @@ public class ProtocolUtil { private static final String UNREGISTER_CHANNEL = "minecraft:unregister"; private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*"); + public static @NotNull UUID readUUID(final @NotNull ByteBuf byteBuf) { + return new UUID(byteBuf.readLong(), byteBuf.readLong()); + } + + public static byte @NotNull [] readByteArray(final ByteBuf byteBuf) { + return readByteArray(byteBuf, Short.MAX_VALUE); + } + + public static byte @NotNull [] readByteArray(final ByteBuf byteBuf, final int cap) { + int length = readVarInt(byteBuf); + checkFrame(length >= 0, "Got a negative-length array"); + checkFrame(length <= cap, "Bad array size"); + checkFrame(byteBuf.isReadable(length), "Trying to read an array that is too long"); + byte[] array = new byte[length]; + byteBuf.readBytes(array); + return array; + } + public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException { final ByteBuf slice = content.slice(); try { From 99c8c2700ff16a006062d2dfc75df0d3475b75bf Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Wed, 29 Nov 2023 13:28:40 +0100 Subject: [PATCH 16/29] fix: chat packet not working on 1.7-1.18.2 --- .../fallback/FallbackVerificationHandler.java | 2 +- .../fallback/protocol/packets/play/Chat.java | 91 ++++++++++--------- .../protocol/packets/play/SetSlot.java | 2 +- 3 files changed, 48 insertions(+), 47 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 936f97a35..49a682eb3 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -570,7 +570,7 @@ private void handleMapCaptcha() { grid[i & 127][i >> 7] = buf; } - for (int i = 0; i < MapInfo.DIMENSIONS; ++i) { + for (int i = 0; i < grid.length; i++) { final MapInfo mapInfo_v1_7 = new MapInfo( captcha.getAnswer(), MapInfo.DIMENSIONS, MapInfo.DIMENSIONS, i, 0, grid[i]); user.delayedWrite(new MapData(0, mapInfo_v1_7)); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java index 1a3e5bda7..e86a99321 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java @@ -94,58 +94,59 @@ public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protoco public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) { message = readString(byteBuf, 256); - if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 - && protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) <= 0) { - final long expiresAt = byteBuf.readLong(); - final long saltLong = byteBuf.readLong(); - final byte[] signatureBytes = readByteArray(byteBuf); - - if (saltLong != 0L && signatureBytes.length > 0) { - signature = signatureBytes; - expiry = Instant.ofEpochMilli(expiresAt); - } else if ((protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0 - || saltLong == 0L) && signatureBytes.length == 0) { - unsigned = true; - } else { - throw new CorruptedFrameException("Invalid signature"); - } - - signedPreview = byteBuf.readBoolean(); - if (signedPreview && unsigned) { - throw new CorruptedFrameException("Signature missing"); - } - - if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { - final int size = readVarInt(byteBuf); - if (size < 0 || size > 5) { - throw new CorruptedFrameException("Invalid previous messages"); + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) { + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) <= 0) { + final long expiresAt = byteBuf.readLong(); + final long saltLong = byteBuf.readLong(); + final byte[] signatureBytes = readByteArray(byteBuf); + + if (saltLong != 0L && signatureBytes.length > 0) { + signature = signatureBytes; + expiry = Instant.ofEpochMilli(expiresAt); + } else if ((protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0 + || saltLong == 0L) && signatureBytes.length == 0) { + unsigned = true; + } else { + throw new CorruptedFrameException("Invalid signature"); } - for (int i = 0; i < size; i++) { - readUUID(byteBuf); - readByteArray(byteBuf); + signedPreview = byteBuf.readBoolean(); + if (signedPreview && unsigned) { + throw new CorruptedFrameException("Signature missing"); } - if (byteBuf.readBoolean()) { - readUUID(byteBuf); - readByteArray(byteBuf); + if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { + final int size = readVarInt(byteBuf); + if (size < 0 || size > 5) { + throw new CorruptedFrameException("Invalid previous messages"); + } + + for (int i = 0; i < size; i++) { + readUUID(byteBuf); + readByteArray(byteBuf); + } + + if (byteBuf.readBoolean()) { + readUUID(byteBuf); + readByteArray(byteBuf); + } } - } - } else { - timestamp = Instant.ofEpochMilli(byteBuf.readLong()); - salt = byteBuf.readLong(); - signed = byteBuf.readBoolean(); - if (signed) { - byte[] sign = new byte[256]; - byteBuf.readBytes(sign); - signature = sign; } else { - signature = new byte[0]; - } + timestamp = Instant.ofEpochMilli(byteBuf.readLong()); + salt = byteBuf.readLong(); + signed = byteBuf.readBoolean(); + if (signed) { + byte[] sign = new byte[256]; + byteBuf.readBytes(sign); + signature = sign; + } else { + signature = new byte[0]; + } - readVarInt(byteBuf); - byte[] bytes = new byte[DIV_FLOOR]; - byteBuf.readBytes(bytes); + readVarInt(byteBuf); + byte[] bytes = new byte[DIV_FLOOR]; + byteBuf.readBytes(bytes); + } } } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java index 786c7b830..13045091a 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/SetSlot.java @@ -46,7 +46,7 @@ public final class SetSlot implements FallbackPacket { private CompoundBinaryTag compoundBinaryTag; public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder() - .put("map", IntBinaryTag.intBinaryTag(0)) + .put("map", IntBinaryTag.intBinaryTag(0)) // map type .build(); @Override From 1657627ac874263a35956f2d263ebed4bd95aa2c Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Wed, 29 Nov 2023 14:18:29 +0100 Subject: [PATCH 17/29] fix: only draw important used pixels --- .../fallback/protocol/map/MapPreparer.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index 4f14b1a98..35474c931 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -76,11 +76,10 @@ public void prepare() { final String dictionary = Sonar.get().getConfig().getVerification().getMap().getDictionary(); for (int i = 0; i < cached.length; i++) { - // Send map data - final BufferedImage image = new BufferedImage(MapInfo.DIMENSIONS, MapInfo.DIMENSIONS, BufferedImage.TYPE_INT_RGB); + // Create image + final BufferedImage image = new BufferedImage(MapInfo.DIMENSIONS, MapInfo.DIMENSIONS, BufferedImage.TYPE_3BYTE_BGR); final Graphics2D graphics = image.createGraphics(); - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setColor(Color.WHITE); final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)]; @@ -90,23 +89,24 @@ public void prepare() { final Font answerFont = new Font(fontType, fontStyle, fontSize); graphics.setFont(answerFont); - final char[] captcha = new char[4]; - for (int _i = 0; _i < captcha.length; _i++) { - captcha[_i] = dictionary.charAt(RANDOM.nextInt(dictionary.length())); + final StringBuilder answerBuilder = new StringBuilder(); + for (int _i = 0; _i < 4; _i++) { + answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length()))); } - final String answer = new String(captcha); + final String answer = answerBuilder.toString(); // Calculate text position final int stringWidth = graphics.getFontMetrics().stringWidth(answer); int _x = image.getWidth() / 2 - stringWidth / 2; int _y = image.getHeight() / 2 + fontSize / 3; + int randomOffsetX = -3, randomOffsetY = -1; // Draw each character one by one - for (final char c : captcha) { - final String character = String.valueOf(c); - final int randomOffsetX = -3 + RANDOM.nextInt(6); - final int randomOffsetY = -7 + RANDOM.nextInt(14); + for (final char c : answer.toCharArray()) { + randomOffsetX = randomOffsetX < 0 ? RANDOM.nextInt(4) : -RANDOM.nextInt(4); + randomOffsetY = randomOffsetY < 0 ? RANDOM.nextInt(5) : -RANDOM.nextInt(5); + final String character = String.valueOf(c); graphics.drawString(character, _x, _y); _x += graphics.getFontMetrics().stringWidth(character); _x += randomOffsetX; @@ -115,10 +115,15 @@ public void prepare() { // Select random color palette final int randomColorPalette = RANDOM.nextInt(COLOR_PALETTE.length); + // Calculate x, y, width, and height + final int __x = image.getWidth() / 2 - stringWidth / 2; + final int __w = __x + stringWidth; + final int __y = image.getHeight() / 2 - fontSize / 2; + final int __h = __y + fontSize; // Store image in buffer final byte[] buffer = new byte[MapInfo.SCALE]; - for (int x = 0; x < image.getWidth(); x++) { - for (int y = 0; y < image.getHeight(); y++) { + for (int x = __x; x < __w; x++) { + for (int y = __y; y < __h; y++) { final int colorIndex = y * image.getWidth() + x; final int pixel = image.getRGB(x, y); if (pixel == -16777216) continue; From 4d9199d458d4ee7f2f245e3b238a428c85034371 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Thu, 30 Nov 2023 17:14:03 +0100 Subject: [PATCH 18/29] feat: optimize color palette --- .../sonar/common/fallback/protocol/map/MapPreparer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index 35474c931..88c52d075 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -114,7 +114,7 @@ public void prepare() { } // Select random color palette - final int randomColorPalette = RANDOM.nextInt(COLOR_PALETTE.length); + final int[] colorPalette = COLOR_PALETTE[RANDOM.nextInt(COLOR_PALETTE.length)]; // Calculate x, y, width, and height final int __x = image.getWidth() / 2 - stringWidth / 2; final int __w = __x + stringWidth; @@ -124,11 +124,11 @@ public void prepare() { final byte[] buffer = new byte[MapInfo.SCALE]; for (int x = __x; x < __w; x++) { for (int y = __y; y < __h; y++) { - final int colorIndex = y * image.getWidth() + x; final int pixel = image.getRGB(x, y); if (pixel == -16777216) continue; - final int randomColor = COLOR_PALETTE[randomColorPalette][RANDOM.nextInt(COLOR_PALETTE[randomColorPalette].length)]; - buffer[colorIndex] = (byte) randomColor; + final int gridIndex = y * image.getWidth() + x; + final int randomColor = colorPalette[RANDOM.nextInt(colorPalette.length)]; + buffer[gridIndex] = (byte) randomColor; } } From 531d6fc04bc485d8b46a263ad7309688f5639a39 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Thu, 30 Nov 2023 18:05:29 +0100 Subject: [PATCH 19/29] feat: set map image dimensions to 86x86 --- .../common/fallback/protocol/map/MapInfo.java | 2 +- .../fallback/protocol/map/MapPreparer.java | 35 +++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java index 427122cb1..c84363c79 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java @@ -23,7 +23,7 @@ @Getter @RequiredArgsConstructor public final class MapInfo { - public static final int DIMENSIONS = 128; + public static final int DIMENSIONS = 86; public static final int SCALE = DIMENSIONS * DIMENSIONS; private final String answer; diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index 88c52d075..d180d28be 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -49,16 +49,17 @@ public class MapPreparer { 50, 51 }, - new int[] { // Gray + new int[] { // Black + 27, 45, 46, 47, }, new int[] { // Green 4, - 5, - 6, - 7, + 28, + 29, + 30, }, new int[] { // Red 16, @@ -82,13 +83,15 @@ public void prepare() { graphics.setColor(Color.WHITE); + // Create random font final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)]; final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)]; - final int fontSize = 33 + RANDOM.nextInt(10); + final int fontSize = 26 + RANDOM.nextInt(5); @SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize); graphics.setFont(answerFont); + // Build answer to the captcha final StringBuilder answerBuilder = new StringBuilder(); for (int _i = 0; _i < 4; _i++) { answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length()))); @@ -99,18 +102,18 @@ public void prepare() { final int stringWidth = graphics.getFontMetrics().stringWidth(answer); int _x = image.getWidth() / 2 - stringWidth / 2; int _y = image.getHeight() / 2 + fontSize / 3; - int randomOffsetX = -3, randomOffsetY = -1; + int randomOffsetX = 0, randomOffsetY = 3; // Draw each character one by one for (final char c : answer.toCharArray()) { - randomOffsetX = randomOffsetX < 0 ? RANDOM.nextInt(4) : -RANDOM.nextInt(4); - randomOffsetY = randomOffsetY < 0 ? RANDOM.nextInt(5) : -RANDOM.nextInt(5); + _x += randomOffsetX; + _y += randomOffsetY; + randomOffsetX = randomOffsetX < 0 ? 1 + RANDOM.nextInt(2) : -1 - RANDOM.nextInt(2); + randomOffsetY = randomOffsetY < 0 ? 1 + RANDOM.nextInt(4) : -1 - RANDOM.nextInt(4); final String character = String.valueOf(c); graphics.drawString(character, _x, _y); _x += graphics.getFontMetrics().stringWidth(character); - _x += randomOffsetX; - _y += randomOffsetY; } // Select random color palette @@ -124,15 +127,17 @@ public void prepare() { final byte[] buffer = new byte[MapInfo.SCALE]; for (int x = __x; x < __w; x++) { for (int y = __y; y < __h; y++) { + final int gridIndex = y * image.getWidth() + x; final int pixel = image.getRGB(x, y); + // If the pixel has no color set, set it to white/light gray if (pixel == -16777216) continue; - final int gridIndex = y * image.getWidth() + x; - final int randomColor = colorPalette[RANDOM.nextInt(colorPalette.length)]; - buffer[gridIndex] = (byte) randomColor; + // Set color of pixel to random color from the palette + buffer[gridIndex] = (byte) colorPalette[RANDOM.nextInt(colorPalette.length)]; } } - - cached[i] = new MapInfo(answer, image.getWidth(), image.getHeight(), 0, 0, buffer); + // Cache buffer to map + cached[i] = new MapInfo(answer, image.getWidth(), image.getHeight(), + image.getWidth() / 4, image.getHeight() / 4, buffer); } } From 4d0597670604575951f9e7f75b437ecdd85fc761 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Thu, 30 Nov 2023 18:05:46 +0100 Subject: [PATCH 20/29] feat: set background color of image to white/light gray --- .../sonar/common/fallback/protocol/map/MapPreparer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java index d180d28be..c4a8425f3 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java @@ -130,7 +130,10 @@ public void prepare() { final int gridIndex = y * image.getWidth() + x; final int pixel = image.getRGB(x, y); // If the pixel has no color set, set it to white/light gray - if (pixel == -16777216) continue; + if (pixel == -16777216) { + buffer[gridIndex] = (byte) (RANDOM.nextBoolean() ? 14 : 26); + continue; + } // Set color of pixel to random color from the palette buffer[gridIndex] = (byte) colorPalette[RANDOM.nextInt(colorPalette.length)]; } From 284fae35ef96cce2deb1cdd0a08efe09d42f2b8d Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Thu, 30 Nov 2023 18:53:43 +0100 Subject: [PATCH 21/29] feat: recode map info preparing and map data packet sending --- .../fallback/FallbackVerificationHandler.java | 34 +++------- .../fallback/protocol/FallbackPreparer.java | 4 +- .../map/{MapType.java => ItemMapType.java} | 2 +- .../common/fallback/protocol/map/MapInfo.java | 3 - ...{MapPreparer.java => MapInfoPreparer.java} | 38 +++++------ .../protocol/map/PreparedMapInfo.java | 66 +++++++++++++++++++ 6 files changed, 97 insertions(+), 50 deletions(-) rename sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/{MapType.java => ItemMapType.java} (98%) rename sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/{MapPreparer.java => MapInfoPreparer.java} (76%) create mode 100644 sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/PreparedMapInfo.java diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 49a682eb3..2779f591f 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -31,9 +31,9 @@ import xyz.jonesdev.sonar.api.model.VerifiedPlayer; import xyz.jonesdev.sonar.api.timer.SystemTimer; import xyz.jonesdev.sonar.common.fallback.protocol.*; -import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfo; -import xyz.jonesdev.sonar.common.fallback.protocol.map.MapPreparer; -import xyz.jonesdev.sonar.common.fallback.protocol.map.MapType; +import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemMapType; +import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer; +import xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo; import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration; import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged; import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*; @@ -63,7 +63,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener @Setter private @NotNull State state = State.LOGIN_ACK; private boolean listenForMovements; - private @Nullable MapInfo captcha; + private @Nullable PreparedMapInfo captcha; private int captchaTriesLeft; private final SystemTimer login = new SystemTimer(); @@ -250,7 +250,7 @@ public void handle(final @NotNull FallbackPacket packet) { if (packet instanceof Chat) { final Chat chat = (Chat) packet; Objects.requireNonNull(captcha); - if (!chat.getMessage().equals(captcha.getAnswer())) { + if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) { // Captcha is incorrect checkFrame(captchaTriesLeft-- > 0, "failed captcha too often"); user.write(incorrectCaptcha); @@ -558,26 +558,10 @@ private void handleMapCaptcha() { // Set slot to map user.delayedWrite(new SetSlot(0, 36, 1, 0, - MapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT)); - // Get random captcha - captcha = MapPreparer.getRandomCaptcha(); - Objects.requireNonNull(captcha); - // Send map data - if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) { - byte[][] grid = new byte[MapInfo.DIMENSIONS][MapInfo.DIMENSIONS]; - for (int i = 0; i < captcha.getBuffer().length; i++) { - final byte buf = captcha.getBuffer()[i]; - grid[i & 127][i >> 7] = buf; - } - - for (int i = 0; i < grid.length; i++) { - final MapInfo mapInfo_v1_7 = new MapInfo( - captcha.getAnswer(), MapInfo.DIMENSIONS, MapInfo.DIMENSIONS, i, 0, grid[i]); - user.delayedWrite(new MapData(0, mapInfo_v1_7)); - } - } else { - user.delayedWrite(new MapData(0, captcha)); - } + ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT)); + // Send random captcha to the player + captcha = MapInfoPreparer.getRandomCaptcha(); + Objects.requireNonNull(captcha).write(user); // Teleport the player to the position above the platform user.delayedWrite(CAPTCHA_POSITION); // Make sure the player cannot move diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index be39ff0c1..0822b44df 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -23,7 +23,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition; import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType; import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock; -import xyz.jonesdev.sonar.common.fallback.protocol.map.MapPreparer; +import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer; import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration; import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync; import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*; @@ -122,7 +122,7 @@ public void prepare() { final SystemTimer timer = new SystemTimer(); Sonar.get().getLogger().info("Precomputing map captcha answers..."); // Precompute captcha answers - MapPreparer.prepare(); + MapInfoPreparer.prepare(); Sonar.get().getLogger().info("Successfully precomputed captcha answers in {}s!", timer); } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapType.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/ItemMapType.java similarity index 98% rename from sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapType.java rename to sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/ItemMapType.java index 0cc9fd664..70ab896e3 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapType.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/ItemMapType.java @@ -25,7 +25,7 @@ @SuppressWarnings("unused") @RequiredArgsConstructor -public enum MapType { +public enum ItemMapType { FILLED_MAP(protocolVersion -> { // Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/items.json switch (protocolVersion) { diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java index c84363c79..a1cc75c57 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfo.java @@ -23,9 +23,6 @@ @Getter @RequiredArgsConstructor public final class MapInfo { - public static final int DIMENSIONS = 86; - public static final int SCALE = DIMENSIONS * DIMENSIONS; - private final String answer; private final int columns, rows; private final int x, y; diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java similarity index 76% rename from sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java rename to sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java index c4a8425f3..03c5b6450 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java @@ -25,7 +25,7 @@ import java.util.Random; @UtilityClass -public class MapPreparer { +public class MapInfoPreparer { private final Random RANDOM = new Random(); private final String[] FONT_TYPES = new String[] { @@ -69,16 +69,16 @@ public class MapPreparer { } }; - private MapInfo[] cached; + private PreparedMapInfo[] cached; public void prepare() { - cached = new MapInfo[Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount()]; + cached = new PreparedMapInfo[Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount()]; final String dictionary = Sonar.get().getConfig().getVerification().getMap().getDictionary(); for (int i = 0; i < cached.length; i++) { // Create image - final BufferedImage image = new BufferedImage(MapInfo.DIMENSIONS, MapInfo.DIMENSIONS, BufferedImage.TYPE_3BYTE_BGR); + final BufferedImage image = new BufferedImage(PreparedMapInfo.DIMENSIONS, PreparedMapInfo.DIMENSIONS, BufferedImage.TYPE_3BYTE_BGR); final Graphics2D graphics = image.createGraphics(); graphics.setColor(Color.WHITE); @@ -103,8 +103,10 @@ public void prepare() { int _x = image.getWidth() / 2 - stringWidth / 2; int _y = image.getHeight() / 2 + fontSize / 3; int randomOffsetX = 0, randomOffsetY = 3; + final int firstPixelX = _x, firstPixelY = _y; // Draw each character one by one + final FontMetrics fontMetrics = graphics.getFontMetrics(); for (final char c : answer.toCharArray()) { _x += randomOffsetX; _y += randomOffsetY; @@ -113,38 +115,36 @@ public void prepare() { final String character = String.valueOf(c); graphics.drawString(character, _x, _y); - _x += graphics.getFontMetrics().stringWidth(character); + _x += fontMetrics.stringWidth(character); } // Select random color palette - final int[] colorPalette = COLOR_PALETTE[RANDOM.nextInt(COLOR_PALETTE.length)]; - // Calculate x, y, width, and height - final int __x = image.getWidth() / 2 - stringWidth / 2; - final int __w = __x + stringWidth; - final int __y = image.getHeight() / 2 - fontSize / 2; - final int __h = __y + fontSize; + final int[] colorPalette = COLOR_PALETTE[i % COLOR_PALETTE.length]; + // Calculate y and height + final int pixelY = firstPixelY - fontSize; + final int height = firstPixelY + fontSize / 3; // Store image in buffer - final byte[] buffer = new byte[MapInfo.SCALE]; - for (int x = __x; x < __w; x++) { - for (int y = __y; y < __h; y++) { - final int gridIndex = y * image.getWidth() + x; + final byte[] buffer = new byte[PreparedMapInfo.SCALE]; + for (int x = firstPixelX; x < _x; x++) { + for (int y = pixelY; y < height; y++) { + final int index = y * image.getWidth() + x; final int pixel = image.getRGB(x, y); // If the pixel has no color set, set it to white/light gray if (pixel == -16777216) { - buffer[gridIndex] = (byte) (RANDOM.nextBoolean() ? 14 : 26); + buffer[index] = (byte) (RANDOM.nextInt(100) < 70 ? 14 : 26); continue; } // Set color of pixel to random color from the palette - buffer[gridIndex] = (byte) colorPalette[RANDOM.nextInt(colorPalette.length)]; + buffer[index] = (byte) colorPalette[RANDOM.nextInt(colorPalette.length)]; } } // Cache buffer to map - cached[i] = new MapInfo(answer, image.getWidth(), image.getHeight(), + cached[i] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), image.getWidth() / 4, image.getHeight() / 4, buffer); } } - public MapInfo getRandomCaptcha() { + public PreparedMapInfo getRandomCaptcha() { return cached[RANDOM.nextInt(cached.length)]; } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/PreparedMapInfo.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/PreparedMapInfo.java new file mode 100644 index 000000000..3201b9a2f --- /dev/null +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/PreparedMapInfo.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 Sonar Contributors + * + * 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 . + */ + +package xyz.jonesdev.sonar.common.fallback.protocol.map; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import xyz.jonesdev.sonar.api.fallback.FallbackUser; +import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapData; + +import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8; + +@Getter +public final class PreparedMapInfo { + public static final int DIMENSIONS = (int) Math.pow(2, 7); + public static final int SCALE = DIMENSIONS * DIMENSIONS; + + private final MapInfo info; + private final MapData[] legacy; + private final MapData modern; + + public PreparedMapInfo(final String answer, + final int columns, final int rows, + final int x, final int y, + final byte @NotNull [] buffer) { + this.info = new MapInfo(answer, columns, rows, x, y, buffer); + + // Prepare 1.7 map data using a grid + final byte[][] grid = new byte[DIMENSIONS][DIMENSIONS]; + for (int i = 0; i < buffer.length; i++) { + final byte buf = buffer[i]; + grid[i & Byte.MAX_VALUE][i >> 7] = buf; + } + this.legacy = new MapData[grid.length]; + for (int i = 0; i < grid.length; i++) { + this.legacy[i] = new MapData(0, new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i])); + } + + // Prepare 1.8+ map data + this.modern = new MapData(0, info); + } + + public void write(final @NotNull FallbackUser user) { + if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) { + for (final MapData data : legacy) { + user.delayedWrite(data); + } + } else { + user.delayedWrite(modern); + } + } +} From 90a2d79d0a5e2050c9c00c13fddcd0d7016d67ea Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Thu, 30 Nov 2023 19:00:01 +0100 Subject: [PATCH 22/29] fix: x/y offset for 1.8+ in map info --- .../fallback/protocol/map/MapInfoPreparer.java | 3 +-- .../fallback/protocol/map/PreparedMapInfo.java | 7 +++---- .../fallback/protocol/packets/play/MapData.java | 14 ++++++-------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java index 03c5b6450..47f37194d 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java @@ -139,8 +139,7 @@ public void prepare() { } } // Cache buffer to map - cached[i] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), - image.getWidth() / 4, image.getHeight() / 4, buffer); + cached[i] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer); } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/PreparedMapInfo.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/PreparedMapInfo.java index 3201b9a2f..9bdfa97a1 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/PreparedMapInfo.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/PreparedMapInfo.java @@ -35,9 +35,8 @@ public final class PreparedMapInfo { public PreparedMapInfo(final String answer, final int columns, final int rows, - final int x, final int y, final byte @NotNull [] buffer) { - this.info = new MapInfo(answer, columns, rows, x, y, buffer); + this.info = new MapInfo(answer, columns, rows, 0, 0, buffer); // Prepare 1.7 map data using a grid final byte[][] grid = new byte[DIMENSIONS][DIMENSIONS]; @@ -47,11 +46,11 @@ public PreparedMapInfo(final String answer, } this.legacy = new MapData[grid.length]; for (int i = 0; i < grid.length; i++) { - this.legacy[i] = new MapData(0, new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i])); + this.legacy[i] = new MapData(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i])); } // Prepare 1.8+ map data - this.modern = new MapData(0, info); + this.modern = new MapData(info); } public void write(final @NotNull FallbackUser user) { diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/MapData.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/MapData.java index a01d443a3..8691569cf 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/MapData.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/MapData.java @@ -34,23 +34,21 @@ @NoArgsConstructor @AllArgsConstructor public final class MapData implements FallbackPacket { - private int scale; private MapInfo mapInfo; @Override public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) { writeVarInt(byteBuf, 0); - final byte[] data = mapInfo.getBuffer(); if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) { - byteBuf.writeShort(data.length + 3); - byteBuf.writeByte(0); + byteBuf.writeShort(mapInfo.getBuffer().length + 3); + byteBuf.writeByte(0); // scaling byteBuf.writeByte(mapInfo.getX()); byteBuf.writeByte(mapInfo.getY()); - byteBuf.writeBytes(data); + byteBuf.writeBytes(mapInfo.getBuffer()); } else { - byteBuf.writeByte(scale); + byteBuf.writeByte(0); // scaling if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0 && protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) < 0) { @@ -72,8 +70,8 @@ public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersio byteBuf.writeByte(mapInfo.getX()); byteBuf.writeByte(mapInfo.getY()); - writeVarInt(byteBuf, data.length); - byteBuf.writeBytes(data); + writeVarInt(byteBuf, mapInfo.getBuffer().length); + byteBuf.writeBytes(mapInfo.getBuffer()); } } From d42d904896cc584404860f335a3aeed830f7e4ca Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Thu, 30 Nov 2023 19:04:34 +0100 Subject: [PATCH 23/29] fix: add prefix to translated messages for the map captcha --- .../sonar/api/config/SonarConfiguration.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index accea5bad..67af020d2 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -925,22 +925,22 @@ public void load() { messagesConfig.getYaml().setComment("verification.welcome", "Message that is shown to the player when they are being checked for valid gravity"); - verification.gravity.youAreBeingChecked = deserialize(messagesConfig.getString("verification.welcome", - "Please wait a moment for the verification to finish...")); + verification.gravity.youAreBeingChecked = deserialize(formatString(messagesConfig.getString("verification.welcome", + "%prefix%Please wait a moment for the verification to finish..."))); messagesConfig.getYaml().setComment("verification.captcha.enter-code", "Message that is shown to the player when they have to enter the answer to the captcha"); - verification.map.enterCode = deserialize(messagesConfig.getString("verification.captcha.enter-code", - "Please enter the code in chat that is displayed on the map.")); + verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code", + "%prefix%Please enter the code in chat that is displayed on the map."))); messagesConfig.getYaml().setComment("verification.captcha.action-bar", "Timer that is shown to the player when they have to enter the answer to the captcha" + LINE_SEPARATOR + "(Set this to '' to disable the action bar message)"); - verification.map.enterCodeActionBar = messagesConfig.getString("verification.captcha.action-bar", - "You have %time-left% seconds left to enter the code in chat"); + verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar", + "%prefix%You have %time-left% seconds left to enter the code in chat")); messagesConfig.getYaml().setComment("verification.captcha.incorrect", "Message that is shown to the player when they enter the wrong answer in chat"); - verification.map.failedCaptcha = deserialize(messagesConfig.getString("verification.captcha.incorrect", - "You have entered the wrong code. Please try again.")); + verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect", + "%prefix%You have entered the wrong code. Please try again."))); messagesConfig.getYaml().setComment("verification.too-many-players", "Disconnect message that is shown when too many players are verifying at the same time"); From c70b879c999a7b5c8715885b112cfa635477f2de Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Thu, 30 Nov 2023 19:11:07 +0100 Subject: [PATCH 24/29] feat: make some map captcha options changeable --- .../sonar/api/config/SonarConfiguration.java | 15 +++++++++++++++ .../protocol/map/MapInfoPreparer.java | 19 ++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java index 67af020d2..355c440f4 100644 --- a/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java +++ b/sonar-api/src/main/java/xyz/jonesdev/sonar/api/config/SonarConfiguration.java @@ -108,6 +108,9 @@ public enum Timing { @Getter public static final class Map { private boolean enabled; + private boolean drawBackgroundNoise; + private boolean randomizePositions; + private boolean randomizeFontSize; private int precomputeAmount; private int maxDuration; private int maxTries; @@ -459,6 +462,18 @@ public void load() { "Should Sonar make the player pass a captcha?"); verification.map.enabled = generalConfig.getBoolean("verification.checks.map-captcha.enabled", false); + generalConfig.getYaml().setComment("verification.checks.map-captcha.background-noise", + "Should Sonar draw a white/light gray rectangle behind the captcha?"); + verification.map.drawBackgroundNoise = generalConfig.getBoolean("verification.checks.map-captcha.background-noise", true); + + generalConfig.getYaml().setComment("verification.checks.map-captcha.random-position", + "Should Sonar randomize the X and Y position of the captcha?"); + verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position", true); + + generalConfig.getYaml().setComment("verification.checks.map-captcha.random-font-size", + "Should Sonar randomize the size of the font used for rendering the captcha?"); + verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size", true); + generalConfig.getYaml().setComment("verification.checks.map-captcha.precompute", "How many answers should Sonar precompute (prepare)?"); verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 10000); diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java index 47f37194d..71b26c4e9 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java @@ -86,7 +86,8 @@ public void prepare() { // Create random font final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)]; final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)]; - final int fontSize = 26 + RANDOM.nextInt(5); + final int fontSize = 30 + + (Sonar.get().getConfig().getVerification().getMap().isRandomizeFontSize() ? RANDOM.nextInt(11) : 10); @SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize); graphics.setFont(answerFont); @@ -108,10 +109,12 @@ public void prepare() { // Draw each character one by one final FontMetrics fontMetrics = graphics.getFontMetrics(); for (final char c : answer.toCharArray()) { - _x += randomOffsetX; - _y += randomOffsetY; - randomOffsetX = randomOffsetX < 0 ? 1 + RANDOM.nextInt(2) : -1 - RANDOM.nextInt(2); - randomOffsetY = randomOffsetY < 0 ? 1 + RANDOM.nextInt(4) : -1 - RANDOM.nextInt(4); + if (Sonar.get().getConfig().getVerification().getMap().isRandomizePositions()) { + _x += randomOffsetX; + _y += randomOffsetY; + randomOffsetX = randomOffsetX < 0 ? 1 + RANDOM.nextInt(2) : -1 - RANDOM.nextInt(2); + randomOffsetY = randomOffsetY < 0 ? 1 + RANDOM.nextInt(4) : -1 - RANDOM.nextInt(4); + } final String character = String.valueOf(c); graphics.drawString(character, _x, _y); @@ -129,9 +132,11 @@ public void prepare() { for (int y = pixelY; y < height; y++) { final int index = y * image.getWidth() + x; final int pixel = image.getRGB(x, y); - // If the pixel has no color set, set it to white/light gray + // If the pixel has no color set, set it to white/light gray (if enabled in the config) if (pixel == -16777216) { - buffer[index] = (byte) (RANDOM.nextInt(100) < 70 ? 14 : 26); + if (Sonar.get().getConfig().getVerification().getMap().isDrawBackgroundNoise()) { + buffer[index] = (byte) (RANDOM.nextInt(100) < 70 ? 14 : 26); + } continue; } // Set color of pixel to random color from the palette From 065bf1ac2d69a9655c5c61683b52d4ae42027b12 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Thu, 30 Nov 2023 19:11:33 +0100 Subject: [PATCH 25/29] fix: set the default font size to the average (instead of max) --- .../sonar/common/fallback/protocol/map/MapInfoPreparer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java index 71b26c4e9..eeee7171d 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/map/MapInfoPreparer.java @@ -87,7 +87,7 @@ public void prepare() { final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)]; final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)]; final int fontSize = 30 - + (Sonar.get().getConfig().getVerification().getMap().isRandomizeFontSize() ? RANDOM.nextInt(11) : 10); + + (Sonar.get().getConfig().getVerification().getMap().isRandomizeFontSize() ? RANDOM.nextInt(11) : 5); @SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize); graphics.setFont(answerFont); From fb8b2856e1f7697c7e7a34f9d968305decd88452 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Sat, 2 Dec 2023 12:53:52 +0100 Subject: [PATCH 26/29] fix: Chat packet not working on 1.17-1.18.1 --- .../protocol/FallbackPacketRegistry.java | 2 +- .../fallback/protocol/packets/play/Chat.java | 23 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java index 30175572a..4e35caf8d 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPacketRegistry.java @@ -227,7 +227,7 @@ public enum FallbackPacketRegistry { map(0x0E, MINECRAFT_1_13, false), map(0x0F, MINECRAFT_1_15, false), map(0x0E, MINECRAFT_1_16, false), - map(0x0F, MINECRAFT_1_18_2, false), + map(0x0F, MINECRAFT_1_17, false), map(0x5F, MINECRAFT_1_19, false), map(0x62, MINECRAFT_1_19_1, false), map(0x60, MINECRAFT_1_19_3, false), diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java index e86a99321..2372e7ad0 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/Chat.java @@ -41,9 +41,15 @@ @AllArgsConstructor public final class Chat implements FallbackPacket { private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L); + private static final int DIV_FLOOR = -Math.floorDiv(-20, 8); + + public static final byte CHAT_TYPE = (byte) 0; + public static final byte SYSTEM_TYPE = (byte) 1; + public static final byte GAME_INFO_TYPE = (byte) 2; + private Component component; private String message; - private byte position; + private byte type; private boolean signedPreview; private boolean unsigned = false; private Instant timestamp; @@ -51,11 +57,6 @@ public final class Chat implements FallbackPacket { private long salt; private boolean signed; private byte[] signature; - private static final int DIV_FLOOR = -Math.floorDiv(-20, 8); - - public static final byte CHAT_TYPE = (byte) 0; - public static final byte SYSTEM_TYPE = (byte) 1; - public static final byte GAME_INFO_TYPE = (byte) 2; // Clientbound LegacyChat public Chat(final Component component) { @@ -63,8 +64,8 @@ public Chat(final Component component) { } // Clientbound LegacyChat - public Chat(final Component component, final byte position) { - this(component, null, position, false, false, + public Chat(final Component component, final byte type) { + this(component, null, type, false, false, null, null, 0L, false, null); } @@ -76,11 +77,11 @@ public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protoco // Type if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) { - byteBuf.writeBoolean(position == GAME_INFO_TYPE); + byteBuf.writeBoolean(type == GAME_INFO_TYPE); } else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) { - writeVarInt(byteBuf, position); + writeVarInt(byteBuf, type); } else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) { - byteBuf.writeByte(position); + byteBuf.writeByte(type); } // Sender From 558a54ab40644dad6cfb978acf7db872615b2a46 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Sat, 2 Dec 2023 12:57:29 +0100 Subject: [PATCH 27/29] fix: only send DefaultSpawnPosition to >=1.18.2 clients --- .../fallback/FallbackVerificationHandler.java | 74 ++++++++++++------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 2779f591f..180a45039 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -20,7 +20,6 @@ import io.netty.buffer.ByteBuf; import io.netty.handler.codec.CorruptedFrameException; import io.netty.handler.codec.DecoderException; -import lombok.Setter; import lombok.val; import net.kyori.adventure.text.minimessage.MiniMessage; import org.jetbrains.annotations.NotNull; @@ -50,26 +49,28 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*; public final class FallbackVerificationHandler implements FallbackPacketListener { + private static final Random RANDOM = new Random(); + + // General + private final SystemTimer login = new SystemTimer(); private final @NotNull FallbackUser user; private final String username; private final UUID playerUuid; + private @NotNull State state = State.LOGIN_ACK; // 1.20.2 + + // Checks private short expectedTransactionId; - private long expectedKeepAliveId; - private int expectedTeleportId = -1; - private int tick, totalReceivedPackets; - private int ignoredMovementTicks; + private int expectedKeepAliveId, expectedTeleportId = -1; + private int tick, totalReceivedPackets, ignoredMovementTicks; private double posX, posY, posZ, lastY; private boolean resolvedClientBrand, resolvedClientSettings; - @Setter - private @NotNull State state = State.LOGIN_ACK; private boolean listenForMovements; - private @Nullable PreparedMapInfo captcha; - private int captchaTriesLeft; - private final SystemTimer login = new SystemTimer(); + // Map captcha private final SystemTimer keepAlive = new SystemTimer(); private final SystemTimer actionBar = new SystemTimer(); - private static final Random RANDOM = new Random(); + private @Nullable PreparedMapInfo captcha; + private int captchaTriesLeft; public enum State { // 1.20.2 configuration state @@ -154,7 +155,8 @@ private void sendJoinGamePacket() { final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0; if (!v1_20_2) { // Set the state to CLIENT_SETTINGS to avoid false positives - // and go on with the flow of the verification. + // (1.20.2 needs the LOGIN_ACK state) and go on with + // the flow of the verification. state = State.CLIENT_SETTINGS; } // Select the JoinGame packet for the respective protocol version @@ -179,8 +181,10 @@ private void sendAbilitiesAndTeleport() { SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION, 0f, -90f, expectedTeleportId, false)); // Make sure the player escapes the 1.18.2+ "Loading terrain" screen - user.delayedWrite(new DefaultSpawnPosition( - SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION, 0f)); + if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) { + user.delayedWrite(new DefaultSpawnPosition( + SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION, 0f)); + } } private void sendChunkData() { @@ -527,32 +531,24 @@ private void handlePositionUpdate(final double x, final double y, final double z tick++; } - private void assertState(final @NotNull State expectedState) { - checkFrame(state == expectedState, "expected " + expectedState + ", got " + state); - } - - private void checkFrame(final boolean condition, final String message) { - if (!condition) { - user.fail(message); - throw new CorruptedFrameException(message); - } - } - private void captchaOrFinish() { if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) { - if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) { + // Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets + state = State.MAP_CAPTCHA; + if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled() + && user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) { // Make sure the player escapes the 1.18.2+ "Loading terrain" screen user.delayedWrite(CAPTCHA_SPAWN_POSITION); } + // Initialize the map captcha handleMapCaptcha(); } else { + // Finish the verification finish(); } } private void handleMapCaptcha() { - state = State.MAP_CAPTCHA; - // Reset max tries captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries(); @@ -592,4 +588,26 @@ private void finish() { .replace("%name%", username) .replace("%time%", login.toString())); } + + /** + * Fails the verification if a certain state is unexpected. + * + * @param expectedState Expected state + */ + private void assertState(final @NotNull State expectedState) { + checkFrame(state == expectedState, "expected " + expectedState + ", got " + state); + } + + /** + * Checks if a certain condition is met, fails the verification if not. + * + * @param condition Condition to fail if it's false + * @param message Messages displayed in the stacktrace + */ + private void checkFrame(final boolean condition, final String message) { + if (!condition) { + user.fail(message); + throw new CorruptedFrameException(message); + } + } } From 158129f80e6542f2bb0e54f060ddc810862f656e Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Sat, 2 Dec 2023 14:04:01 +0100 Subject: [PATCH 28/29] fix: return if captcha is correct to prevent further code execution --- .../sonar/common/fallback/FallbackVerificationHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 180a45039..1dc6eb751 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -262,6 +262,7 @@ public void handle(final @NotNull FallbackPacket packet) { } // Captcha is correct finish(); + return; } // Every second From cfc489c47329a6afb09c5f9a820b2745ba7a0081 Mon Sep 17 00:00:00 2001 From: Michel Elkenwaat Date: Sat, 2 Dec 2023 14:10:37 +0100 Subject: [PATCH 29/29] feat: cache dynamic DefaultSpawnPosition packet --- .../sonar/common/fallback/FallbackVerificationHandler.java | 3 +-- .../sonar/common/fallback/protocol/FallbackPreparer.java | 6 +++++- .../protocol/packets/play/DefaultSpawnPosition.java | 3 +-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java index 1dc6eb751..e0a4ab559 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/FallbackVerificationHandler.java @@ -182,8 +182,7 @@ private void sendAbilitiesAndTeleport() { 0f, -90f, expectedTeleportId, false)); // Make sure the player escapes the 1.18.2+ "Loading terrain" screen if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) { - user.delayedWrite(new DefaultSpawnPosition( - SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION, 0f)); + user.delayedWrite(dynamicSpawnPosition); } } diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java index 0822b44df..17ae87932 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/FallbackPreparer.java @@ -50,6 +50,8 @@ public class FallbackPreparer { public FallbackPacket joinGame; // Update Section Blocks public FallbackPacket updateSectionBlocks; + // Default Spawn Position + public FallbackPacket dynamicSpawnPosition; // Collisions public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum) @@ -61,8 +63,9 @@ public class FallbackPreparer { public final FallbackPacket CAPTCHA_POSITION = new PositionLook( SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false); public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPosition( - SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f); + SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION); + // Blocks private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW]; public int maxMovementTick, dynamicSpawnYPosition; @@ -93,6 +96,7 @@ public void prepare() { // Set the dynamic block and collide Y position based on the maximum fall distance dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance); + dynamicSpawnPosition = new DefaultSpawnPosition(SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION); // Prepare collision platform positions int index = 0; diff --git a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/DefaultSpawnPosition.java b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/DefaultSpawnPosition.java index 91a1babc6..59a745a76 100644 --- a/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/DefaultSpawnPosition.java +++ b/sonar-common/src/main/java/xyz/jonesdev/sonar/common/fallback/protocol/packets/play/DefaultSpawnPosition.java @@ -35,7 +35,6 @@ @AllArgsConstructor public final class DefaultSpawnPosition implements FallbackPacket { private int x, y, z; - private float angle; @Override public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) { @@ -51,7 +50,7 @@ public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protoco byteBuf.writeLong(encoded); if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) { - byteBuf.writeFloat(angle); + byteBuf.writeFloat(0f); } } }