// TMW2 functions.
// Author:
// Jesusalva
// Description:
// Random CAPTCHA check against AFK robots.
// TODO: Put this in a blackbox for bots which sniff text chat.
// Variables:
// gettimetick(2) variables: CAPTCHA_TIME | CAPTCHA_OK
// CAPTCHA_1 | CAPTCHA_2 | CAPTCHA_ANSWER => The correct captcha reply
// CAPTCHA_OP$ => The operation, defaults to "+"
// $@BOTCHECK_TARGET => The account ID of the char being probed
// @captcha_cooldown => anti-flood
// CAPTCHA_NPC$ => The Map which the nurse can clear the Botter Syndrome
// $CAPTCHA (bitmask)
// 0 - Captcha Disabled
// 1 - Captcha Enabled (Banhammer)
// 2 - Display warnings (@captcha_lastwarning/@captcha_lastwarningt)
// 4 - Botter Syndrome enabled
// 8 - Disable during events
// CaptchName, names the number
function script CaptchName {
switch (getarg(0)) {
case 0: return l("zero");
case 1: return l("one");
case 2: return l("two");
case 3: return l("three");
case 4: return l("four");
case 5: return l("five");
case 6: return l("six");
case 7: return l("seven");
case 8: return l("eight");
case 9: return l("nine");
case 10: return l("ten");
case 11: return l("eleven");
case 12: return l("twelve");
case 13: return l("thirteen");
case 14: return l("fourteen");
case 15: return l("fifteen");
case 16: return l("sixteen");
case 17: return l("seventeen");
case 18: return l("eighteen");
case 19: return l("nineteen");
case 20: return l("twenty");
}
return getarg(0);
}
// MakeCaptch, makes a captcha and saves it
function script MakeCaptch {
CAPTCHA_TIME=gettimetick(2);
CAPTCHA_OK=CAPTCHA_TIME;
CAPTCHA_1=rand2(21);
CAPTCHA_2=rand2(20);
// select a operation
switch (rand2(1)) {
case 0:
CAPTCHA_OP$="-";
CAPTCHA_ANSWER=CAPTCHA_1-CAPTCHA_2;
break;
default:
CAPTCHA_OP$="+";
CAPTCHA_ANSWER=CAPTCHA_1+CAPTCHA_2;
break;
}
return;
}
function script CaptchVal {
// Make it nice
.@c$=any(
CAPTCHA_1+CAPTCHA_OP$+CAPTCHA_2,
CaptchName(CAPTCHA_1)+CAPTCHA_OP$+CAPTCHA_2,
CAPTCHA_1+CAPTCHA_OP$+CaptchName(CAPTCHA_2),
CaptchName(CAPTCHA_1)+CAPTCHA_OP$+CaptchName(CAPTCHA_2),
CAPTCHA_1+" "+CAPTCHA_OP$+" "+CAPTCHA_2,
CaptchName(CAPTCHA_1)+" "+CAPTCHA_OP$+" "+CAPTCHA_2,
CAPTCHA_1+" "+CAPTCHA_OP$+" "+CaptchName(CAPTCHA_2),
CaptchName(CAPTCHA_1)+" "+CAPTCHA_OP$+" "+CaptchName(CAPTCHA_2));
return .@c$;
}
function script CaptchExample {
if (!CAPTCHA_TIME || getarg(0, false)) {
dispbottom("##1TO REPLY TO CAPTCHAS: @captcha <numeric answer>##1");
dispbottom l("Example: Give the answer for the following: one+1");
dispbottom l("Reply: %s", b("@captcha 2"));
dispbottom b(l("This example will not be shown again."));
}
return;
}
- script @captcha 32767,{
function captchaProbe;
end;
OnCall:
// Command disabled, ignore
if (!$CAPTCHA & 1)
end;
// Not you, ignore
if (getcharid(3) != $@BOTCHECK_TARGET)
end;
// Attempt cooldown
if (@captcha_cooldown > gettimetick(2)) {
dispbottom l("CAPTCHA: Cooldown in effect."); end;}
// Process answer
@captcha_cooldown=gettimetick(2)+.antiflood;
// Lets be reasonable
if (gettimetick(2)+2 > CAPTCHA_TIME) {
dispbottom l("CAPTCHA: An error happened, try again."); end;}
// Verify answer
.@ans$ = implode(.@atcmd_parameters$, " ");
.@ans=atoi(.@ans$);
if (.@ans == CAPTCHA_ANSWER) {
CAPTCHA_OK=gettimetick(2)+.cooldown;
$@BOTCHECK_TARGET=0;
dispbottom any(
l("Captcha successful"),
l("Captcha ok"),
l("Correct"),
l("Understood"),
l("Not bad"),
l("Hmpf. That'll do."),
l("A bit longer and I would have jailed you %%\\ "),
l("%%\\ that'll do."),
l("%%N"));
dispbottom l("Remember: Players can also help enforcing no-AFK-bot rule!");
//dispbottom l("Remember: Never lend your toothbrush to a slime!");
} else {
dispbottom l("CAPTCHA: Incorrect answer. Wait %ds and try again.", .antiflood); // Max 10 attempts total
}
end;
OnInit:
.thr=180; // Seconds to reply
.cooldown=3600; // Captcha Immunity
.antiflood=18; // Seconds between captcha failed attempts
bindatcmd "captcha", "@captcha::OnCall", 0, 0, 0;
initnpctimer;
end;
function syndroCheck {
.@k = getarg(0);
.@t = getarg(1, 2000);
sleep2(.@t);
if (!playerattached()) return false;
if (BaseExp != .@k) return true;
return false;
}
// Restart if it somehow get struck
OnTimer60000:
OnTimer150000:
OnTimer470000:
OnTimer600000:
OnTimer800000:
initnpctimer;
end;
// Pick a random target for captcha checks, which happens every 12 seconds
OnTimer12000:
// Script disabled by admins
if (!$CAPTCHA) {
initnpctimer;
end;
}
// Event in progress and flag to skip is set
if ($CAPTCHA & 8) {
if ($@MK_SCENE || $@GM_EVENT) {
initnpctimer;
end;
}
}
// If we're using the banhammer, handle the target
if ($CAPTCHA & 1) {
if ($@BOTCHECK_TARGET) captchaProbe();
}
// Maybe we will conduct a captcha
if (rand2(28) < 3 || $@GM_OVERRIDE) {
// This can be slow, beware
.@c = getunits(BL_PC, .@players, MAX_CYCLE_PC);
array_shuffle(.@players);
for (.@i = 0; .@i < .@c; .@i++) {
// Too lazy to botcheck you today (check chance = 3%)
if (rand2(100) > 3)
continue;
// Okay, lets do it
attachrid(.@players[.@i]);
// Inform the GM Override Console about ongoing botcheck
if ($@GM_OVERRIDE)
debugmes "[BOT] Conducting botcheck for %d", getcharid(3);
// TODO: What about jailed players?
//if (getstatus(SC_JAILED))
// continue;
// 1. Player in immunity, who is next one
if (CAPTCHA_OK > gettimetick(2)) {
detachrid();
continue;
}
// 2. Player must be jailed, and we continue
if (CAPTCHA_TIME < CAPTCHA_OK) {
atcommand("@jailfor 40mn "+strcharinfo(0));
dispbottom l("You failed to reply to the captcha in time and were arrested for AFK Botting. You can use @jailtime to keep track of time left.");
CAPTCHA_OK=CAPTCHA_TIME;
detachrid();
continue;
}
// 2.1 Player is AFK for more than 30 seconds
if (checkidle() > 30) {
detachrid();
continue;
}
// 3. This is a good target, lets do this
// If banhammer is enabled
if ($CAPTCHA & 1) {
.@g$="";
CaptchExample();
MakeCaptch();
.@g$=any(
"CAPTCHA: Please reply the following: "+CaptchVal(),
"BOTCHECK: Please reply the following: "+CaptchVal(),
"CAPTCHA: You must answer this: "+CaptchVal(),
"BOTCHECK: You must answer this: "+CaptchVal());
// 4. Find a random method
switch (rand2(2)) {
case 0:
message(getcharid(3), .@g$);
message(getcharid(3), "Example for one + one: @captcha 2");
break;
case 1:
announce(.@g$, bc_self|bc_pc);
announce("Example for one + one: @captcha 2", bc_self|bc_pc);
break;
default:
dispbottom(.@g$);
dispbottom("Example for one + one: @captcha 2");
break;
}
}
// 3.1 This is a good target, lets do this
// If Botter Syndrome is enabled
if ($CAPTCHA & 4) {
// Players are exempt of Botting Syndrome until unlocking Tulimshar
// But if they're deemed overleveled, this exemption dies
if (!getq(General_Narrator) && BaseLevel < 15)
continue;
// We will now study you for a short while. First, take a sample
.@exp = BaseExp; .@jxp = JobExp; .@mpt = Mobpt; .@hp = Hp;
.@wgt = Weight; .@m$ = getmap(); .@mpk = MONSTERS_KILLED;
.@hon = HONOR; .@k = BaseExp;
// We'll now wait in some intervals. Killing in them cause
// the K variables to be set, and contribute towards bot score.
.@k1 = syndroCheck(.@k); .@k = BaseExp;
.@k2 = syndroCheck(.@k); .@k = BaseExp;
.@k3 = syndroCheck(.@k); .@k = BaseExp;
.@k4 = syndroCheck(.@k); .@k = BaseExp;
.@k5 = syndroCheck(.@k); .@k = BaseExp;
.@k6 = syndroCheck(.@k); .@k = BaseExp;
.@k7 = syndroCheck(.@k); .@k = BaseExp;
sleep2(1000);
// You logged out? LAME! Anyway, carry on
if (!playerattached())
break;
/////////////////////////////////////////////////////////////////
// The chance of you contracting the Syndrome starts at 65.0%
.@chance = 600;
// Every 2 seconds you spent without killing is 1.5%
if (!.@k1)
.@chance -= 15;
if (!.@k2)
.@chance -= 15;
if (!.@k3)
.@chance -= 15;
if (!.@k4)
.@chance -= 15;
if (!.@k5)
.@chance -= 15;
if (!.@k6)
.@chance -= 15;
if (!.@k7)
.@chance -= 15;
// You killed less than 7 monsters, fall to 40.0%
if (.@mpk + 7 > MONSTERS_KILLED)
.@chance -= 200;
// (If you killed less than 12 monsters, fall to 55.0% instead)
else if (.@mpk + 12 > MONSTERS_KILLED)
.@chance -= 125;
// If you interacted with any NPC, it falls to 27.5%
if (@npctalk+600 > gettimetick(2))
.@chance -= 100;
// You're a light carrier (40% weight), fall to 25.0%
if (Weight * 4 <= MaxWeight / 10)
.@chance -= 30;
// You assigned all your stat points, fall to 20.0%
if (!StatusPoint)
.@chance -= 25;
// Monster points did not increase, fall to 17.5%
if (Mobpt <= .@mpt)
.@chance -= 25;
// Your weight decreased or did not increase, fall to 12.5%
if (Weight <= .@wgt)
.@chance -= 25;
// Your experience did not increase or level up, fall to 10.0%
if (BaseExp <= .@exp)
.@chance -= 25;
// Same for job experience, fall to 7.5%
if (JobExp <= .@jxp)
.@chance -= 25;
// Your map changed, fall to 2.0%
if (getmap() != .@m$)
.@chance -= 55;
// You are/were in a SuperMMO zone, fall to -0.5%
if (getmapinfo(MAPINFO_ZONE, .@m$) == "SuperMMO")
.@chance -= 30;
// (If it is a MMO zone, fall to 0.5% instead)
else if (getmapinfo(MAPINFO_ZONE, .@m$) == "MMO")
.@chance -= 15;
// Your HP increased or remained, fall to -3.0%
if (Hp >= .@hp)
.@chance -= 40;
// You are staff or sponsor or whatever, total at -3.5% bonus
if (getgmlevel())
.@chance -= 5;
// You haven't disconnected recently, total at -4.0%
if (CHAREG_CLEANUP+3600 < gettimetick(2))
.@chance -= 5;
// You already used a skill recently/today, total at -5.0%
if (@skillId)
.@chance -= 10;
// You killed someone in PVP or died in PVP, total at -10.0%
if (.@hon != HONOR)
.@chance -= 50;
// You are still a novice, total at -12.0%
if (!REBIRTH && BaseLevel < 70 && JobLevel < 40)
.@chance -= 20;
// TODO: Total connection time? Item usage? Chat? AOE?
// TODO: Reduce chance if you've already contracted the Syndrome?
/////////////////////////////////////////////////////////////////
if ($@GM_OVERRIDE)
debugmes "Botter Syndrome [%s] %d%%", strcharinfo(0), .@chance/10;
if (rand2(1000) < .@chance) {
// You've contracted the Botter Syndrome!
// Your rate drop in 1% ~ 15% depending on your botting score
.@eff = 1 + (.@chance / 50);
// If you already have one running, change effects
if (getstatus(SC_BOTTER_SYNDROME)) {
.@t = 1 + getstatus(SC_BOTTER_SYNDROME, 5) / 3600000;
.@eff += .@t + min(100, getstatus(SC_BOTTER_SYNDROME, 1));
}
// Sanitize the effect (it can go past 100% but...)
.@eff = min(100, .@eff);
// The syndrome starts weak and gradually strengthens
SC_Bonus(3600, SC_BOTTER_SYNDROME, .@eff);
.@t = getstatus(SC_BOTTER_SYNDROME, 5) / 1000;
.@eff = limit(0, getstatus(SC_BOTTER_SYNDROME, 1), 100);
// Select random Nurse which can cure you
// But if your symptoms worsen, keep the previous nurse
if (CAPTCHA_NPC$ == "") {
if (BaseLevel >= 70)
CAPTCHA_NPC$=any("009-4", "024-8", "004-1", "012-6", "005-7", "006-2-1");
else if (BaseLevel >= 30)
CAPTCHA_NPC$=any("004-1", "012-6", "005-7", "006-2-1");
else
CAPTCHA_NPC$=any("004-1", "005-7");
}
// Inform which Nurse can cure you
if (CAPTCHA_NPC$ == "009-4") .@n$=l("Halinarzo");
else if (CAPTCHA_NPC$ == "024-8") .@n$=l("Frostia");
else if (CAPTCHA_NPC$ == "004-1") .@n$=l("Tulimshar");
else if (CAPTCHA_NPC$ == "012-6") .@n$=l("Hurnscald");
else if (CAPTCHA_NPC$ == "005-7") .@n$=l("Candor");
else if (CAPTCHA_NPC$ == "006-2-1") .@n$=l("Piou Isles");
else .@n$="???";
// Send information message
dispbottom l("----------------------------------------------");
dispbottom l("* You can find a Nurse in %s to heal, wait %s, or drink a %s to dispel.", b(.@n$), FuzzyTime(gettimetick(2)+.@t), getitemlink(ElixirOfLife));
dispbottom l("OH NOES! You have contracted the %s! Your experience gain and drop rate was greatly reduced by %d%%!", b(col(l("Botter's Syndrome"), 1)), .@eff);
}
}
// 5. Detach rid, target is set
$@BOTCHECK_TARGET=getcharid(3);
detachrid();
break;
}
}
// Continue this timer forever
initnpctimer;
end;
function captchaProbe {
// Attach rid
.@online=attachrid($@BOTCHECK_TARGET);
// User disconnected, next captcha they'll be arrested because timer will expire
if (!.@online) {
$@BOTCHECK_TARGET=false;
CAPTCHA_OK=false;
}
// Timer expired? Ban hammer
if (CAPTCHA_TIME+.thr > gettimetick(2) && CAPTCHA_OK <= CAPTCHA_TIME) {
atcommand("@jailfor 30mn "+strcharinfo(0));
dispbottom l("You failed to reply to the captcha in time and were arrested for AFK Botting. You can use @jailtime to keep track of time left.");
CaptchExample(true);
$@BOTCHECK_TARGET=false;
CAPTCHA_OK=CAPTCHA_TIME;
}
// Nothing happened, lets wait
if ($CAPTCHA & 2) {
if (!@captcha_lastwarningt)
@captcha_lastwarningt=3;
if (!@captcha_lastwarning)
@captcha_lastwarning=gettimetick(2);
if (@captcha_lastwarning < gettimetick(2)) {
dispbottom l("CAPTCHA: You have %s minute(s) remaining", CaptchName(@captcha_lastwarningt));
@captcha_lastwarningt-=1;
@captcha_lastwarning+=60;
}
}
return;
}
}