Bot protection for games

Players click a few images. Bots get stuck. Your game server signs and checks every token itself, so nothing changes on your side when you tune the rules.

Preview & setup

Try it

A sample question. Pick every animal that matches it, then press Next.

How it works

  1. 1
    Game server→Game client

    A player attacks or fishes. The server holds the action and gives the client a token signed with your secret key: which player, when, and a one-time code.

  2. 2
    Game client→Captcha service

    The client sends this token to the service and asks for a question. The service checks the signature, so only requests from your server get questions.

  3. 3
    Captcha service→Game client

    The service sends an image, a question and a question id.

  4. 4
    Game client→Captcha service

    The player picks the matching animals. The client sends the clicks together with the question id.

  5. 5
    Captcha service→Game client

    Correct: the service sends a pass token signed with your secret key, carrying the same one-time code. Wrong: the player waits a few seconds, then the client asks again with the same token.

  6. 6
    Game client→Game server

    The client sends the pass token to the server. The server checks the signature with your secret key and that the one-time code is the one it issued, then lets the action through. The server never calls the service.

Controlled from the panel

  • Turn bot checks on or off without updating your server
  • Set how many challenges a player must pass per level range
  • Skip checks above a chosen level
  • Questions in the player's language, English by default

Metin2 example setup

Uses the Crypto++ and rapidjson that are already in the source, so no extra library is needed.

Download the files from your panel

The files on this page are templates. The copies in your panel already contain your API address, site ID and secret, so every server gets its own files. Keep the server files private: anyone who has the secret in them can create valid captcha tokens for your server. The client files only contain the API address.

Client (UI)

  1. 1

    Add thick_captcha.js from your panel to your page.

    thick_captcha.js

    <script src="thick_captcha.js"></script>
  2. 2

    Open it with the token from your game server and send the pass token back to the server.

    <div id="captcha"></div>
    <script>
      ThickCaptcha.open(document.getElementById("captcha"), {
        token: REQUEST_TOKEN_FROM_GAME_SERVER,
        lang: navigator.language,
        onPass: function (passToken) {
          sendToGameServer("/thick_captcha " + passToken);
        },
        onRenew: function () {
          return askGameServerForNewToken();
        },
      });
    </script>

