mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 17:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
475bf28ecf | ||
|
|
9c29753084 | ||
|
|
f0f5196fbe | ||
|
|
d98ce28a59 | ||
|
|
e0d5c67cdf | ||
|
|
65e059368c | ||
|
|
b7a031da12 | ||
|
|
e15206a382 | ||
|
|
5e4af3053e |
No files matched your search
+2
-1
@@ -6,7 +6,7 @@ buildscript {
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("io.github.goooler.shadow") version "8.1.5"
|
||||
id("io.github.goooler.shadow") version "8.1.7"
|
||||
id("io.toolebox.git-versioner") version "1.6.7"
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ tasks {
|
||||
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
|
||||
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
|
||||
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
|
||||
relocate("xyz.jonesdev.capja", "xyz.jonesdev.sonar.libs.capja")
|
||||
|
||||
// TODO: actually fix this :/
|
||||
// We have to be careful here, so we don't accidentally break adventure
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.22
|
||||
version=2.0.24
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
@@ -173,24 +173,28 @@ public final class SonarConfiguration {
|
||||
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
|
||||
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
|
||||
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
|
||||
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
|
||||
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
|
||||
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks"), 1, 128);
|
||||
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
|
||||
|
||||
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
|
||||
verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position");
|
||||
verification.map.distortionsFactorX = clamp(generalConfig.getInt(
|
||||
"verification.checks.map-captcha.distortions-factor-x"), 0, 100) / 100D;
|
||||
verification.map.distortionsFactorY = clamp(generalConfig.getInt(
|
||||
"verification.checks.map-captcha.distortions-factor-y"), 0, 100) / 100D;
|
||||
verification.map.randomLinesAmount = generalConfig.getInt("verification.checks.map-captcha.random-lines");
|
||||
verification.map.randomOvalsAmount = generalConfig.getInt("verification.checks.map-captcha.random-ovals");
|
||||
verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size");
|
||||
|
||||
verification.map.flare = generalConfig.getBoolean("verification.checks.map-captcha.effects.flare");
|
||||
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
|
||||
verification.map.ripple = generalConfig.getBoolean("verification.checks.map-captcha.effects.ripple");
|
||||
verification.map.smear = generalConfig.getBoolean("verification.checks.map-captcha.effects.smear");
|
||||
verification.map.pinch = generalConfig.getBoolean("verification.checks.map-captcha.effects.pinch");
|
||||
verification.map.saturation = (float) generalConfig.getYaml().getDouble(
|
||||
"verification.checks.map-captcha.effects.saturation", 0.3);
|
||||
verification.map.distortion = (float) generalConfig.getYaml().getDouble(
|
||||
"verification.checks.map-captcha.effects.distortion", 2);
|
||||
|
||||
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute");
|
||||
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration");
|
||||
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries");
|
||||
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary");
|
||||
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.fonts");
|
||||
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.font-names");
|
||||
|
||||
verification.brand.enabled = generalConfig.getBoolean("verification.checks.client-brand.enabled");
|
||||
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
|
||||
@@ -436,12 +440,13 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
public static final class Map {
|
||||
private Timing timing;
|
||||
private boolean randomizePositions;
|
||||
private boolean randomizeFontSize;
|
||||
private double distortionsFactorX;
|
||||
private double distortionsFactorY;
|
||||
private int randomLinesAmount;
|
||||
private int randomOvalsAmount;
|
||||
private boolean flare;
|
||||
private boolean scratches;
|
||||
private boolean ripple;
|
||||
private boolean smear;
|
||||
private boolean pinch;
|
||||
private float saturation;
|
||||
private float distortion;
|
||||
private int precomputeAmount;
|
||||
private int maxDuration;
|
||||
private int maxTries;
|
||||
@@ -456,6 +461,7 @@ public final class SonarConfiguration {
|
||||
public static final class Gravity {
|
||||
private boolean enabled;
|
||||
private boolean checkCollisions;
|
||||
private boolean captchaOnFail;
|
||||
private Gamemode gamemode;
|
||||
private int maxMovementTicks;
|
||||
private int maxIgnoredTicks;
|
||||
|
||||
+18
-11
@@ -27,6 +27,7 @@ import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
@@ -43,7 +44,6 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class VerifiedPlayerController {
|
||||
private static final Map<String, Collection<UUID>> MAP = new ConcurrentHashMap<>();
|
||||
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
|
||||
@@ -64,6 +64,7 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
};
|
||||
|
||||
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
|
||||
private @Nullable ConnectionSource connectionSource;
|
||||
private Dao<VerifiedPlayer, Integer> dao;
|
||||
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
|
||||
@@ -198,7 +199,7 @@ public final class VerifiedPlayerController {
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
private void _remove(final @NotNull String inetAddress) {
|
||||
MAP.remove(inetAddress);
|
||||
cache.remove(inetAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,24 +237,30 @@ public final class VerifiedPlayerController {
|
||||
* @param player VerifiedPlayer model
|
||||
*/
|
||||
private void _add(final @NotNull VerifiedPlayer player) {
|
||||
MAP.computeIfAbsent(player.getInetAddress(), v -> new Vector<>())
|
||||
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
|
||||
.add(player.getPlayerUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of verified IP addresses
|
||||
*
|
||||
* @return Estimated size of the local cache
|
||||
*/
|
||||
public synchronized int estimatedSize() {
|
||||
return MAP.values().stream()
|
||||
.mapToInt(Collection::size)
|
||||
.sum();
|
||||
/*
|
||||
* TODO: Work on this
|
||||
* return MAP.values().stream()
|
||||
* .mapToInt(Collection::size)
|
||||
* .sum();
|
||||
*/
|
||||
return cache.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return List of UUIDs associated with one IP address
|
||||
*/
|
||||
public Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
|
||||
return MAP.getOrDefault(inetAddress, Collections.emptyList());
|
||||
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
|
||||
return cache.getOrDefault(inetAddress, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,7 +269,7 @@ public final class VerifiedPlayerController {
|
||||
*/
|
||||
public void clearAll() {
|
||||
try {
|
||||
MAP.clear();
|
||||
cache.clear();
|
||||
|
||||
// Only update the column if the database type is not NONE
|
||||
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
|
||||
@@ -279,7 +286,7 @@ public final class VerifiedPlayerController {
|
||||
* @return Whether the local cache contains the IP and UUID
|
||||
*/
|
||||
public boolean has(final @NotNull String inetAddress, final @NotNull UUID uuid) {
|
||||
final Collection<UUID> got = MAP.get(inetAddress);
|
||||
final Collection<UUID> got = cache.get(inetAddress);
|
||||
if (got != null) {
|
||||
return got.contains(uuid);
|
||||
}
|
||||
@@ -301,7 +308,7 @@ public final class VerifiedPlayerController {
|
||||
* @return Whether the local cache contains the IP
|
||||
*/
|
||||
public boolean has(final @NotNull String inetAddress) {
|
||||
return MAP.containsKey(inetAddress);
|
||||
return cache.containsKey(inetAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
|
||||
implementation("xyz.jonesdev.capja:capja:0.1.2")
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
+39
-26
@@ -32,8 +32,7 @@ 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.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.map.MapCaptchaInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
@@ -73,7 +72,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Map captcha
|
||||
private final SystemTimer keepAlive = new SystemTimer();
|
||||
private final SystemTimer actionBar = new SystemTimer();
|
||||
private @Nullable PreparedMapInfo captcha;
|
||||
private @Nullable MapCaptchaInfo captcha;
|
||||
private int captchaTriesLeft;
|
||||
|
||||
public enum State {
|
||||
@@ -262,9 +261,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// Handle incoming chat messages
|
||||
if (packet instanceof UniversalChatPacket) {
|
||||
checkFrame(captcha != null, "Captcha not sent yet");
|
||||
final UniversalChatPacket chat = (UniversalChatPacket) packet;
|
||||
Objects.requireNonNull(captcha);
|
||||
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
|
||||
if (!chat.getMessage().equals(captcha.getAnswer())) {
|
||||
// Captcha is incorrect
|
||||
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
@@ -473,6 +472,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
tick = 1;
|
||||
posY = -1;
|
||||
expectedTeleportId = -1;
|
||||
// Check for ground state in the first packet
|
||||
checkFrame(!ground, "invalid ground state");
|
||||
}
|
||||
|
||||
posX = x;
|
||||
@@ -533,8 +534,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
if (ground) {
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
|
||||
// The player is colliding to blocks, finish verification
|
||||
captchaOrFinish();
|
||||
// Check if the player is not spoofing ground
|
||||
// We cannot use checkFrame as it interferes with the CAPTCHA
|
||||
if (collisionOffsetY > -1) {
|
||||
// The player is colliding to blocks, finish verification
|
||||
captchaOrFinish();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// Make sure the player is colliding with blocks but is sending an invalid ground state
|
||||
@@ -567,27 +572,27 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
private void captchaOrFinish() {
|
||||
if (FALLBACK.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()
|
||||
&& 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();
|
||||
handleCAPTCHA();
|
||||
return;
|
||||
}
|
||||
// Finish the verification
|
||||
finish();
|
||||
}
|
||||
|
||||
private void handleMapCaptcha() {
|
||||
private void handleCAPTCHA() {
|
||||
if (!MAP_INFO_PREPARER.isCaptchaAvailable()) {
|
||||
// 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()
|
||||
&& 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);
|
||||
}
|
||||
// Reset max tries
|
||||
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
@@ -595,8 +600,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
user.delayedWrite(new SetSlotPacket(0, 36, 1, 0,
|
||||
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
|
||||
// Send random captcha to the player
|
||||
captcha = MapInfoPreparer.getRandomCaptcha();
|
||||
Objects.requireNonNull(captcha).write(user);
|
||||
captcha = MAP_INFO_PREPARER.getRandomCaptcha();
|
||||
Objects.requireNonNull(captcha).delayedWrite(user);
|
||||
// Teleport the player to the position above the platform
|
||||
user.delayedWrite(CAPTCHA_POSITION);
|
||||
// Make sure the player cannot move
|
||||
@@ -608,6 +613,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
// Something must've gone horribly wrong...
|
||||
if (state == State.SUCCESS) return;
|
||||
|
||||
state = State.SUCCESS;
|
||||
|
||||
// Increment amount of total successful verifications
|
||||
@@ -646,6 +654,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
*/
|
||||
private void checkFrame(final boolean condition, final String message) {
|
||||
if (!condition) {
|
||||
if (state == State.POSITION // Gravity check
|
||||
&& Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
handleCAPTCHA();
|
||||
return;
|
||||
}
|
||||
user.fail(message);
|
||||
throw new CorruptedFrameException(message);
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.netty;
|
||||
package xyz.jonesdev.sonar.common.fallback.netty;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufHolder;
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.netty;
|
||||
package xyz.jonesdev.sonar.common.fallback.netty;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
-4
@@ -98,10 +98,6 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
}
|
||||
} else {
|
||||
// Packets are always ByteBufs - if we detect an unknown object, fail verification
|
||||
user.fail("packet is not a ByteBuf");
|
||||
throw new CorruptedFrameException("Packet is not a ByteBuf?!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-3
@@ -23,7 +23,7 @@ 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;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.CaptchaPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
@@ -82,6 +82,9 @@ public class FallbackPreparer {
|
||||
public double[] preparedCachedYMotions;
|
||||
public double maxFallDistance;
|
||||
|
||||
// CAPTCHA
|
||||
public final CaptchaPreparer MAP_INFO_PREPARER = new CaptchaPreparer();
|
||||
|
||||
public void prepare() {
|
||||
joinGame = new JoinGamePacket(1,
|
||||
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
|
||||
@@ -128,12 +131,13 @@ public class FallbackPreparer {
|
||||
invalidUsername = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidUsername(), true);
|
||||
|
||||
// Precompute captcha answers
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER) {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|
||||
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
enterCodeMessage = new UniversalChatPacket(Sonar.get().getConfig().getVerification().getMap().getEnterCode(), UniversalChatPacket.SYSTEM_TYPE);
|
||||
incorrectCaptcha = new UniversalChatPacket(
|
||||
Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha(), UniversalChatPacket.SYSTEM_TYPE);
|
||||
|
||||
MapInfoPreparer.prepare();
|
||||
MAP_INFO_PREPARER.prepare();
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import xyz.jonesdev.capja.CaptchaGenerator;
|
||||
import xyz.jonesdev.capja.CaptchaHolder;
|
||||
import xyz.jonesdev.capja.config.CaptchaConfiguration;
|
||||
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.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class CaptchaPreparer {
|
||||
private static final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private MapCaptchaInfo[] cached;
|
||||
private int preparedAmount;
|
||||
|
||||
public void prepare() {
|
||||
// Make sure we're not preparing when Sonar is already preparing answers
|
||||
if (cached != null && preparedAmount != cached.length) return;
|
||||
preparedAmount = 0;
|
||||
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Asynchronously preparing CAPTCHA answers...");
|
||||
Sonar.get().getLogger().info("Players will be able to join even if the preparation isn't finished");
|
||||
|
||||
// Prepare cache
|
||||
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
|
||||
cached = new MapCaptchaInfo[config.getPrecomputeAmount()];
|
||||
|
||||
// Prepare everything asynchronously
|
||||
PREPARATION_SERVICE.execute(() -> {
|
||||
final List<String> fonts = config.getFonts();
|
||||
if (fonts.isEmpty()) {
|
||||
throw new IllegalArgumentException("No fonts specified!");
|
||||
}
|
||||
|
||||
// Create the configuration for the image generation
|
||||
final String[] fontNames = fonts.toArray(new String[0]);
|
||||
final CaptchaConfiguration configuration = new CaptchaConfiguration(
|
||||
128, 128, config.getDictionary().toCharArray(), 5,
|
||||
config.isFlare(), config.isScratches(), config.isRipple(), config.isSmear(), config.isPinch(),
|
||||
config.getSaturation(), config.getDistortion(), new int[]{Font.PLAIN, Font.BOLD}, fontNames);
|
||||
|
||||
for (int i = 0; i < config.getPrecomputeAmount(); i++) {
|
||||
final CaptchaGenerator captchaGenerator = new CaptchaGenerator(configuration);
|
||||
final CaptchaHolder captchaHolder = captchaGenerator.generate();
|
||||
final byte[] buffer = MapColorPalette.getBufferFromImage(captchaHolder.getImage());
|
||||
cached[i] = new MapCaptchaInfo(captchaHolder.getAnswer(), buffer);
|
||||
preparedAmount++;
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isCaptchaAvailable() {
|
||||
return cached != null && cached[0] != null;
|
||||
}
|
||||
|
||||
public MapCaptchaInfo getRandomCaptcha() {
|
||||
// Give the player a random CAPTCHA out of the ones that we've already prepared
|
||||
return cached[RANDOM.nextInt(preparedAmount)];
|
||||
}
|
||||
}
|
||||
+17
-17
@@ -25,41 +25,41 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapDataPacket;
|
||||
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;
|
||||
public final class MapCaptchaInfo {
|
||||
private final String answer;
|
||||
private final byte[] buffer;
|
||||
|
||||
private final MapInfo info;
|
||||
private final MapDataPacket[] legacy;
|
||||
private final MapDataPacket modern;
|
||||
|
||||
public PreparedMapInfo(final String answer,
|
||||
final int columns, final int rows,
|
||||
final byte @NotNull [] buffer) {
|
||||
this.info = new MapInfo(answer, columns, rows, 0, 0, buffer);
|
||||
public MapCaptchaInfo(final @NotNull String answer, final byte @NotNull [] buffer) {
|
||||
this.answer = answer;
|
||||
this.buffer = buffer;
|
||||
|
||||
// Prepare 1.7 map data using a grid
|
||||
final byte[][] grid = new byte[DIMENSIONS][DIMENSIONS];
|
||||
final byte[][] grid = new byte[128][128];
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
final byte buf = buffer[i];
|
||||
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
|
||||
}
|
||||
this.legacy = new MapDataPacket[grid.length];
|
||||
for (int i = 0; i < grid.length; i++) {
|
||||
this.legacy[i] = new MapDataPacket(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i]));
|
||||
this.legacy[i] = new MapDataPacket(grid[i], i, 0);
|
||||
}
|
||||
|
||||
// Prepare 1.8+ map data
|
||||
this.modern = new MapDataPacket(info);
|
||||
this.modern = new MapDataPacket(buffer, 0, 0);
|
||||
}
|
||||
|
||||
public void write(final @NotNull FallbackUser user) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
for (final MapDataPacket data : legacy) {
|
||||
user.delayedWrite(data);
|
||||
}
|
||||
} else {
|
||||
public void delayedWrite(final @NotNull FallbackUser user) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
user.delayedWrite(modern);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.7.2-1.7.10
|
||||
for (final MapDataPacket legacyPacket : legacy) {
|
||||
user.delayedWrite(legacyPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
@UtilityClass
|
||||
public class MapColorPalette {
|
||||
// https://minecraft.wiki/w/Map_item_format#Full_color_tables
|
||||
public static final int[] COLOR_MAP = {
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-10912473,
|
||||
-9594576,
|
||||
-8408520,
|
||||
-12362211,
|
||||
-5331853,
|
||||
-2766452,
|
||||
-530013,
|
||||
-8225962,
|
||||
-7697782,
|
||||
-5658199,
|
||||
-3815995,
|
||||
-9934744,
|
||||
-4980736,
|
||||
-2359296,
|
||||
-65536,
|
||||
-7929856,
|
||||
-9408332,
|
||||
-7697700,
|
||||
-6250241,
|
||||
-11250553,
|
||||
-9079435,
|
||||
-7303024,
|
||||
-5789785,
|
||||
-10987432,
|
||||
-16754944,
|
||||
-16750080,
|
||||
-16745472,
|
||||
-16760576,
|
||||
-4934476,
|
||||
-2302756,
|
||||
-1,
|
||||
-7895161,
|
||||
-9210239,
|
||||
-7499618,
|
||||
-5986120,
|
||||
-11118495,
|
||||
-8304095,
|
||||
-6464728,
|
||||
-4756945,
|
||||
-10471400,
|
||||
-11579569,
|
||||
-10461088,
|
||||
-9408400,
|
||||
-12895429,
|
||||
-10136525,
|
||||
-8689858,
|
||||
-7375032,
|
||||
-11780314,
|
||||
-13816396,
|
||||
-13158436,
|
||||
-12566273,
|
||||
-14605945,
|
||||
-4935252,
|
||||
-2303533,
|
||||
-779,
|
||||
-7895679,
|
||||
-6792924,
|
||||
-4559572,
|
||||
-2588877,
|
||||
-9288933,
|
||||
-8571496,
|
||||
-6733382,
|
||||
-5092136,
|
||||
-10606478,
|
||||
-12030824,
|
||||
-10976070,
|
||||
-10053160,
|
||||
-13217422,
|
||||
-6184668,
|
||||
-3816148,
|
||||
-1710797,
|
||||
-8816357,
|
||||
-10907631,
|
||||
-9588715,
|
||||
-8401895,
|
||||
-12358643,
|
||||
-5613196,
|
||||
-3117682,
|
||||
-884827,
|
||||
-8371369,
|
||||
-13290187,
|
||||
-12500671,
|
||||
-11776948,
|
||||
-14145496,
|
||||
-9671572,
|
||||
-8092540,
|
||||
-6710887,
|
||||
-11447983,
|
||||
-13280916,
|
||||
-12489340,
|
||||
-11763815,
|
||||
-14138543,
|
||||
-10933123,
|
||||
-9619815,
|
||||
-8437838,
|
||||
-12377762,
|
||||
-14404227,
|
||||
-13876839,
|
||||
-13415246,
|
||||
-14997410,
|
||||
-12045020,
|
||||
-10993364,
|
||||
-10073037,
|
||||
-13228005,
|
||||
-12035804,
|
||||
-10982100,
|
||||
-10059981,
|
||||
-13221093,
|
||||
-9690076,
|
||||
-8115156,
|
||||
-6737101,
|
||||
-11461861,
|
||||
-15658735,
|
||||
-15395563,
|
||||
-15132391,
|
||||
-15921907,
|
||||
-5199818,
|
||||
-2634430,
|
||||
-332211,
|
||||
-8094168,
|
||||
-12543338,
|
||||
-11551561,
|
||||
-10691627,
|
||||
};
|
||||
|
||||
public byte[] getBufferFromImage(final @NotNull BufferedImage bufferedImage) {
|
||||
final int width = bufferedImage.getWidth();
|
||||
final int height = bufferedImage.getHeight();
|
||||
|
||||
final int[] pixels = new int[128 * 128];
|
||||
final byte[] buffer = new byte[128 * 128];
|
||||
|
||||
bufferedImage.getRGB(0, 0, width, height, pixels, 0, width);
|
||||
|
||||
for (int i = 0; i < pixels.length; i++) {
|
||||
buffer[i] = mapColorToMinecraft(pixels[i]);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte mapColorToMinecraft(final int argb) {
|
||||
int minDistance = Integer.MAX_VALUE;
|
||||
byte closestIndex = 0;
|
||||
for (byte i = 0; i < COLOR_MAP.length; i++) {
|
||||
final int distance = calculateColorDistance(argb, COLOR_MAP[i]);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
private int calculateColorDistance(final int argb0, final int argb1) {
|
||||
final int red0 = (argb0 >> 16) & 0xFF;
|
||||
final int green0 = (argb0 >> 8) & 0xFF;
|
||||
final int blue0 = argb0 & 0xFF;
|
||||
final int red1 = (argb1 >> 16) & 0xFF;
|
||||
final int green1 = (argb1 >> 8) & 0xFF;
|
||||
final int blue1 = argb1 & 0xFF;
|
||||
// Color distance metric (e.g. squared Euclidean distance)
|
||||
return (red0 - red1) * (red0 - red1)
|
||||
+ (green0 - green1) * (green0 - green1)
|
||||
+ (blue0 - blue1) * (blue0 - blue1);
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class MapInfo {
|
||||
private final String answer;
|
||||
private final int columns, rows;
|
||||
private final int x, y;
|
||||
private final byte[] buffer;
|
||||
}
|
||||
-248
@@ -1,248 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final int[] FONT_STYLES = new int[]{
|
||||
PLAIN,
|
||||
BOLD,
|
||||
ITALIC,
|
||||
ITALIC | BOLD
|
||||
};
|
||||
|
||||
private final int[][] COLOR_PALETTE = new int[][]{
|
||||
new int[]{ // Blue
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51
|
||||
},
|
||||
new int[]{ // Orange
|
||||
60,
|
||||
61,
|
||||
62,
|
||||
63,
|
||||
},
|
||||
new int[]{ // Green
|
||||
4,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
},
|
||||
new int[]{ // Black
|
||||
27,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
},
|
||||
new int[]{ // Red
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19
|
||||
}
|
||||
};
|
||||
|
||||
private PreparedMapInfo[] cached;
|
||||
@Getter
|
||||
private int preparedCAPTCHAs;
|
||||
private boolean currentlyPreparing;
|
||||
|
||||
public PreparedMapInfo getRandomCaptcha() {
|
||||
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++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -30,4 +30,10 @@ public final class LoginAcknowledgedPacket implements FallbackPacket {
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledgedPacket.java#L41
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -24,8 +24,8 @@ 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.netty.FastUUID;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.netty.FastUUID;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
+12
-12
@@ -25,7 +25,6 @@ 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;
|
||||
|
||||
@@ -34,19 +33,20 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class MapDataPacket implements FallbackPacket {
|
||||
private MapInfo mapInfo;
|
||||
private byte[] buffer;
|
||||
private int x, y;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(mapInfo.getBuffer().length + 3);
|
||||
byteBuf.writeShort(buffer.length + 3);
|
||||
byteBuf.writeByte(0); // scaling
|
||||
byteBuf.writeByte(mapInfo.getX());
|
||||
byteBuf.writeByte(mapInfo.getY());
|
||||
byteBuf.writeByte(x);
|
||||
byteBuf.writeByte(y);
|
||||
|
||||
byteBuf.writeBytes(mapInfo.getBuffer());
|
||||
byteBuf.writeBytes(buffer);
|
||||
} else {
|
||||
byteBuf.writeByte(0); // scaling
|
||||
|
||||
@@ -65,13 +65,13 @@ public final class MapDataPacket implements FallbackPacket {
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
|
||||
byteBuf.writeByte(mapInfo.getColumns());
|
||||
byteBuf.writeByte(mapInfo.getRows());
|
||||
byteBuf.writeByte(mapInfo.getX());
|
||||
byteBuf.writeByte(mapInfo.getY());
|
||||
byteBuf.writeByte(128); // rows
|
||||
byteBuf.writeByte(128); // columns
|
||||
byteBuf.writeByte(x);
|
||||
byteBuf.writeByte(y);
|
||||
|
||||
writeVarInt(byteBuf, mapInfo.getBuffer().length);
|
||||
byteBuf.writeBytes(mapInfo.getBuffer());
|
||||
writeVarInt(byteBuf, buffer.length);
|
||||
byteBuf.writeBytes(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -22,8 +22,8 @@ import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.DeferredByteBufHolder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.netty.DeferredByteBufHolder;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
|
||||
@@ -78,6 +78,8 @@ verification:
|
||||
enabled: true
|
||||
# Měl by Sonar zkoušet jestli se hráč dotýka bloku? (Doporučováno)
|
||||
check-collisions: true
|
||||
# Měl by Sonar přimět hráče, aby vyřešil CAPTCHA, pokud neuspějí při kontrole gravitace?
|
||||
captcha-on-fail: false
|
||||
# Maximální počet ticků, který má hráč padat aby byl puštěn na verifikační platformu
|
||||
max-movement-ticks: 8
|
||||
# Maximum ignorovaných změn Y-nové souřadnice hráče, aby neprošel verifikací
|
||||
@@ -97,34 +99,39 @@ verification:
|
||||
# - DURING_ATTACK: Noví hráči dostanou map captchu jenom při útoku
|
||||
# - NEVER: Noví hráči nikdy nedostanou map CAPTCHU (Doporučeno)
|
||||
timing: NEVER
|
||||
# Měl by Sonar randomizovat pozici X a Y na CAPTCHE?
|
||||
random-position: true
|
||||
# Jak moc by měl Sonar znehodnotit charaktery (faktor pro randomizaci)
|
||||
distortions-factor-x: 50
|
||||
distortions-factor-y: 50
|
||||
# Kolik náhodných čar by měl Sonar vykreslit za CAPTCHOU
|
||||
random-lines: 4
|
||||
# Kolik náhodných oválů by měl Sonar vykreslit za CAPTCHOU
|
||||
random-ovals: 1
|
||||
# Měl by Sonar randomizovat font pro vykreslování textu v CAPTCHE?
|
||||
random-font-size: true
|
||||
# Options for the effects on the image shown to the player
|
||||
effects:
|
||||
# Adds a glow effect to the image
|
||||
flare: true
|
||||
# Adds random lines to the image
|
||||
scratches: true
|
||||
# Applies a ripple (sine) distortion filter
|
||||
ripple: true
|
||||
# Applies a horizontal distortion filter
|
||||
smear: true
|
||||
# Adds a bit of blurriness/distortion to the image
|
||||
pinch: true
|
||||
# Applies color correction (color saturation)
|
||||
saturation: 0.15
|
||||
# Applies a horizontal triangular ripple filter
|
||||
distortion: 2
|
||||
# Kolik odpovědí by měl Sonar předgenerovat?
|
||||
# Tento úkol běží souběžně v pozadí;
|
||||
# Hráči jsou schopni se připojit pokud je CAPTCHA připravena
|
||||
precompute: 1000
|
||||
# Tento úkol běží souběžně v pozadí
|
||||
precompute: 500
|
||||
# Jak dlouho by měl Sonar čekat než hráč selže CAPTCHU?
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
max-duration: 45000
|
||||
# Kolikrát musí hráč selhat CAPTCHU před celkovém selhání verifikace?
|
||||
max-tries: 3
|
||||
# Znaky (čísla a písmena), které jsou povoleny k zobrazování v CAPTCHE
|
||||
dictionary: '123456789'
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
dictionary: '1235689'
|
||||
# Jaké typy fontů by měl Sonar používat pro map CAPTCHU?
|
||||
fonts:
|
||||
font-names:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
- Monospaced
|
||||
|
||||
# Zkouší pokuď hráč posílá správný "client brand packet" serveru
|
||||
client-brand:
|
||||
|
||||
@@ -75,6 +75,8 @@ verification:
|
||||
enabled: true
|
||||
# Soll Sonar prüfen, ob der Spieler mit Blöcken kollidiert? (Empfohlen)
|
||||
check-collisions: true
|
||||
# Soll Sonar den Spieler dazu bringen, ein CAPTCHA zu lösen, wenn er die Schwerkraftprüfung nicht besteht?
|
||||
captcha-on-fail: false
|
||||
# Maximale Anzahl an Ticks, die der Spieler fallen muss, um die Plattform erreichen zu dürfen
|
||||
max-movement-ticks: 8
|
||||
# Maximale Anzahl an ignorierten Y-Bewegungsänderungen, bevor ein Spieler die Überprüfung nicht besteht
|
||||
@@ -94,36 +96,40 @@ verification:
|
||||
# - DURING_ATTACK: Neue Spieler erhalten nur während eines Angriffs ein CAPTCHA
|
||||
# - NEVER: Neue Spieler erhalten nie ein CAPTCHA (Empfohlen)
|
||||
timing: NEVER
|
||||
# Soll Sonar die X- und Y-Position des CAPTCHAs zufällig wählen?
|
||||
random-position: true
|
||||
# Wie stark sollen Zeichen verzerrt werden (Faktor für Zufälligkeit)?
|
||||
distortions-factor-x: 50
|
||||
# Wie stark sollen Zeichen verzerrt werden (Faktor für Zufälligkeit)?
|
||||
distortions-factor-y: 50
|
||||
# Wie viele zufällige Linien hinter dem CAPTCHA soll Sonar zeichnen?
|
||||
random-lines: 4
|
||||
# Wie viele zufällige Ovale hinter dem CAPTCHA soll Sonar zeichnen?
|
||||
random-ovals: 1
|
||||
# Soll Sonar die Schriftgröße für das Rendern des CAPTCHAs zufällig wählen?
|
||||
random-font-size: true
|
||||
# Options for the effects on the image shown to the player
|
||||
effects:
|
||||
# Adds a glow effect to the image
|
||||
flare: true
|
||||
# Adds random lines to the image
|
||||
scratches: true
|
||||
# Applies a ripple (sine) distortion filter
|
||||
ripple: true
|
||||
# Applies a horizontal distortion filter
|
||||
smear: true
|
||||
# Adds a bit of blurriness/distortion to the image
|
||||
pinch: true
|
||||
# Applies color correction (color saturation)
|
||||
saturation: 0.15
|
||||
# Applies a horizontal triangular ripple filter
|
||||
distortion: 2
|
||||
# Wie viele Antworten soll Sonar vorbereiten?
|
||||
# Dieser Vorgang erfolgt asynchron im Hintergrund;
|
||||
# Spieler können beitreten, sobald ein CAPTCHA vorbereitet wurde
|
||||
precompute: 1000
|
||||
# Dieser Vorgang erfolgt asynchron im Hintergrund
|
||||
precompute: 500
|
||||
# Wie lange soll Sonar warten, bis der Spieler das CAPTCHA nicht besteht?
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
max-duration: 45000
|
||||
# Wie oft muss ein Spieler das CAPTCHA nicht bestehen, bevor er die Überprüfung nicht besteht?
|
||||
max-tries: 3
|
||||
# Zeichen (Buchstaben und Zahlen), die im Antwort-CAPTCHA erscheinen dürfen
|
||||
dictionary: '123456789'
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
dictionary: '1235689'
|
||||
# Welche Schriftarten soll Sonar für die Map-CAPTCHA-Codes verwenden?
|
||||
fonts:
|
||||
font-names:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
|
||||
- Monospaced
|
||||
|
||||
# Überprüft, ob die Spieler eine gültige Client-Marke an den Server senden
|
||||
client-brand:
|
||||
# Soll Sonar auf gültige Client-Marken prüfen? (Empfohlen)
|
||||
|
||||
@@ -77,6 +77,8 @@ verification:
|
||||
enabled: true
|
||||
# Should Sonar check if the player collides with blocks? (Recommended)
|
||||
check-collisions: true
|
||||
# Should Sonar make the player solve a CAPTCHA if they fail the gravity check?
|
||||
captcha-on-fail: false
|
||||
# Maximum number of ticks the player has to fall in order to be allowed to hit the platform
|
||||
max-movement-ticks: 8
|
||||
# Maximum number of ignored Y movement changes before a player fails verification
|
||||
@@ -96,34 +98,39 @@ verification:
|
||||
# - DURING_ATTACK: New players will only receive a CAPTCHA during an attack
|
||||
# - NEVER: New players will never receive a CAPTCHA (Recommended)
|
||||
timing: NEVER
|
||||
# Should Sonar randomize the X and Y position of the CAPTCHA?
|
||||
random-position: true
|
||||
# How much should Sonar distort characters (factor for randomization)?
|
||||
distortions-factor-x: 50
|
||||
distortions-factor-y: 50
|
||||
# How many random lines behind the CAPTCHA should Sonar draw?
|
||||
random-lines: 4
|
||||
# How many random ovals behind the CAPTCHA should Sonar draw?
|
||||
random-ovals: 1
|
||||
# Should Sonar randomize the size of the font used for rendering the CAPTCHA?
|
||||
random-font-size: true
|
||||
# Options for the effects on the image shown to the player
|
||||
effects:
|
||||
# Adds a glow effect to the image
|
||||
flare: true
|
||||
# Adds random lines to the image
|
||||
scratches: true
|
||||
# Applies a ripple (sine) distortion filter
|
||||
ripple: true
|
||||
# Applies a horizontal distortion filter
|
||||
smear: true
|
||||
# Adds a bit of blurriness/distortion to the image
|
||||
pinch: true
|
||||
# Applies color correction (color saturation)
|
||||
saturation: 0.15
|
||||
# Applies a horizontal triangular ripple filter
|
||||
distortion: 2
|
||||
# How many answers should Sonar prepare?
|
||||
# This task happens asynchronously in the background;
|
||||
# Players are able to join once one CAPTCHA has been prepared
|
||||
precompute: 1000
|
||||
# This task happens asynchronously in the background
|
||||
precompute: 500
|
||||
# How long should Sonar wait until the player fails the CAPTCHA?
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
max-duration: 45000
|
||||
# How many times must a player fail the CAPTCHA before failing the verification?
|
||||
max-tries: 3
|
||||
# Characters (letters and numbers) that are allowed to appear in the answer to the CAPTCHA
|
||||
dictionary: '123456789'
|
||||
# Which font types should Sonar use for the map CAPTCHA codes?
|
||||
fonts:
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
dictionary: '1235689'
|
||||
# Which fonts should Sonar use for the map CAPTCHA codes?
|
||||
font-names:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
- Monospaced
|
||||
|
||||
# Checks if the players is sending a valid client brand to the server
|
||||
client-brand:
|
||||
|
||||
@@ -77,6 +77,8 @@ verification:
|
||||
enabled: true
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს?
|
||||
check-collisions: true
|
||||
# უნდა აიძულოს თუ არა Sonar-მა მოთამაშეს გადაჭრას CAPTCHA, თუ ისინი ვერ შეასრულებენ სიმძიმის შემოწმებას?
|
||||
captcha-on-fail: false
|
||||
# ტიკების მაქსიმალური რაოდენობა რომელითაც მოთამაშე უნდა დაეცეს, რათა პლატფორმაზე დაცემა შეეძლოს
|
||||
max-movement-ticks: 8
|
||||
# იგნორირებული Y მოძრაობის მაქსიმალური რაოდენობა სანამ, მოთამაშე ვერიფიკაციას ვერ გაივლის
|
||||
@@ -96,34 +98,39 @@ verification:
|
||||
# - DURING_ATTACK: ახალი მოთამაშებს მოუწევთ CAPTCHA-ს გაკეთება მხოლოდ შეტევის დროს
|
||||
# - NEVER: ახალ მოთამაშეებს არასდროს მოუწევთ CAPTCHA-ს გაკეთება (Recommended)
|
||||
timing: NEVER
|
||||
# მოახდინოს თუ არა Sonar-მა CAPTCHA-ს X და Y პოზიციების რანდომიზაცია?
|
||||
random-position: true
|
||||
# რამდენად უნდა დაამახინჯოს Sonar-მა სიმბოლოები (რანდომიზაციის ფაქტორი)?
|
||||
distortions-factor-x: 50
|
||||
distortions-factor-y: 50
|
||||
# რამდენი რანდომული ხაზი უნდა დახატოს Sonar-მა CAPTCHA-ს უკან?
|
||||
random-lines: 4
|
||||
# რამდენი რანდომული ოვალი უნდა დახატოს სონარმა CAPTCHA-ს უკან?
|
||||
random-ovals: 1
|
||||
# დაარანდომაიზოს Sonar-მა თუ არა შერჩევისას შრიფტის ზომა, რომელიც გამოიყენება CAPTCHA-ზე?
|
||||
random-font-size: true
|
||||
# Options for the effects on the image shown to the player
|
||||
effects:
|
||||
# Adds a glow effect to the image
|
||||
flare: true
|
||||
# Adds random lines to the image
|
||||
scratches: true
|
||||
# Applies a ripple (sine) distortion filter
|
||||
ripple: true
|
||||
# Applies a horizontal distortion filter
|
||||
smear: true
|
||||
# Adds a bit of blurriness/distortion to the image
|
||||
pinch: true
|
||||
# Applies color correction (color saturation)
|
||||
saturation: 0.15
|
||||
# Applies a horizontal triangular ripple filter
|
||||
distortion: 2
|
||||
# რამდენი პასუხი უნდა მოამზადოს Sonar-მა?
|
||||
# ეს ხდება ასინქრონულად უკანა ფონზე;
|
||||
# მოთამაშეებს შეუძლიათ შესვლა, როდესაც ერთი CAPTCHA მზად არის
|
||||
precompute: 1000
|
||||
# ეს ხდება ასინქრონულად უკანა ფონზე
|
||||
precompute: 500
|
||||
# რამდენ ხანს უნდა დაელოდოს Sonar-ი, სანამ მოთამაშე ვერ გააკეთებს წარმატებულად CAPTCHA-ს?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
max-duration: 45000
|
||||
# რამდენჯერ ვერ უდნა გაიაროს CAPTCHA მოთამაშემ სანამ ვერიფიკაციას ვერ გაივლის?
|
||||
max-tries: 3
|
||||
# სიმბოლოები (ასოები და რიცხვები), რომლებიც დაშვებულია გამოჩნდეს CAPTCHA-ს პასუხში
|
||||
dictionary: '123456789'
|
||||
# რომელი შრიფტის ტიპები უნდა გამოიყენოს Sonar-მა რუკის CAPTCHA კოდებისთვის?
|
||||
fonts:
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
dictionary: '1235689'
|
||||
# Which fonts should Sonar use for the map CAPTCHA codes?
|
||||
font-names:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
- Monospaced
|
||||
|
||||
#ამოწმებს, აგზავნიან თუ არა მოთამაშეები სერვერზე რეალურ კლიენტის სახელს
|
||||
client-brand:
|
||||
|
||||
@@ -77,6 +77,8 @@ verification:
|
||||
enabled: true
|
||||
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
|
||||
check-collisions: true
|
||||
# Должен ли Sonar заставлять игрока решать CAPTCHA, если он не проходит проверку на гравитацию?
|
||||
captcha-on-fail: false
|
||||
# Максимальное количество тиков, на которое должен упасть игрок, чтобы ему было разрешено упасть на платформу
|
||||
max-movement-ticks: 8
|
||||
# Максимальное количество проигнорированных изменений движения Y, прежде чем игрок не пройдет проверку
|
||||
@@ -96,34 +98,39 @@ verification:
|
||||
# - DURING_ATTACK: Новые игроки будут получать капчу только во время атаки
|
||||
# - NEVER: Новые игроки никогда не получат капчу (Рекомендуется)
|
||||
timing: NEVER
|
||||
# Должен ли Sonar рандомизировать положение X и Y капчи?
|
||||
random-position: true
|
||||
# Насколько сильно Sonar должен искажать символы (фактор для рандомизации)?
|
||||
distortions-factor-x: 50
|
||||
distortions-factor-y: 50
|
||||
# Сколько случайных линий за капчей должен нарисовать Sonar?
|
||||
random-lines: 4
|
||||
# Сколько случайных овалов за капчей должен нарисовать Sonar?
|
||||
random-ovals: 1
|
||||
# Должен ли Sonar рандомизировать размер шрифта, используемого для отображения капчи?
|
||||
random-font-size: true
|
||||
# Options for the effects on the image shown to the player
|
||||
effects:
|
||||
# Adds a glow effect to the image
|
||||
flare: true
|
||||
# Adds random lines to the image
|
||||
scratches: true
|
||||
# Applies a ripple (sine) distortion filter
|
||||
ripple: true
|
||||
# Applies a horizontal distortion filter
|
||||
smear: true
|
||||
# Adds a bit of blurriness/distortion to the image
|
||||
pinch: true
|
||||
# Applies color correction (color saturation)
|
||||
saturation: 0.15
|
||||
# Applies a horizontal triangular ripple filter
|
||||
distortion: 2
|
||||
# Сколько капч должен подготовить Sonar?
|
||||
# Эта задача выполняется асинхронно в фоновом режиме
|
||||
# Игроки смогут присоединиться, как только будет подготовлена одна капча
|
||||
precompute: 1000
|
||||
precompute: 500
|
||||
# Как долго Sonar должен ждать, пока игрок не пройдет капча?
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
max-duration: 45000
|
||||
# Сколько раз игрок должен завалить капчу, чтобы не пройти проверку?
|
||||
max-tries: 3
|
||||
# Символы (буквы и цифры), которые разрешено использовать в ответе на капчу
|
||||
dictionary: '123456789'
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
dictionary: '1235689'
|
||||
# Какие типы шрифтов следует использовать в Sonar для текста в капче?
|
||||
fonts:
|
||||
font-names:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
- Monospaced
|
||||
|
||||
# Проверяет, отправляет ли игрок на сервер действительный клиентский бренд.
|
||||
client-brand:
|
||||
|
||||
Reference in new issue
Block a user