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 & setupTry it
A sample question. Pick every animal that matches it, then press Next.
How it works
- 1Game 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.
- 2Game 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.
- 3Captcha service→Game client
The service sends an image, a question and a question id.
- 4Game client→Captcha service
The player picks the matching animals. The client sends the clicks together with the question id.
- 5Captcha 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.
- 6Game 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
Add thick_captcha.js from your panel to your page.
thick_captcha.js
<script src="thick_captcha.js"></script> - 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
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
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
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
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
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
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
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
To protect any other action, add the same check there.
#ifdef ENABLE_THICK_CAPTCHA if (!CThickCaptchaManager::instance().CheckAction(ch)) return; #endif