// 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 // 16 - Allow players to treat each other // 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 ##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; OnClinic: // Syndrome disabled, ignore if (!$CAPTCHA & 4) end; // Command disabled, ignore if (!$CAPTCHA & 16) end; // You haven't learned how to treat anyone if (getq(General_Narrator) < 1 && !getq(CandorQuest_Nurse)) end; // You cannot treat anyone if (array_find($CAPTCHA_BLIST, getcharid(3)) >= 0) end; // Verify the target .@request$ = ""; .@request$ += implode(.@atcmd_parameters$, " "); // TODO: Quest - Treat a NPC with Botter's Syndrome (Trainer?) // Player is not attached or is yourself .@id = getcharid(3, .@request$); if (!.@id) { Exception("Player not found.", RB_ISFATAL|RB_DISPBOTTOM); } else if (.@id == getcharid(3)) { Exception("You cannot treat yourself.", RB_ISFATAL|RB_DISPBOTTOM); } // You already treated someone recently if (CHAREG_CLEANUP+300 > gettimetick(2)) Exception(sprintf("You need to be online for longer than %d minute(s) to treat someone.", 5), RB_ISFATAL|RB_DISPBOTTOM); if (@treatcaptcha+300 > gettimetick(2)) Exception(sprintf("You need to wait %s before treat someone else again.", FuzzyTime(@treatcaptcha+300)), RB_ISFATAL|RB_DISPBOTTOM); // Treatment confirmation mes ".:: " + l("Treatment") + " ::."; mes l("Botter Syndrome is a common ailment which, while it mostly affect bots, even regular adventurers and NPCs may contract."); mes l("If you believe \"%s\" is ##Bnot##b botting, then you may treat them yourself."); mes ""; mes l("- Treating a nearby player will almost always succeed."); mes l("- Treating a player in the same map has <70%% chance of succeeding."); mes l("- Treating a player in another map has <30%% chance of succeeding."); mes l("- Treating a bot may result in the Alliance confiscating your treatment kit."); mes l("- Treating yourself via an alt may also result in a %s for one of your accounts.", b(l("permanent ban"))); mes ""; mes l("It takes %d minute(s) to treat someone else after this.", 5); next; mesc ("Do you want to treat \"%s\"?", b(.@request$)), 1; if (askyesno() == ASK_NO) { closeclientdialog; end; } // Regardless of whatever happens now, the treatment was attempted .@m1$ = getmap(); .@caster = getcharid(3); .@chance = 30; .@benef$ = strcharinfo(0); @treatcaptcha = gettimetick(2); closeclientdialog; // Attach the one you intend to heal if (attachplayer(.@id)) { // Not infected, so do nothing if (!getstatus(SC_BOTTER_SYNDROME)) end; // Same map check getmapxy(.@m$, .@x, .@y, 0); if (.@m$ == .@m1$) { .@chance = 70; // Check if caster and treated are visible .@c=getunits(BL_PC, .@pcs, MAX_CYCLE_PC, .@m$, .@x-14, .@y-14, .@x+14, .@y+14); for (.@i = 0; .@i < .@c; .@i++) { if (.@pcs[.@i] == .@caster) { .@chance = 100; break; } // Caster loop } // for loop } // Map loop // If we went this far, it is fine to log the result logmes sprintf("@treat: \"%s\" treated \"%s\" Botter's Syndrome.", .@benef$, strcharinfo(0)), LOGMES_ATCOMMAND; // The chance is further modified by how intense the syndrome is // Every 5% effect steals 1% of treatment chance .@chance -= limit(0, getstatus(SC_BOTTER_SYNDROME, 1) / 5, 25); // Now we have .@chance, so decide if treatment worked if (rand2(100) < .@chance) { sc_end SC_BOTTER_SYNDROME; dispbottom l("Your %s was treated by %s.", b(l("Botter's Syndrome")),.@benef$); } else { dispbottom l("%s tried to treat your syndrome, but was NOT successful.", .@benef$); } } // attachplayer() 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; bindatcmd "treat", "@captcha::OnClinic", 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; } // CAPTCHA_NPC$=any("009-4", "024-8", "004-1", "012-6", "005-7", "006-2-1"); // Halin Frost Tulim Hurns Candor Pious function syndroNurse { if (CAPTCHA_NPC$ != "") return; if (LOCATION$ == "Candor" && !compare(getmap(), "005")) CAPTCHA_NPC$ = "005-7"; else if (LOCATION$ == "Candor") CAPTCHA_NPC$ = "006-2-1"; else if (LOCATION$ == "Artis") CAPTCHA_NPC$ = "005-7"; else if (LOCATION$ == "Hurns" && !compare(getmap(), "012")) CAPTCHA_NPC$ = "012-6"; else if (LOCATION$ == "Hurns") CAPTCHA_NPC$ = "009-4"; else if (LOCATION$ == "LoF") CAPTCHA_NPC$ = "012-6"; else if (LOCATION$ == "Lilit") CAPTCHA_NPC$ = "012-6"; else if (LOCATION$ == "Fort") CAPTCHA_NPC$ = "012-6"; else if (LOCATION$ == "Nival" && BaseLevel >= 70) CAPTCHA_NPC$ = "024-8"; else if (LOCATION$ == "Frostia") CAPTCHA_NPC$ = "024-8"; else if (LOCATION$ == "Halin" && !compare(getmap(), "009")) CAPTCHA_NPC$ = "009-4"; else if (LOCATION$ == "Halin" && $HURNS_LIBDATE) CAPTCHA_NPC$ = "012-6"; else if (LOCATION$ == "Halin") CAPTCHA_NPC$ = "004-1"; else if (LOCATION$ == "Tulim" && !compare(getmap(), "004")) CAPTCHA_NPC$ = "004-1"; else if (LOCATION$ == "Tulim" && BaseLevel >= 40) CAPTCHA_NPC$ = "009-4"; // All else failed (several cases where it may), chose one randomly else if (BaseLevel >= 70 && $NIVALIS_LIBDATE) CAPTCHA_NPC$=any("009-4", "024-8", "004-1", "012-6", "005-7", "006-2-1"); else if (BaseLevel >= 30 && $HURNS_LIBDATE) CAPTCHA_NPC$=any("004-1", "012-6", "005-7", "006-2-1"); else CAPTCHA_NPC$=any("004-1", "005-7"); return; } // 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 // TODO: Maybe up to Lv 30? 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; .@k8 = syndroCheck(.@k); .@k = BaseExp; // You logged out? LAME! Anyway, carry on if (!playerattached()) break; ///////////////////////////////////////////////////////////////// // The chance of you contracting the Syndrome starts at 60.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; if (!.@k8) .@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 a nearby Nurse which can cure you // But if your symptoms worsen, keep the previous nurse syndroNurse(); // 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; } }