Game server

  1. 1

    Turn the system on with this define. Remove the line to turn it off completely.

    common/service.h (CommonDefines.h)

    #define ENABLE_THICK_CAPTCHA
  2. 2

    Copy thick_captcha.h and thick_captcha.cpp from your panel into game/src and add the .cpp to the Makefile.

    game/src/Makefile

    CPPFILE = ... thick_captcha.cpp
  3. 3

    Create the manager in main.cpp.

    game/src/main.cpp

    #ifdef ENABLE_THICK_CAPTCHA
    #include "thick_captcha.h"
    #endif
    
    #ifdef ENABLE_THICK_CAPTCHA
    	CThickCaptchaManager thick_captcha_manager;
    #endif
  4. 4

    Set CONFIG_PATH so every core points to the same file. GM commands save their settings there and every core reloads the file once a minute, so a change made on one channel reaches all channels and survives restarts. The file is created on first start.

    game/src/thick_captcha.cpp, thick_captcha.txt

    const char*	CONFIG_PATH = "../../thick_captcha.txt";
    
    // thick_captcha.txt
    enabled 1
    difficulty 1
    interval 1 30
    interval 75 60
    interval 120 0
  5. 5

    Ask on the first attack on a monster or metin and on the first fishing after login, then again once the interval for the player's level has passed (30 minutes by default, set with thick_captcha_interval). Attacks on players (wars, guild wars, PvP) and idle players are never asked.

    game/src/char_battle.cpp, char_skill.cpp, char.cpp

    // CHARACTER::Attack, after battle_is_attackable
    #ifdef ENABLE_THICK_CAPTCHA
    	if (IsPC() && !CThickCaptchaManager::instance().CheckAttack(this, pkVictim))
    		return false;
    #endif
    
    // CHARACTER::ComputeSkill, at the top
    #ifdef ENABLE_THICK_CAPTCHA
    	if (IsPC() && pkVictim && !CThickCaptchaManager::instance().CheckAttack(this, pkVictim))
    		return BATTLE_NONE;
    #endif
    
    // CHARACTER::fishing, after the "if (m_pkFishingEvent) { fishing_take(); return; }" block
    #ifdef ENABLE_THICK_CAPTCHA
    	if (!CThickCaptchaManager::instance().CheckAction(this))
    		return;
    #endif
  6. 6

    Clean up when the player disconnects. A pending captcha is sent again on the player's next attack or fishing after logging back in.

    game/src/char.cpp

    // CHARACTER::Disconnect
    #ifdef ENABLE_THICK_CAPTCHA
    	CThickCaptchaManager::instance().OnLogout(this);
    #endif
  7. 7

    Add the player commands and the GM commands. thick_captcha_ask sends a test captcha, thick_captcha_toggle turns the system on or off, thick_captcha_difficulty 1-10 sets the difficulty, thick_captcha_interval <level> <minutes> sets how often players from that level are asked (0 = never, del <level> removes a rule, no arguments lists the rules). Changes are saved to the config file and reach every channel within a minute.

    game/src/cmd.cpp, cmd_general.cpp

    #ifdef ENABLE_THICK_CAPTCHA
    ACMD(do_thick_captcha);
    ACMD(do_thick_captcha_renew);
    ACMD(do_thick_captcha_ask);
    ACMD(do_thick_captcha_toggle);
    ACMD(do_thick_captcha_difficulty);
    ACMD(do_thick_captcha_interval);
    #endif
    
    #ifdef ENABLE_THICK_CAPTCHA
    	{ "thick_captcha",       do_thick_captcha,       0, POS_DEAD, GM_PLAYER     },
    	{ "thick_captcha_renew", do_thick_captcha_renew, 0, POS_DEAD, GM_PLAYER     },
    	{ "thick_captcha_ask",   do_thick_captcha_ask,   0, POS_DEAD, GM_LOW_WIZARD },
    	{ "thick_captcha_toggle",     do_thick_captcha_toggle,     0, POS_DEAD, GM_IMPLEMENTOR },
    	{ "thick_captcha_difficulty", do_thick_captcha_difficulty, 0, POS_DEAD, GM_IMPLEMENTOR },
    	{ "thick_captcha_interval",   do_thick_captcha_interval,   0, POS_DEAD, GM_IMPLEMENTOR },
    #endif
    
    #ifdef ENABLE_THICK_CAPTCHA
    ACMD(do_thick_captcha)
    {
    	while (*argument == ' ')
    		++argument;
    	if (!CThickCaptchaManager::instance().Verify(ch, argument))
    		ch->ChatPacket(CHAT_TYPE_COMMAND, "ThickCaptchaFailed");
    }
    
    ACMD(do_thick_captcha_renew)
    {
    	CThickCaptchaManager::instance().Renew(ch);
    }
    
    ACMD(do_thick_captcha_ask)
    {
    	char arg1[256];
    	one_argument(argument, arg1, sizeof(arg1));
    	LPCHARACTER tch = *arg1 ? CHARACTER_MANAGER::instance().FindPC(arg1) : ch;
    	if (tch)
    		CThickCaptchaManager::instance().Request(tch);
    }
    
    ACMD(do_thick_captcha_toggle)
    {
    	CThickCaptchaManager& mgr = CThickCaptchaManager::instance();
    	mgr.SetEnabled(!mgr.IsEnabled());
    	ch->ChatPacket(CHAT_TYPE_INFO, "Thick Captcha: %s", mgr.IsEnabled() ? "on" : "off");
    }
    
    ACMD(do_thick_captcha_difficulty)
    {
    	char arg1[256];
    	one_argument(argument, arg1, sizeof(arg1));
    	CThickCaptchaManager& mgr = CThickCaptchaManager::instance();
    	if (*arg1 && isdigit(*arg1))
    		mgr.SetDifficulty(atoi(arg1));
    	ch->ChatPacket(CHAT_TYPE_INFO, "Thick Captcha difficulty: %d (1-10)", mgr.GetDifficulty());
    }
    
    ACMD(do_thick_captcha_interval)
    {
    	char arg1[256], arg2[256];
    	two_arguments(argument, arg1, sizeof(arg1), arg2, sizeof(arg2));
    	CThickCaptchaManager& mgr = CThickCaptchaManager::instance();
    
    	if (!strcmp(arg1, "del") && isdigit(*arg2))
    		mgr.RemoveInterval(atoi(arg2));
    	else if (isdigit(*arg1) && isdigit(*arg2))
    		mgr.SetInterval(atoi(arg1), atoi(arg2));
    
    	for (const auto& rule : mgr.GetIntervals())
    	{
    		if (rule.second)
    			ch->ChatPacket(CHAT_TYPE_INFO, "Thick Captcha level %d+: every %d min", rule.first, rule.second);
    		else
    			ch->ChatPacket(CHAT_TYPE_INFO, "Thick Captcha level %d+: off", rule.first);
    	}
    }
    #endif
  8. 8

    To protect any other action, add the same check there.

    #ifdef ENABLE_THICK_CAPTCHA
    	if (!CThickCaptchaManager::instance().CheckAction(ch))
    		return;
    #endif