Compare commits

...
3 Commits
Author SHA1 Message Date
Jones 8db3d0257f Release 2.0.11 (#129) 2023-12-17 14:50:36 +01:00
Jones 106a44f78a Recode map captcha so it's harder for AI to solve (#128)
* feat: make map captcha harder for AI to solve

* feat: make distortions configurable

* fix: use correct distortions factor dividor

* fix: memory issues with map precomputing/preparing

* fix: characters sometimes ended up out of the frame

* fix: don't randomize x position of the character

* fix: font size issue

* feat: optimize FallbackChannelHandler

* feat: make captcha timing configurable

* fix: 1.7 and 1.8 clients dying because of my stupidity
2023-12-17 14:47:42 +01:00
Jones a5872f1b6e Fix Chat packet using the wrong packet id in 1.20.2 (#127) 2023-12-15 17:54:22 +01:00
10 changed files with 273 additions and 129 deletions

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.10
version=2.0.11
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
@@ -27,8 +27,10 @@ import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.dependencies.Dependency;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.awt.*;
import java.io.File;
import java.net.InetAddress;
import java.util.List;
import java.util.*;
import java.util.regex.Pattern;
@@ -105,10 +107,13 @@ public final class SonarConfiguration {
@Getter
public static final class Map {
private boolean enabled;
private boolean drawBackgroundNoise;
private Timing timing;
private boolean randomizePositions;
private boolean randomizeFontSize;
private double distortionsFactorX;
private double distortionsFactorY;
private int randomLinesAmount;
private int randomOvalsAmount;
private int precomputeAmount;
private int maxDuration;
private int maxTries;
@@ -116,6 +121,7 @@ public final class SonarConfiguration {
private Component enterCode;
private Component failedCaptcha;
private String enterCodeActionBar;
private List<String> fonts;
}
@Getter
@@ -169,6 +175,7 @@ public final class SonarConfiguration {
private Component alreadyQueued;
private Component blacklisted;
private Component protocolBlacklisted;
private Component currentlyPreparing;
}
@Getter
@@ -401,15 +408,13 @@ public final class SonarConfiguration {
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.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity" +
".max-movement-ticks", 8),
2, 100);
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.gravity.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"
@@ -418,31 +423,53 @@ public final class SonarConfiguration {
+ 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.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.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);
"Make the player type a code from a virtual map in chat");
generalConfig.getYaml().setComment("verification.checks.map-captcha.timing",
"When should Sonar make the player solve a captcha?"
+ LINE_SEPARATOR + "Possible types: ALWAYS, DURING_ATTACK, NEVER"
+ LINE_SEPARATOR + "- ALWAYS: New players will always receive a captcha"
+ LINE_SEPARATOR + "- DURING_ATTACK: New players will only receive a captcha during an attack"
+ LINE_SEPARATOR + "- NEVER: New players will never receive a captcha (Recommended)");
verification.map.timing = Verification.Timing.valueOf(
generalConfig.getString("verification.checks.map-captcha.timing",
Verification.Timing.NEVER.name()).toUpperCase());
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.distortions-factor-x",
"How much should Sonar distort characters (factor for randomization)?");
verification.map.distortionsFactorX = clamp(generalConfig.getInt(
"verification.checks.map-captcha.distortions-factor-x", 50), 0, 100) / 100D;
generalConfig.getYaml().setComment("verification.checks.map-captcha.distortions-factor-y",
"How much should Sonar distort characters (factor for randomization)?");
verification.map.distortionsFactorY = clamp(generalConfig.getInt(
"verification.checks.map-captcha.distortions-factor-y", 50), 0, 100) / 100D;
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-lines",
"How many random lines behind the captcha should Sonar draw?");
verification.map.randomLinesAmount = generalConfig.getInt("verification.checks.map-captcha.random-lines", 4);
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-ovals",
"How many random ovals behind the captcha should Sonar draw?");
verification.map.randomOvalsAmount = generalConfig.getInt("verification.checks.map-captcha.random-ovals", 1);
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)?");
"How many answers should Sonar precompute (prepare)?"
+ LINE_SEPARATOR + "This task happens asynchronously in the background;"
+ LINE_SEPARATOR + "Players are able to join once one captcha has been prepared");
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 1000);
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-duration",
@@ -457,6 +484,11 @@ public final class SonarConfiguration {
"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");
generalConfig.getYaml().setComment("verification.checks.map-captcha.fonts",
"Which font types should Sonar use for the map captcha codes?");
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.fonts",
Arrays.asList(Font.DIALOG, Font.DIALOG_INPUT, Font.SERIF, Font.SANS_SERIF));
generalConfig.getYaml().setComment("verification.checks.valid-name-regex",
"Regex for validating usernames during verification");
verification.validNameRegex = Pattern.compile(generalConfig.getString(
@@ -891,6 +923,17 @@ public final class SonarConfiguration {
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect",
"%prefix%<red>You have entered the wrong code. Please try again.")));
messagesConfig.getYaml().setComment("verification.currently-preparing",
"Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet");
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList(
"verification.currently-preparing",
Arrays.asList(
"%header%",
"<yellow>Your anti-bot data has not been prepared yet.",
"<gray>Please wait a few seconds before trying to verify again.",
"%footer%"
))));
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",
@@ -20,7 +20,6 @@ package xyz.jonesdev.sonar.api.fallback;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
@@ -67,7 +66,14 @@ public final class Fallback {
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean shouldVerifyNewPlayers() {
val timing = Sonar.get().getConfig().getVerification().getTiming();
return performTimingCheck(Sonar.get().getConfig().getVerification().getTiming());
}
public boolean shouldDoMapCaptcha() {
return performTimingCheck(Sonar.get().getConfig().getVerification().getMap().getTiming());
}
private boolean performTimingCheck(final SonarConfiguration.Verification.Timing timing) {
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
@@ -224,7 +224,7 @@ public final class FallbackInitialHandler extends InitialHandler {
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData()));
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData(), inetAddress));
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
@@ -24,20 +24,21 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import java.net.InetSocketAddress;
import java.net.InetAddress;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
private final @NotNull String username;
private final @NotNull InetAddress inetAddress;
@Override
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
// Remove the IP address from the queue
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
FALLBACK.getQueue().remove(inetAddress);
// Make sure other handlers also know we've disconnected
ctx.fireChannelUnregistered();
}
}
@@ -537,7 +537,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
private void captchaOrFinish() {
if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) {
if (user.getFallback().shouldDoMapCaptcha()) {
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
// This should not happen, but we have to return if there is no captcha prepared
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
return;
}
// 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()
@@ -235,7 +235,7 @@ public enum FallbackPacketRegistry {
map(0x60, MINECRAFT_1_19_3, false),
map(0x64, MINECRAFT_1_19_4, false),
map(0x67, MINECRAFT_1_20_2, false),
map(0x69, MINECRAFT_1_20_2, false));
map(0x69, MINECRAFT_1_20_3, false));
clientbound.register(GameEvent.class, GameEvent::new,
map(0x20, MINECRAFT_1_20_3, true));
@@ -19,7 +19,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
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;
@@ -121,15 +121,12 @@ public class FallbackPreparer {
youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked());
}
if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) {
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER) {
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
MapInfoPreparer.prepare();
Sonar.get().getLogger().info("Successfully precomputed captcha answers in {}s!", timer);
}
}
}
@@ -17,29 +17,37 @@
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.awt.Font.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo.DIMENSIONS;
@UtilityClass
public class MapInfoPreparer {
private final Random RANDOM = new Random();
private final String[] FONT_TYPES = new String[]{
Font.DIALOG_INPUT,
Font.DIALOG,
Font.SANS_SERIF,
Font.SERIF
};
private final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
private final int[] FONT_STYLES = new int[]{
Font.PLAIN,
Font.BOLD,
Font.ITALIC,
Font.ITALIC | Font.BOLD
PLAIN,
BOLD,
ITALIC,
ITALIC | BOLD
};
private final int[][] COLOR_PALETTE = new int[][]{
@@ -49,11 +57,11 @@ public class MapInfoPreparer {
50,
51
},
new int[]{ // Black
27,
45,
46,
47,
new int[]{ // Orange
60,
61,
62,
63,
},
new int[]{ // Green
4,
@@ -61,6 +69,12 @@ public class MapInfoPreparer {
29,
30,
},
new int[]{ // Black
27,
45,
46,
47,
},
new int[]{ // Red
16,
17,
@@ -70,85 +84,164 @@ public class MapInfoPreparer {
};
private PreparedMapInfo[] cached;
public void prepare() {
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(PreparedMapInfo.DIMENSIONS, PreparedMapInfo.DIMENSIONS,
BufferedImage.TYPE_3BYTE_BGR);
final Graphics2D graphics = image.createGraphics();
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 = 30
+ (Sonar.get().getConfig().getVerification().getMap().isRandomizeFontSize() ? RANDOM.nextInt(11) : 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())));
}
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 = 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()) {
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);
_x += fontMetrics.stringWidth(character);
}
// Select random color palette
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[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 enabled in the config)
if (pixel == -16777216) {
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
buffer[index] = (byte) colorPalette[RANDOM.nextInt(colorPalette.length)];
}
}
// Cache buffer to map
cached[i] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
}
}
@Getter
private int preparedCAPTCHAs;
private boolean currentlyPreparing;
public PreparedMapInfo getRandomCaptcha() {
return cached[RANDOM.nextInt(cached.length)];
return cached[RANDOM.nextInt(preparedCAPTCHAs)];
}
public void prepare() {
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Precomputing map captcha answers...");
// Make sure we're not running into concurrency issues
if (currentlyPreparing) {
Sonar.get().getLogger().warn("Did not precompute map captcha answers as another task is running.");
return;
}
// Reset all statistics
preparedCAPTCHAs = 0;
currentlyPreparing = true;
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
cached = null;
cached = new PreparedMapInfo[config.getPrecomputeAmount()];
// Prepare fonts from config
final String dictionary = config.getDictionary();
final List<String> fonts = config.getFonts();
if (fonts.isEmpty()) {
Sonar.get().getLogger().warn("No fonts found, using fallback font...");
fonts.add(DIALOG);
}
final String[] fontTypes = fonts.toArray(new String[0]);
// Prepare distortions values from config
final double distortionsFactorX = config.getDistortionsFactorX();
final double distortionsFactorY = config.getDistortionsFactorY();
final double halfDistortionsFactorX = distortionsFactorX / 2D;
final double halfDistortionsFactorY = distortionsFactorY / 2D;
for (int _i = 0; _i < cached.length; _i++) {
final int currentIndex = _i;
final byte[] buffer = new byte[PreparedMapInfo.SCALE];
PREPARATION_SERVICE.execute(() -> {
// Create image
final BufferedImage image = new BufferedImage(DIMENSIONS, DIMENSIONS, BufferedImage.TYPE_3BYTE_BGR);
final Graphics2D graphics = (Graphics2D) image.getGraphics();
try {
// Create random font
final String fontType = fontTypes[RANDOM.nextInt(fontTypes.length)];
final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)];
final int fontSize = 27
+ (config.isRandomizeFontSize()
? RANDOM.nextInt(6) - 2 : 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 _j = 0; _j < 5; _j++) {
answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length())));
}
final String answer = answerBuilder.toString();
// Calculate text position
final int stringWidth = graphics.getFontMetrics().stringWidth(answer);
final int halfWidth = image.getWidth() / 2;
final int halfHeight = image.getHeight() / 2;
final int spacing = 5;
double _x = halfWidth - stringWidth / 2f - spacing;
double _y = halfHeight + fontSize / 3f;
// Draw each character one by one
final FontRenderContext fontRenderContext = graphics.getFontRenderContext();
for (final char c : answer.toCharArray()) {
// Randomize x and y
if (config.isRandomizePositions()) {
_y += RANDOM.nextInt(8) - 4;
}
// Apply random distortion
final double distortionFactorX = RANDOM.nextDouble() * distortionsFactorX - halfDistortionsFactorX;
final double distortionFactorY = RANDOM.nextDouble() * distortionsFactorY - halfDistortionsFactorY;
// Create a GlyphVector for the character
final String character = String.valueOf(c);
final GlyphVector glyphVector = graphics.getFont().createGlyphVector(fontRenderContext, character);
// Apply the distortion to the GlyphVector using AffineTransform
final AffineTransform transform = AffineTransform.getTranslateInstance(_x, _y);
transform.shear(distortionFactorX, distortionFactorY);
final Shape distortedCharacter = glyphVector.getOutline();
final Shape distorted = transform.createTransformedShape(distortedCharacter);
// Draw the distorted character
graphics.fill(distorted);
// Update x by width
_x += glyphVector.getVisualBounds().getWidth() + spacing;
}
// TODO: Color converter
graphics.setColor(Color.RED);
// Draw random lines
for (int i = 0; i < config.getRandomLinesAmount(); i++) {
final int startX = RANDOM.nextInt(halfWidth);
final int startY = RANDOM.nextInt(halfHeight);
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
graphics.drawLine(startX, startY, endX, endY);
}
// Draw random ovals
for (int i = 0; i < config.getRandomOvalsAmount(); i++) {
final int startX = RANDOM.nextInt(halfWidth);
final int startY = RANDOM.nextInt(halfHeight);
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
graphics.drawOval(startX, startY, endX, endY);
}
// Select random color palette
final int[] colorPalette = COLOR_PALETTE[currentIndex % COLOR_PALETTE.length];
// Select the next color palette for geometry
final int[] nextColorPalette = COLOR_PALETTE[(currentIndex + 1) % COLOR_PALETTE.length];
// Clear background
Arrays.fill(buffer, (byte) 57);
// Color every pixel individually
final int spacingY = halfHeight - fontSize;
for (int x = spacing; x < image.getWidth() - spacing; x++) {
for (int y = spacingY; y < image.getHeight() - spacingY; y++) {
final int pixel = image.getRGB(x, y);
final int index = y * image.getWidth() + x;
if (pixel == -16777216 && RANDOM.nextInt(100) < 97) continue;
// Set color of pixel to random color from the palette
final byte color = (byte) (pixel == -16777216 ? RANDOM.nextInt(100) < 75 ? 14 : RANDOM.nextInt(Byte.MAX_VALUE)
: pixel != -65536 ? colorPalette[RANDOM.nextInt(colorPalette.length)]
: nextColorPalette[RANDOM.nextInt(nextColorPalette.length)]);
// Write pixel color to buffer
buffer[index] = color;
}
}
// Cache buffer to map
cached[currentIndex] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
} finally {
// Dispose graphics
graphics.dispose();
// Finished?
if (currentIndex == cached.length - 1) {
Sonar.get().getLogger().info("Successfully precomputed map captcha answers in {}s!", timer);
currentlyPreparing = false;
}
preparedCAPTCHAs++;
}
});
}
}
}
@@ -228,7 +228,7 @@ public final class FallbackListener {
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername()));
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername(), inetAddress));
// Queue the connection for further processing
fallback.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
@@ -277,8 +277,7 @@ public final class FallbackListener {
final FallbackUserWrapper user = new FallbackUserWrapper(
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(protocolId)
);
ProtocolVersion.fromId(protocolId));
// Disconnect if the protocol version could not be resolved
if (user.getProtocolVersion().isUnknown()) {