2 # irpg bot v3.1.2 by jotun, jotun@idlerpg.net, et al. See http://idlerpg.net/
4 # Some code within this file was written by authors other than myself. As such,
5 # distributing this code or distributing modified versions of this code is
6 # strictly prohibited without written authorization from the authors. Contact
7 # jotun@idlerpg.net. Please note that this may change (at any time, no less) if
8 # authorization for distribution is given by patch submitters.
10 # As a side note, patches submitted for this project are automatically taken to
11 # be freely distributable and modifiable for any use, public or private, though
12 # I make no claim to ownership; original copyrights will be retained.. except as
15 # Please mail bugs, etc. to me. Patches are welcome to fix bugs or clean up
16 # the code, but please do not use a radically different coding style. Thanks
17 # to everyone that's contributed!
19 # NOTE: This code should NOT be run as root. You deserve anything that happens
20 # to you if you run this code as a superuser. Also, note that giving a
21 # user admin access to the bot effectively gives them full access to the
22 # user under which your bot runs, as they can use the PEVAL command to
23 # execute any command, or possibly even change your password. I sincerely
24 # suggest that you exercise extreme caution when giving someone admin
25 # access to your bot, or that you disable the PEVAL command for non-owner
26 # accounts in your config file, .irpg.conf
39 my $version = "3.1.2";
41 # command line overrides .irpg.conf
94 "dbfile|irpgdb|db|d=s",
95 ) or debug("Error: Could not parse command line. Try $0 --help\n",1);
97 $opts{help} and do { help(); exit 0; };
99 debug("Config: read $_: ".Dumper($opts{$_})) for keys(%opts);
101 my $outbytes = 0; # sent bytes
102 my $primnick = $opts{botnick}; # for regain or register checks
103 my $inbytes = 0; # received bytes
104 my %onchan; # users on game channel
105 my %rps; # role-players
108 p1 => [], # point 1 for q2
109 p2 => [], # point 2 for q2
110 qtime => time() + int(rand(21600)), # first quest starts in <=6 hours
113 stage => 1); # quest info
115 my $rpreport = 0; # constant for reporting top players
116 my %prev_online; # user@hosts online on restart, die
117 my %auto_login; # users to automatically log back on
118 my @bans; # bans auto-set by the bot, saved to be removed after 1 hour
119 my $pausemode = 0; # pausemode on/off flag
120 my $silentmode = 0; # silent mode 0/1/2/3, see head of file
121 my @queue; # outgoing message queue
122 my $lastreg = 0; # holds the time of the last reg. cleared every second.
123 # prevents more than one account being registered / second
124 my $registrations = 0; # count of registrations this period
125 my $sel; # IO::Select object
126 my $lasttime = 1; # last time that rpcheck() was run
127 my $buffer; # buffer for socket stuff
128 my $conn_tries = 0; # number of connection tries. gives up after trying each
130 my $sock; # IO::Socket::INET object
131 my %split; # holds nick!user@hosts for clients that have been netsplit
132 my $freemessages = 4; # number of "free" privmsgs we can send. 0..$freemessages
134 sub daemonize(); # prototype to avoid warnings
136 if (! -e $opts{dbfile}) {
139 print "$opts{dbfile} does not appear to exist. I'm guessing this is your ".
140 "first time using IRPG. Please give an account name that you would ".
141 "like to have admin access [$opts{owner}]: ";
142 chomp(my $uname = <STDIN>);
144 $uname = length($uname)?$uname:$opts{owner};
145 print "Enter a character class for this account: ";
146 chomp(my $uclass = <STDIN>);
147 $rps{$uname}{class} = substr($uclass,0,30);
148 print "Enter a password for this account: ";
149 if ($^O ne "MSWin32") {
150 system("stty -echo");
152 chomp(my $upass = <STDIN>);
153 if ($^O ne "MSWin32") {
156 $rps{$uname}{pass} = crypt($upass,mksalt());
157 $rps{$uname}{next} = $opts{rpbase};
158 $rps{$uname}{nick} = "";
159 $rps{$uname}{userhost} = "";
160 $rps{$uname}{level} = 0;
161 $rps{$uname}{online} = 0;
162 $rps{$uname}{idled} = 0;
163 $rps{$uname}{created} = time();
164 $rps{$uname}{lastlogin} = time();
165 $rps{$uname}{x} = int(rand($opts{mapx}));
166 $rps{$uname}{y} = int(rand($opts{mapy}));
167 $rps{$uname}{alignment}="n";
168 $rps{$uname}{isadmin} = 1;
169 for my $item ("ring","amulet","charm","weapon","helm",
170 "tunic","pair of gloves","shield",
171 "set of leggings","pair of boots") {
172 $rps{$uname}{item}{$item} = 0;
174 for my $pen ("pen_mesg","pen_nick","pen_part",
175 "pen_kick","pen_quit","pen_quest",
176 "pen_logout","pen_logout") {
177 $rps{$uname}{$pen} = 0;
180 print "OK, wrote you into $opts{dbfile}.\n";
183 # this is almost silly...
184 if ($opts{checkupdates}) {
185 print "Checking for updates...\n\n";
186 my $tempsock = IO::Socket::INET->new(PeerAddr=>"jotun.ultrazone.org:80",
189 print $tempsock "GET /g7/version.php?version=$version HTTP/1.1\r\n".
190 "Host: jotun.ultrazone.org:80\r\n\r\n";
191 my($line,$newversion);
192 while ($line=<$tempsock>) {
195 if ($line =~ /^Current version : (\S+)/) {
196 if ($version ne $1) {
197 print "There is an update available! Changes include:\n";
201 print "You are running the latest version (v$1).\n";
206 elsif ($newversion && $line =~ /^( -? .+)/) { print "$1\n"; }
207 elsif ($newversion && $line =~ /^URL: (.+)/) {
208 print "\nGet the newest version from $1!\n";
214 else { print debug("Could not connect to update server.")."\n"; }
217 print "\n".debug("Becoming a daemon...")."\n";
220 $SIG{HUP} = "readconfig"; # sighup = reread config file
226 while (!$sock && $conn_tries < 2*@{$opts{servers}}) {
227 debug("Connecting to $opts{servers}->[0]...");
228 my %sockinfo = (PeerAddr => $opts{servers}->[0],
230 if ($opts{localaddr}) { $sockinfo{LocalAddr} = $opts{localaddr}; }
231 $sock = IO::Socket::INET->new(%sockinfo) or
232 debug("Error: failed to connect: $!\n");
235 # cycle front server to back if connection failed
236 push(@{$opts{servers}},shift(@{$opts{servers}}));
238 else { debug("Connected."); }
242 debug("Error: Too many connection failures, exhausted server list.\n",1);
247 $sel = IO::Select->new($sock);
249 sts("NICK $opts{botnick}");
250 sts("USER $opts{botuser} 0 0 :$opts{botrlnm}");
253 my($readable) = IO::Select->select($sel,undef,undef,0.5);
254 if (defined($readable)) {
255 my $fh = $readable->[0];
257 $fh->recv($buffer2,512,0);
258 if (length($buffer2)) {
260 while (index($buffer,"\n") != -1) {
261 my $line = substr($buffer,0,index($buffer,"\n")+1);
262 $buffer = substr($buffer,length($line));
267 # uh oh, we've been disconnected from the server, possibly before
268 # we've logged in the users in %auto_login. so, we'll set those
269 # users' online flags to 1, rewrite db, and attempt to reconnect
270 # (if that's wanted of us)
271 $rps{$_}{online}=1 for keys(%auto_login);
277 if ($opts{reconnect}) {
280 debug("Socket closed; disconnected. Cleared outgoing message ".
281 "queue. Waiting $opts{reconnect_wait}s before next ".
282 "connection attempt...");
283 sleep($opts{reconnect_wait});
286 else { debug("Socket closed; disconnected.",1); }
289 else { select(undef,undef,undef,1); }
290 if ((time()-$lasttime) >= $opts{self_clock}) { rpcheck(); }
296 $inbytes += length($in); # increase parsed byte count
297 $in =~ s/[\r\n]//g; # strip all \r and \n
299 my @arg = split(/\s/,$in); # split into "words"
300 my $usernick = substr((split(/!/,$arg[0]))[0],1);
301 # logged in char name of nickname, or undef if nickname is not online
302 my $username = finduser($usernick);
303 if (lc($arg[0]) eq 'ping') { sts("PONG $arg[1]",1); }
304 elsif (lc($arg[0]) eq 'error') {
305 # uh oh, we've been disconnected from the server, possibly before we've
306 # logged in the users in %auto_login. so, we'll set those users' online
307 # flags to 1, rewrite db, and attempt to reconnect (if that's wanted of
309 $rps{$_}{online}=1 for keys(%auto_login);
313 $arg[1] = lc($arg[1]); # original case no longer matters
314 if ($arg[1] eq '433' && $opts{botnick} eq $arg[3]) {
316 sts("NICK $opts{botnick}");
318 elsif ($arg[1] eq 'join') {
319 # %onchan holds time user joined channel. used for the advertisement ban
320 $onchan{$usernick}=time();
321 if ($opts{'detectsplits'} && exists($split{substr($arg[0],1)})) {
322 delete($split{substr($arg[0],1)});
324 elsif ($opts{botnick} eq $usernick) {
325 sts("WHO $opts{botchan}");
326 (my $opcmd = $opts{botopcmd}) =~ s/%botnick%/$opts{botnick}/eg;
328 $lasttime = time(); # start rpcheck()
331 elsif ($arg[1] eq 'quit') {
332 # if we see our nick come open, grab it (skipping queue)
333 if ($usernick eq $primnick) { sts("NICK $primnick",1); }
334 elsif ($opts{'detectsplits'} &&
335 "@arg[2..$#arg]" =~ /^:\S+\.\S+ \S+\.\S+$/) {
336 if (defined($username)) { # user was online
337 $split{substr($arg[0],1)}{time}=time();
338 $split{substr($arg[0],1)}{account}=$username;
342 penalize($username,"quit");
344 delete($onchan{$usernick});
346 elsif ($arg[1] eq 'nick') {
347 # if someone (nickserv) changes our nick for us, update $opts{botnick}
348 if ($usernick eq $opts{botnick}) {
349 $opts{botnick} = substr($arg[2],1);
351 # if we see our nick come open, grab it (skipping queue), unless it was
352 # us who just lost it
353 elsif ($usernick eq $primnick) { sts("NICK $primnick",1); }
355 penalize($username,"nick",$arg[2]);
356 $onchan{substr($arg[2],1)} = delete($onchan{$usernick});
359 elsif ($arg[1] eq 'part') {
360 penalize($username,"part");
361 delete($onchan{$usernick});
363 elsif ($arg[1] eq 'kick') {
365 penalize(finduser($usernick),"kick");
366 delete($onchan{$usernick});
368 # don't penalize /notices to the bot
369 elsif ($arg[1] eq 'notice' && $arg[2] ne $opts{botnick}) {
370 penalize($username,"notice",length("@arg[3..$#arg]")-1);
372 elsif ($arg[1] eq '001') {
373 # send our identify command, set our usermode, join channel
374 sts($opts{botident});
375 sts("MODE $opts{botnick} :$opts{botmodes}");
376 sts("JOIN $opts{botchan}");
377 $opts{botchan} =~ s/ .*//; # strip channel key if present
379 elsif ($arg[1] eq '315') {
380 # 315 is /WHO end. report who we automagically signed online iff it will
382 if (keys(%auto_login)) {
383 # not a true measure of size, but easy
384 if (length("%auto_login") < 1024 && $opts{senduserlist}) {
385 chanmsg(scalar(keys(%auto_login))." users matching ".
386 scalar(keys(%prev_online))." hosts automatically ".
387 "logged in; accounts: ".join(", ",keys(%auto_login)));
390 chanmsg(scalar(keys(%auto_login))." users matching ".
391 scalar(keys(%prev_online))." hosts automatically ".
394 if ($opts{voiceonlogin}) {
395 my @vnicks = map { $rps{$_}{nick} } keys(%auto_login);
397 sts("MODE $opts{botchan} +".
398 ('v' x $opts{modesperline})." ".
399 join(" ",@vnicks[0..$opts{modesperline}-1]));
400 splice(@vnicks,0,$opts{modesperline});
404 else { chanmsg("0 users qualified for auto login."); }
408 elsif ($arg[1] eq '005') {
409 if ("@arg" =~ /MODES=(\d+)/) { $opts{modesperline}=$1; }
411 elsif ($arg[1] eq '352') {
413 # 352 is one line of /WHO. check that the nick!user@host exists as a key
414 # in %prev_online, the list generated in loaddb(). the value is the user
416 $onchan{$arg[7]}=time();
417 if (exists($prev_online{$arg[7]."!".$arg[4]."\@".$arg[5]})) {
418 $rps{$prev_online{$arg[7]."!".$arg[4]."\@".$arg[5]}}{online} = 1;
419 $auto_login{$prev_online{$arg[7]."!".$arg[4]."\@".$arg[5]}}=1;
422 elsif ($arg[1] eq 'privmsg') {
423 $arg[0] = substr($arg[0],1); # strip leading : from privmsgs
424 if (lc($arg[2]) eq lc($opts{botnick})) { # to us, not channel
425 $arg[3] = lc(substr($arg[3],1)); # lowercase, strip leading :
426 if ($arg[3] eq "\1version\1") {
427 notice("\1VERSION IRPG bot v$version by jotun; ".
428 "http://idlerpg.net/\1",$usernick);
430 elsif ($arg[3] eq "peval") {
431 if (!ha($username) || ($opts{ownerpevalonly} &&
432 $opts{owner} ne $username)) {
433 privmsg("You don't have access to PEVAL.", $usernick);
436 my @peval = eval "@arg[4..$#arg]";
437 if (@peval >= 4 || length("@peval") > 1024) {
438 privmsg("Command produced too much output to send ".
439 "outright; queueing ".length("@peval").
440 " bytes in ".scalar(@peval)." items. Use ".
441 "CLEARQ to clear queue if needed.",$usernick,1);
442 privmsg($_,$usernick) for @peval;
444 else { privmsg($_,$usernick, 1) for @peval; }
445 privmsg("EVAL ERROR: $@", $usernick, 1) if $@;
448 elsif ($arg[3] eq "register") {
449 if (defined $username) {
450 privmsg("Sorry, you are already online as $username.",
454 if ($#arg < 6 || $arg[6] eq "") {
455 privmsg("Try: REGISTER <char name> <password> <class>",
457 privmsg("IE : REGISTER Poseidon MyPassword God of the ".
461 privmsg("Sorry, new accounts may not be registered ".
462 "while the bot is in pause mode; please wait ".
463 "a few minutes and try again.",$usernick);
465 elsif (exists $rps{$arg[4]} || ($opts{casematters} &&
466 scalar(grep { lc($arg[4]) eq lc($_) } keys(%rps)))) {
467 privmsg("Sorry, that character name is already in use.",
470 elsif (lc($arg[4]) eq lc($opts{botnick}) ||
471 lc($arg[4]) eq lc($primnick)) {
472 privmsg("Sorry, that character name cannot be ".
473 "registered.",$usernick);
475 elsif (!exists($onchan{$usernick})) {
476 privmsg("Sorry, you're not in $opts{botchan}.",
479 elsif (length($arg[4]) > 16 || length($arg[4]) < 1) {
480 privmsg("Sorry, character names must be < 17 and > 0 ".
481 "chars long.", $usernick);
483 elsif ($arg[4] =~ /^#/) {
484 privmsg("Sorry, character names may not begin with #.",
487 elsif ($arg[4] =~ /\001/) {
488 privmsg("Sorry, character names may not include ".
489 "character \\001.",$usernick);
491 elsif ($opts{noccodes} && ($arg[4] =~ /[[:cntrl:]]/ ||
492 "@arg[6..$#arg]" =~ /[[:cntrl:]]/)) {
493 privmsg("Sorry, neither character names nor classes ".
494 "may include control codes.",$usernick);
496 elsif ($opts{nononp} && ($arg[4] =~ /[[:^print:]]/ ||
497 "@arg[6..$#arg]" =~ /[[:^print:]]/)) {
498 privmsg("Sorry, neither character names nor classes ".
499 "may include non-printable chars.",$usernick);
501 elsif (length("@arg[6..$#arg]") > 30) {
502 privmsg("Sorry, character classes must be < 31 chars ".
505 elsif (time() == $lastreg) {
506 privmsg("Wait 1 second and try again.",$usernick);
509 if ($opts{voiceonlogin}) {
510 sts("MODE $opts{botchan} +v :$usernick");
514 $rps{$arg[4]}{next} = $opts{rpbase};
515 $rps{$arg[4]}{class} = "@arg[6..$#arg]";
516 $rps{$arg[4]}{level} = 0;
517 $rps{$arg[4]}{online} = 1;
518 $rps{$arg[4]}{nick} = $usernick;
519 $rps{$arg[4]}{userhost} = $arg[0];
520 $rps{$arg[4]}{created} = time();
521 $rps{$arg[4]}{lastlogin} = time();
522 $rps{$arg[4]}{pass} = crypt($arg[5],mksalt());
523 $rps{$arg[4]}{x} = int(rand($opts{mapx}));
524 $rps{$arg[4]}{y} = int(rand($opts{mapy}));
525 $rps{$arg[4]}{alignment}="n";
526 $rps{$arg[4]}{isadmin} = 0;
527 for my $item ("ring","amulet","charm","weapon","helm",
528 "tunic","pair of gloves","shield",
529 "set of leggings","pair of boots") {
530 $rps{$arg[4]}{item}{$item} = 0;
532 for my $pen ("pen_mesg","pen_nick","pen_part",
533 "pen_kick","pen_quit","pen_quest",
534 "pen_logout","pen_logout") {
535 $rps{$arg[4]}{$pen} = 0;
537 chanmsg("Welcome $usernick\'s new player $arg[4], the ".
538 "@arg[6..$#arg]! Next level in ".
539 duration($opts{rpbase}).".");
540 privmsg("Success! Account $arg[4] created. You have ".
541 "$opts{rpbase} seconds idleness until you ".
542 "reach level 1. ", $usernick);
543 privmsg("NOTE: The point of the game is to see who ".
544 "can idle the longest. As such, talking in ".
545 "the channel, parting, quitting, and changing ".
546 "nicks all penalize you.",$usernick);
547 if ($opts{phonehome}) {
548 my $tempsock = IO::Socket::INET->new(PeerAddr=>
549 "jotun.ultrazone.org:80");
552 "GET /g7/count.php?new=1 HTTP/1.1\r\n".
553 "Host: jotun.ultrazone.org:80\r\n\r\n";
561 elsif ($arg[3] eq "delold") {
562 if (!ha($username)) {
563 privmsg("You don't have access to DELOLD.", $usernick);
565 # insure it is a number
566 elsif ($arg[4] !~ /^[\d\.]+$/) {
567 privmsg("Try: DELOLD <# of days>", $usernick, 1);
570 my @oldaccounts = grep { (time()-$rps{$_}{lastlogin}) >
572 !$rps{$_}{online} } keys(%rps);
573 delete(@rps{@oldaccounts});
574 chanmsg(scalar(@oldaccounts)." accounts not accessed in ".
575 "the last $arg[4] days removed by $arg[0].");
578 elsif ($arg[3] eq "del") {
579 if (!ha($username)) {
580 privmsg("You don't have access to DEL.", $usernick);
582 elsif (!defined($arg[4])) {
583 privmsg("Try: DEL <char name>", $usernick, 1);
585 elsif (!exists($rps{$arg[4]})) {
586 privmsg("No such account $arg[4].", $usernick, 1);
589 delete($rps{$arg[4]});
590 chanmsg("Account $arg[4] removed by $arg[0].");
593 elsif ($arg[3] eq "mkadmin") {
594 if (!ha($username) || ($opts{owneraddonly} &&
595 $opts{owner} ne $username)) {
596 privmsg("You don't have access to MKADMIN.", $usernick);
598 elsif (!defined($arg[4])) {
599 privmsg("Try: MKADMIN <char name>", $usernick, 1);
601 elsif (!exists($rps{$arg[4]})) {
602 privmsg("No such account $arg[4].", $usernick, 1);
605 $rps{$arg[4]}{isadmin}=1;
606 privmsg("Account $arg[4] is now a bot admin.",$usernick, 1);
609 elsif ($arg[3] eq "deladmin") {
610 if (!ha($username) || ($opts{ownerdelonly} &&
611 $opts{owner} ne $username)) {
612 privmsg("You don't have access to DELADMIN.", $usernick);
614 elsif (!defined($arg[4])) {
615 privmsg("Try: DELADMIN <char name>", $usernick, 1);
617 elsif (!exists($rps{$arg[4]})) {
618 privmsg("No such account $arg[4].", $usernick, 1);
620 elsif ($arg[4] eq $opts{owner}) {
621 privmsg("Cannot DELADMIN owner account.", $usernick, 1);
624 $rps{$arg[4]}{isadmin}=0;
625 privmsg("Account $arg[4] is no longer a bot admin.",
629 elsif ($arg[3] eq "hog") {
630 if (!ha($username)) {
631 privmsg("You don't have access to HOG.", $usernick);
634 chanmsg("$usernick has summoned the Hand of God.");
638 elsif ($arg[3] eq "rehash") {
639 if (!ha($username)) {
640 privmsg("You don't have access to REHASH.", $usernick);
644 privmsg("Reread config file.",$usernick,1);
645 $opts{botchan} =~ s/ .*//; # strip channel key if present
648 elsif ($arg[3] eq "chpass") {
649 if (!ha($username)) {
650 privmsg("You don't have access to CHPASS.", $usernick);
652 elsif (!defined($arg[5])) {
653 privmsg("Try: CHPASS <char name> <new pass>", $usernick, 1);
655 elsif (!exists($rps{$arg[4]})) {
656 privmsg("No such username $arg[4].", $usernick, 1);
659 $rps{$arg[4]}{pass} = crypt($arg[5],mksalt());
660 privmsg("Password for $arg[4] changed.", $usernick, 1);
663 elsif ($arg[3] eq "chuser") {
664 if (!ha($username)) {
665 privmsg("You don't have access to CHUSER.", $usernick);
667 elsif (!defined($arg[5])) {
668 privmsg("Try: CHUSER <char name> <new char name>",
671 elsif (!exists($rps{$arg[4]})) {
672 privmsg("No such username $arg[4].", $usernick, 1);
674 elsif (exists($rps{$arg[5]})) {
675 privmsg("Username $arg[5] is already taken.", $usernick,1);
678 $rps{$arg[5]} = delete($rps{$arg[4]});
679 privmsg("Username for $arg[4] changed to $arg[5].",
683 elsif ($arg[3] eq "chclass") {
684 if (!ha($username)) {
685 privmsg("You don't have access to CHCLASS.", $usernick);
687 elsif (!defined($arg[5])) {
688 privmsg("Try: CHCLASS <char name> <new char class>",
691 elsif (!exists($rps{$arg[4]})) {
692 privmsg("No such username $arg[4].", $usernick, 1);
695 $rps{$arg[4]}{class} = "@arg[5..$#arg]";
696 privmsg("Class for $arg[4] changed to @arg[5..$#arg].",
700 elsif ($arg[3] eq "push") {
701 if (!ha($username)) {
702 privmsg("You don't have access to PUSH.", $usernick);
704 # insure it's a positive or negative, integral number of seconds
705 elsif ($arg[5] !~ /^\-?\d+$/) {
706 privmsg("Try: PUSH <char name> <seconds>", $usernick, 1);
708 elsif (!exists($rps{$arg[4]})) {
709 privmsg("No such username $arg[4].", $usernick, 1);
711 elsif ($arg[5] > $rps{$arg[4]}{next}) {
712 privmsg("Time to level for $arg[4] ($rps{$arg[4]}{next}s) ".
713 "is lower than $arg[5]; setting TTL to 0.",
715 chanmsg("$usernick has pushed $arg[4] $rps{$arg[4]}{next} ".
716 "seconds toward level ".($rps{$arg[4]}{level}+1));
717 $rps{$arg[4]}{next}=0;
720 $rps{$arg[4]}{next} -= $arg[5];
721 chanmsg("$usernick has pushed $arg[4] $arg[5] seconds ".
722 "toward level ".($rps{$arg[4]}{level}+1).". ".
723 "$arg[4] reaches next level in ".
724 duration($rps{$arg[4]}{next}).".");
727 elsif ($arg[3] eq "logout") {
728 if (defined($username)) {
729 penalize($username,"logout");
732 privmsg("You are not logged in.", $usernick);
735 elsif ($arg[3] eq "quest") {
736 if (!@{$quest{questers}}) {
737 privmsg("There is no active quest.",$usernick);
739 elsif ($quest{type} == 1) {
740 privmsg(join(", ",(@{$quest{questers}})[0..2]).", and ".
741 "$quest{questers}->[3] are on a quest to ".
742 "$quest{text}. Quest to complete in ".
743 duration($quest{qtime}-time()).".",$usernick);
745 elsif ($quest{type} == 2) {
746 privmsg(join(", ",(@{$quest{questers}})[0..2]).", and ".
747 "$quest{questers}->[3] are on a quest to ".
748 "$quest{text}. Participants must first reach ".
749 "[$quest{p1}->[0],$quest{p1}->[1]], then ".
750 "[$quest{p2}->[0],$quest{p2}->[1]].".
751 ($opts{mapurl}?" See $opts{mapurl} to monitor ".
752 "their journey's progress.":""),$usernick);
755 elsif ($arg[3] eq "status" && $opts{statuscmd}) {
756 if (!defined($username)) {
757 privmsg("You are not logged in.", $usernick);
759 # argument is optional
760 elsif ($arg[4] && !exists($rps{$arg[4]})) {
761 privmsg("No such user.",$usernick);
763 elsif ($arg[4]) { # optional 'user' argument
764 privmsg("$arg[4]: Level $rps{$arg[4]}{level} ".
765 "$rps{$arg[4]}{class}; Status: O".
766 ($rps{$arg[4]}{online}?"n":"ff")."line; ".
767 "TTL: ".duration($rps{$arg[4]}{next})."; ".
768 "Idled: ".duration($rps{$arg[4]}{idled}).
769 "; Item sum: ".itemsum($arg[4]),$usernick);
771 else { # no argument, look up this user
772 privmsg("$username: Level $rps{$username}{level} ".
773 "$rps{$username}{class}; Status: O".
774 ($rps{$username}{online}?"n":"ff")."line; ".
775 "TTL: ".duration($rps{$username}{next})."; ".
776 "Idled: ".duration($rps{$username}{idled})."; ".
777 "Item sum: ".itemsum($username),$usernick);
780 elsif ($arg[3] eq "whoami") {
781 if (!defined($username)) {
782 privmsg("You are not logged in.", $usernick);
785 privmsg("You are $username, the level ".
786 $rps{$username}{level}." $rps{$username}{class}. ".
787 "Next level in ".duration($rps{$username}{next}),
791 elsif ($arg[3] eq "newpass") {
792 if (!defined($username)) {
793 privmsg("You are not logged in.", $usernick)
795 elsif (!defined($arg[4])) {
796 privmsg("Try: NEWPASS <new password>", $usernick);
799 $rps{$username}{pass} = crypt($arg[4],mksalt());
800 privmsg("Your password was changed.",$usernick);
803 elsif ($arg[3] eq "align") {
804 if (!defined($username)) {
805 privmsg("You are not logged in.", $usernick)
807 elsif (!defined($arg[4]) || (lc($arg[4]) ne "good" &&
808 lc($arg[4]) ne "neutral" && lc($arg[4]) ne "evil")) {
809 privmsg("Try: ALIGN <good|neutral|evil>", $usernick);
812 $rps{$username}{alignment} = substr(lc($arg[4]),0,1);
813 chanmsg("$username has changed alignment to: ".lc($arg[4]).
815 privmsg("Your alignment was changed to ".lc($arg[4]).".",
819 elsif ($arg[3] eq "removeme") {
820 if (!defined($username)) {
821 privmsg("You are not logged in.", $usernick)
824 privmsg("Account $username removed.",$usernick);
825 chanmsg("$arg[0] removed his account, $username, the ".
826 $rps{$username}{class}.".");
827 delete($rps{$username});
830 elsif ($arg[3] eq "help") {
831 if (!ha($username)) {
832 privmsg("For information on IRPG bot commands, see ".
833 $opts{helpurl}, $usernick);
836 privmsg("Help URL is $opts{helpurl}", $usernick, 1);
837 privmsg("Admin commands URL is $opts{admincommurl}",
841 elsif ($arg[3] eq "die") {
842 if (!ha($username)) {
843 privmsg("You do not have access to DIE.", $usernick);
846 $opts{reconnect} = 0;
848 sts("QUIT :DIE from $arg[0]",1);
851 elsif ($arg[3] eq "reloaddb") {
852 if (!ha($username)) {
853 privmsg("You do not have access to RELOADDB.", $usernick);
855 elsif (!$pausemode) {
856 privmsg("ERROR: Can only use LOADDB while in PAUSE mode.",
861 privmsg("Reread player database file; ".scalar(keys(%rps)).
862 " accounts loaded.",$usernick,1);
865 elsif ($arg[3] eq "backup") {
866 if (!ha($username)) {
867 privmsg("You do not have access to BACKUP.", $usernick);
871 privmsg("$opts{dbfile} copied to ".
872 ".dbbackup/$opts{dbfile}".time(),$usernick,1);
875 elsif ($arg[3] eq "pause") {
876 if (!ha($username)) {
877 privmsg("You do not have access to PAUSE.", $usernick);
880 $pausemode = $pausemode ? 0 : 1;
881 privmsg("PAUSE_MODE set to $pausemode.",$usernick,1);
884 elsif ($arg[3] eq "silent") {
885 if (!ha($username)) {
886 privmsg("You do not have access to SILENT.", $usernick);
888 elsif (!defined($arg[4]) || $arg[4] < 0 || $arg[4] > 3) {
889 privmsg("Try: SILENT <mode>", $usernick,1);
892 $silentmode = $arg[4];
893 privmsg("SILENT_MODE set to $silentmode.",$usernick,1);
896 elsif ($arg[3] eq "jump") {
897 if (!ha($username)) {
898 privmsg("You do not have access to JUMP.", $usernick);
900 elsif (!defined($arg[4])) {
901 privmsg("Try: JUMP <server[:port]>", $usernick, 1);
905 sts("QUIT :JUMP to $arg[4] from $arg[0]");
906 unshift(@{$opts{servers}},$arg[4]);
912 elsif ($arg[3] eq "restart") {
913 if (!ha($username)) {
914 privmsg("You do not have access to RESTART.", $usernick);
918 sts("QUIT :RESTART from $arg[0]",1);
923 elsif ($arg[3] eq "clearq") {
924 if (!ha($username)) {
925 privmsg("You do not have access to CLEARQ.", $usernick);
929 chanmsg("Outgoing message queue cleared by $arg[0].");
930 privmsg("Outgoing message queue cleared.",$usernick,1);
933 elsif ($arg[3] eq "info") {
935 if (!ha($username) && $opts{allowuserinfo}) {
936 $info = "IRPG bot v$version by jotun, ".
937 "http://idlerpg.net/. On via server: ".
938 $opts{servers}->[0].". Admins online: ".
939 join(", ", map { $rps{$_}{nick} }
940 grep { $rps{$_}{isadmin} &&
941 $rps{$_}{online} } keys(%rps)).".";
942 privmsg($info, $usernick);
944 elsif (!ha($username) && !$opts{allowuserinfo}) {
945 privmsg("You do not have access to INFO.", $usernick);
949 $queuedbytes += (length($_)+2) for @queue; # +2 = \r\n
951 "%.2fkb sent, %.2fkb received in %s. %d IRPG users ".
952 "online of %d total users. %d accounts created since ".
953 "startup. PAUSE_MODE is %d, SILENT_MODE is %d. ".
954 "Outgoing queue is %d bytes in %d items. On via: %s. ".
955 "Admins online: %s.",
958 duration(time()-$^T),
959 scalar(grep { $rps{$_}{online} } keys(%rps)),
967 join(", ",map { $rps{$_}{nick} }
968 grep { $rps{$_}{isadmin} && $rps{$_}{online} }
970 privmsg($info, $usernick, 1);
973 elsif ($arg[3] eq "login") {
974 if (defined($username)) {
975 notice("Sorry, you are already online as $username.",
979 if ($#arg < 5 || $arg[5] eq "") {
980 notice("Try: LOGIN <username> <password>", $usernick);
982 elsif (!exists $rps{$arg[4]}) {
983 notice("Sorry, no such account name. Note that ".
984 "account names are case sensitive.",$usernick);
986 elsif (!exists $onchan{$usernick}) {
987 notice("Sorry, you're not in $opts{botchan}.",
990 elsif ($rps{$arg[4]}{pass} ne
991 crypt($arg[5],$rps{$arg[4]}{pass})) {
992 notice("Wrong password.", $usernick);
995 if ($opts{voiceonlogin}) {
996 sts("MODE $opts{botchan} +v :$usernick");
998 $rps{$arg[4]}{online} = 1;
999 $rps{$arg[4]}{nick} = $usernick;
1000 $rps{$arg[4]}{userhost} = $arg[0];
1001 $rps{$arg[4]}{lastlogin} = time();
1002 chanmsg("$arg[4], the level $rps{$arg[4]}{level} ".
1003 "$rps{$arg[4]}{class}, is now online from ".
1004 "nickname $usernick. Next level in ".
1005 duration($rps{$arg[4]}{next}).".");
1006 notice("Logon successful. Next level in ".
1007 duration($rps{$arg[4]}{next}).".", $usernick);
1012 # penalize returns true if user was online and successfully penalized.
1013 # if the user is not logged in, then penalize() fails. so, if user is
1014 # offline, and they say something including "http:", and they've been on
1015 # the channel less than 90 seconds, and the http:-style ban is on, then
1016 # check to see if their url is in @{$opts{okurl}}. if not, kickban them
1017 elsif (!penalize($username,"privmsg",length("@arg[3..$#arg]")) &&
1018 index(lc("@arg[3..$#arg]"),"http:") != -1 &&
1019 (time()-$onchan{$usernick}) < 90 && $opts{doban}) {
1021 for (@{$opts{okurl}}) {
1022 if (index(lc("@arg[3..$#arg]"),lc($_)) != -1) { $isokurl = 1; }
1025 sts("MODE $opts{botchan} +b $arg[0]");
1026 sts("KICK $opts{botchan} $usernick :No advertising; ban will ".
1027 "be lifted within the hour.");
1028 push(@bans,$arg[0]) if @bans < 12;
1034 sub sts { # send to server
1035 my($text,$skipq) = @_;
1038 print $sock "$text\r\n";
1039 $outbytes += length($text) + 2;
1043 # something is wrong. the socket is closed. clear the queue
1045 debug("\$sock isn't writeable in sts(), cleared outgoing queue.\n");
1051 debug(sprintf("(q%03d) = %s\n",$#queue,$text));
1055 sub fq { # deliver message(s) from queue
1057 ++$freemessages if $freemessages < 4;
1061 for (0..$freemessages) {
1062 last() if !@queue; # no messages left to send
1063 # lower number of "free" messages we have left
1064 my $line=shift(@queue);
1065 # if we have already sent one message, and the next message to be sent
1066 # plus the previous messages we have sent this call to fq() > 768 bytes,
1067 # then requeue this message and return. we don't want to flood off,
1069 if ($_ != 0 && (length($line)+$sentbytes) > 768) {
1070 unshift(@queue,$line);
1074 debug("(fm$freemessages) -> $line");
1075 --$freemessages if $freemessages > 0;
1076 print $sock "$line\r\n";
1077 $sentbytes += length($line) + 2;
1081 debug("Disconnected: cleared outgoing message queue.");
1084 $outbytes += length($line) + 2;
1088 sub duration { # return human duration of seconds
1090 return "NA ($s)" if $s !~ /^\d+$/;
1091 return sprintf("%d day%s, %02d:%02d:%02d",$s/86400,int($s/86400)==1?"":"s",
1092 ($s%86400)/3600,($s%3600)/60,($s%60));
1095 sub ts { # timestamp
1096 my @ts = localtime(time());
1097 return sprintf("[%02d/%02d/%02d %02d:%02d:%02d] ",
1098 $ts[4]+1,$ts[3],$ts[5]%100,$ts[2],$ts[1],$ts[0]);
1101 sub hog { # summon the hand of god
1102 my @players = grep { $rps{$_}{online} } keys(%rps);
1103 my $player = $players[rand(@players)];
1104 my $win = int(rand(5));
1105 my $time = int(((5 + int(rand(71)))/100) * $rps{$player}{next});
1107 chanmsg(clog("Verily I say unto thee, the Heavens have burst forth, ".
1108 "and the blessed hand of God carried $player ".
1109 duration($time)." toward level ".($rps{$player}{level}+1).
1111 $rps{$player}{next} -= $time;
1114 chanmsg(clog("Thereupon He stretched out His little finger among them ".
1115 "and consumed $player with fire, slowing the heathen ".
1116 duration($time)." from level ".($rps{$player}{level}+1).
1118 $rps{$player}{next} += $time;
1120 chanmsg("$player reaches next level in ".duration($rps{$player}{next}).".");
1123 sub rpcheck { # check levels, update database
1124 # check splits hash to see if any split users have expired
1125 checksplits() if $opts{detectsplits};
1126 # send out $freemessages lines of text from the outgoing message queue
1128 # clear registration limiting
1130 my $online = scalar(grep { $rps{$_}{online} } keys(%rps));
1131 # there's really nothing to do here if there are no online users
1132 return unless $online;
1133 my $onlineevil = scalar(grep { $rps{$_}{online} &&
1134 $rps{$_}{alignment} eq "e" } keys(%rps));
1135 my $onlinegood = scalar(grep { $rps{$_}{online} &&
1136 $rps{$_}{alignment} eq "g" } keys(%rps));
1137 if (!$opts{noscale}) {
1138 if (rand((20*86400)/$opts{self_clock}) < $online) { hog(); }
1139 if (rand((24*86400)/$opts{self_clock}) < $online) { team_battle(); }
1140 if (rand((8*86400)/$opts{self_clock}) < $online) { calamity(); }
1141 if (rand((4*86400)/$opts{self_clock}) < $online) { godsend(); }
1144 hog() if rand(4000) < 1;
1145 team_battle() if rand(4000) < 1;
1146 calamity() if rand(4000) < 1;
1147 godsend() if rand(2000) < 1;
1149 if (rand((8*86400)/$opts{self_clock}) < $onlineevil) { evilness(); }
1150 if (rand((12*86400)/$opts{self_clock}) < $onlinegood) { goodness(); }
1154 # statements using $rpreport do not bother with scaling by the clock because
1155 # $rpreport is adjusted by the number of seconds since last rpcheck()
1156 if ($rpreport%120==0 && $opts{writequestfile}) { writequestfile(); }
1157 if (time() > $quest{qtime}) {
1158 if (!@{$quest{questers}}) { quest(); }
1159 elsif ($quest{type} == 1) {
1160 chanmsg(clog(join(", ",(@{$quest{questers}})[0..2]).", and ".
1161 "$quest{questers}->[3] have blessed the realm by ".
1162 "completing their quest! 25% of their burden is ".
1164 for (@{$quest{questers}}) {
1165 $rps{$_}{next} = int($rps{$_}{next} * .75);
1167 undef(@{$quest{questers}});
1168 $quest{qtime} = time() + 21600;
1170 # quest type 2 awards are handled in moveplayers()
1172 if ($rpreport && $rpreport%36000==0) { # 10 hours
1173 my @u = sort { $rps{$b}{level} <=> $rps{$a}{level} ||
1174 $rps{$a}{next} <=> $rps{$b}{next} } keys(%rps);
1175 chanmsg("Idle RPG Top Players:") if @u;
1178 chanmsg("$u[$i], the level $rps{$u[$i]}{level} ".
1179 "$rps{$u[$i]}{class}, is #" . ($i + 1) . "! Next level in ".
1180 (duration($rps{$u[$i]}{next})).".");
1184 if ($rpreport%3600==0 && $rpreport) { # 1 hour
1185 my @players = grep { $rps{$_}{online} &&
1186 $rps{$_}{level} > 44 } keys(%rps);
1187 # 20% of all players must be level 45+
1188 if ((scalar(@players)/scalar(grep { $rps{$_}{online} } keys(%rps))) > .15) {
1189 challenge_opp($players[int(rand(@players))]);
1192 sts("MODE $opts{botchan} -bbbb :@bans[0..3]");
1196 if ($rpreport%1800==0) { # 30 mins
1197 if ($opts{botnick} ne $primnick) {
1198 sts($opts{botghostcmd}) if $opts{botghostcmd};
1199 sts("NICK $primnick");
1202 if ($rpreport%600==0 && $pausemode) { # warn every 10m
1203 chanmsg("WARNING: Cannot write database in PAUSE mode!");
1205 # do not write in pause mode, and do not write if not yet connected. (would
1206 # log everyone out if the bot failed to connect. $lasttime = time() on
1207 # successful join to $opts{botchan}, initial value is 1). if fails to open
1208 # $opts{dbfile}, will not update $lasttime and so should have correct values
1209 # on next rpcheck().
1210 if ($lasttime != 1) {
1212 for my $k (keys(%rps)) {
1213 if ($rps{$k}{online} && exists $rps{$k}{nick} &&
1214 $rps{$k}{nick} && exists $onchan{$rps{$k}{nick}}) {
1215 $rps{$k}{next} -= ($curtime - $lasttime);
1216 $rps{$k}{idled} += ($curtime - $lasttime);
1217 if ($rps{$k}{next} < 1) {
1219 if ($rps{$k}{level} > 60) {
1220 $rps{$k}{next} = int(($opts{rpbase} *
1221 ($opts{rpstep}**60)) +
1222 (86400*($rps{$k}{level} - 60)));
1225 $rps{$k}{next} = int($opts{rpbase} *
1226 ($opts{rpstep}**$rps{$k}{level}));
1228 chanmsg("$k, the $rps{$k}{class}, has attained level ".
1229 "$rps{$k}{level}! Next level in ".
1230 duration($rps{$k}{next}).".");
1235 # attempt to make sure this is an actual user, and not just an
1236 # artifact of a bad PEVAL
1238 if (!$pausemode && $rpreport%60==0) { writedb(); }
1239 $rpreport += $opts{self_clock};
1240 $lasttime = $curtime;
1244 sub challenge_opp { # pit argument player against random player
1246 if ($rps{$u}{level} < 25) { return unless rand(4) < 1; }
1247 my @opps = grep { $rps{$_}{online} && $u ne $_ } keys(%rps);
1248 return unless @opps;
1249 my $opp = $opps[int(rand(@opps))];
1250 $opp = $primnick if rand(@opps+1) < 1;
1251 my $mysum = itemsum($u,1);
1252 my $oppsum = itemsum($opp,1);
1253 my $myroll = int(rand($mysum));
1254 my $opproll = int(rand($oppsum));
1255 if ($myroll >= $opproll) {
1256 my $gain = ($opp eq $primnick)?20:int($rps{$opp}{level}/4);
1257 $gain = 7 if $gain < 7;
1258 $gain = int(($gain/100)*$rps{$u}{next});
1259 chanmsg(clog("$u [$myroll/$mysum] has challenged $opp [$opproll/".
1260 "$oppsum] in combat and won! ".duration($gain)." is ".
1261 "removed from $u\'s clock."));
1262 $rps{$u}{next} -= $gain;
1263 chanmsg("$u reaches next level in ".duration($rps{$u}{next}).".");
1264 my $csfactor = $rps{$u}{alignment} eq "g" ? 50 :
1265 $rps{$u}{alignment} eq "e" ? 20 :
1267 if (rand($csfactor) < 1 && $opp ne $primnick) {
1268 $gain = int(((5 + int(rand(20)))/100) * $rps{$opp}{next});
1269 chanmsg(clog("$u has dealt $opp a Critical Strike! ".
1270 duration($gain)." is added to $opp\'s clock."));
1271 $rps{$opp}{next} += $gain;
1272 chanmsg("$opp reaches next level in ".duration($rps{$opp}{next}).
1275 elsif (rand(25) < 1 && $opp ne $primnick && $rps{$u}{level} > 19) {
1276 my @items = ("ring","amulet","charm","weapon","helm","tunic",
1277 "pair of gloves","set of leggings","shield",
1279 my $type = $items[rand(@items)];
1280 if (int($rps{$opp}{item}{$type}) > int($rps{$u}{item}{$type})) {
1281 chanmsg(clog("In the fierce battle, $opp dropped his level ".
1282 int($rps{$opp}{item}{$type})." $type! $u picks ".
1283 "it up, tossing his old level ".
1284 int($rps{$u}{item}{$type})." $type to $opp."));
1285 my $tempitem = $rps{$u}{item}{$type};
1286 $rps{$u}{item}{$type}=$rps{$opp}{item}{$type};
1287 $rps{$opp}{item}{$type} = $tempitem;
1292 my $gain = ($opp eq $primnick)?10:int($rps{$opp}{level}/7);
1293 $gain = 7 if $gain < 7;
1294 $gain = int(($gain/100)*$rps{$u}{next});
1295 chanmsg(clog("$u [$myroll/$mysum] has challenged $opp [$opproll/".
1296 "$oppsum] in combat and lost! ".duration($gain)." is ".
1297 "added to $u\'s clock."));
1298 $rps{$u}{next} += $gain;
1299 chanmsg("$u reaches next level in ".duration($rps{$u}{next}).".");
1303 sub team_battle { # pit three players against three other players
1304 my @opp = grep { $rps{$_}{online} } keys(%rps);
1306 splice(@opp,int(rand(@opp)),1) while @opp > 6;
1307 fisher_yates_shuffle(\@opp);
1308 my $mysum = itemsum($opp[0],1) + itemsum($opp[1],1) + itemsum($opp[2],1);
1309 my $oppsum = itemsum($opp[3],1) + itemsum($opp[4],1) + itemsum($opp[5],1);
1310 my $gain = $rps{$opp[0]}{next};
1312 $gain = $rps{$opp[$p]}{next} if $gain > $rps{$opp[$p]}{next};
1314 $gain = int($gain*.20);
1315 my $myroll = int(rand($mysum));
1316 my $opproll = int(rand($oppsum));
1317 if ($myroll >= $opproll) {
1318 chanmsg(clog("$opp[0], $opp[1], and $opp[2] [$myroll/$mysum] have ".
1319 "team battled $opp[3], $opp[4], and $opp[5] [$opproll/".
1320 "$oppsum] and won! ".duration($gain)." is removed from ".
1322 $rps{$opp[0]}{next} -= $gain;
1323 $rps{$opp[1]}{next} -= $gain;
1324 $rps{$opp[2]}{next} -= $gain;
1327 chanmsg(clog("$opp[0], $opp[1], and $opp[2] [$myroll/$mysum] have ".
1328 "team battled $opp[3], $opp[4], and $opp[5] [$opproll/".
1329 "$oppsum] and lost! ".duration($gain)." is added to ".
1331 $rps{$opp[0]}{next} += $gain;
1332 $rps{$opp[1]}{next} += $gain;
1333 $rps{$opp[2]}{next} += $gain;
1337 sub find_item { # find item for argument player
1339 my @items = ("ring","amulet","charm","weapon","helm","tunic",
1340 "pair of gloves","set of leggings","shield","pair of boots");
1341 my $type = $items[rand(@items)];
1344 for my $num (1 .. int($rps{$u}{level}*1.5)) {
1345 if (rand(1.4**($num/4)) < 1) {
1349 if ($rps{$u}{level} >= 25 && rand(40) < 1) {
1350 $ulevel = 50+int(rand(25));
1351 if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{helm})) {
1352 notice("The light of the gods shines down upon you! You have ".
1353 "found the level $ulevel Mattt's Omniscience Grand Crown! ".
1354 "Your enemies fall before you as you anticipate their ".
1355 "every move.",$rps{$u}{nick});
1356 $rps{$u}{item}{helm} = $ulevel."a";
1360 elsif ($rps{$u}{level} >= 25 && rand(40) < 1) {
1361 $ulevel = 50+int(rand(25));
1362 if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{ring})) {
1363 notice("The light of the gods shines down upon you! You have ".
1364 "found the level $ulevel Juliet's Glorious Ring of ".
1365 "Sparkliness! You enemies are blinded by both its glory ".
1366 "and their greed as you bring desolation upon them.",
1368 $rps{$u}{item}{ring} = $ulevel."h";
1372 elsif ($rps{$u}{level} >= 30 && rand(40) < 1) {
1373 $ulevel = 75+int(rand(25));
1374 if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{tunic})) {
1375 notice("The light of the gods shines down upon you! You have ".
1376 "found the level $ulevel Res0's Protectorate Plate Mail! ".
1377 "Your enemies cower in fear as their attacks have no ".
1378 "effect on you.",$rps{$u}{nick});
1379 $rps{$u}{item}{tunic} = $ulevel."b";
1383 elsif ($rps{$u}{level} >= 35 && rand(40) < 1) {
1384 $ulevel = 100+int(rand(25));
1385 if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{amulet})) {
1386 notice("The light of the gods shines down upon you! You have ".
1387 "found the level $ulevel Dwyn's Storm Magic Amulet! Your ".
1388 "enemies are swept away by an elemental fury before the ".
1389 "war has even begun",$rps{$u}{nick});
1390 $rps{$u}{item}{amulet} = $ulevel."c";
1394 elsif ($rps{$u}{level} >= 40 && rand(40) < 1) {
1395 $ulevel = 150+int(rand(25));
1396 if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{weapon})) {
1397 notice("The light of the gods shines down upon you! You have ".
1398 "found the level $ulevel Jotun's Fury Colossal Sword! Your ".
1399 "enemies' hatred is brought to a quick end as you arc your ".
1400 "wrist, dealing the crushing blow.",$rps{$u}{nick});
1401 $rps{$u}{item}{weapon} = $ulevel."d";
1405 elsif ($rps{$u}{level} >= 45 && rand(40) < 1) {
1406 $ulevel = 175+int(rand(26));
1407 if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{weapon})) {
1408 notice("The light of the gods shines down upon you! You have ".
1409 "found the level $ulevel Drdink's Cane of Blind Rage! Your ".
1410 "enemies are tossed aside as you blindly swing your arm ".
1411 "around hitting stuff.",$rps{$u}{nick});
1412 $rps{$u}{item}{weapon} = $ulevel."e";
1416 elsif ($rps{$u}{level} >= 48 && rand(40) < 1) {
1417 $ulevel = 250+int(rand(51));
1418 if ($ulevel >= $level && $ulevel >
1419 int($rps{$u}{item}{"pair of boots"})) {
1420 notice("The light of the gods shines down upon you! You have ".
1421 "found the level $ulevel Mrquick's Magical Boots of ".
1422 "Swiftness! Your enemies are left choking on your dust as ".
1423 "you run from them very, very quickly.",$rps{$u}{nick});
1424 $rps{$u}{item}{"pair of boots"} = $ulevel."f";
1428 elsif ($rps{$u}{level} >= 52 && rand(40) < 1) {
1429 $ulevel = 300+int(rand(51));
1430 if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{weapon})) {
1431 notice("The light of the gods shines down upon you! You have ".
1432 "found the level $ulevel Jeff's Cluehammer of Doom! Your ".
1433 "enemies are left with a sudden and intense clarity of ".
1434 "mind... even as you relieve them of it.",$rps{$u}{nick});
1435 $rps{$u}{item}{weapon} = $ulevel."g";
1439 if ($level > int($rps{$u}{item}{$type})) {
1440 notice("You found a level $level $type! Your current $type is only ".
1441 "level ".int($rps{$u}{item}{$type}).", so it seems Luck is ".
1442 "with you!",$rps{$u}{nick});
1443 $rps{$u}{item}{$type} = $level;
1446 notice("You found a level $level $type. Your current $type is level ".
1447 int($rps{$u}{item}{$type}).", so it seems Luck is against you. ".
1448 "You toss the $type.",$rps{$u}{nick});
1452 sub loaddb { # load the players database
1456 if (!open(RPS,$opts{dbfile}) && -e $opts{dbfile}) {
1457 sts("QUIT :loaddb() failed: $!");
1461 next if $l =~ /^#/; # skip comments
1462 next if $l =~ /^\s*$/; # skip empty lines
1463 my @i = split("\t",$l);
1464 print Dumper(@i) if @i != 32;
1466 sts("QUIT: Anomaly in loaddb(); line $. of $opts{dbfile} has ".
1467 "wrong fields (".scalar(@i).")");
1468 debug("Anomaly in loaddb(); line $. of $opts{dbfile} has wrong ".
1469 "fields (".scalar(@i).")",1);
1471 if (!$sock) { # if not RELOADDB
1472 if ($i[8]) { $prev_online{$i[7]}=$i[0]; } # log back in
1475 $rps{$i[0]}{isadmin},
1480 $rps{$i[0]}{userhost},
1481 $rps{$i[0]}{online},
1485 $rps{$i[0]}{pen_mesg},
1486 $rps{$i[0]}{pen_nick},
1487 $rps{$i[0]}{pen_part},
1488 $rps{$i[0]}{pen_kick},
1489 $rps{$i[0]}{pen_quit},
1490 $rps{$i[0]}{pen_quest},
1491 $rps{$i[0]}{pen_logout},
1492 $rps{$i[0]}{created},
1493 $rps{$i[0]}{lastlogin},
1494 $rps{$i[0]}{item}{amulet},
1495 $rps{$i[0]}{item}{charm},
1496 $rps{$i[0]}{item}{helm},
1497 $rps{$i[0]}{item}{"pair of boots"},
1498 $rps{$i[0]}{item}{"pair of gloves"},
1499 $rps{$i[0]}{item}{ring},
1500 $rps{$i[0]}{item}{"set of leggings"},
1501 $rps{$i[0]}{item}{shield},
1502 $rps{$i[0]}{item}{tunic},
1503 $rps{$i[0]}{item}{weapon},
1504 $rps{$i[0]}{alignment}) = (@i[1..7],($sock?$i[8]:0),@i[9..$#i]);
1507 debug("loaddb(): loaded ".scalar(keys(%rps))." accounts, ".
1508 scalar(keys(%prev_online))." previously online.");
1512 return unless $lasttime > 1;
1513 my $onlinecount = grep { $rps{$_}{online} } keys %rps;
1514 return unless $onlinecount;
1515 for (my $i=0;$i<$opts{self_clock};++$i) {
1516 # temporary hash to hold player positions, detect collisions
1518 if ($quest{type} == 2 && @{$quest{questers}}) {
1519 my $allgo = 1; # have all users reached <p1|p2>?
1520 for (@{$quest{questers}}) {
1521 if ($quest{stage}==1) {
1522 if ($rps{$_}{x} != $quest{p1}->[0] ||
1523 $rps{$_}{y} != $quest{p1}->[1]) {
1529 if ($rps{$_}{x} != $quest{p2}->[0] ||
1530 $rps{$_}{y} != $quest{p2}->[1]) {
1536 # all participants have reached point 1, now point 2
1537 if ($quest{stage}==1 && $allgo) {
1539 $allgo=0; # have not all reached p2 yet
1541 elsif ($quest{stage} == 2 && $allgo) {
1542 chanmsg(clog(join(", ",(@{$quest{questers}})[0..2]).", ".
1543 "and $quest{questers}->[3] have completed their ".
1544 "journey! 25% of their burden is eliminated."));
1545 for (@{$quest{questers}}) {
1546 $rps{$_}{next} = int($rps{$_}{next} * .75);
1548 undef(@{$quest{questers}});
1549 $quest{qtime} = time() + 21600; # next quest starts in 6 hours
1550 $quest{type} = 1; # probably not needed
1555 # load keys of %temp with online users
1556 ++@temp{grep { $rps{$_}{online} } keys(%rps)};
1557 # delete questers from list
1558 delete(@temp{@{$quest{questers}}});
1559 while ($player = each(%temp)) {
1560 $rps{$player}{x} += int(rand(3))-1;
1561 $rps{$player}{y} += int(rand(3))-1;
1562 # if player goes over edge, wrap them back around
1563 if ($rps{$player}{x} > $opts{mapx}) { $rps{$player}{x}=0; }
1564 if ($rps{$player}{y} > $opts{mapy}) { $rps{$player}{y}=0; }
1565 if ($rps{$player}{x} < 0) { $rps{$player}{x}=$opts{mapx}; }
1566 if ($rps{$player}{y} < 0) { $rps{$player}{y}=$opts{mapy}; }
1568 if (exists($positions{$rps{$player}{x}}{$rps{$player}{y}}) &&
1569 !$positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}) {
1570 if ($rps{$positions{$rps{$player}{x}}{$rps{$player}{y}}{user}}{isadmin} &&
1571 !$rps{$player}{isadmin} && rand(100) < 1) {
1572 chanmsg("$player encounters ".
1573 $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}.
1574 " and bows humbly.");
1576 if (rand($onlinecount) < 1) {
1577 $positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}=1;
1578 collision_fight($player,
1579 $positions{$rps{$player}{x}}{$rps{$player}{y}}{user});
1583 $positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}=0;
1584 $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}=$player;
1587 for (@{$quest{questers}}) {
1588 if ($quest{stage} == 1) {
1589 if (rand(100) < 1) {
1590 if ($rps{$_}{x} != $quest{p1}->[0]) {
1591 $rps{$_}{x} += ($rps{$_}{x} < $quest{p1}->[0] ?
1594 if ($rps{$_}{y} != $quest{p1}->[1]) {
1595 $rps{$_}{y} += ($rps{$_}{y} < $quest{p1}->[1] ?
1600 elsif ($quest{stage}==2) {
1601 if (rand(100) < 1) {
1602 if ($rps{$_}{x} != $quest{p2}->[0]) {
1603 $rps{$_}{x} += ($rps{$_}{x} < $quest{p2}->[0] ?
1606 if ($rps{$_}{y} != $quest{p2}->[1]) {
1607 $rps{$_}{y} += ($rps{$_}{y} < $quest{p2}->[1] ?
1616 for my $player (keys(%rps)) {
1617 next unless $rps{$player}{online};
1618 $rps{$player}{x} += int(rand(3))-1;
1619 $rps{$player}{y} += int(rand(3))-1;
1620 # if player goes over edge, wrap them back around
1621 if ($rps{$player}{x} > $opts{mapx}) { $rps{$player}{x} = 0; }
1622 if ($rps{$player}{y} > $opts{mapy}) { $rps{$player}{y} = 0; }
1623 if ($rps{$player}{x} < 0) { $rps{$player}{x} = $opts{mapx}; }
1624 if ($rps{$player}{y} < 0) { $rps{$player}{y} = $opts{mapy}; }
1625 if (exists($positions{$rps{$player}{x}}{$rps{$player}{y}}) &&
1626 !$positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}) {
1627 if ($rps{$positions{$rps{$player}{x}}{$rps{$player}{y}}{user}}{isadmin} &&
1628 !$rps{$player}{isadmin} && rand(100) < 1) {
1629 chanmsg("$player encounters ".
1630 $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}.
1631 " and bows humbly.");
1633 if (rand($onlinecount) < 1) {
1634 $positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}=1;
1635 collision_fight($player,
1636 $positions{$rps{$player}{x}}{$rps{$player}{y}}{user});
1640 $positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}=0;
1641 $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}=$player;
1648 sub mksalt { # generate a random salt for passwds
1649 join '',('a'..'z','A'..'Z','0'..'9','/','.')[rand(64), rand(64)];
1652 sub chanmsg { # send a message to the channel
1653 my $msg = shift or return undef;
1654 if ($silentmode & 1) { return undef; }
1655 privmsg($msg, $opts{botchan}, shift);
1658 sub privmsg { # send a message to an arbitrary entity
1659 my $msg = shift or return undef;
1660 my $target = shift or return undef;
1662 if (($silentmode == 3 || ($target !~ /^[\+\&\#]/ && $silentmode == 2))
1666 while (length($msg)) {
1667 sts("PRIVMSG $target :".substr($msg,0,450),$force);
1668 substr($msg,0,450)="";
1672 sub notice { # send a notice to an arbitrary entity
1673 my $msg = shift or return undef;
1674 my $target = shift or return undef;
1676 if (($silentmode == 3 || ($target !~ /^[\+\&\#]/ && $silentmode == 2))
1680 while (length($msg)) {
1681 sts("NOTICE $target :".substr($msg,0,450),$force);
1682 substr($msg,0,450)="";
1686 sub help { # print help message
1687 (my $prog = $0) =~ s/^.*\///;
1690 usage: $prog [OPTIONS]
1691 --help, -h Print this message
1692 --verbose, -v Print verbose messages
1693 --server, -s Specify IRC server:port to connect to
1694 --botnick, -n Bot's IRC nick
1695 --botuser, -u Bot's username
1696 --botrlnm, -r Bot's real name
1697 --botchan, -c IRC channel to join
1698 --botident, -p Specify identify-to-services command
1699 --botmodes, -m Specify usermodes for the bot to set upon connect
1700 --botopcmd, -o Specify command to send to server on successful connect
1701 --botghostcmd, -g Specify command to send to server to regain primary
1702 nickname when in use
1703 --doban Advertisement ban on/off flag
1704 --okurl, -k Bot will not ban for web addresses that contain these
1706 --debug Debug on/off flag
1707 --helpurl URL to refer new users to
1708 --admincommurl URL to refer admins to
1711 --rpbase Base time to level up
1712 --rpstep Time to next level = rpbase * (rpstep ** CURRENT_LEVEL)
1713 --rppenstep PENALTY_SECS=(PENALTY*(RPPENSTEP**CURRENT_LEVEL))
1720 # is this for a battle? if so, good users get a 10% boost and evil users get
1723 return -1 unless defined $user;
1725 if ($user eq $primnick) {
1726 for my $u (keys(%rps)) {
1727 $sum = itemsum($u) if $sum < itemsum($u);
1731 if (!exists($rps{$user})) { return -1; }
1732 $sum += int($rps{$user}{item}{$_}) for keys(%{$rps{$user}{item}});
1734 return $rps{$user}{alignment} eq 'e' ? int($sum*.9) :
1735 $rps{$user}{alignment} eq 'g' ? int($sum*1.1) :
1742 # win32 doesn't daemonize (this way?)
1743 if ($^O eq "MSWin32") {
1744 print debug("Nevermind, this is Win32, no I'm not.")."\n";
1748 $SIG{CHLD} = sub { };
1749 fork() && exit(0); # kill parent
1750 POSIX::setsid() || debug("POSIX::setsid() failed: $!",1);
1751 $SIG{CHLD} = sub { };
1752 fork() && exit(0); # kill the parent as the process group leader
1753 $SIG{CHLD} = sub { };
1754 open(STDIN,'/dev/null') || debug("Cannot read /dev/null: $!",1);
1755 open(STDOUT,'>/dev/null') || debug("Cannot write to /dev/null: $!",1);
1756 open(STDERR,'>/dev/null') || debug("Cannot write to /dev/null: $!",1);
1757 # write our PID to $opts{pidfile}, or return semi-silently on failure
1758 open(PIDFILE,">$opts{pidfile}") || do {
1759 debug("Error: failed opening pid file: $!");
1766 sub calamity { # suffer a little one
1767 my @players = grep { $rps{$_}{online} } keys(%rps);
1768 return unless @players;
1769 my $player = $players[rand(@players)];
1771 my @items = ("amulet","charm","weapon","tunic","set of leggings",
1773 my $type = $items[rand(@items)];
1774 if ($type eq "amulet") {
1775 chanmsg(clog("$player fell, chipping the stone in his amulet! ".
1776 "$player\'s $type loses 10% of its effectiveness."));
1778 elsif ($type eq "charm") {
1779 chanmsg(clog("$player slipped and dropped his charm in a dirty ".
1780 "bog! $player\'s $type loses 10% of its ".
1783 elsif ($type eq "weapon") {
1784 chanmsg(clog("$player left his weapon out in the rain to rust! ".
1785 "$player\'s $type loses 10% of its effectiveness."));
1787 elsif ($type eq "tunic") {
1788 chanmsg(clog("$player spilled a level 7 shrinking potion on his ".
1789 "tunic! $player\'s $type loses 10% of its ".
1792 elsif ($type eq "shield") {
1793 chanmsg(clog("$player\'s shield was damaged by a dragon's fiery ".
1794 "breath! $player\'s $type loses 10% of its ".
1798 chanmsg(clog("$player burned a hole through his leggings while ".
1799 "ironing them! $player\'s $type loses 10% of its ".
1803 if ($rps{$player}{item}{$type} =~ /(\D)$/) { $suffix=$1; }
1804 $rps{$player}{item}{$type} = int(int($rps{$player}{item}{$type}) * .9);
1805 $rps{$player}{item}{$type}.=$suffix;
1808 my $time = int(int(5 + rand(8)) / 100 * $rps{$player}{next});
1809 if (!open(Q,$opts{eventsfile})) {
1810 return chanmsg("ERROR: Failed to open $opts{eventsfile}: $!");
1813 while (my $line = <Q>) {
1815 if ($line =~ /^C (.*)/ && rand(++$i) < 1) { $actioned = $1; }
1817 chanmsg(clog("$player $actioned. This terrible calamity has slowed ".
1818 "them ".duration($time)." from level ".
1819 ($rps{$player}{level}+1)."."));
1820 $rps{$player}{next} += $time;
1821 chanmsg("$player reaches next level in ".duration($rps{$player}{next}).
1826 sub godsend { # bless the unworthy
1827 my @players = grep { $rps{$_}{online} } keys(%rps);
1828 return unless @players;
1829 my $player = $players[rand(@players)];
1831 my @items = ("amulet","charm","weapon","tunic","set of leggings",
1833 my $type = $items[rand(@items)];
1834 if ($type eq "amulet") {
1835 chanmsg(clog("$player\'s amulet was blessed by a passing cleric! ".
1836 "$player\'s $type gains 10% effectiveness."));
1838 elsif ($type eq "charm") {
1839 chanmsg(clog("$player\'s charm ate a bolt of lightning! ".
1840 "$player\'s $type gains 10% effectiveness."));
1842 elsif ($type eq "weapon") {
1843 chanmsg(clog("$player sharpened the edge of his weapon! ".
1844 "$player\'s $type gains 10% effectiveness."));
1846 elsif ($type eq "tunic") {
1847 chanmsg(clog("A magician cast a spell of Rigidity on $player\'s ".
1848 "tunic! $player\'s $type gains 10% effectiveness."));
1850 elsif ($type eq "shield") {
1851 chanmsg(clog("$player reinforced his shield with a dragon's ".
1852 "scales! $player\'s $type gains 10% effectiveness."));
1855 chanmsg(clog("The local wizard imbued $player\'s pants with a ".
1856 "Spirit of Fortitude! $player\'s $type gains 10% ".
1860 if ($rps{$player}{item}{$type} =~ /(\D)$/) { $suffix=$1; }
1861 $rps{$player}{item}{$type} = int(int($rps{$player}{item}{$type}) * 1.1);
1862 $rps{$player}{item}{$type}.=$suffix;
1865 my $time = int(int(5 + rand(8)) / 100 * $rps{$player}{next});
1867 if (!open(Q,$opts{eventsfile})) {
1868 return chanmsg("ERROR: Failed to open $opts{eventsfile}: $!");
1871 while (my $line = <Q>) {
1873 if ($line =~ /^G (.*)/ && rand(++$i) < 1) {
1877 chanmsg(clog("$player $actioned! This wondrous godsend has ".
1878 "accelerated them ".duration($time)." towards level ".
1879 ($rps{$player}{level}+1)."."));
1880 $rps{$player}{next} -= $time;
1881 chanmsg("$player reaches next level in ".duration($rps{$player}{next}).
1887 @{$quest{questers}} = grep { $rps{$_}{online} && $rps{$_}{level} > 39 &&
1888 time()-$rps{$_}{lastlogin}>36000 } keys(%rps);
1889 if (@{$quest{questers}} < 4) { return undef(@{$quest{questers}}); }
1890 while (@{$quest{questers}} > 4) {
1891 splice(@{$quest{questers}},int(rand(@{$quest{questers}})),1);
1893 if (!open(Q,$opts{eventsfile})) {
1894 return chanmsg("ERROR: Failed to open $opts{eventsfile}: $!");
1897 while (my $line = <Q>) {
1899 if ($line =~ /^Q/ && rand(++$i) < 1) {
1900 if ($line =~ /^Q1 (.*)/) {
1903 $quest{qtime} = time() + 43200 + int(rand(43201)); # 12-24 hours
1905 elsif ($line =~ /^Q2 (\d+) (\d+) (\d+) (\d+) (.*)/) {
1906 $quest{p1} = [$1,$2];
1907 $quest{p2} = [$3,$4];
1915 if ($quest{type} == 1) {
1916 chanmsg(join(", ",(@{$quest{questers}})[0..2]).", and ".
1917 "$quest{questers}->[3] have been chosen by the gods to ".
1918 "$quest{text}. Quest to end in ".duration($quest{qtime}-time()).
1921 elsif ($quest{type} == 2) {
1922 chanmsg(join(", ",(@{$quest{questers}})[0..2]).", and ".
1923 "$quest{questers}->[3] have been chosen by the gods to ".
1924 "$quest{text}. Participants must first reach [$quest{p1}->[0],".
1925 "$quest{p1}->[1]], then [$quest{p2}->[0],$quest{p2}->[1]].".
1926 ($opts{mapurl}?" See $opts{mapurl} to monitor their journey's ".
1934 my ($quester,$player);
1935 for $quester (@{$quest{questers}}) {
1936 if ($quester eq $k) {
1937 chanmsg(clog("$k\'s prudence and self-regard has brought the ".
1938 "wrath of the gods upon the realm. All your great ".
1939 "wickedness makes you as it were heavy with lead, ".
1940 "and to tend downwards with great weight and ".
1941 "pressure towards hell. Therefore have you drawn ".
1942 "yourselves 15 steps closer to that gaping maw."));
1943 for $player (grep { $rps{$_}{online} } keys %rps) {
1944 my $gain = int(15 * ($opts{rppenstep}**$rps{$player}{level}));
1945 $rps{$player}{pen_quest} += $gain;
1946 $rps{$player}{next} += $gain;
1948 undef(@{$quest{questers}});
1949 $quest{qtime} = time() + 43200; # 12 hours
1956 open(B,">>$opts{modsfile}") or do {
1957 debug("Error: Cannot open $opts{modsfile}: $!");
1958 chanmsg("Error: Cannot open $opts{modsfile}: $!");
1961 print B ts()."$mesg\n";
1967 if (! -d ".dbbackup/") { mkdir(".dbbackup",0700); }
1968 if ($^O ne "MSWin32") {
1969 system("cp $opts{dbfile} .dbbackup/$opts{dbfile}".time());
1972 system("copy $opts{dbfile} .dbbackup\\$opts{dbfile}".time());
1977 my $username = shift;
1978 return 0 if !defined($username);
1979 return 0 if !exists($rps{$username});
1982 questpencheck($username);
1983 if ($type eq "quit") {
1984 $pen = int(20 * ($opts{rppenstep}**$rps{$username}{level}));
1985 if ($opts{limitpen} && $pen > $opts{limitpen}) {
1986 $pen = $opts{limitpen};
1988 $rps{$username}{pen_quit}+=$pen;
1989 $rps{$username}{online}=0;
1991 elsif ($type eq "nick") {
1992 my $newnick = shift;
1993 $pen = int(30 * ($opts{rppenstep}**$rps{$username}{level}));
1994 if ($opts{limitpen} && $pen > $opts{limitpen}) {
1995 $pen = $opts{limitpen};
1997 $rps{$username}{pen_nick}+=$pen;
1998 $rps{$username}{nick} = substr($newnick,1);
1999 substr($rps{$username}{userhost},0,length($rps{$username}{nick})) =
2001 notice("Penalty of ".duration($pen)." added to your timer for ".
2002 "nick change.",$rps{$username}{nick});
2004 elsif ($type eq "privmsg" || $type eq "notice") {
2005 $pen = int(shift(@_) * ($opts{rppenstep}**$rps{$username}{level}));
2006 if ($opts{limitpen} && $pen > $opts{limitpen}) {
2007 $pen = $opts{limitpen};
2009 $rps{$username}{pen_mesg}+=$pen;
2010 notice("Penalty of ".duration($pen)." added to your timer for ".
2011 $type.".",$rps{$username}{nick});
2013 elsif ($type eq "part") {
2014 $pen = int(200 * ($opts{rppenstep}**$rps{$username}{level}));
2015 if ($opts{limitpen} && $pen > $opts{limitpen}) {
2016 $pen = $opts{limitpen};
2018 $rps{$username}{pen_part}+=$pen;
2019 notice("Penalty of ".duration($pen)." added to your timer for ".
2020 "parting.",$rps{$username}{nick});
2021 $rps{$username}{online}=0;
2023 elsif ($type eq "kick") {
2024 $pen = int(250 * ($opts{rppenstep}**$rps{$username}{level}));
2025 if ($opts{limitpen} && $pen > $opts{limitpen}) {
2026 $pen = $opts{limitpen};
2028 $rps{$username}{pen_kick}+=$pen;
2029 notice("Penalty of ".duration($pen)." added to your timer for ".
2030 "being kicked.",$rps{$username}{nick});
2031 $rps{$username}{online}=0;
2033 elsif ($type eq "logout") {
2034 $pen = int(20 * ($opts{rppenstep}**$rps{$username}{level}));
2035 if ($opts{limitpen} && $pen > $opts{limitpen}) {
2036 $pen = $opts{limitpen};
2038 $rps{$username}{pen_logout} += $pen;
2039 notice("Penalty of ".duration($pen)." added to your timer for ".
2040 "LOGOUT command.",$rps{$username}{nick});
2041 $rps{$username}{online}=0;
2043 $rps{$username}{next} += $pen;
2044 return 1; # successfully penalized a user! woohoo!
2048 (my $text = shift) =~ s/[\r\n]//g;
2050 if ($opts{debug} || $opts{verbose}) {
2051 open(DBG,">>$opts{debugfile}") or do {
2052 chanmsg("Error: Cannot open debug file: $!");
2055 print DBG ts()."$text\n";
2058 if ($die) { die("$text\n"); }
2064 return undef if !defined($nick);
2065 for my $user (keys(%rps)) {
2066 next unless $rps{$user}{online};
2067 if ($rps{$user}{nick} eq $nick) { return $user; }
2072 sub ha { # return 0/1 if username has access
2074 if (!defined($user) || !exists($rps{$user})) {
2075 debug("Error: Attempted ha() for invalid username \"$user\"");
2078 return $rps{$user}{isadmin};
2081 sub checksplits { # removed expired split hosts from the hash
2083 while ($host = each(%split)) {
2084 if (time()-$split{$host}{time} > $opts{splitwait}) {
2085 $rps{$split{$host}{account}}{online} = 0;
2086 delete($split{$host});
2091 sub collision_fight {
2093 my $mysum = itemsum($u,1);
2094 my $oppsum = itemsum($opp,1);
2095 my $myroll = int(rand($mysum));
2096 my $opproll = int(rand($oppsum));
2097 if ($myroll >= $opproll) {
2098 my $gain = int($rps{$opp}{level}/4);
2099 $gain = 7 if $gain < 7;
2100 $gain = int(($gain/100)*$rps{$u}{next});
2101 chanmsg(clog("$u [$myroll/$mysum] has come upon $opp [$opproll/$oppsum".
2102 "] and taken them in combat! ".duration($gain)." is ".
2103 "removed from $u\'s clock."));
2104 $rps{$u}{next} -= $gain;
2105 chanmsg("$u reaches next level in ".duration($rps{$u}{next}).".");
2106 if (rand(35) < 1 && $opp ne $primnick) {
2107 $gain = int(((5 + int(rand(20)))/100) * $rps{$opp}{next});
2108 chanmsg(clog("$u has dealt $opp a Critical Strike! ".
2109 duration($gain)." is added to $opp\'s clock."));
2110 $rps{$opp}{next} += $gain;
2111 chanmsg("$opp reaches next level in ".duration($rps{$opp}{next}).
2114 elsif (rand(25) < 1 && $opp ne $primnick && $rps{$u}{level} > 19) {
2115 my @items = ("ring","amulet","charm","weapon","helm","tunic",
2116 "pair of gloves","set of leggings","shield",
2118 my $type = $items[rand(@items)];
2119 if (int($rps{$opp}{item}{$type}) > int($rps{$u}{item}{$type})) {
2120 chanmsg("In the fierce battle, $opp dropped his level ".
2121 int($rps{$opp}{item}{$type})." $type! $u picks it up, ".
2122 "tossing his old level ".int($rps{$u}{item}{$type}).
2124 my $tempitem = $rps{$u}{item}{$type};
2125 $rps{$u}{item}{$type}=$rps{$opp}{item}{$type};
2126 $rps{$opp}{item}{$type} = $tempitem;
2131 my $gain = ($opp eq $primnick)?10:int($rps{$opp}{level}/7);
2132 $gain = 7 if $gain < 7;
2133 $gain = int(($gain/100)*$rps{$u}{next});
2134 chanmsg(clog("$u [$myroll/$mysum] has come upon $opp [$opproll/$oppsum".
2135 "] and been defeated in combat! ".duration($gain)." is ".
2136 "added to $u\'s clock."));
2137 $rps{$u}{next} += $gain;
2138 chanmsg("$u reaches next level in ".duration($rps{$u}{next}).".");
2142 sub writequestfile {
2143 return unless $opts{writequestfile};
2144 open(QF,">$opts{questfilename}") or do {
2145 chanmsg("Error: Cannot open $opts{questfilename}: $!");
2148 # if no active quest, just empty questfile. otherwise, write it
2149 if (@{$quest{questers}}) {
2150 if ($quest{type}==1) {
2151 print QF "T $quest{text}\n".
2153 "S $quest{qtime}\n".
2154 "P1 $quest{questers}->[0]\n".
2155 "P2 $quest{questers}->[1]\n".
2156 "P3 $quest{questers}->[2]\n".
2157 "P4 $quest{questers}->[3]\n";
2159 elsif ($quest{type}==2) {
2160 print QF "T $quest{text}\n".
2162 "S $quest{stage}\n".
2163 "P $quest{p1}->[0] $quest{p1}->[1] $quest{p2}->[0] ".
2164 "$quest{p2}->[1]\n".
2165 "P1 $quest{questers}->[0] $rps{$quest{questers}->[0]}{x} ".
2166 "$rps{$quest{questers}->[0]}{y}\n".
2167 "P2 $quest{questers}->[1] $rps{$quest{questers}->[1]}{x} ".
2168 "$rps{$quest{questers}->[1]}{y}\n".
2169 "P3 $quest{questers}->[2] $rps{$quest{questers}->[2]}{x} ".
2170 "$rps{$quest{questers}->[2]}{y}\n".
2171 "P4 $quest{questers}->[3] $rps{$quest{questers}->[3]}{x} ".
2172 "$rps{$quest{questers}->[3]}{y}\n";
2179 my @players = grep { $rps{$_}{alignment} eq "g" &&
2180 $rps{$_}{online} } keys(%rps);
2181 return unless @players > 1;
2182 splice(@players,int(rand(@players)),1) while @players > 2;
2183 my $gain = 5 + int(rand(8));
2184 chanmsg(clog("$players[0] and $players[1] have not let the iniquities of ".
2185 "evil men poison them. Together have they prayed to their ".
2186 "god, and it is his light that now shines upon them. $gain\% ".
2187 "of their time is removed from their clocks."));
2188 $rps{$players[0]}{next} = int($rps{$players[0]}{next}*(1 - ($gain/100)));
2189 $rps{$players[1]}{next} = int($rps{$players[1]}{next}*(1 - ($gain/100)));
2190 chanmsg("$players[0] reaches next level in ".
2191 duration($rps{$players[0]}{next}).".");
2192 chanmsg("$players[1] reaches next level in ".
2193 duration($rps{$players[1]}{next}).".");
2197 my @evil = grep { $rps{$_}{alignment} eq "e" &&
2198 $rps{$_}{online} } keys(%rps);
2199 return unless @evil;
2200 my $me = $evil[rand(@evil)];
2201 if (int(rand(2)) < 1) {
2202 # evil only steals from good :^(
2203 my @good = grep { $rps{$_}{alignment} eq "g" &&
2204 $rps{$_}{online} } keys(%rps);
2205 my $target = $good[rand(@good)];
2206 my @items = ("ring","amulet","charm","weapon","helm","tunic",
2207 "pair of gloves","set of leggings","shield",
2209 my $type = $items[rand(@items)];
2210 if (int($rps{$target}{item}{$type}) > int($rps{$me}{item}{$type})) {
2211 my $tempitem = $rps{$me}{item}{$type};
2212 $rps{$me}{item}{$type} = $rps{$target}{item}{$type};
2213 $rps{$target}{item}{$type} = $tempitem;
2214 chanmsg(clog("$me stole $target\'s level ".
2215 int($rps{$me}{item}{$type})." $type while they were ".
2216 "sleeping! $me leaves his old level ".
2217 int($rps{$target}{item}{$type})." $type behind, ".
2218 "which $target then takes."));
2221 notice("You made to steal $target\'s $type, but realized it was ".
2222 "lower level than your own. You creep back into the ".
2223 "shadows.",$rps{$me}{nick});
2226 else { # being evil only pays about half of the time...
2227 my $gain = 1 + int(rand(5));
2228 chanmsg(clog("$me is forsaken by his evil god. ".
2229 duration(int($rps{$me}{next} * ($gain/100)))." is added ".
2231 $rps{$me}{next} = int($rps{$me}{next} * (1 + ($gain/100)));
2232 chanmsg("$me reaches next level in ".duration($rps{$me}{next}).".");
2236 sub fisher_yates_shuffle {
2239 for ($i = @$array; --$i; ) {
2240 my $j = int rand ($i+1);
2242 @$array[$i,$j] = @$array[$j,$i];
2247 open(RPS,">$opts{dbfile}") or do {
2248 chanmsg("ERROR: Cannot write $opts{dbfile}: $!");
2251 print RPS join("\t","# username",
2284 keys(%rps); # reset internal pointer
2285 while ($k=each(%rps)) {
2286 if (exists($rps{$k}{next}) && defined($rps{$k}{next})) {
2287 print RPS join("\t",$k,
2304 $rps{$k}{pen_quest},
2305 $rps{$k}{pen_logout},
2307 $rps{$k}{lastlogin},
2308 $rps{$k}{item}{amulet},
2309 $rps{$k}{item}{charm},
2310 $rps{$k}{item}{helm},
2311 $rps{$k}{item}{"pair of boots"},
2312 $rps{$k}{item}{"pair of gloves"},
2313 $rps{$k}{item}{ring},
2314 $rps{$k}{item}{"set of leggings"},
2315 $rps{$k}{item}{shield},
2316 $rps{$k}{item}{tunic},
2317 $rps{$k}{item}{weapon},
2318 $rps{$k}{alignment})."\n";
2325 if (! -e ".irpg.conf") {
2326 debug("Error: Cannot find .irpg.conf. Copy it to this directory, ".
2330 open(CONF,"<.irpg.conf") or do {
2331 debug("Failed to open config file .irpg.conf: $!",1);
2333 my($line,$key,$val);
2334 while ($line=<CONF>) {
2335 next() if $line =~ /^#/; # skip comments
2336 $line =~ s/[\r\n]//g;
2338 next() if !length($line); # skip blank lines
2339 ($key,$val) = split(/\s+/,$line,2);
2341 if (lc($val) eq "on" || lc($val) eq "yes") { $val = 1; }
2342 elsif (lc($val) eq "off" || lc($val) eq "no") { $val = 0; }
2343 if ($key eq "die") {
2344 die("Please edit the file .irpg.conf to setup your bot's ".
2345 "options. Also, read the README file if you haven't ".
2348 elsif ($key eq "server") { push(@{$opts{servers}},$val); }
2349 elsif ($key eq "okurl") { push(@{$opts{okurl}},$val); }
2350 else { $opts{$key} = $val; }