diff --git a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java index edee54dcea..6c2f3f9a6b 100644 --- a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java +++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java @@ -113,6 +113,9 @@ public class BungeeCord extends ProxyServer * Current operation state. */ public volatile boolean isRunning; + //BotFilter + @Getter + private boolean enabled; /** * Configuration. */ @@ -334,6 +337,7 @@ public void run() independentThreadStop( getTranslation( "restart" ), false ); } } ); + enabled = true; } public void startListeners() diff --git a/proxy/src/main/java/ru/leymooo/botfilter/BotFilter.java b/proxy/src/main/java/ru/leymooo/botfilter/BotFilter.java index 1263b994af..bf900319d4 100644 --- a/proxy/src/main/java/ru/leymooo/botfilter/BotFilter.java +++ b/proxy/src/main/java/ru/leymooo/botfilter/BotFilter.java @@ -24,10 +24,10 @@ import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.netty.HandlerBoss; import net.md_5.bungee.protocol.Protocol; -import ru.leymooo.botfilter.caching.CachedCaptcha; import ru.leymooo.botfilter.caching.PacketUtils; import ru.leymooo.botfilter.caching.PacketUtils.KickType; import ru.leymooo.botfilter.captcha.CaptchaGeneration; +import ru.leymooo.botfilter.captcha.CaptchaGenerationException; import ru.leymooo.botfilter.config.Settings; import ru.leymooo.botfilter.utils.GeoIp; import ru.leymooo.botfilter.utils.ManyChecksUtils; @@ -74,9 +74,13 @@ public BotFilter(boolean startup) Settings.IMP.reload( new File( "BotFilter", "config.yml" ) ); Scoreboard.DISABLE_DUBLICATE = Settings.IMP.FIX_SCOREBOARD_TEAMS; checkForUpdates( startup ); - if ( !CachedCaptcha.generated ) + //Глушим исключение при попытке сгенерировать капчу. Если капча уже генерируется и бот фильтр был почему-то + //перезапущен, капча сгенерируется все равно и доступ к ней будет прежний. + try { CaptchaGeneration.generateImages(); + } catch ( CaptchaGenerationException ignored ) + { } normalState = getCheckState( Settings.IMP.PROTECTION.NORMAL ); attackState = getCheckState( Settings.IMP.PROTECTION.ON_ATTACK ); diff --git a/proxy/src/main/java/ru/leymooo/botfilter/BotFilterCommand.java b/proxy/src/main/java/ru/leymooo/botfilter/BotFilterCommand.java index bfeb035c6f..4b14c7744b 100644 --- a/proxy/src/main/java/ru/leymooo/botfilter/BotFilterCommand.java +++ b/proxy/src/main/java/ru/leymooo/botfilter/BotFilterCommand.java @@ -14,6 +14,8 @@ import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.plugin.Command; +import ru.leymooo.botfilter.captcha.CaptchaGeneration; +import ru.leymooo.botfilter.captcha.CaptchaGenerationException; import ru.leymooo.botfilter.config.Settings; public class BotFilterCommand extends Command @@ -39,6 +41,7 @@ public void execute(CommandSender sender, String[] args) sender.sendMessage( "§r> §lbotfilter stat §6- §aПоказать статистику" ); sender.sendMessage( "§r> §lbotfilter export §6- §aВыгрузить список игроков, которые прошли проверку" ); sender.sendMessage( "§r> §lbotfilter protection on/off §6- §aВключить или выключить ручной режим 'под атакой'" ); + sender.sendMessage( "§r> §lbotfilter generate §6- §aСгенерировать новую капчу" ); sender.sendMessage( "§r--------------- §bBotFilter §r-----------------" ); } else if ( args[0].equalsIgnoreCase( "reload" ) ) { @@ -52,6 +55,16 @@ public void execute(CommandSender sender, String[] args) { export( sender, args ); sender.sendMessage( "§aКоманда выполнена" ); + } else if ( args[0].equalsIgnoreCase( "generate" ) ) + { + try + { + CaptchaGeneration.generateImages(); + sender.sendMessage( "§aКоманда выполнена" ); + } catch ( CaptchaGenerationException e ) + { + sender.sendMessage( "§cОшибка: " + e.getMessage() ); + } } else if ( args[0].equalsIgnoreCase( "protection" ) ) { if ( args.length >= 2 ) diff --git a/proxy/src/main/java/ru/leymooo/botfilter/BotFilterThread.java b/proxy/src/main/java/ru/leymooo/botfilter/BotFilterThread.java index 798d185677..25c84b6de6 100644 --- a/proxy/src/main/java/ru/leymooo/botfilter/BotFilterThread.java +++ b/proxy/src/main/java/ru/leymooo/botfilter/BotFilterThread.java @@ -7,6 +7,8 @@ import ru.leymooo.botfilter.BotFilter.CheckState; import ru.leymooo.botfilter.caching.PacketUtils.KickType; import ru.leymooo.botfilter.caching.PacketsPosition; +import ru.leymooo.botfilter.captcha.CaptchaGeneration; +import ru.leymooo.botfilter.captcha.CaptchaGenerationException; import ru.leymooo.botfilter.config.Settings; import ru.leymooo.botfilter.utils.FailedUtils; import ru.leymooo.botfilter.utils.ManyChecksUtils; @@ -107,12 +109,13 @@ public static void startCleanUpThread() { Thread t = new Thread( () -> { - byte counter = 0; + byte counterClean = 0; + int counterCaptcha = 0; while ( !Thread.interrupted() && sleep( 5 * 1000 ) ) { - if ( ++counter == 12 ) + if ( ++counterClean == 12 ) { - counter = 0; + counterClean = 0; ManyChecksUtils.cleanUP(); if ( bungee.getBotFilter() != null ) { @@ -132,6 +135,22 @@ public static void startCleanUpThread() } } FailedUtils.flushQueue(); + int captchaMin = Settings.IMP.CAPTCHA.CAPTCHA_REGENERATION_TIME; + if ( captchaMin <= 0 ) + { + continue; + } + if ( ++counterCaptcha == ( 12 * captchaMin ) ) + { + counterCaptcha = 0; + + try + { + CaptchaGeneration.generateImages(); + } catch ( CaptchaGenerationException ignored ) + { + } + } } }, "CleanUp thread" ); t.setDaemon( true ); diff --git a/proxy/src/main/java/ru/leymooo/botfilter/Connector.java b/proxy/src/main/java/ru/leymooo/botfilter/Connector.java index a1a700d052..737e53a345 100644 --- a/proxy/src/main/java/ru/leymooo/botfilter/Connector.java +++ b/proxy/src/main/java/ru/leymooo/botfilter/Connector.java @@ -354,6 +354,11 @@ public void sendPing() private void sendCaptcha() { CaptchaHolder captchaHolder = PacketUtils.captchas.randomCaptcha(); + if ( captchaHolder == null ) + { + failed( KickType.FAILED_CAPTCHA, "Captcha was not generated" ); + return; + } captchaAnswer = captchaHolder.getAnswer(); channel.write( PacketUtils.getCachedPacket( PacketsPosition.SETSLOT_MAP ).get( version ), channel.voidPromise() ); captchaHolder.write( channel, version, true ); diff --git a/proxy/src/main/java/ru/leymooo/botfilter/caching/CachedCaptcha.java b/proxy/src/main/java/ru/leymooo/botfilter/caching/CachedCaptcha.java index 5d35ca92a9..59d61f4bed 100644 --- a/proxy/src/main/java/ru/leymooo/botfilter/caching/CachedCaptcha.java +++ b/proxy/src/main/java/ru/leymooo/botfilter/caching/CachedCaptcha.java @@ -2,37 +2,29 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; -import java.util.Random; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import net.md_5.bungee.protocol.ProtocolConstants; import ru.leymooo.botfilter.packets.MapDataPacket; /** * @author Leymooo */ +@Setter public class CachedCaptcha { - - //уже пора с этим чтото придумать - //В принципе я вроде чтото придумал для версии под Velocity, но будет ли она?.... private static final int PACKETID_18 = 0x34; private static final int PACKETID_19and119 = 0x24; private static final int PACKETID_113and114and116 = 0x26; private static final int PACKETID_115and117 = 0x27; private static final int PACKETID_1162 = 0x25; + private List captchas = null; - - private static final Random random = new Random(); - - private static final CaptchaHolder[] captchas = new CaptchaHolder[900]; - private static final AtomicInteger counter = new AtomicInteger(); - - public static boolean generated = false; - - public void createCaptchaPacket(MapDataPacket map, String answer) + public static CaptchaHolder createCaptchaPacket(MapDataPacket map, String answer) { ByteBuf byteBuf18 = PacketUtils.createPacket( map, PACKETID_18, ProtocolConstants.MINECRAFT_1_8 ); @@ -44,14 +36,29 @@ public void createCaptchaPacket(MapDataPacket map, String answer) ByteBuf byteBuf117 = PacketUtils.createPacket( map, PACKETID_115and117, ProtocolConstants.MINECRAFT_1_17 ); ByteBuf byteBuf119 = PacketUtils.createPacket( map, PACKETID_19and119, ProtocolConstants.MINECRAFT_1_19 ); - captchas[counter.getAndIncrement()] = new CaptchaHolder( answer, byteBuf18, byteBuf19, byteBuf113, byteBuf114And116, byteBuf115, byteBuf1162, byteBuf117, byteBuf119 ); + return new CaptchaHolder( answer, byteBuf18, byteBuf19, byteBuf113, byteBuf114And116, byteBuf115, byteBuf1162, byteBuf117, byteBuf119 ); + } - //TODO: Do something with this shit. + public void clear() + { + if ( captchas == null ) + { + return; + } + for ( CaptchaHolder holder : captchas ) + { + holder.release(); + } + captchas = null; } public CaptchaHolder randomCaptcha() { - return captchas[random.nextInt( captchas.length )]; + if ( this.captchas == null || this.captchas.isEmpty() ) + { + return null; + } + return captchas.get( ThreadLocalRandom.current().nextInt( captchas.size() ) ); } @RequiredArgsConstructor @@ -63,7 +70,6 @@ public static class CaptchaHolder public void write(Channel channel, int version, boolean flush) { - if ( version == ProtocolConstants.MINECRAFT_1_8 ) { channel.write( buf18.retainedDuplicate(), channel.voidPromise() ); @@ -100,5 +106,16 @@ public void write(Channel channel, int version, boolean flush) channel.flush(); } } + public void release() + { + buf18.release(); + buf19.release(); + buf113.release(); + buf114And116.release(); + buf115.release(); + buf1162.release(); + buf117.release(); + buf119.release(); + } } } diff --git a/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGeneration.java b/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGeneration.java index bc8e246936..a1588526d8 100644 --- a/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGeneration.java +++ b/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGeneration.java @@ -1,21 +1,22 @@ package ru.leymooo.botfilter.captcha; -import java.awt.Color; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.awt.Font; -import java.awt.image.BufferedImage; -import java.util.Random; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.logging.Level; import lombok.experimental.UtilityClass; import net.md_5.bungee.BungeeCord; import ru.leymooo.botfilter.caching.CachedCaptcha; import ru.leymooo.botfilter.caching.PacketUtils; -import ru.leymooo.botfilter.captcha.generator.CaptchaPainter; -import ru.leymooo.botfilter.captcha.generator.map.CraftMapCanvas; import ru.leymooo.botfilter.captcha.generator.map.MapPalette; -import ru.leymooo.botfilter.packets.MapDataPacket; +import ru.leymooo.botfilter.config.Settings; /** * @author Leymooo @@ -23,98 +24,105 @@ @UtilityClass public class CaptchaGeneration { - Random rnd = new Random(); + private static volatile boolean generation = false; - public void generateImages() + public static synchronized void generateImages() throws CaptchaGenerationException { - Font[] fonts = new Font[] + if ( generation ) { - new Font( Font.SANS_SERIF, Font.PLAIN, 50 ), - new Font( Font.SERIF, Font.PLAIN, 50 ), - new Font( Font.MONOSPACED, Font.BOLD, 50 ) - }; - - ExecutorService executor = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors() ); - CaptchaPainter painter = new CaptchaPainter(); - MapPalette.prepareColors(); - for ( int i = 100; i <= 999; i++ ) - { - executor.execute( () -> - { - String answer = randomAnswer(); - BufferedImage image = painter.draw( fonts[rnd.nextInt( fonts.length )], randomNotWhiteColor(), answer ); - final CraftMapCanvas map = new CraftMapCanvas(); - map.drawImage( 0, 0, image ); - MapDataPacket packet = new MapDataPacket( 0, (byte) 0, map.getMapData() ); - PacketUtils.captchas.createCaptchaPacket( packet, answer ); - } ); + throw new CaptchaGenerationException( "Капча уже генерируется!" ); } - long start = System.currentTimeMillis(); - ThreadPoolExecutor ex = (ThreadPoolExecutor) executor; - while ( ex.getActiveCount() != 0 ) + generation = true; + Thread thread = new Thread( CaptchaGeneration::generateCaptchas ); + thread.setName( "CaptchaGenerationProvider-thread" ); + thread.setPriority( Thread.MIN_PRIORITY ); + thread.start(); + } + private static void generateCaptchas() + { + try { - BungeeCord.getInstance().getLogger().log( Level.INFO, "[BotFilter] Генерирую капчу [{0}/900]", 900 - ex.getQueue().size() - ex.getActiveCount() ); - try + List fonts = Arrays.asList( + new Font( Font.SANS_SERIF, Font.PLAIN, 50 ), + new Font( Font.SERIF, Font.PLAIN, 50 ), + new Font( Font.MONOSPACED, Font.BOLD, 50 ) ); + //Перед началом генерации, нужно удалить старую капчу, освободив байтовый буфер. + PacketUtils.captchas.clear(); + BungeeCord.getInstance().getLogger().log( Level.INFO, "[BotFilter] " + ( BungeeCord.getInstance().isEnabled() ? "Начата генерация капчи в фоне." : "Генерация капчи продолжится параллельно с загрузкой BungeeCord." ) ); + ExecutorService executor = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors(), + new ThreadFactoryBuilder() + .setPriority( Thread.MIN_PRIORITY ) + .setNameFormat( "CaptchaGenerationTask-thread-%d" ) + .build() ); + MapPalette.prepareColors(); + + int captchaCount = Settings.IMP.CAPTCHA.COUNT; + + if ( captchaCount <= 0 ) { - Thread.sleep( 1000L ); - } catch ( InterruptedException ex1 ) + captchaCount = 1; + } + List tasks = new ArrayList<>(); + for ( int i = 1; i <= captchaCount; i++ ) { - BungeeCord.getInstance().getLogger().log( Level.WARNING, "[BotFilter] Не могу сгенерировать капчу. Выключаю банджу", ex1 ); - System.exit( 0 ); - return; + tasks.add( new CaptchaGenerationTask( executor, fonts ) ); } - } - CachedCaptcha.generated = true; - executor.shutdownNow(); - System.gc(); - BungeeCord.getInstance().getLogger().log( Level.INFO, "[BotFilter] Капча сгенерированна за {0} мс", System.currentTimeMillis() - start ); - } + List> futureHolders = new ArrayList<>(); + for ( CaptchaGenerationTask task : tasks ) + { + futureHolders.add( executor.submit( task ) ); + } + long start = System.currentTimeMillis(); + ThreadPoolExecutor ex = (ThreadPoolExecutor) executor; + while ( ex.getActiveCount() != 0 ) + { + //Отображаем прогресс генерации только после полной загрузки банжи, чтобы логи не путались с другими важными логами при включении + if ( BungeeCord.getInstance().isEnabled() ) + { + BungeeCord.getInstance().getLogger().log( Level.INFO, "[BotFilter] Генерирую капчу [" + ( captchaCount - ex.getQueue().size() ) + "/" + captchaCount + "]" ); + } + //Вставляем сгенерированные капчи + PacketUtils.captchas.setCaptchas( findDoneTasks( futureHolders ) ); + try + { + Thread.sleep( 1000L ); + } catch ( InterruptedException ex1 ) + { + BungeeCord.getInstance().getLogger().log( Level.WARNING, "[BotFilter] Не могу сгенерировать капчу. Выключаю банджу", ex1 ); + System.exit( 0 ); + return; + } + } + executor.shutdownNow(); - private Color randomNotWhiteColor() - { - Color color = MapPalette.colors[rnd.nextInt( MapPalette.colors.length )]; - - int r = color.getRed(); - int g = color.getGreen(); - int b = color.getBlue(); - - if ( r == 255 && g == 255 && b == 255 ) - { - return randomNotWhiteColor(); - } - if ( r == 220 && g == 220 && b == 220 ) + //Окончательно устанавливаем оставшиеся капчи + PacketUtils.captchas.setCaptchas( findDoneTasks( futureHolders ) ); + System.gc(); + BungeeCord.getInstance().getLogger().log( Level.INFO, "[BotFilter] Капча сгенерирована за {0} мс", System.currentTimeMillis() - start ); + } catch ( Exception e ) { - return randomNotWhiteColor(); - } - if ( r == 199 && g == 199 && b == 199 ) - { - return randomNotWhiteColor(); - } - if ( r == 255 && g == 252 && b == 245 ) + e.printStackTrace(); + } finally { - return randomNotWhiteColor(); + generation = false; } - if ( r == 220 && g == 217 && b == 211 ) - { - return randomNotWhiteColor(); - } - if ( r == 247 && g == 233 && b == 163 ) - { - return randomNotWhiteColor(); - } - return color; } - - private String randomAnswer() + private static List findDoneTasks(List> futureHolders) throws ExecutionException, InterruptedException { - if ( rnd.nextBoolean() ) + List doneTasks = new ArrayList<>(); + for ( Future future : futureHolders ) { - return Integer.toString( rnd.nextInt( ( 99999 - 10000 ) + 1 ) + 10000 ); - } else - { - return Integer.toString( rnd.nextInt( ( 9999 - 1000 ) + 1 ) + 1000 ); + if ( future.isDone() ) + { + CachedCaptcha.CaptchaHolder holder = future.get(); + if ( holder != null ) + { + doneTasks.add( holder ); + } + } } + return doneTasks; } } diff --git a/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGenerationException.java b/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGenerationException.java new file mode 100644 index 0000000000..bb0f266b51 --- /dev/null +++ b/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGenerationException.java @@ -0,0 +1,9 @@ +package ru.leymooo.botfilter.captcha; + +public class CaptchaGenerationException extends Exception +{ + public CaptchaGenerationException(String msg) + { + super( msg ); + } +} diff --git a/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGenerationTask.java b/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGenerationTask.java new file mode 100644 index 0000000000..620691140d --- /dev/null +++ b/proxy/src/main/java/ru/leymooo/botfilter/captcha/CaptchaGenerationTask.java @@ -0,0 +1,92 @@ +package ru.leymooo.botfilter.captcha; + +import java.awt.Color; +import java.awt.Font; +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadLocalRandom; +import lombok.AllArgsConstructor; +import lombok.Data; +import ru.leymooo.botfilter.caching.CachedCaptcha; +import ru.leymooo.botfilter.captcha.generator.CaptchaPainter; +import ru.leymooo.botfilter.captcha.generator.map.CraftMapCanvas; +import ru.leymooo.botfilter.captcha.generator.map.MapPalette; +import ru.leymooo.botfilter.packets.MapDataPacket; + +@Data +@AllArgsConstructor +public class CaptchaGenerationTask implements Callable +{ + private final ExecutorService executor; + private final List fonts; + @Override + public CachedCaptcha.CaptchaHolder call() + { + try + { + Random rnd = ThreadLocalRandom.current(); + String answer = randomAnswer( rnd ); + CaptchaPainter painter = new CaptchaPainter( rnd ); + BufferedImage image = painter.draw( this.fonts.get( rnd.nextInt( this.fonts.size() ) ), randomNotWhiteColor( rnd ), answer ); + final CraftMapCanvas map = new CraftMapCanvas(); + map.drawImage( 0, 0, image ); + MapDataPacket packet = new MapDataPacket( 0, (byte) 0, map.getMapData() ); + return CachedCaptcha.createCaptchaPacket( packet, answer ); + } catch ( Throwable e ) + { + //Прекращаем генерацию если случилась любая ошибка + e.printStackTrace(); + this.executor.shutdownNow(); + } + return null; + } + + private static Color randomNotWhiteColor(Random rnd) + { + Color color = MapPalette.colors[rnd.nextInt( MapPalette.colors.length )]; + + int r = color.getRed(); + int g = color.getGreen(); + int b = color.getBlue(); + + if ( r == 255 && g == 255 && b == 255 ) + { + return randomNotWhiteColor( rnd ); + } + if ( r == 220 && g == 220 && b == 220 ) + { + return randomNotWhiteColor( rnd ); + } + if ( r == 199 && g == 199 && b == 199 ) + { + return randomNotWhiteColor( rnd ); + } + if ( r == 255 && g == 252 && b == 245 ) + { + return randomNotWhiteColor( rnd ); + } + if ( r == 220 && g == 217 && b == 211 ) + { + return randomNotWhiteColor( rnd ); + } + if ( r == 247 && g == 233 && b == 163 ) + { + return randomNotWhiteColor( rnd ); + } + return color; + } + + private static String randomAnswer(Random rnd) + { + if ( rnd.nextBoolean() ) + { + return Integer.toString( rnd.nextInt( ( 99999 - 10000 ) + 1 ) + 10000 ); + } else + { + return Integer.toString( rnd.nextInt( ( 9999 - 1000 ) + 1 ) + 1000 ); + } + } +} diff --git a/proxy/src/main/java/ru/leymooo/botfilter/captcha/generator/CaptchaPainter.java b/proxy/src/main/java/ru/leymooo/botfilter/captcha/generator/CaptchaPainter.java index 66af52aa87..70b03aa0e5 100644 --- a/proxy/src/main/java/ru/leymooo/botfilter/captcha/generator/CaptchaPainter.java +++ b/proxy/src/main/java/ru/leymooo/botfilter/captcha/generator/CaptchaPainter.java @@ -14,14 +14,16 @@ import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.util.Random; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor public class CaptchaPainter { private final int width = 128; private final int height = 128; private final Color background = Color.WHITE; - private final Random rnd = new Random(); + private final Random rnd; public BufferedImage draw(Font font, Color fGround, String text) { diff --git a/proxy/src/main/java/ru/leymooo/botfilter/config/Settings.java b/proxy/src/main/java/ru/leymooo/botfilter/config/Settings.java index cade126ff0..e270b731c1 100644 --- a/proxy/src/main/java/ru/leymooo/botfilter/config/Settings.java +++ b/proxy/src/main/java/ru/leymooo/botfilter/config/Settings.java @@ -35,6 +35,8 @@ public class Settings extends Config @Create public PROTECTION PROTECTION; @Create + public CAPTCHA CAPTCHA; + @Create public SQL SQL; @Comment( { @@ -250,4 +252,13 @@ public static class DIMENSIONS }) public int TYPE = 0; } + + @Comment("Настройка капчи") + public static class CAPTCHA + { + @Comment("Сколько экземпляров капчи нужно сгенерировать. Большое количество может занять много оперативной памяти.") + public int COUNT = 900; + @Comment("Как часто капча должна сама регенерироваться в минутах. Укажите -1 чтобы отключить.") + public int CAPTCHA_REGENERATION_TIME = 360; + } }