From cd7d23ef9af3ba06e20afe73119a5e43232a565a Mon Sep 17 00:00:00 2001 From: Someone Date: Sat, 19 Mar 2011 01:48:44 +0100 Subject: [PATCH] newline-user fix --- ChangeLog.txt | 651 ++++++++++++++ README | 62 ++ bot.v3.1.2.pl | 2353 +++++++++++++++++++++++++++++++++++++++++++++++++ events.txt | 71 ++ irpgdbtool | 469 ++++++++++ 5 files changed, 3606 insertions(+) create mode 100644 ChangeLog.txt create mode 100644 README create mode 100644 bot.v3.1.2.pl create mode 100644 events.txt create mode 100644 irpgdbtool diff --git a/ChangeLog.txt b/ChangeLog.txt new file mode 100644 index 0000000..2985918 --- /dev/null +++ b/ChangeLog.txt @@ -0,0 +1,651 @@ +This is the changelog for the Idle RPG bot by jotun, jotun@idlerpg.net, +http://idlerpg.net. Entries are written backwards. That is, items at the bottom +of the file were added first, and each subsequent addition is placed on a line +before it. Don't ask me why I do it that way: I do not know. + +Thanks for your interest in the Idle RPG! Feel free to contact me with ideas and +comments, or post them in the forum on the website for public view. + +-------------------------------------------------------------------------------- + v3.1.2: released 6/6/04 +-------------------------------------------------------------------------------- + - applied a user-submitted patch to fix a sprintf() bug (anonymous @ forum) + - quest() now calls writequestfile() when it completes + - loaddb() now calls backup() before it starts + + +-------------------------------------------------------------------------------- + v3.1.1: released 6/3/04 +-------------------------------------------------------------------------------- + - fixed 2 typos having to deal with die() messages when reading the config + file (meij, et al) + - quest() now calls writequestfile() when it completes so that it is + immediately available + - loaddb() now calls backup() before it starts so you always have an + up-to-date backup if your bot mangles your db + - readconfig() now strips out \r and \n (meij, kylemson, et al) + + +-------------------------------------------------------------------------------- + v3.1.0: released 6/2/04 +-------------------------------------------------------------------------------- + - added a config file so you don't have to re-setup configuration options + everytime you upgrade. added a function readconfig() to read this file or + die() if it cannot be found + - SIGHUP will now call readconfig() + - added a DELADMIN command to remove admin status from a username. format: + /msg bot DELADMIN + owner account cannot be DELADMINed. DELADMIN may be limited to owner account + depending on configuration (TGS) + - added an 'owner' account option. owner cannot be DELADMINed, among other + things + - added an 'owneraddonly' option. if set, only owner account can MKADMIN + - added an 'ownerdelonly' option. if set, only owner account can DELADMIN + - renamed disablepeval to ownerpevalonly. if set, only owner account can PEVAL + - added missing command line options for %opts options + - fq() will limit itself to either 1 message or <= 768 bytes output per call, + regardless of $freemessages + - added an option to turn off the sending of the list of users automatically + logged back in on a bot restart, even if the list < 1 k + - added an option to limit the penalty a single event can incur, 'limitpen'. + set to 0 to disable the feature, otherwise is taken to be an integral number + of seconds + - Win32 no longer tries to turn terminal echo on/off via "stty" + - tarball now unpacks files to their own directory instead of . + - removed unused $v, $debug variables + - auto-login would not voice users that it logged in even if voiceonlogin was + set, fixed (wogi, et al) + - fixed a comment having to do with auto login + - $rpreport should not have been adjusted by $curtime, but by + $opts{self_clock} to keep it reliable. this may have caused some loss of + data as the bot neglected to properly backup its database. so sorry! should + now be fixed (many) + - item godsend was removing 10% of item's value instead of adding it. another + copy/paste error. sorry! (Jim Dew) + - fixed a bug that resulted in users that caused quests to fail not being + penalized, as they were set as 'offline' before the bot had a chance to + penalize them (Jim Dew) + - added options to define the size of your IRPG grid, 'mapx' and 'mapy' (Rick) + - team battle now shows roll/sum like other battles (anonymous @ forum) + - added REHASH command to call readconfig() + - added an option to define the number of modes per line to use when voicing + users after an auto login. this variable may be overriden by the server's + directive (Rick) + + +-------------------------------------------------------------------------------- + v3.0.2: released 5/30/04 +-------------------------------------------------------------------------------- + - calls evilness() and goodness() should have been checking against the number + of online evil users and good users respectively, not the total number of + online users (SickMind) + - changed max length of auto-login text to 1k before bot will refuse to send + - fixed a problem with the bot penalizing the kicker instead of the kickee + when someone was kicked (Preston, anonymous, Soc @ idlerpg.net forum) + + +-------------------------------------------------------------------------------- + v3.0.1: released 5/29/04 +-------------------------------------------------------------------------------- + - forgot to add some sort of mechanism for setting up admins for new bots. + whoops. bot will now prompt for an owner's account details if it cannot find + $opts{dbfile} (Secret, anonymous @ idlerpg.net forum) + - fixed a typo in the ChangeLog ;) + - Vayanla noted that there was STILL a time discrepancy for very large games + (or very slow computers). many thanks to him for his help! this is now + fixed + +-------------------------------------------------------------------------------- + v3.0: released 5/29/04 +-------------------------------------------------------------------------------- + - fixed a bug causing RESTART not to work unless the bot's filename happened + to be the same as its nickname + - item modifiers as well as time modifiers are now stored in the modifiers + file. changed name of tlog sub to clog (time -> character) + - changed database write to every minute instead of every $opts{self_clock}. + to lower the chance of lost stats, the bot calls writedb() if you request a + DIE, JUMP, or RESTART. this should cut down on much of the cpu usage + - added a function writedb() which writes out the bot's db from memory so it + can be done outside of rpcheck() + - the team battle code would choose 6 random users to participate in a team + battle, but would not then randomize these users as far as teams go. that + is, if a username generally appeared at the end of a keys(%rps) list, and + made it into the list of 6 random users, that user would always defend + instead of attacking, as he would be at the end of the list. the list of 6 + users is now shuffled using a Fisher-Yates shuffle, code from The Perl + Cookbook (by O'Reilly. a really great read!) (Peter Beentje) + - added a "user alignment" feature. users may align with good, neutral, or + evil. 'good' users have a 10% boost to their item sum for battles, and a + 1/12 chance each day that they, along with a 'good' friend, will have the + light of their god shine upon them, accelerating them 5-12% toward their + next level. 'evil' users have a 10% detriment to their item sum for battles + (ever forsaken in their time of most need...), but have a 1/8 chance each + day that they will either a) attempt to steal an item from a 'good' user + (whom they cannot help but hate) or b) be forsaken (for 1-5% of their TTL) + by their evil god. after all, we all know that crime doesn't pay. also, + 'good' users have only a 1/50 chance of landing a critical strike when + battling, while 'evil' users (who always fight dirty) have a 1/20 chance. + neutral users haven't had anything changed, and all users start off as + neutral. to change your alignment: + /msg bot ALIGN + I haven't run the numbers to see which alignment it is better to follow, so + the stats for this feature may change in the future (FishyTowel @ + idlerpg.net forum) + - added new item, Juliet's Glorious Ring of Sparkliness, item level 50-74, + required user level 25+, chance 1/40, tag 'h' + - rather than error when PEVAL produces > 15 lines of output, PEVAL will now + queue its text if lines of output created >= 4 or size of text > 1k + - LOGIN command now responds via notice rather than privmsg + - added "named items," meaning that unique items have a letter appended to + them, saying which unique item they are. Mattt's Omniscience Grand Crown is + "a," Res0's Protectorate Plate Mail is "b," Dwyn's Storm Magic Amulet is + "c," Jotun's Fury Colossal Sword is "d," Drdink's Cane of Blind Rage is "e," + Mrquick's Magical Boots of Swiftness is "f," and Jeff's Cluehammer of Doom + is "g" + - changed split() on incoming data to split on /\s/ instead of / /; users + could otherwise register usernames or classes containing tabs, which would + cause the bot to die when reading the (tab-delimited) database (chris young) + - changed the SIGHUP handler from '0' to 'sub { };'. should eliminate the + "Signal handler '0' not defined" warning (too many to list) + - added an item calamity and an item godsend. if you are calamitized, you have + a 10% chance of one of your amulet, charm, weapon, tunic, set of leggings, + or shield losing 10% of its item value. if you are godsent, you have a 10% + chance that one of the above items will gain 10% of its item value (carl + wyles @ idlerpg.net forum) + - %botnick% in $opts{botopcmd} will be evaluated to the bot's current nickname + to avoid opping another, more evil user when the bot's nickname is in use + - added an option to give non-admin users limited access to the INFO command. + when enabled, non-admin users can use the INFO command to learn to which + server the bot is connected and the nicknames of online admins (mike @ + idlerpg.net forum) + - added an option to disallow the registration of usernames and classes + containing "non-printable" characters. it's a good idea to leave this option + on, as I have had problems in the past with using binary hash keys (TGS) + - whenever a non-admin players walks over an admin player on the map, he/she + has a 1% chance to bow ;) (mike stewart) + - changed sending of WHO and $opts{botopcmd} from numeric 001 to receipt of + bot's JOIN + - added an option to disable the PEVAL command for users that want to have + less than trustworthy admins ;^) (TGS) + - Run noted that (undernet?) servers allow you a certain number of "free" + messages before output should be limited to 1 message / 2 seconds. fq() now + sends as many of these "free" messages as it can, rather than sending only + one message per call (Run) + - removed some odd sts("MODE $opts{botchan}"); -- not sure why i put that in + - added rudimentary netsplit detection, which a) gives no penalty and b) logs + users in upon return. will pick up quit messages that match + /^\S+\.\S+ \S+\.\S+$/. if your network (or server) does not prefix quit + messages with "Quit: " (or some other string), or otherwise disallows faked + netsplit quit messages, then users can cheat this at their whim. added + option to turn netsplit detection on or off. added option of how long to + wait before automatically logging split users out and forgetting they ever + existed. added sub checksplits() which will iterate over the list of split + nick!user@hosts, remove those which have expired ($opts{splitwait}), and log + the user out. would love input on this feature, as i expect bugs + - HELP command for non-admins is now less helpful. generates one line of text + containing URL for help + - attempting to PUSH a user more than their TTL now sets their TTL to 0 as + well as generating a notice to the admin. successful PUSH now only + chanmsg()s instead of chanmsg()ing and privmsg()ing the admin + - $arg[3] changed to lowercased, leading-:-stripped $arg[3] in privmsg block + - cleaned up more code. changed (most) elses elsifs where appropriate. cleaned + up some logic. dropped all uses of next(). attempted to add () to function + calls wherever it was missing + - private messages and notices to the bot no longer penalize you (mrChewie) + - changed ha() to find access by username instead of nickname + - added a finduser() sub to return a logged-in username matching a given + nickname. + - changed case of $arg[1] after PONG rather than lc()ing it for every + comparison (mrChewie) + - at least 15% of all online players must be level 45+ for the hourly battle + of a level 45+ player to occur (anonymous @ idlerpg.net forum) + - fixed a serious bug with the bot not tracking changes to its nick (ie, by + NickServ or PEVAL) -- this caused all messages sent to the bot to be + penalized (TGS) + - added $opts{casematters}, which, when set, will not allow the registration + of usernames that already exist in a different case (MeBeHere) + - changed db backup, top players report to every 10 hours + - added $opts{modsfile}, which is where Time Modifier texts are appended. + also, tlog() now debug()s and chanmsg()s an error message if it cannot open + the file (MeBeHere) + - HOG was only 5-74%, corrected to 5-75% of TTL + - added an option $opts{self_clock} which is rather like the old $alrmint var, + except this probably works without exploding if you change it. calamities, + godsends, etc. should take this number into account when calculating odds + - added a server list rather than static server. bot should try each server in + the server list twice before giving up (meij) + - added a PID filename option, to which the bot will write its PID (meij) + - added a few more cattle-themed calamities (anonymous @ idlerpg.net forum) + - added a trailing '!' to godsend() text instead of the drab old '.' + - added some new quests/calamities/godsends contributed by users. edited some + old calamities/quests/godsends. please feel free to post your ideas for more + to http://idlerpg.net/forum.php! (anonymous, Afbc0m, anjira, jv, mrChewie, + W8TVI, et al) + - attempted to scale occurences of HOGs, godsends, calamities, and team + battles. HOGs should occur about 1/user/20 days, calamities 1/user/8 days, + godsends 1/user/4 days, team battles 1/user/4 days. does this by calculating + the odds of a HOG as rand(20*86400/clock) < NUMBER_ONLINE_PLAYERS, odds of + calamities as rand(8*86400/clock) < NUMBER_ONLINE_PLAYERS, etc. this appears + to work great with at least 10 clients (tested up to 300), but doesn't seem + to work as well below that. would appreciate input + - debug information is now written to a file instead of STDOUT. bot now + daemonizes even though it is in debug mode. added sub debug() which takes an + argument of text to write to the debug file (yeoj) + - added new item, Jeff's Cluehammer of Doom, item level 300-350, required user + level 52+, chance 1/40 + - bot will try to regain primary nickname if he sees it come open through a + /nick or /quit + - DELOLD command will removed non-logged-in accounts that have not been logged + into in more than N days. format is: + /msg bot DELOLD + DELOLD is a p0, admin command (DinTn) + - added option to enable a STATUS command. this p0 command gives information + on a user, such as level, class, time to level, item sum, etc. useful for + those IRPGs that lack a website. format is: + /msg bot STATUS [username] + if username argument is not passed, returns information on the user issuing + command. must be logged in to use STATUS (TGS) + - possibly added option to choose which local address/hostname and local port + to bind to. let me know if this does/doesn't work (DARKutz, Brad) + - added security note to head of file + - will not send auto-login user list if text > 2048 bytes + - added $opts{botmodes} which will set the bot's usermode to given string on + connect + - removed reset of last login time on auto-login; last login should be when + user last logged in, not when the bot logged them back in + - levels after level 60 have a next time to level of (time to level @ 60) + + (1 day) * (level - 60). levels below 60 have not changed. the exponential + code was getting a little too heavy by itself (TGS) + - RELOADDB would log all players out; fixed + - sts() & fq() check state of socket before attempting write. if cannot write, + outgoing queue is cleared + - added new item, Mrquick's Magical Boots of Swiftness, item level 250-300, + required user level 48+, chance 1/40 + - debug messages are now timestamped. added a few extra debug messages, mostly + for fun + - top player report no longer occurs immediately after startup, but every 6 + hours from then on + - added option to disallow registration of usernames/character classes + containing ctrl codes (Skill0) + - changed auto-login code from list to hash. would take several minutes to + synch to a channel with hundreds of users. now takes < 1s (Vayanla et al) + - removed the hard-coded "#G7" from the top players list.. whoops :^) that's + been there for some time now, can't believe no one ever noticed (HaRRo) + - added option to voice users on login/register. if you +m your channel, this + will cut down on spam, but won't allow non-logged-in or devoiced/deopped + clients to talk (pingh, wishes, aphade, et al) + - added a penalize() sub to make penalties a little cleaner + - added %quest hash to keep up with active quest info + - added option to write active quest info to file; this makes it readable by + outside programs + - can now specify multiple words for the advertisement ban, or turn feature + off entirely + - removed all alarm() code, should now run on any system supporting select(2) + (including, but not limited to, Win32). should also fix a nasty, terrible + time drift bug; with the amount of processing done in rpcheck(), the next + alarm() would come later than expected, awarding the user with idling less + than he had actually idled. it's a small amount, but adds up to about 205 + seconds difference in the clocks after ~9.25 hours (in my tests; will differ + from machine to machine). MANY thanks to this bug's reporter, Ville + Luolajan-Mikkola + - added CLEARQ command, will clear all items in outgoing message queue + - INFO shows outgoing queue status, registrations this period, and total users + - cleaned up options section a little bit, made a few other code clean-ups + - created this file, ChangeLog.txt. the bot's code was getting too long + - registrations are now limited to 1 / sec. this should keep floodbots from + registering hundreds of accounts + - removed ALERT command + - removed $opts{admin} array, there is now a field in the db to mark this. + there is also a MKADMIN command to set an account as having admin access. + syntax is: + /msg bot MKADMIN + there is no way to remove an account's administrator privileges, save from + editing the database by hand (well, there is PEVAL). so, don't assign this + lightly. admins have full access to the account under which you run the bot + - die() call if could not write irpg.db is now a chanmsg() instead + - added an outgoing message queue. all messages are now output at 1 message / + second, unless a non-zero $skipq flag is passed to sts(). privmsg(), + notice(), and chanmsg() calls pass their $force flag as a $skipq flag. + PONGs pass a $skipq of 1 (LexCyber) + - fixed a rather huge bug that someone on slashnet noticed. registering + \001PING\001 (or any other ctcp) would send that CTCP to the channel, then + penalize any who responded. usernames may no longer include a \001 + - godsends, calamities, and quests were all moved to one file, + $opts{eventsfile}. quests are prefixed with a Q1 or Q2, depending on their + type; calamities with a C; and godsends with a G. quests are also prefixed + by their required coordinates if the quest type is 2 + - fixed a typo in the Dwyn's Storm text + - changed code indentation to four spaces. reworked a lot of code to fit <= 80 + columns + - changed QUEST command and quest() output to be a little more grammar- + friendly + - item stealing! if you are level >= 20, and you win a battle against a + player, you have a slightly less than 2% chance of stealing an item from + them. the 2% comes from a) you have a 1/25 chance to attempt to steal an + item and b) you have a 50% chance that your item (type is random) is lower + than theirs. the reason it's 'slightly less than' 2% is because you cannot + both Critical Strike a user and steal an item, so it's (2 - (1/35))%. you + cannot steal an item of lower level than your current item (Afbc0m) + - fixed duration() to show 1 day without trailing 's' + - re-added report of TTL after battle and after critical strike + - quest() function now chanmsg()s an error if it cannot open the + $opts{eventsfile} file + - INFO command now uses privmsg() force flag + - added grid! thanks to Joakim @ orkut for this great idea. within the irpg + world are all of the players on a 500x500 "grid" or map. every second, your + character has an equal chance to step left, right, or neither, and an equal + chance to step up, down, or neither. if your character encounters another + player, you have a 1/(# of online players) chance to battle. also, some + quests require all characters to reach some point on the map. quest + penalties and awards have not changed + - added REMOVEME command for users. if you are logged in, + /msg bot REMOVEME + will delete your account. this is a p0 command :^) + - added NEWPASS command for users. if you are logged in, + /msg bot NEWPASS + will set a new password. this is a p0 command + + +-------------------------------------------------------------------------------- + v2.4.1: unreleased +-------------------------------------------------------------------------------- + + - PEVAL will now error and refuse to send output > 15 lines. this is to avoid + my own errors + + +-------------------------------------------------------------------------------- + v2.4+fixes: released 2/20/04 +-------------------------------------------------------------------------------- + + - items are set to 0 on account creation; they were previous undefined + - bug with QUEST command fixed; would say no active quest even when a quest + was active + + +-------------------------------------------------------------------------------- + v2.4: released 10/13/03 +-------------------------------------------------------------------------------- + + - updated privmsg() function to avoid annoying substr()/uninitialized value + warnings + - few small bugs in battling bot fixed. a win against bot awards you with 20% + of your TTL removed. a loss to bot adds 10% of your TTL to your clock + - bot's item sum is now the highest item sum of all users + 1 (mumkin) + - fixed RESTART command to clear alarm() before trying to exec() + - WHOAMI displays class, TTL (Minhiriath) + - CALC command removed + - added notice() function which mirrors the operation of privmsg() + - SILENT command allows admin to switch bot between 4 modes of silence. in + mode 0, bot sends all privmsgs. in mode 1, only chanmsg() is disabled. in + mode 2, only privmsg()/notice() to non-channels is disabled. in mode 3, + privmsgs/notices to users and channels are disabled. silent mode is also + configurable as $opts{'silentmode'}, so you can setup a bot in any channel + without it interrupting the channel with its privmsgs (???) + - third parameter added to privmsg()/notice(); force flag ignores $silentmode + - hard-coded check for OKish URLs to bot's 'http:'-style banning now + configurable (sean) + - JUMP command no longer penalizes if required argument is left blank + - BACKUP admin command tells bot to copy $opts{'dbfile'} to + .dbbackup/$opts{'dbfile'}TIMESTAMP; added backup() function to handle this + - RELOADDB command allows admin to force bot to reload player database file, + rewriting all memory. RELOADDB can only be used while in pause mode + - PAUSE command allows admin to place bot into pause mode. in pause mode, bot + will update player stats, but will not write database. combined with + RELOADDB, very effective for updating all players stats through external + script without taking bot offline. new accounts cannot be registered + while in pause mode + - QUEST command (p0) tells the active quest, its participants, and its time + left to completion + - ban message for 'http:'-type bans now makes unban-time more clear + - things have been sped up a bit. random battles for users level 45+ now occur + every hour. random chance for HOG, Godsends, Calamities, and Team Battles + were increased by a factor of 5 + - time between quests upped to 12 hours. level requirement for quests upped to + 40+. in addition, must have been online for at least 10 hours to be selected + for quests. number of persons on quest lowered to 4. quest penalty is now a + p15 instead of 2% of your TTL. this makes more sense, as users who were very + close to leveling were penalized almost nothing (inkblot et al) + - fixed spelling of 'caffeinated' (sean) + - botchan variable now shows how to join channel with key (Dan) + + +-------------------------------------------------------------------------------- + v2.3.1: released 9/20/03 +-------------------------------------------------------------------------------- + + - fixed bug with item finding; bad logic sometimes resulted in user not + finding any item (thanks mumkin!) + + +-------------------------------------------------------------------------------- + v2.3: released 8/29/03 +-------------------------------------------------------------------------------- + + - Jotun's Fury max level dropped back to 174 + - added the Drdink's Cane of Blind Rage with item level 175-200 + - all time modifiers (battles, HoG, etc) are now written to modifiers.txt + - function tlog() logs a string to modifiers.txt and returns the string + - changed WHOAMI to not use $_ + - fixed another bug where changing your nick would prevent you from being a + candidate for auto-login + - LOGOUT command added as a p20 + - you may now only be logged in under one character at a time. this will help + protect the bot from being flooded when a single user signs on under 10 + accounts, then is penalized and warned 10 times. attempts to login under two + names are not penalized + - fixed a bug where all of your accounts were automatically logged on so long + as they shared the same host as you, regardless of whether they were online + before (on bot restart) + - there is a 1/20,000 chance of a calamity occuring every 5 seconds. the + calamity() function chooses a random user, then smites them with bad luck. + the penalty for a calamity is a random 5-12% of next TTL. users are only + chosen from the pool of online players + - there is a 1/10,000 chance of a godsend occuring every 5 seconds. the + godsend() function chooses a random user, then betters their luck. the + award for a godsend is a random 5-12% of next TTL. users are only chosen + from the pool of online players + - there are now 'quests' -- six level 30+ users are chosen to go on a quest + at a time. if all six users make it to the quest's end, all questers are + awarded by removing 25% of their TTL (ie, their TTL at quest's end). to + complete a quest, no user can be penalized until the quest's end. quests + last a random time between 12 and 24 hours. if the quest is not completed, + ALL online users are penalized 2% of their time as punishment. users are + only chosen from the pool of online players (original idea from Nerje; quest + ideas from Tristan, brt) + - quests are read from file 'quests.txt' every time quest() is called. this + allows you to add or remove quests while the bot is still running. quests + are not picked in order, but chosen at random from the file + - fixed bug in PUSH, allowing to push into negative TTL + - db times changed to ctime format in lieu of scalar localtime() (now + sortable) + - added db fields for total time idled; total times penalized for privmsg, + nick change, part, kick, LOGOUT, quest, and quit; and time account created + - REGISTER no longer penalizes if you are already logged in and the command + fails + - fixed 'http:' checking to only look at message text, not entire string + - messages passed through privmsg() are split into 450-byte chunks and then + passed to their target + - bans put into place by the 'http:' method are now removed after 1 hour to + prevent filling the banlist. bans are stored in @bans, which will hold at + most 12 bans to prevent the bot from flooding on unban. after 12, bans are + still set, but not stored + - 'license' in header slightly changed + - battle results now include item sums and the random number rolled for each + player. format is [roll/sum] + - bot will try to regain his nickname every 30 mins if it is in use at + sign-on. added vars $primnick and $opts{'botghostcmd'}. $primnick is set + to $opts{'botnick'} (which may change) on load, and $opts{'botghostcmd'} + is a nickserv ghost command string + - the bot's nick ($opts{'botnick'} and $primnick) cannot be registered as + character names + - bot is now a fightable player. his item sum is random 250-650. (someone; + mail me if this was your idea). chances of fighting him are equal to + fighting any other player + - bot now daemonizes when starting (jwbozzy) + - fixed duration code to use the correct secs/day (drdink/inkblot) + - added a penalty to Team Battle. players will now receive or lose 20% of the + lowest team member's TTL (drdink) + - changed battling to award tie to challenger, not challengee. random number + is also, now, an integer, not a float + - every 3.5 hours, a level 45+, online player will battle; this will make it + easier for high-level users to level + - added function itemsum() to return item sum for supplied username + - battle results written to battles.txt are now timestamped (Juliet) + + +-------------------------------------------------------------------------------- + v2.2.2 (schmolli): released 7/18/03 +-------------------------------------------------------------------------------- + + * The changes in this version are based almost completely on a patch sent to + me by Ed Schmollinger, schmolli@IRC. Many thanks to him for his help! Here + are his changes: + - SECURITY: added subroutine mksalt to generate random salt for passwds + - CLEANUP: added subroutines chanmsg and privmsg to send messages to + bot's channel and to a specified user, respectively + - FEATURE: added command line argument processing and removed TEST_MODE + (TEST_MODE is no longer necessary.) Part of this includes moving most + of the variables into %opts. + - FIX: added check for number of existing players when printing top 3 + - CLEANUP: changed "in:" and "out:" debug message to "<-" and "->" + - CLEANUP: indented concatenated lines + + +-------------------------------------------------------------------------------- + v2.2.1: released 7/16/03 +-------------------------------------------------------------------------------- + + - fixed a bug in item finding; if unique item was better than helm, not + better than its class, you would get the item (emad) + + +-------------------------------------------------------------------------------- + v2.2 +-------------------------------------------------------------------------------- + + - added 1/20000 chance of 'team battle' every 5 seconds. team battle is 3 + players versus 3 other players. if the first three players win, their time + is lowered by 20% of the lowest of the three's TTL. if they lose, no time is + removed from any players. there is no chance for critical strike in a team + battle (Asterax) + - max level of Jotun's Fury Colossal Sword changed to 175 + - fixed 'kick' bug; users that were kicked were not logged out + - kick added as a p250 + - bot now only bans those non-logged in users that say 'http:' that've been in + the channel < 90 seconds + - bot won't ban for #G7-type URLs + - bot now shows nick of user when new account is registered + - forgot to close filehandle in loaddb(); fixed + - added a db backup every 6 hours + + +-------------------------------------------------------------------------------- + v2.1.3 +-------------------------------------------------------------------------------- + + - fixed bug where users changing their nick would not be candidates for + auto-login on a bot restart + - changed some messages to make them more friendly to female players (LapCat) + + +-------------------------------------------------------------------------------- + v2.1.2 +-------------------------------------------------------------------------------- + + - HoG can now carry or displace a player 5 - 75% toward the next level + - fixed CTCP version bug + - battling was changed from all users within 7 levels of you to all online + users + - added "unique" items, or a chance starting at level 25 to roll + higher-than-normal items + + +-------------------------------------------------------------------------------- + v2.1.1 +-------------------------------------------------------------------------------- + + - DIE, JUMP, RESTART, INFO, and PEVAL now send warnings to users that don't + have access to tell them so. they are still penalized + - bot will now penalize users without the proper access that try to use an + admin command + - add commands CHCLASS, CHUSER, and PUSH to adjust class names, usernames, + and next time to level, respectively + - HoG could occur for offline users; this is no longer the case + - bot now responds to CTCP version requests (drdink) + + +-------------------------------------------------------------------------------- + v2.1 +-------------------------------------------------------------------------------- + + - bot bans non-logged-in users that say 'http:' + - INFO did not check ha(); fixed + - bot will automagically log you back in if you were logged in before a bot + restart, and if you haven't changed your nick!user@host since then + - removed logging + - dropped functions relating to old database in favor of the new one + - changed level up report from seconds to duration() + - changed item/userinfo db's to one file; battles still in battles.txt + - changed challenge report from seconds to duration() + - changed penalty text to display duration() instead of seconds + - added critical strike, 1/35 chance upon winning battle to cause opponent to + lose time (dwyn) + - changed summon text for HoG (res0) + - changed access to base off of irpg username in lieu of host + - changed top player report to every 6 hours + - changed positive HoG text (res0) + - changed random HoG chance to 1/20000 every 5 seconds + + +-------------------------------------------------------------------------------- + v2.0.3 +-------------------------------------------------------------------------------- + + - dropped top players back to 3 + - removed STATUS; TTL available through website. + - battle history added to website; added logging of battles to battles.txt + - peval did not next(); fixed. + - added HOG command, randomly chooses someone, then randomly raises/lowers + their TTL (20% raise, 80% lower). HOG is, of course, an abbreviation for + Hand of God + - added a 1/7500 random HoG into rpcheck() + + +-------------------------------------------------------------------------------- + v2.0.2 +-------------------------------------------------------------------------------- + + - STATUS would log you out; fixed. + - could STATUS if not online; fixed. + - added DEL command to remove accounts + - added ALERT command to make channel alerts + - changed admin HELP command text to display website + + +-------------------------------------------------------------------------------- + v2.0.1 +-------------------------------------------------------------------------------- + + - fixed self-battle bug + - changed chance to battle from 20% to 25% if level < 25, 100% if >= 25 + - setup companion website + - updated HELP command to reflect website + - changed battle gain to (max(7,opplevel/4)/100)*your_next_ttl + - added battle loss of (max(7,opplevel/7)/100)*your_next_ttl + + +-------------------------------------------------------------------------------- + v2.0 +-------------------------------------------------------------------------------- + + - added item finding and battling + - added penalties for QUIT, PART, instead of resetting time to the beginning + of that level + + +-------------------------------------------------------------------------------- + v1.0 +-------------------------------------------------------------------------------- + + - initial version diff --git a/README b/README new file mode 100644 index 0000000..b07a016 --- /dev/null +++ b/README @@ -0,0 +1,62 @@ +-------------------------------------------------------------------------------- +First-time users: +-------------------------------------------------------------------------------- + +1. Using your favorite text editor, open the bot's source. Read the file header. + If you don't agree with the license, please delete the source and remove + each of your brain cells associated with it. Kthx. +2. Open the file .irpg.conf and edit the bot's options to suit you. You must + also move this file into the same directory where the bot resides. +3. Run it with: perl bot.filename.pl +4. If you have problems, try running it in debug mode: + perl bot.filename.pl --debug + If you cannot diagnose the problem, post to http://idlerpg.net/forum.php +5. Thanks for your interest in Idle RPG! + +-------------------------------------------------------------------------------- +IRPG 3.0 users looking to upgrade: +-------------------------------------------------------------------------------- + +1. Using your favorite text editor, open the bot's source. Read the file header. + If you don't agree with the license, please delete the source and remove + each of your brain cells associated with it. Kthx. +2. Open the file .irpg.conf and edit the bot's options to suit you. You must + also move this file into the same directory where the bot resides. +3. Replace your old bot source with the new one, ie, rm -f that old, buggy crap. +4. Run it with: perl bot.filename.pl +5. If you have problems, try running it in debug mode: + perl bot.filename.pl --debug + If you cannot diagnose the problem, post to http://idlerpg.net/forum.php +6. Thanks for your interest in Idle RPG! + + +-------------------------------------------------------------------------------- +IRPG 2.4 users looking to upgrade: +-------------------------------------------------------------------------------- + +1. Using your favorite text editor, open the bot's source. Read the file header. + If you don't agree with the license, please delete the source and remove + each of your brain cells associated with it. Kthx. +2. Run the db conversion tool: perl irpgdbtool +3. Answer the questions to suit you. +4. Open the file .irpg.conf and edit the bot's options to suit you. You must + also move this file into the same directory where the bot resides. +5. Run it with: perl bot.filename.pl +6. If you have problems, try running it in debug mode: + perl bot.filename.pl --debug + If you cannot diagnose the problem, post to http://idlerpg.net/forum.php +7. Thanks for your interest in Idle RPG! + + +-------------------------------------------------------------------------------- +Pre-2.4 users looking to upgrade: +-------------------------------------------------------------------------------- + +1. Using your favorite text editor, open the bot's source. Read the file header. + If you don't agree with the license, please delete the source and remove + each of your brain cells associated with it. Kthx. +2. I don't think the irpgdbtool will help you unless you're comfortable with + Perl, sorry. :/ If you are, though, you can pull the loaddb() sub from the + bot that you're currently using instead of using the loaddb() supplied in + irpgdbtool. You'll also need to add code to add in the other missing fields + that exist in v2.4. diff --git a/bot.v3.1.2.pl b/bot.v3.1.2.pl new file mode 100644 index 0000000..1ba6315 --- /dev/null +++ b/bot.v3.1.2.pl @@ -0,0 +1,2353 @@ +#!/usr/local/bin/perl +# irpg bot v3.1.2 by jotun, jotun@idlerpg.net, et al. See http://idlerpg.net/ +# +# Some code within this file was written by authors other than myself. As such, +# distributing this code or distributing modified versions of this code is +# strictly prohibited without written authorization from the authors. Contact +# jotun@idlerpg.net. Please note that this may change (at any time, no less) if +# authorization for distribution is given by patch submitters. +# +# As a side note, patches submitted for this project are automatically taken to +# be freely distributable and modifiable for any use, public or private, though +# I make no claim to ownership; original copyrights will be retained.. except as +# I've just stated. +# +# Please mail bugs, etc. to me. Patches are welcome to fix bugs or clean up +# the code, but please do not use a radically different coding style. Thanks +# to everyone that's contributed! +# +# NOTE: This code should NOT be run as root. You deserve anything that happens +# to you if you run this code as a superuser. Also, note that giving a +# user admin access to the bot effectively gives them full access to the +# user under which your bot runs, as they can use the PEVAL command to +# execute any command, or possibly even change your password. I sincerely +# suggest that you exercise extreme caution when giving someone admin +# access to your bot, or that you disable the PEVAL command for non-owner +# accounts in your config file, .irpg.conf + +use strict; +use warnings; +use IO::Socket; +use IO::Select; +use Data::Dumper; +use Getopt::Long; + +my %opts; + +readconfig(); + +my $version = "3.1.2"; + +# command line overrides .irpg.conf +GetOptions(\%opts, + "help|h", + "verbose|v", + "debug", + "debugfile=s", + "server|s=s", + "botnick|n=s", + "botuser|u=s", + "botrlnm|r=s", + "botchan|c=s", + "botident|p=s", + "botmodes|m=s", + "botopcmd|o=s", + "localaddr=s", + "botghostcmd|g=s", + "helpurl=s", + "admincommurl=s", + "doban", + "silentmode=i", + "writequestfile", + "questfilename=s", + "voiceonlogin", + "noccodes", + "nononp", + "mapurl=s", + "statuscmd", + "pidfile=s", + "reconnect", + "reconnect_wait=i", + "self_clock=i", + "modsfile=s", + "casematters", + "detectsplits", + "splitwait=i", + "allowuserinfo", + "noscale", + "phonehome", + "owner=s", + "owneraddonly", + "ownerdelonly", + "ownerpevalonly", + "checkupdates", + "senduserlist", + "limitpen=i", + "mapx=i", + "mapy=i", + "modesperline=i", + "okurl|k=s@", + "eventsfile=s", + "rpstep=f", + "rpbase=i", + "rppenstep=f", + "dbfile|irpgdb|db|d=s", +) or debug("Error: Could not parse command line. Try $0 --help\n",1); + +$opts{help} and do { help(); exit 0; }; + +debug("Config: read $_: ".Dumper($opts{$_})) for keys(%opts); + +my $outbytes = 0; # sent bytes +my $primnick = $opts{botnick}; # for regain or register checks +my $inbytes = 0; # received bytes +my %onchan; # users on game channel +my %rps; # role-players +my %quest = ( + questers => [], + p1 => [], # point 1 for q2 + p2 => [], # point 2 for q2 + qtime => time() + int(rand(21600)), # first quest starts in <=6 hours + text => "", + type => 1, + stage => 1); # quest info + +my $rpreport = 0; # constant for reporting top players +my %prev_online; # user@hosts online on restart, die +my %auto_login; # users to automatically log back on +my @bans; # bans auto-set by the bot, saved to be removed after 1 hour +my $pausemode = 0; # pausemode on/off flag +my $silentmode = 0; # silent mode 0/1/2/3, see head of file +my @queue; # outgoing message queue +my $lastreg = 0; # holds the time of the last reg. cleared every second. + # prevents more than one account being registered / second +my $registrations = 0; # count of registrations this period +my $sel; # IO::Select object +my $lasttime = 1; # last time that rpcheck() was run +my $buffer; # buffer for socket stuff +my $conn_tries = 0; # number of connection tries. gives up after trying each + # server twice +my $sock; # IO::Socket::INET object +my %split; # holds nick!user@hosts for clients that have been netsplit +my $freemessages = 4; # number of "free" privmsgs we can send. 0..$freemessages + +sub daemonize(); # prototype to avoid warnings + +if (! -e $opts{dbfile}) { + $|=1; + %rps = (); + print "$opts{dbfile} does not appear to exist. I'm guessing this is your ". + "first time using IRPG. Please give an account name that you would ". + "like to have admin access [$opts{owner}]: "; + chomp(my $uname = ); + $uname =~ s/\s.*//g; + $uname = length($uname)?$uname:$opts{owner}; + print "Enter a character class for this account: "; + chomp(my $uclass = ); + $rps{$uname}{class} = substr($uclass,0,30); + print "Enter a password for this account: "; + if ($^O ne "MSWin32") { + system("stty -echo"); + } + chomp(my $upass = ); + if ($^O ne "MSWin32") { + system("stty echo"); + } + $rps{$uname}{pass} = crypt($upass,mksalt()); + $rps{$uname}{next} = $opts{rpbase}; + $rps{$uname}{nick} = ""; + $rps{$uname}{userhost} = ""; + $rps{$uname}{level} = 0; + $rps{$uname}{online} = 0; + $rps{$uname}{idled} = 0; + $rps{$uname}{created} = time(); + $rps{$uname}{lastlogin} = time(); + $rps{$uname}{x} = int(rand($opts{mapx})); + $rps{$uname}{y} = int(rand($opts{mapy})); + $rps{$uname}{alignment}="n"; + $rps{$uname}{isadmin} = 1; + for my $item ("ring","amulet","charm","weapon","helm", + "tunic","pair of gloves","shield", + "set of leggings","pair of boots") { + $rps{$uname}{item}{$item} = 0; + } + for my $pen ("pen_mesg","pen_nick","pen_part", + "pen_kick","pen_quit","pen_quest", + "pen_logout","pen_logout") { + $rps{$uname}{$pen} = 0; + } + writedb(); + print "OK, wrote you into $opts{dbfile}.\n"; +} + +# this is almost silly... +if ($opts{checkupdates}) { + print "Checking for updates...\n\n"; + my $tempsock = IO::Socket::INET->new(PeerAddr=>"jotun.ultrazone.org:80", + Timeout => 15); + if ($tempsock) { + print $tempsock "GET /g7/version.php?version=$version HTTP/1.1\r\n". + "Host: jotun.ultrazone.org:80\r\n\r\n"; + my($line,$newversion); + while ($line=<$tempsock>) { + chomp($line); + next() unless $line; + if ($line =~ /^Current version : (\S+)/) { + if ($version ne $1) { + print "There is an update available! Changes include:\n"; + $newversion=1; + } + else { + print "You are running the latest version (v$1).\n"; + close($tempsock); + last(); + } + } + elsif ($newversion && $line =~ /^( -? .+)/) { print "$1\n"; } + elsif ($newversion && $line =~ /^URL: (.+)/) { + print "\nGet the newest version from $1!\n"; + close($tempsock); + last(); + } + } + } + else { print debug("Could not connect to update server.")."\n"; } +} + +print "\n".debug("Becoming a daemon...")."\n"; +daemonize(); + +$SIG{HUP} = "readconfig"; # sighup = reread config file + +CONNECT: # cheese. + +loaddb(); + +while (!$sock && $conn_tries < 2*@{$opts{servers}}) { + debug("Connecting to $opts{servers}->[0]..."); + my %sockinfo = (PeerAddr => $opts{servers}->[0], + PeerPort => 6667); + if ($opts{localaddr}) { $sockinfo{LocalAddr} = $opts{localaddr}; } + $sock = IO::Socket::INET->new(%sockinfo) or + debug("Error: failed to connect: $!\n"); + ++$conn_tries; + if (!$sock) { + # cycle front server to back if connection failed + push(@{$opts{servers}},shift(@{$opts{servers}})); + } + else { debug("Connected."); } +} + +if (!$sock) { + debug("Error: Too many connection failures, exhausted server list.\n",1); +} + +$conn_tries=0; + +$sel = IO::Select->new($sock); + +sts("NICK $opts{botnick}"); +sts("USER $opts{botuser} 0 0 :$opts{botrlnm}"); + +while (1) { + my($readable) = IO::Select->select($sel,undef,undef,0.5); + if (defined($readable)) { + my $fh = $readable->[0]; + my $buffer2; + $fh->recv($buffer2,512,0); + if (length($buffer2)) { + $buffer .= $buffer2; + while (index($buffer,"\n") != -1) { + my $line = substr($buffer,0,index($buffer,"\n")+1); + $buffer = substr($buffer,length($line)); + parse($line); + } + } + else { + # uh oh, we've been disconnected from the server, possibly before + # we've logged in the users in %auto_login. so, we'll set those + # users' online flags to 1, rewrite db, and attempt to reconnect + # (if that's wanted of us) + $rps{$_}{online}=1 for keys(%auto_login); + writedb(); + + close($fh); + $sel->remove($fh); + + if ($opts{reconnect}) { + undef(@queue); + undef($sock); + debug("Socket closed; disconnected. Cleared outgoing message ". + "queue. Waiting $opts{reconnect_wait}s before next ". + "connection attempt..."); + sleep($opts{reconnect_wait}); + goto CONNECT; + } + else { debug("Socket closed; disconnected.",1); } + } + } + else { select(undef,undef,undef,1); } + if ((time()-$lasttime) >= $opts{self_clock}) { rpcheck(); } +} + + +sub parse { + my($in) = shift; + $inbytes += length($in); # increase parsed byte count + $in =~ s/[\r\n]//g; # strip all \r and \n + debug("<- $in"); + my @arg = split(/\s/,$in); # split into "words" + my $usernick = substr((split(/!/,$arg[0]))[0],1); + # logged in char name of nickname, or undef if nickname is not online + my $username = finduser($usernick); + if (lc($arg[0]) eq 'ping') { sts("PONG $arg[1]",1); } + elsif (lc($arg[0]) eq 'error') { + # uh oh, we've been disconnected from the server, possibly before we've + # logged in the users in %auto_login. so, we'll set those users' online + # flags to 1, rewrite db, and attempt to reconnect (if that's wanted of + # us) + $rps{$_}{online}=1 for keys(%auto_login); + writedb(); + return; + } + $arg[1] = lc($arg[1]); # original case no longer matters + if ($arg[1] eq '433' && $opts{botnick} eq $arg[3]) { + $opts{botnick} .= 0; + sts("NICK $opts{botnick}"); + } + elsif ($arg[1] eq 'join') { + # %onchan holds time user joined channel. used for the advertisement ban + $onchan{$usernick}=time(); + if ($opts{'detectsplits'} && exists($split{substr($arg[0],1)})) { + delete($split{substr($arg[0],1)}); + } + elsif ($opts{botnick} eq $usernick) { + sts("WHO $opts{botchan}"); + (my $opcmd = $opts{botopcmd}) =~ s/%botnick%/$opts{botnick}/eg; + sts($opcmd); + $lasttime = time(); # start rpcheck() + } + } + elsif ($arg[1] eq 'quit') { + # if we see our nick come open, grab it (skipping queue) + if ($usernick eq $primnick) { sts("NICK $primnick",1); } + elsif ($opts{'detectsplits'} && + "@arg[2..$#arg]" =~ /^:\S+\.\S+ \S+\.\S+$/) { + if (defined($username)) { # user was online + $split{substr($arg[0],1)}{time}=time(); + $split{substr($arg[0],1)}{account}=$username; + } + } + else { + penalize($username,"quit"); + } + delete($onchan{$usernick}); + } + elsif ($arg[1] eq 'nick') { + # if someone (nickserv) changes our nick for us, update $opts{botnick} + if ($usernick eq $opts{botnick}) { + $opts{botnick} = substr($arg[2],1); + } + # if we see our nick come open, grab it (skipping queue), unless it was + # us who just lost it + elsif ($usernick eq $primnick) { sts("NICK $primnick",1); } + else { + penalize($username,"nick",$arg[2]); + $onchan{substr($arg[2],1)} = delete($onchan{$usernick}); + } + } + elsif ($arg[1] eq 'part') { + penalize($username,"part"); + delete($onchan{$usernick}); + } + elsif ($arg[1] eq 'kick') { + $usernick = $arg[3]; + penalize(finduser($usernick),"kick"); + delete($onchan{$usernick}); + } + # don't penalize /notices to the bot + elsif ($arg[1] eq 'notice' && $arg[2] ne $opts{botnick}) { + penalize($username,"notice",length("@arg[3..$#arg]")-1); + } + elsif ($arg[1] eq '001') { + # send our identify command, set our usermode, join channel + sts($opts{botident}); + sts("MODE $opts{botnick} :$opts{botmodes}"); + sts("JOIN $opts{botchan}"); + $opts{botchan} =~ s/ .*//; # strip channel key if present + } + elsif ($arg[1] eq '315') { + # 315 is /WHO end. report who we automagically signed online iff it will + # print < 1k of text + if (keys(%auto_login)) { + # not a true measure of size, but easy + if (length("%auto_login") < 1024 && $opts{senduserlist}) { + chanmsg(scalar(keys(%auto_login))." users matching ". + scalar(keys(%prev_online))." hosts automatically ". + "logged in; accounts: ".join(", ",keys(%auto_login))); + } + else { + chanmsg(scalar(keys(%auto_login))." users matching ". + scalar(keys(%prev_online))." hosts automatically ". + "logged in."); + } + if ($opts{voiceonlogin}) { + my @vnicks = map { $rps{$_}{nick} } keys(%auto_login); + while (@vnicks) { + sts("MODE $opts{botchan} +". + ('v' x $opts{modesperline})." ". + join(" ",@vnicks[0..$opts{modesperline}-1])); + splice(@vnicks,0,$opts{modesperline}); + } + } + } + else { chanmsg("0 users qualified for auto login."); } + undef(%prev_online); + undef(%auto_login); + } + elsif ($arg[1] eq '005') { + if ("@arg" =~ /MODES=(\d+)/) { $opts{modesperline}=$1; } + } + elsif ($arg[1] eq '352') { + my $user; + # 352 is one line of /WHO. check that the nick!user@host exists as a key + # in %prev_online, the list generated in loaddb(). the value is the user + # to login + $onchan{$arg[7]}=time(); + if (exists($prev_online{$arg[7]."!".$arg[4]."\@".$arg[5]})) { + $rps{$prev_online{$arg[7]."!".$arg[4]."\@".$arg[5]}}{online} = 1; + $auto_login{$prev_online{$arg[7]."!".$arg[4]."\@".$arg[5]}}=1; + } + } + elsif ($arg[1] eq 'privmsg') { + $arg[0] = substr($arg[0],1); # strip leading : from privmsgs + if (lc($arg[2]) eq lc($opts{botnick})) { # to us, not channel + $arg[3] = lc(substr($arg[3],1)); # lowercase, strip leading : + if ($arg[3] eq "\1version\1") { + notice("\1VERSION IRPG bot v$version by jotun; ". + "http://idlerpg.net/\1",$usernick); + } + elsif ($arg[3] eq "peval") { + if (!ha($username) || ($opts{ownerpevalonly} && + $opts{owner} ne $username)) { + privmsg("You don't have access to PEVAL.", $usernick); + } + else { + my @peval = eval "@arg[4..$#arg]"; + if (@peval >= 4 || length("@peval") > 1024) { + privmsg("Command produced too much output to send ". + "outright; queueing ".length("@peval"). + " bytes in ".scalar(@peval)." items. Use ". + "CLEARQ to clear queue if needed.",$usernick,1); + privmsg($_,$usernick) for @peval; + } + else { privmsg($_,$usernick, 1) for @peval; } + privmsg("EVAL ERROR: $@", $usernick, 1) if $@; + } + } + elsif ($arg[3] eq "register") { + if (defined $username) { + privmsg("Sorry, you are already online as $username.", + $usernick); + } + else { + if ($#arg < 6 || $arg[6] eq "") { + privmsg("Try: REGISTER ", + $usernick); + privmsg("IE : REGISTER Poseidon MyPassword God of the ". + "Sea",$usernick); + } + elsif ($pausemode) { + privmsg("Sorry, new accounts may not be registered ". + "while the bot is in pause mode; please wait ". + "a few minutes and try again.",$usernick); + } + elsif (exists $rps{$arg[4]} || ($opts{casematters} && + scalar(grep { lc($arg[4]) eq lc($_) } keys(%rps)))) { + privmsg("Sorry, that character name is already in use.", + $usernick); + } + elsif (lc($arg[4]) eq lc($opts{botnick}) || + lc($arg[4]) eq lc($primnick)) { + privmsg("Sorry, that character name cannot be ". + "registered.",$usernick); + } + elsif (!exists($onchan{$usernick})) { + privmsg("Sorry, you're not in $opts{botchan}.", + $usernick); + } + elsif (length($arg[4]) > 16 || length($arg[4]) < 1) { + privmsg("Sorry, character names must be < 17 and > 0 ". + "chars long.", $usernick); + } + elsif ($arg[4] =~ /^#/) { + privmsg("Sorry, character names may not begin with #.", + $usernick); + } + elsif ($arg[4] =~ /\001/) { + privmsg("Sorry, character names may not include ". + "character \\001.",$usernick); + } + elsif ($opts{noccodes} && ($arg[4] =~ /[[:cntrl:]]/ || + "@arg[6..$#arg]" =~ /[[:cntrl:]]/)) { + privmsg("Sorry, neither character names nor classes ". + "may include control codes.",$usernick); + } + elsif ($opts{nononp} && ($arg[4] =~ /[[:^print:]]/ || + "@arg[6..$#arg]" =~ /[[:^print:]]/)) { + privmsg("Sorry, neither character names nor classes ". + "may include non-printable chars.",$usernick); + } + elsif (length("@arg[6..$#arg]") > 30) { + privmsg("Sorry, character classes must be < 31 chars ". + "long.",$usernick); + } + elsif (time() == $lastreg) { + privmsg("Wait 1 second and try again.",$usernick); + } + else { + if ($opts{voiceonlogin}) { + sts("MODE $opts{botchan} +v :$usernick"); + } + ++$registrations; + $lastreg = time(); + $rps{$arg[4]}{next} = $opts{rpbase}; + $rps{$arg[4]}{class} = "@arg[6..$#arg]"; + $rps{$arg[4]}{level} = 0; + $rps{$arg[4]}{online} = 1; + $rps{$arg[4]}{nick} = $usernick; + $rps{$arg[4]}{userhost} = $arg[0]; + $rps{$arg[4]}{created} = time(); + $rps{$arg[4]}{lastlogin} = time(); + $rps{$arg[4]}{pass} = crypt($arg[5],mksalt()); + $rps{$arg[4]}{x} = int(rand($opts{mapx})); + $rps{$arg[4]}{y} = int(rand($opts{mapy})); + $rps{$arg[4]}{alignment}="n"; + $rps{$arg[4]}{isadmin} = 0; + for my $item ("ring","amulet","charm","weapon","helm", + "tunic","pair of gloves","shield", + "set of leggings","pair of boots") { + $rps{$arg[4]}{item}{$item} = 0; + } + for my $pen ("pen_mesg","pen_nick","pen_part", + "pen_kick","pen_quit","pen_quest", + "pen_logout","pen_logout") { + $rps{$arg[4]}{$pen} = 0; + } + chanmsg("Welcome $usernick\'s new player $arg[4], the ". + "@arg[6..$#arg]! Next level in ". + duration($opts{rpbase})."."); + privmsg("Success! Account $arg[4] created. You have ". + "$opts{rpbase} seconds idleness until you ". + "reach level 1. ", $usernick); + privmsg("NOTE: The point of the game is to see who ". + "can idle the longest. As such, talking in ". + "the channel, parting, quitting, and changing ". + "nicks all penalize you.",$usernick); + if ($opts{phonehome}) { + my $tempsock = IO::Socket::INET->new(PeerAddr=> + "jotun.ultrazone.org:80"); + if ($tempsock) { + print $tempsock + "GET /g7/count.php?new=1 HTTP/1.1\r\n". + "Host: jotun.ultrazone.org:80\r\n\r\n"; + sleep(1); + close($tempsock); + } + } + } + } + } + elsif ($arg[3] eq "delold") { + if (!ha($username)) { + privmsg("You don't have access to DELOLD.", $usernick); + } + # insure it is a number + elsif ($arg[4] !~ /^[\d\.]+$/) { + privmsg("Try: DELOLD <# of days>", $usernick, 1); + } + else { + my @oldaccounts = grep { (time()-$rps{$_}{lastlogin}) > + ($arg[4] * 86400) && + !$rps{$_}{online} } keys(%rps); + delete(@rps{@oldaccounts}); + chanmsg(scalar(@oldaccounts)." accounts not accessed in ". + "the last $arg[4] days removed by $arg[0]."); + } + } + elsif ($arg[3] eq "del") { + if (!ha($username)) { + privmsg("You don't have access to DEL.", $usernick); + } + elsif (!defined($arg[4])) { + privmsg("Try: DEL ", $usernick, 1); + } + elsif (!exists($rps{$arg[4]})) { + privmsg("No such account $arg[4].", $usernick, 1); + } + else { + delete($rps{$arg[4]}); + chanmsg("Account $arg[4] removed by $arg[0]."); + } + } + elsif ($arg[3] eq "mkadmin") { + if (!ha($username) || ($opts{owneraddonly} && + $opts{owner} ne $username)) { + privmsg("You don't have access to MKADMIN.", $usernick); + } + elsif (!defined($arg[4])) { + privmsg("Try: MKADMIN ", $usernick, 1); + } + elsif (!exists($rps{$arg[4]})) { + privmsg("No such account $arg[4].", $usernick, 1); + } + else { + $rps{$arg[4]}{isadmin}=1; + privmsg("Account $arg[4] is now a bot admin.",$usernick, 1); + } + } + elsif ($arg[3] eq "deladmin") { + if (!ha($username) || ($opts{ownerdelonly} && + $opts{owner} ne $username)) { + privmsg("You don't have access to DELADMIN.", $usernick); + } + elsif (!defined($arg[4])) { + privmsg("Try: DELADMIN ", $usernick, 1); + } + elsif (!exists($rps{$arg[4]})) { + privmsg("No such account $arg[4].", $usernick, 1); + } + elsif ($arg[4] eq $opts{owner}) { + privmsg("Cannot DELADMIN owner account.", $usernick, 1); + } + else { + $rps{$arg[4]}{isadmin}=0; + privmsg("Account $arg[4] is no longer a bot admin.", + $usernick, 1); + } + } + elsif ($arg[3] eq "hog") { + if (!ha($username)) { + privmsg("You don't have access to HOG.", $usernick); + } + else { + chanmsg("$usernick has summoned the Hand of God."); + hog(); + } + } + elsif ($arg[3] eq "rehash") { + if (!ha($username)) { + privmsg("You don't have access to REHASH.", $usernick); + } + else { + readconfig(); + privmsg("Reread config file.",$usernick,1); + $opts{botchan} =~ s/ .*//; # strip channel key if present + } + } + elsif ($arg[3] eq "chpass") { + if (!ha($username)) { + privmsg("You don't have access to CHPASS.", $usernick); + } + elsif (!defined($arg[5])) { + privmsg("Try: CHPASS ", $usernick, 1); + } + elsif (!exists($rps{$arg[4]})) { + privmsg("No such username $arg[4].", $usernick, 1); + } + else { + $rps{$arg[4]}{pass} = crypt($arg[5],mksalt()); + privmsg("Password for $arg[4] changed.", $usernick, 1); + } + } + elsif ($arg[3] eq "chuser") { + if (!ha($username)) { + privmsg("You don't have access to CHUSER.", $usernick); + } + elsif (!defined($arg[5])) { + privmsg("Try: CHUSER ", + $usernick, 1); + } + elsif (!exists($rps{$arg[4]})) { + privmsg("No such username $arg[4].", $usernick, 1); + } + elsif (exists($rps{$arg[5]})) { + privmsg("Username $arg[5] is already taken.", $usernick,1); + } + else { + $rps{$arg[5]} = delete($rps{$arg[4]}); + privmsg("Username for $arg[4] changed to $arg[5].", + $usernick, 1); + } + } + elsif ($arg[3] eq "chclass") { + if (!ha($username)) { + privmsg("You don't have access to CHCLASS.", $usernick); + } + elsif (!defined($arg[5])) { + privmsg("Try: CHCLASS ", + $usernick, 1); + } + elsif (!exists($rps{$arg[4]})) { + privmsg("No such username $arg[4].", $usernick, 1); + } + else { + $rps{$arg[4]}{class} = "@arg[5..$#arg]"; + privmsg("Class for $arg[4] changed to @arg[5..$#arg].", + $usernick, 1); + } + } + elsif ($arg[3] eq "push") { + if (!ha($username)) { + privmsg("You don't have access to PUSH.", $usernick); + } + # insure it's a positive or negative, integral number of seconds + elsif ($arg[5] !~ /^\-?\d+$/) { + privmsg("Try: PUSH ", $usernick, 1); + } + elsif (!exists($rps{$arg[4]})) { + privmsg("No such username $arg[4].", $usernick, 1); + } + elsif ($arg[5] > $rps{$arg[4]}{next}) { + privmsg("Time to level for $arg[4] ($rps{$arg[4]}{next}s) ". + "is lower than $arg[5]; setting TTL to 0.", + $usernick, 1); + chanmsg("$usernick has pushed $arg[4] $rps{$arg[4]}{next} ". + "seconds toward level ".($rps{$arg[4]}{level}+1)); + $rps{$arg[4]}{next}=0; + } + else { + $rps{$arg[4]}{next} -= $arg[5]; + chanmsg("$usernick has pushed $arg[4] $arg[5] seconds ". + "toward level ".($rps{$arg[4]}{level}+1).". ". + "$arg[4] reaches next level in ". + duration($rps{$arg[4]}{next})."."); + } + } + elsif ($arg[3] eq "logout") { + if (defined($username)) { + penalize($username,"logout"); + } + else { + privmsg("You are not logged in.", $usernick); + } + } + elsif ($arg[3] eq "quest") { + if (!@{$quest{questers}}) { + privmsg("There is no active quest.",$usernick); + } + elsif ($quest{type} == 1) { + privmsg(join(", ",(@{$quest{questers}})[0..2]).", and ". + "$quest{questers}->[3] are on a quest to ". + "$quest{text}. Quest to complete in ". + duration($quest{qtime}-time()).".",$usernick); + } + elsif ($quest{type} == 2) { + privmsg(join(", ",(@{$quest{questers}})[0..2]).", and ". + "$quest{questers}->[3] are on a quest to ". + "$quest{text}. Participants must first reach ". + "[$quest{p1}->[0],$quest{p1}->[1]], then ". + "[$quest{p2}->[0],$quest{p2}->[1]].". + ($opts{mapurl}?" See $opts{mapurl} to monitor ". + "their journey's progress.":""),$usernick); + } + } + elsif ($arg[3] eq "status" && $opts{statuscmd}) { + if (!defined($username)) { + privmsg("You are not logged in.", $usernick); + } + # argument is optional + elsif ($arg[4] && !exists($rps{$arg[4]})) { + privmsg("No such user.",$usernick); + } + elsif ($arg[4]) { # optional 'user' argument + privmsg("$arg[4]: Level $rps{$arg[4]}{level} ". + "$rps{$arg[4]}{class}; Status: O". + ($rps{$arg[4]}{online}?"n":"ff")."line; ". + "TTL: ".duration($rps{$arg[4]}{next})."; ". + "Idled: ".duration($rps{$arg[4]}{idled}). + "; Item sum: ".itemsum($arg[4]),$usernick); + } + else { # no argument, look up this user + privmsg("$username: Level $rps{$username}{level} ". + "$rps{$username}{class}; Status: O". + ($rps{$username}{online}?"n":"ff")."line; ". + "TTL: ".duration($rps{$username}{next})."; ". + "Idled: ".duration($rps{$username}{idled})."; ". + "Item sum: ".itemsum($username),$usernick); + } + } + elsif ($arg[3] eq "whoami") { + if (!defined($username)) { + privmsg("You are not logged in.", $usernick); + } + else { + privmsg("You are $username, the level ". + $rps{$username}{level}." $rps{$username}{class}. ". + "Next level in ".duration($rps{$username}{next}), + $usernick); + } + } + elsif ($arg[3] eq "newpass") { + if (!defined($username)) { + privmsg("You are not logged in.", $usernick) + } + elsif (!defined($arg[4])) { + privmsg("Try: NEWPASS ", $usernick); + } + else { + $rps{$username}{pass} = crypt($arg[4],mksalt()); + privmsg("Your password was changed.",$usernick); + } + } + elsif ($arg[3] eq "align") { + if (!defined($username)) { + privmsg("You are not logged in.", $usernick) + } + elsif (!defined($arg[4]) || (lc($arg[4]) ne "good" && + lc($arg[4]) ne "neutral" && lc($arg[4]) ne "evil")) { + privmsg("Try: ALIGN ", $usernick); + } + else { + $rps{$username}{alignment} = substr(lc($arg[4]),0,1); + chanmsg("$username has changed alignment to: ".lc($arg[4]). + "."); + privmsg("Your alignment was changed to ".lc($arg[4]).".", + $usernick); + } + } + elsif ($arg[3] eq "removeme") { + if (!defined($username)) { + privmsg("You are not logged in.", $usernick) + } + else { + privmsg("Account $username removed.",$usernick); + chanmsg("$arg[0] removed his account, $username, the ". + $rps{$username}{class}."."); + delete($rps{$username}); + } + } + elsif ($arg[3] eq "help") { + if (!ha($username)) { + privmsg("For information on IRPG bot commands, see ". + $opts{helpurl}, $usernick); + } + else { + privmsg("Help URL is $opts{helpurl}", $usernick, 1); + privmsg("Admin commands URL is $opts{admincommurl}", + $usernick, 1); + } + } + elsif ($arg[3] eq "die") { + if (!ha($username)) { + privmsg("You do not have access to DIE.", $usernick); + } + else { + $opts{reconnect} = 0; + writedb(); + sts("QUIT :DIE from $arg[0]",1); + } + } + elsif ($arg[3] eq "reloaddb") { + if (!ha($username)) { + privmsg("You do not have access to RELOADDB.", $usernick); + } + elsif (!$pausemode) { + privmsg("ERROR: Can only use LOADDB while in PAUSE mode.", + $usernick, 1); + } + else { + loaddb(); + privmsg("Reread player database file; ".scalar(keys(%rps)). + " accounts loaded.",$usernick,1); + } + } + elsif ($arg[3] eq "backup") { + if (!ha($username)) { + privmsg("You do not have access to BACKUP.", $usernick); + } + else { + backup(); + privmsg("$opts{dbfile} copied to ". + ".dbbackup/$opts{dbfile}".time(),$usernick,1); + } + } + elsif ($arg[3] eq "pause") { + if (!ha($username)) { + privmsg("You do not have access to PAUSE.", $usernick); + } + else { + $pausemode = $pausemode ? 0 : 1; + privmsg("PAUSE_MODE set to $pausemode.",$usernick,1); + } + } + elsif ($arg[3] eq "silent") { + if (!ha($username)) { + privmsg("You do not have access to SILENT.", $usernick); + } + elsif (!defined($arg[4]) || $arg[4] < 0 || $arg[4] > 3) { + privmsg("Try: SILENT ", $usernick,1); + } + else { + $silentmode = $arg[4]; + privmsg("SILENT_MODE set to $silentmode.",$usernick,1); + } + } + elsif ($arg[3] eq "jump") { + if (!ha($username)) { + privmsg("You do not have access to JUMP.", $usernick); + } + elsif (!defined($arg[4])) { + privmsg("Try: JUMP ", $usernick, 1); + } + else { + writedb(); + sts("QUIT :JUMP to $arg[4] from $arg[0]"); + unshift(@{$opts{servers}},$arg[4]); + close($sock); + sleep(3); + goto CONNECT; + } + } + elsif ($arg[3] eq "restart") { + if (!ha($username)) { + privmsg("You do not have access to RESTART.", $usernick); + } + else { + writedb(); + sts("QUIT :RESTART from $arg[0]",1); + close($sock); + exec("perl $0"); + } + } + elsif ($arg[3] eq "clearq") { + if (!ha($username)) { + privmsg("You do not have access to CLEARQ.", $usernick); + } + else { + undef(@queue); + chanmsg("Outgoing message queue cleared by $arg[0]."); + privmsg("Outgoing message queue cleared.",$usernick,1); + } + } + elsif ($arg[3] eq "info") { + my $info; + if (!ha($username) && $opts{allowuserinfo}) { + $info = "IRPG bot v$version by jotun, ". + "http://idlerpg.net/. On via server: ". + $opts{servers}->[0].". Admins online: ". + join(", ", map { $rps{$_}{nick} } + grep { $rps{$_}{isadmin} && + $rps{$_}{online} } keys(%rps))."."; + privmsg($info, $usernick); + } + elsif (!ha($username) && !$opts{allowuserinfo}) { + privmsg("You do not have access to INFO.", $usernick); + } + else { + my $queuedbytes = 0; + $queuedbytes += (length($_)+2) for @queue; # +2 = \r\n + $info = sprintf( + "%.2fkb sent, %.2fkb received in %s. %d IRPG users ". + "online of %d total users. %d accounts created since ". + "startup. PAUSE_MODE is %d, SILENT_MODE is %d. ". + "Outgoing queue is %d bytes in %d items. On via: %s. ". + "Admins online: %s.", + $outbytes/1024, + $inbytes/1024, + duration(time()-$^T), + scalar(grep { $rps{$_}{online} } keys(%rps)), + scalar(keys(%rps)), + $registrations, + $pausemode, + $silentmode, + $queuedbytes, + scalar(@queue), + $opts{servers}->[0], + join(", ",map { $rps{$_}{nick} } + grep { $rps{$_}{isadmin} && $rps{$_}{online} } + keys(%rps))); + privmsg($info, $usernick, 1); + } + } + elsif ($arg[3] eq "login") { + if (defined($username)) { + notice("Sorry, you are already online as $username.", + $usernick); + } + else { + if ($#arg < 5 || $arg[5] eq "") { + notice("Try: LOGIN ", $usernick); + } + elsif (!exists $rps{$arg[4]}) { + notice("Sorry, no such account name. Note that ". + "account names are case sensitive.",$usernick); + } + elsif (!exists $onchan{$usernick}) { + notice("Sorry, you're not in $opts{botchan}.", + $usernick); + } + elsif ($rps{$arg[4]}{pass} ne + crypt($arg[5],$rps{$arg[4]}{pass})) { + notice("Wrong password.", $usernick); + } + else { + if ($opts{voiceonlogin}) { + sts("MODE $opts{botchan} +v :$usernick"); + } + $rps{$arg[4]}{online} = 1; + $rps{$arg[4]}{nick} = $usernick; + $rps{$arg[4]}{userhost} = $arg[0]; + $rps{$arg[4]}{lastlogin} = time(); + chanmsg("$arg[4], the level $rps{$arg[4]}{level} ". + "$rps{$arg[4]}{class}, is now online from ". + "nickname $usernick. Next level in ". + duration($rps{$arg[4]}{next})."."); + notice("Logon successful. Next level in ". + duration($rps{$arg[4]}{next}).".", $usernick); + } + } + } + } + # penalize returns true if user was online and successfully penalized. + # if the user is not logged in, then penalize() fails. so, if user is + # offline, and they say something including "http:", and they've been on + # the channel less than 90 seconds, and the http:-style ban is on, then + # check to see if their url is in @{$opts{okurl}}. if not, kickban them + elsif (!penalize($username,"privmsg",length("@arg[3..$#arg]")) && + index(lc("@arg[3..$#arg]"),"http:") != -1 && + (time()-$onchan{$usernick}) < 90 && $opts{doban}) { + my $isokurl = 0; + for (@{$opts{okurl}}) { + if (index(lc("@arg[3..$#arg]"),lc($_)) != -1) { $isokurl = 1; } + } + if (!$isokurl) { + sts("MODE $opts{botchan} +b $arg[0]"); + sts("KICK $opts{botchan} $usernick :No advertising; ban will ". + "be lifted within the hour."); + push(@bans,$arg[0]) if @bans < 12; + } + } + } +} + +sub sts { # send to server + my($text,$skipq) = @_; + if ($skipq) { + if ($sock) { + print $sock "$text\r\n"; + $outbytes += length($text) + 2; + debug("-> $text"); + } + else { + # something is wrong. the socket is closed. clear the queue + undef(@queue); + debug("\$sock isn't writeable in sts(), cleared outgoing queue.\n"); + return; + } + } + else { + push(@queue,$text); + debug(sprintf("(q%03d) = %s\n",$#queue,$text)); + } +} + +sub fq { # deliver message(s) from queue + if (!@queue) { + ++$freemessages if $freemessages < 4; + return undef; + } + my $sentbytes = 0; + for (0..$freemessages) { + last() if !@queue; # no messages left to send + # lower number of "free" messages we have left + my $line=shift(@queue); + # if we have already sent one message, and the next message to be sent + # plus the previous messages we have sent this call to fq() > 768 bytes, + # then requeue this message and return. we don't want to flood off, + # after all + if ($_ != 0 && (length($line)+$sentbytes) > 768) { + unshift(@queue,$line); + last(); + } + if ($sock) { + debug("(fm$freemessages) -> $line"); + --$freemessages if $freemessages > 0; + print $sock "$line\r\n"; + $sentbytes += length($line) + 2; + } + else { + undef(@queue); + debug("Disconnected: cleared outgoing message queue."); + last(); + } + $outbytes += length($line) + 2; + } +} + +sub duration { # return human duration of seconds + my $s = shift; + return "NA ($s)" if $s !~ /^\d+$/; + return sprintf("%d day%s, %02d:%02d:%02d",$s/86400,int($s/86400)==1?"":"s", + ($s%86400)/3600,($s%3600)/60,($s%60)); +} + +sub ts { # timestamp + my @ts = localtime(time()); + return sprintf("[%02d/%02d/%02d %02d:%02d:%02d] ", + $ts[4]+1,$ts[3],$ts[5]%100,$ts[2],$ts[1],$ts[0]); +} + +sub hog { # summon the hand of god + my @players = grep { $rps{$_}{online} } keys(%rps); + my $player = $players[rand(@players)]; + my $win = int(rand(5)); + my $time = int(((5 + int(rand(71)))/100) * $rps{$player}{next}); + if ($win) { + chanmsg(clog("Verily I say unto thee, the Heavens have burst forth, ". + "and the blessed hand of God carried $player ". + duration($time)." toward level ".($rps{$player}{level}+1). + ".")); + $rps{$player}{next} -= $time; + } + else { + chanmsg(clog("Thereupon He stretched out His little finger among them ". + "and consumed $player with fire, slowing the heathen ". + duration($time)." from level ".($rps{$player}{level}+1). + ".")); + $rps{$player}{next} += $time; + } + chanmsg("$player reaches next level in ".duration($rps{$player}{next})."."); +} + +sub rpcheck { # check levels, update database + # check splits hash to see if any split users have expired + checksplits() if $opts{detectsplits}; + # send out $freemessages lines of text from the outgoing message queue + fq(); + # clear registration limiting + $lastreg = 0; + my $online = scalar(grep { $rps{$_}{online} } keys(%rps)); + # there's really nothing to do here if there are no online users + return unless $online; + my $onlineevil = scalar(grep { $rps{$_}{online} && + $rps{$_}{alignment} eq "e" } keys(%rps)); + my $onlinegood = scalar(grep { $rps{$_}{online} && + $rps{$_}{alignment} eq "g" } keys(%rps)); + if (!$opts{noscale}) { + if (rand((20*86400)/$opts{self_clock}) < $online) { hog(); } + if (rand((24*86400)/$opts{self_clock}) < $online) { team_battle(); } + if (rand((8*86400)/$opts{self_clock}) < $online) { calamity(); } + if (rand((4*86400)/$opts{self_clock}) < $online) { godsend(); } + } + else { + hog() if rand(4000) < 1; + team_battle() if rand(4000) < 1; + calamity() if rand(4000) < 1; + godsend() if rand(2000) < 1; + } + if (rand((8*86400)/$opts{self_clock}) < $onlineevil) { evilness(); } + if (rand((12*86400)/$opts{self_clock}) < $onlinegood) { goodness(); } + + moveplayers(); + + # statements using $rpreport do not bother with scaling by the clock because + # $rpreport is adjusted by the number of seconds since last rpcheck() + if ($rpreport%120==0 && $opts{writequestfile}) { writequestfile(); } + if (time() > $quest{qtime}) { + if (!@{$quest{questers}}) { quest(); } + elsif ($quest{type} == 1) { + chanmsg(clog(join(", ",(@{$quest{questers}})[0..2]).", and ". + "$quest{questers}->[3] have blessed the realm by ". + "completing their quest! 25% of their burden is ". + "eliminated.")); + for (@{$quest{questers}}) { + $rps{$_}{next} = int($rps{$_}{next} * .75); + } + undef(@{$quest{questers}}); + $quest{qtime} = time() + 21600; + } + # quest type 2 awards are handled in moveplayers() + } + if ($rpreport && $rpreport%36000==0) { # 10 hours + my @u = sort { $rps{$b}{level} <=> $rps{$a}{level} || + $rps{$a}{next} <=> $rps{$b}{next} } keys(%rps); + chanmsg("Idle RPG Top Players:") if @u; + for my $i (0..2) { + $#u >= $i and + chanmsg("$u[$i], the level $rps{$u[$i]}{level} ". + "$rps{$u[$i]}{class}, is #" . ($i + 1) . "! Next level in ". + (duration($rps{$u[$i]}{next}))."."); + } + backup(); + } + if ($rpreport%3600==0 && $rpreport) { # 1 hour + my @players = grep { $rps{$_}{online} && + $rps{$_}{level} > 44 } keys(%rps); + # 20% of all players must be level 45+ + if ((scalar(@players)/scalar(grep { $rps{$_}{online} } keys(%rps))) > .15) { + challenge_opp($players[int(rand(@players))]); + } + while (@bans) { + sts("MODE $opts{botchan} -bbbb :@bans[0..3]"); + splice(@bans,0,4); + } + } + if ($rpreport%1800==0) { # 30 mins + if ($opts{botnick} ne $primnick) { + sts($opts{botghostcmd}) if $opts{botghostcmd}; + sts("NICK $primnick"); + } + } + if ($rpreport%600==0 && $pausemode) { # warn every 10m + chanmsg("WARNING: Cannot write database in PAUSE mode!"); + } + # do not write in pause mode, and do not write if not yet connected. (would + # log everyone out if the bot failed to connect. $lasttime = time() on + # successful join to $opts{botchan}, initial value is 1). if fails to open + # $opts{dbfile}, will not update $lasttime and so should have correct values + # on next rpcheck(). + if ($lasttime != 1) { + my $curtime=time(); + for my $k (keys(%rps)) { + if ($rps{$k}{online} && exists $rps{$k}{nick} && + $rps{$k}{nick} && exists $onchan{$rps{$k}{nick}}) { + $rps{$k}{next} -= ($curtime - $lasttime); + $rps{$k}{idled} += ($curtime - $lasttime); + if ($rps{$k}{next} < 1) { + $rps{$k}{level}++; + if ($rps{$k}{level} > 60) { + $rps{$k}{next} = int(($opts{rpbase} * + ($opts{rpstep}**60)) + + (86400*($rps{$k}{level} - 60))); + } + else { + $rps{$k}{next} = int($opts{rpbase} * + ($opts{rpstep}**$rps{$k}{level})); + } + chanmsg("$k, the $rps{$k}{class}, has attained level ". + "$rps{$k}{level}! Next level in ". + duration($rps{$k}{next})."."); + find_item($k); + challenge_opp($k); + } + } + # attempt to make sure this is an actual user, and not just an + # artifact of a bad PEVAL + } + if (!$pausemode && $rpreport%60==0) { writedb(); } + $rpreport += $opts{self_clock}; + $lasttime = $curtime; + } +} + +sub challenge_opp { # pit argument player against random player + my $u = shift; + if ($rps{$u}{level} < 25) { return unless rand(4) < 1; } + my @opps = grep { $rps{$_}{online} && $u ne $_ } keys(%rps); + return unless @opps; + my $opp = $opps[int(rand(@opps))]; + $opp = $primnick if rand(@opps+1) < 1; + my $mysum = itemsum($u,1); + my $oppsum = itemsum($opp,1); + my $myroll = int(rand($mysum)); + my $opproll = int(rand($oppsum)); + if ($myroll >= $opproll) { + my $gain = ($opp eq $primnick)?20:int($rps{$opp}{level}/4); + $gain = 7 if $gain < 7; + $gain = int(($gain/100)*$rps{$u}{next}); + chanmsg(clog("$u [$myroll/$mysum] has challenged $opp [$opproll/". + "$oppsum] in combat and won! ".duration($gain)." is ". + "removed from $u\'s clock.")); + $rps{$u}{next} -= $gain; + chanmsg("$u reaches next level in ".duration($rps{$u}{next})."."); + my $csfactor = $rps{$u}{alignment} eq "g" ? 50 : + $rps{$u}{alignment} eq "e" ? 20 : + 35; + if (rand($csfactor) < 1 && $opp ne $primnick) { + $gain = int(((5 + int(rand(20)))/100) * $rps{$opp}{next}); + chanmsg(clog("$u has dealt $opp a Critical Strike! ". + duration($gain)." is added to $opp\'s clock.")); + $rps{$opp}{next} += $gain; + chanmsg("$opp reaches next level in ".duration($rps{$opp}{next}). + "."); + } + elsif (rand(25) < 1 && $opp ne $primnick && $rps{$u}{level} > 19) { + my @items = ("ring","amulet","charm","weapon","helm","tunic", + "pair of gloves","set of leggings","shield", + "pair of boots"); + my $type = $items[rand(@items)]; + if (int($rps{$opp}{item}{$type}) > int($rps{$u}{item}{$type})) { + chanmsg(clog("In the fierce battle, $opp dropped his level ". + int($rps{$opp}{item}{$type})." $type! $u picks ". + "it up, tossing his old level ". + int($rps{$u}{item}{$type})." $type to $opp.")); + my $tempitem = $rps{$u}{item}{$type}; + $rps{$u}{item}{$type}=$rps{$opp}{item}{$type}; + $rps{$opp}{item}{$type} = $tempitem; + } + } + } + else { + my $gain = ($opp eq $primnick)?10:int($rps{$opp}{level}/7); + $gain = 7 if $gain < 7; + $gain = int(($gain/100)*$rps{$u}{next}); + chanmsg(clog("$u [$myroll/$mysum] has challenged $opp [$opproll/". + "$oppsum] in combat and lost! ".duration($gain)." is ". + "added to $u\'s clock.")); + $rps{$u}{next} += $gain; + chanmsg("$u reaches next level in ".duration($rps{$u}{next})."."); + } +} + +sub team_battle { # pit three players against three other players + my @opp = grep { $rps{$_}{online} } keys(%rps); + return if @opp < 6; + splice(@opp,int(rand(@opp)),1) while @opp > 6; + fisher_yates_shuffle(\@opp); + my $mysum = itemsum($opp[0],1) + itemsum($opp[1],1) + itemsum($opp[2],1); + my $oppsum = itemsum($opp[3],1) + itemsum($opp[4],1) + itemsum($opp[5],1); + my $gain = $rps{$opp[0]}{next}; + for my $p (1,2) { + $gain = $rps{$opp[$p]}{next} if $gain > $rps{$opp[$p]}{next}; + } + $gain = int($gain*.20); + my $myroll = int(rand($mysum)); + my $opproll = int(rand($oppsum)); + if ($myroll >= $opproll) { + chanmsg(clog("$opp[0], $opp[1], and $opp[2] [$myroll/$mysum] have ". + "team battled $opp[3], $opp[4], and $opp[5] [$opproll/". + "$oppsum] and won! ".duration($gain)." is removed from ". + "their clocks.")); + $rps{$opp[0]}{next} -= $gain; + $rps{$opp[1]}{next} -= $gain; + $rps{$opp[2]}{next} -= $gain; + } + else { + chanmsg(clog("$opp[0], $opp[1], and $opp[2] [$myroll/$mysum] have ". + "team battled $opp[3], $opp[4], and $opp[5] [$opproll/". + "$oppsum] and lost! ".duration($gain)." is added to ". + "their clocks.")); + $rps{$opp[0]}{next} += $gain; + $rps{$opp[1]}{next} += $gain; + $rps{$opp[2]}{next} += $gain; + } +} + +sub find_item { # find item for argument player + my $u = shift; + my @items = ("ring","amulet","charm","weapon","helm","tunic", + "pair of gloves","set of leggings","shield","pair of boots"); + my $type = $items[rand(@items)]; + my $level = 1; + my $ulevel; + for my $num (1 .. int($rps{$u}{level}*1.5)) { + if (rand(1.4**($num/4)) < 1) { + $level = $num; + } + } + if ($rps{$u}{level} >= 25 && rand(40) < 1) { + $ulevel = 50+int(rand(25)); + if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{helm})) { + notice("The light of the gods shines down upon you! You have ". + "found the level $ulevel Mattt's Omniscience Grand Crown! ". + "Your enemies fall before you as you anticipate their ". + "every move.",$rps{$u}{nick}); + $rps{$u}{item}{helm} = $ulevel."a"; + return; + } + } + elsif ($rps{$u}{level} >= 25 && rand(40) < 1) { + $ulevel = 50+int(rand(25)); + if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{ring})) { + notice("The light of the gods shines down upon you! You have ". + "found the level $ulevel Juliet's Glorious Ring of ". + "Sparkliness! You enemies are blinded by both its glory ". + "and their greed as you bring desolation upon them.", + $rps{$u}{nick}); + $rps{$u}{item}{ring} = $ulevel."h"; + return; + } + } + elsif ($rps{$u}{level} >= 30 && rand(40) < 1) { + $ulevel = 75+int(rand(25)); + if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{tunic})) { + notice("The light of the gods shines down upon you! You have ". + "found the level $ulevel Res0's Protectorate Plate Mail! ". + "Your enemies cower in fear as their attacks have no ". + "effect on you.",$rps{$u}{nick}); + $rps{$u}{item}{tunic} = $ulevel."b"; + return; + } + } + elsif ($rps{$u}{level} >= 35 && rand(40) < 1) { + $ulevel = 100+int(rand(25)); + if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{amulet})) { + notice("The light of the gods shines down upon you! You have ". + "found the level $ulevel Dwyn's Storm Magic Amulet! Your ". + "enemies are swept away by an elemental fury before the ". + "war has even begun",$rps{$u}{nick}); + $rps{$u}{item}{amulet} = $ulevel."c"; + return; + } + } + elsif ($rps{$u}{level} >= 40 && rand(40) < 1) { + $ulevel = 150+int(rand(25)); + if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{weapon})) { + notice("The light of the gods shines down upon you! You have ". + "found the level $ulevel Jotun's Fury Colossal Sword! Your ". + "enemies' hatred is brought to a quick end as you arc your ". + "wrist, dealing the crushing blow.",$rps{$u}{nick}); + $rps{$u}{item}{weapon} = $ulevel."d"; + return; + } + } + elsif ($rps{$u}{level} >= 45 && rand(40) < 1) { + $ulevel = 175+int(rand(26)); + if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{weapon})) { + notice("The light of the gods shines down upon you! You have ". + "found the level $ulevel Drdink's Cane of Blind Rage! Your ". + "enemies are tossed aside as you blindly swing your arm ". + "around hitting stuff.",$rps{$u}{nick}); + $rps{$u}{item}{weapon} = $ulevel."e"; + return; + } + } + elsif ($rps{$u}{level} >= 48 && rand(40) < 1) { + $ulevel = 250+int(rand(51)); + if ($ulevel >= $level && $ulevel > + int($rps{$u}{item}{"pair of boots"})) { + notice("The light of the gods shines down upon you! You have ". + "found the level $ulevel Mrquick's Magical Boots of ". + "Swiftness! Your enemies are left choking on your dust as ". + "you run from them very, very quickly.",$rps{$u}{nick}); + $rps{$u}{item}{"pair of boots"} = $ulevel."f"; + return; + } + } + elsif ($rps{$u}{level} >= 52 && rand(40) < 1) { + $ulevel = 300+int(rand(51)); + if ($ulevel >= $level && $ulevel > int($rps{$u}{item}{weapon})) { + notice("The light of the gods shines down upon you! You have ". + "found the level $ulevel Jeff's Cluehammer of Doom! Your ". + "enemies are left with a sudden and intense clarity of ". + "mind... even as you relieve them of it.",$rps{$u}{nick}); + $rps{$u}{item}{weapon} = $ulevel."g"; + return; + } + } + if ($level > int($rps{$u}{item}{$type})) { + notice("You found a level $level $type! Your current $type is only ". + "level ".int($rps{$u}{item}{$type}).", so it seems Luck is ". + "with you!",$rps{$u}{nick}); + $rps{$u}{item}{$type} = $level; + } + else { + notice("You found a level $level $type. Your current $type is level ". + int($rps{$u}{item}{$type}).", so it seems Luck is against you. ". + "You toss the $type.",$rps{$u}{nick}); + } +} + +sub loaddb { # load the players database + backup(); + my $l; + %rps = (); + if (!open(RPS,$opts{dbfile}) && -e $opts{dbfile}) { + sts("QUIT :loaddb() failed: $!"); + } + while ($l=) { + chomp($l); + next if $l =~ /^#/; # skip comments + next if $l =~ /^\s*$/; # skip empty lines + my @i = split("\t",$l); + print Dumper(@i) if @i != 32; + if (@i != 32) { + sts("QUIT: Anomaly in loaddb(); line $. of $opts{dbfile} has ". + "wrong fields (".scalar(@i).")"); + debug("Anomaly in loaddb(); line $. of $opts{dbfile} has wrong ". + "fields (".scalar(@i).")",1); + } + if (!$sock) { # if not RELOADDB + if ($i[8]) { $prev_online{$i[7]}=$i[0]; } # log back in + } + ($rps{$i[0]}{pass}, + $rps{$i[0]}{isadmin}, + $rps{$i[0]}{level}, + $rps{$i[0]}{class}, + $rps{$i[0]}{next}, + $rps{$i[0]}{nick}, + $rps{$i[0]}{userhost}, + $rps{$i[0]}{online}, + $rps{$i[0]}{idled}, + $rps{$i[0]}{x}, + $rps{$i[0]}{y}, + $rps{$i[0]}{pen_mesg}, + $rps{$i[0]}{pen_nick}, + $rps{$i[0]}{pen_part}, + $rps{$i[0]}{pen_kick}, + $rps{$i[0]}{pen_quit}, + $rps{$i[0]}{pen_quest}, + $rps{$i[0]}{pen_logout}, + $rps{$i[0]}{created}, + $rps{$i[0]}{lastlogin}, + $rps{$i[0]}{item}{amulet}, + $rps{$i[0]}{item}{charm}, + $rps{$i[0]}{item}{helm}, + $rps{$i[0]}{item}{"pair of boots"}, + $rps{$i[0]}{item}{"pair of gloves"}, + $rps{$i[0]}{item}{ring}, + $rps{$i[0]}{item}{"set of leggings"}, + $rps{$i[0]}{item}{shield}, + $rps{$i[0]}{item}{tunic}, + $rps{$i[0]}{item}{weapon}, + $rps{$i[0]}{alignment}) = (@i[1..7],($sock?$i[8]:0),@i[9..$#i]); + } + close(RPS); + debug("loaddb(): loaded ".scalar(keys(%rps))." accounts, ". + scalar(keys(%prev_online))." previously online."); +} + +sub moveplayers { + return unless $lasttime > 1; + my $onlinecount = grep { $rps{$_}{online} } keys %rps; + return unless $onlinecount; + for (my $i=0;$i<$opts{self_clock};++$i) { + # temporary hash to hold player positions, detect collisions + my %positions = (); + if ($quest{type} == 2 && @{$quest{questers}}) { + my $allgo = 1; # have all users reached ? + for (@{$quest{questers}}) { + if ($quest{stage}==1) { + if ($rps{$_}{x} != $quest{p1}->[0] || + $rps{$_}{y} != $quest{p1}->[1]) { + $allgo=0; + last(); + } + } + else { + if ($rps{$_}{x} != $quest{p2}->[0] || + $rps{$_}{y} != $quest{p2}->[1]) { + $allgo=0; + last(); + } + } + } + # all participants have reached point 1, now point 2 + if ($quest{stage}==1 && $allgo) { + $quest{stage}=2; + $allgo=0; # have not all reached p2 yet + } + elsif ($quest{stage} == 2 && $allgo) { + chanmsg(clog(join(", ",(@{$quest{questers}})[0..2]).", ". + "and $quest{questers}->[3] have completed their ". + "journey! 25% of their burden is eliminated.")); + for (@{$quest{questers}}) { + $rps{$_}{next} = int($rps{$_}{next} * .75); + } + undef(@{$quest{questers}}); + $quest{qtime} = time() + 21600; # next quest starts in 6 hours + $quest{type} = 1; # probably not needed + writequestfile(); + } + else { + my(%temp,$player); + # load keys of %temp with online users + ++@temp{grep { $rps{$_}{online} } keys(%rps)}; + # delete questers from list + delete(@temp{@{$quest{questers}}}); + while ($player = each(%temp)) { + $rps{$player}{x} += int(rand(3))-1; + $rps{$player}{y} += int(rand(3))-1; + # if player goes over edge, wrap them back around + if ($rps{$player}{x} > $opts{mapx}) { $rps{$player}{x}=0; } + if ($rps{$player}{y} > $opts{mapy}) { $rps{$player}{y}=0; } + if ($rps{$player}{x} < 0) { $rps{$player}{x}=$opts{mapx}; } + if ($rps{$player}{y} < 0) { $rps{$player}{y}=$opts{mapy}; } + + if (exists($positions{$rps{$player}{x}}{$rps{$player}{y}}) && + !$positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}) { + if ($rps{$positions{$rps{$player}{x}}{$rps{$player}{y}}{user}}{isadmin} && + !$rps{$player}{isadmin} && rand(100) < 1) { + chanmsg("$player encounters ". + $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}. + " and bows humbly."); + } + if (rand($onlinecount) < 1) { + $positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}=1; + collision_fight($player, + $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}); + } + } + else { + $positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}=0; + $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}=$player; + } + } + for (@{$quest{questers}}) { + if ($quest{stage} == 1) { + if (rand(100) < 1) { + if ($rps{$_}{x} != $quest{p1}->[0]) { + $rps{$_}{x} += ($rps{$_}{x} < $quest{p1}->[0] ? + 1 : -1); + } + if ($rps{$_}{y} != $quest{p1}->[1]) { + $rps{$_}{y} += ($rps{$_}{y} < $quest{p1}->[1] ? + 1 : -1); + } + } + } + elsif ($quest{stage}==2) { + if (rand(100) < 1) { + if ($rps{$_}{x} != $quest{p2}->[0]) { + $rps{$_}{x} += ($rps{$_}{x} < $quest{p2}->[0] ? + 1 : -1); + } + if ($rps{$_}{y} != $quest{p2}->[1]) { + $rps{$_}{y} += ($rps{$_}{y} < $quest{p2}->[1] ? + 1 : -1); + } + } + } + } + } + } + else { + for my $player (keys(%rps)) { + next unless $rps{$player}{online}; + $rps{$player}{x} += int(rand(3))-1; + $rps{$player}{y} += int(rand(3))-1; + # if player goes over edge, wrap them back around + if ($rps{$player}{x} > $opts{mapx}) { $rps{$player}{x} = 0; } + if ($rps{$player}{y} > $opts{mapy}) { $rps{$player}{y} = 0; } + if ($rps{$player}{x} < 0) { $rps{$player}{x} = $opts{mapx}; } + if ($rps{$player}{y} < 0) { $rps{$player}{y} = $opts{mapy}; } + if (exists($positions{$rps{$player}{x}}{$rps{$player}{y}}) && + !$positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}) { + if ($rps{$positions{$rps{$player}{x}}{$rps{$player}{y}}{user}}{isadmin} && + !$rps{$player}{isadmin} && rand(100) < 1) { + chanmsg("$player encounters ". + $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}. + " and bows humbly."); + } + if (rand($onlinecount) < 1) { + $positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}=1; + collision_fight($player, + $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}); + } + } + else { + $positions{$rps{$player}{x}}{$rps{$player}{y}}{battled}=0; + $positions{$rps{$player}{x}}{$rps{$player}{y}}{user}=$player; + } + } + } + } +} + +sub mksalt { # generate a random salt for passwds + join '',('a'..'z','A'..'Z','0'..'9','/','.')[rand(64), rand(64)]; +} + +sub chanmsg { # send a message to the channel + my $msg = shift or return undef; + if ($silentmode & 1) { return undef; } + privmsg($msg, $opts{botchan}, shift); +} + +sub privmsg { # send a message to an arbitrary entity + my $msg = shift or return undef; + my $target = shift or return undef; + my $force = shift; + if (($silentmode == 3 || ($target !~ /^[\+\&\#]/ && $silentmode == 2)) + && !$force) { + return undef; + } + while (length($msg)) { + sts("PRIVMSG $target :".substr($msg,0,450),$force); + substr($msg,0,450)=""; + } +} + +sub notice { # send a notice to an arbitrary entity + my $msg = shift or return undef; + my $target = shift or return undef; + my $force = shift; + if (($silentmode == 3 || ($target !~ /^[\+\&\#]/ && $silentmode == 2)) + && !$force) { + return undef; + } + while (length($msg)) { + sts("NOTICE $target :".substr($msg,0,450),$force); + substr($msg,0,450)=""; + } +} + +sub help { # print help message + (my $prog = $0) =~ s/^.*\///; + + print " +usage: $prog [OPTIONS] + --help, -h Print this message + --verbose, -v Print verbose messages + --server, -s Specify IRC server:port to connect to + --botnick, -n Bot's IRC nick + --botuser, -u Bot's username + --botrlnm, -r Bot's real name + --botchan, -c IRC channel to join + --botident, -p Specify identify-to-services command + --botmodes, -m Specify usermodes for the bot to set upon connect + --botopcmd, -o Specify command to send to server on successful connect + --botghostcmd, -g Specify command to send to server to regain primary + nickname when in use + --doban Advertisement ban on/off flag + --okurl, -k Bot will not ban for web addresses that contain these + strings + --debug Debug on/off flag + --helpurl URL to refer new users to + --admincommurl URL to refer admins to + + Timing parameters: + --rpbase Base time to level up + --rpstep Time to next level = rpbase * (rpstep ** CURRENT_LEVEL) + --rppenstep PENALTY_SECS=(PENALTY*(RPPENSTEP**CURRENT_LEVEL)) + +"; +} + +sub itemsum { + my $user = shift; + # is this for a battle? if so, good users get a 10% boost and evil users get + # a 10% detriment + my $battle = shift; + return -1 unless defined $user; + my $sum = 0; + if ($user eq $primnick) { + for my $u (keys(%rps)) { + $sum = itemsum($u) if $sum < itemsum($u); + } + return $sum+1; + } + if (!exists($rps{$user})) { return -1; } + $sum += int($rps{$user}{item}{$_}) for keys(%{$rps{$user}{item}}); + if ($battle) { + return $rps{$user}{alignment} eq 'e' ? int($sum*.9) : + $rps{$user}{alignment} eq 'g' ? int($sum*1.1) : + $sum; + } + return $sum; +} + +sub daemonize() { + # win32 doesn't daemonize (this way?) + if ($^O eq "MSWin32") { + print debug("Nevermind, this is Win32, no I'm not.")."\n"; + return; + } + use POSIX 'setsid'; + $SIG{CHLD} = sub { }; + fork() && exit(0); # kill parent + POSIX::setsid() || debug("POSIX::setsid() failed: $!",1); + $SIG{CHLD} = sub { }; + fork() && exit(0); # kill the parent as the process group leader + $SIG{CHLD} = sub { }; + open(STDIN,'/dev/null') || debug("Cannot read /dev/null: $!",1); + open(STDOUT,'>/dev/null') || debug("Cannot write to /dev/null: $!",1); + open(STDERR,'>/dev/null') || debug("Cannot write to /dev/null: $!",1); + # write our PID to $opts{pidfile}, or return semi-silently on failure + open(PIDFILE,">$opts{pidfile}") || do { + debug("Error: failed opening pid file: $!"); + return; + }; + print PIDFILE $$; + close(PIDFILE); +} + +sub calamity { # suffer a little one + my @players = grep { $rps{$_}{online} } keys(%rps); + return unless @players; + my $player = $players[rand(@players)]; + if (rand(10) < 1) { + my @items = ("amulet","charm","weapon","tunic","set of leggings", + "shield"); + my $type = $items[rand(@items)]; + if ($type eq "amulet") { + chanmsg(clog("$player fell, chipping the stone in his amulet! ". + "$player\'s $type loses 10% of its effectiveness.")); + } + elsif ($type eq "charm") { + chanmsg(clog("$player slipped and dropped his charm in a dirty ". + "bog! $player\'s $type loses 10% of its ". + "effectiveness.")); + } + elsif ($type eq "weapon") { + chanmsg(clog("$player left his weapon out in the rain to rust! ". + "$player\'s $type loses 10% of its effectiveness.")); + } + elsif ($type eq "tunic") { + chanmsg(clog("$player spilled a level 7 shrinking potion on his ". + "tunic! $player\'s $type loses 10% of its ". + "effectiveness.")); + } + elsif ($type eq "shield") { + chanmsg(clog("$player\'s shield was damaged by a dragon's fiery ". + "breath! $player\'s $type loses 10% of its ". + "effectiveness.")); + } + else { + chanmsg(clog("$player burned a hole through his leggings while ". + "ironing them! $player\'s $type loses 10% of its ". + "effectiveness.")); + } + my $suffix=""; + if ($rps{$player}{item}{$type} =~ /(\D)$/) { $suffix=$1; } + $rps{$player}{item}{$type} = int(int($rps{$player}{item}{$type}) * .9); + $rps{$player}{item}{$type}.=$suffix; + } + else { + my $time = int(int(5 + rand(8)) / 100 * $rps{$player}{next}); + if (!open(Q,$opts{eventsfile})) { + return chanmsg("ERROR: Failed to open $opts{eventsfile}: $!"); + } + my($i,$actioned); + while (my $line = ) { + chomp($line); + if ($line =~ /^C (.*)/ && rand(++$i) < 1) { $actioned = $1; } + } + chanmsg(clog("$player $actioned. This terrible calamity has slowed ". + "them ".duration($time)." from level ". + ($rps{$player}{level}+1).".")); + $rps{$player}{next} += $time; + chanmsg("$player reaches next level in ".duration($rps{$player}{next}). + "."); + } +} + +sub godsend { # bless the unworthy + my @players = grep { $rps{$_}{online} } keys(%rps); + return unless @players; + my $player = $players[rand(@players)]; + if (rand(10) < 1) { + my @items = ("amulet","charm","weapon","tunic","set of leggings", + "shield"); + my $type = $items[rand(@items)]; + if ($type eq "amulet") { + chanmsg(clog("$player\'s amulet was blessed by a passing cleric! ". + "$player\'s $type gains 10% effectiveness.")); + } + elsif ($type eq "charm") { + chanmsg(clog("$player\'s charm ate a bolt of lightning! ". + "$player\'s $type gains 10% effectiveness.")); + } + elsif ($type eq "weapon") { + chanmsg(clog("$player sharpened the edge of his weapon! ". + "$player\'s $type gains 10% effectiveness.")); + } + elsif ($type eq "tunic") { + chanmsg(clog("A magician cast a spell of Rigidity on $player\'s ". + "tunic! $player\'s $type gains 10% effectiveness.")); + } + elsif ($type eq "shield") { + chanmsg(clog("$player reinforced his shield with a dragon's ". + "scales! $player\'s $type gains 10% effectiveness.")); + } + else { + chanmsg(clog("The local wizard imbued $player\'s pants with a ". + "Spirit of Fortitude! $player\'s $type gains 10% ". + "effectiveness.")); + } + my $suffix=""; + if ($rps{$player}{item}{$type} =~ /(\D)$/) { $suffix=$1; } + $rps{$player}{item}{$type} = int(int($rps{$player}{item}{$type}) * 1.1); + $rps{$player}{item}{$type}.=$suffix; + } + else { + my $time = int(int(5 + rand(8)) / 100 * $rps{$player}{next}); + my $actioned; + if (!open(Q,$opts{eventsfile})) { + return chanmsg("ERROR: Failed to open $opts{eventsfile}: $!"); + } + my $i; + while (my $line = ) { + chomp($line); + if ($line =~ /^G (.*)/ && rand(++$i) < 1) { + $actioned = $1; + } + } + chanmsg(clog("$player $actioned! This wondrous godsend has ". + "accelerated them ".duration($time)." towards level ". + ($rps{$player}{level}+1).".")); + $rps{$player}{next} -= $time; + chanmsg("$player reaches next level in ".duration($rps{$player}{next}). + "."); + } +} + +sub quest { + @{$quest{questers}} = grep { $rps{$_}{online} && $rps{$_}{level} > 39 && + time()-$rps{$_}{lastlogin}>36000 } keys(%rps); + if (@{$quest{questers}} < 4) { return undef(@{$quest{questers}}); } + while (@{$quest{questers}} > 4) { + splice(@{$quest{questers}},int(rand(@{$quest{questers}})),1); + } + if (!open(Q,$opts{eventsfile})) { + return chanmsg("ERROR: Failed to open $opts{eventsfile}: $!"); + } + my $i; + while (my $line = ) { + chomp($line); + if ($line =~ /^Q/ && rand(++$i) < 1) { + if ($line =~ /^Q1 (.*)/) { + $quest{text} = $1; + $quest{type} = 1; + $quest{qtime} = time() + 43200 + int(rand(43201)); # 12-24 hours + } + elsif ($line =~ /^Q2 (\d+) (\d+) (\d+) (\d+) (.*)/) { + $quest{p1} = [$1,$2]; + $quest{p2} = [$3,$4]; + $quest{text} = $5; + $quest{type} = 2; + $quest{stage} = 1; + } + } + } + close(Q); + if ($quest{type} == 1) { + chanmsg(join(", ",(@{$quest{questers}})[0..2]).", and ". + "$quest{questers}->[3] have been chosen by the gods to ". + "$quest{text}. Quest to end in ".duration($quest{qtime}-time()). + "."); + } + elsif ($quest{type} == 2) { + chanmsg(join(", ",(@{$quest{questers}})[0..2]).", and ". + "$quest{questers}->[3] have been chosen by the gods to ". + "$quest{text}. Participants must first reach [$quest{p1}->[0],". + "$quest{p1}->[1]], then [$quest{p2}->[0],$quest{p2}->[1]].". + ($opts{mapurl}?" See $opts{mapurl} to monitor their journey's ". + "progress.":"")); + } + writequestfile(); +} + +sub questpencheck { + my $k = shift; + my ($quester,$player); + for $quester (@{$quest{questers}}) { + if ($quester eq $k) { + chanmsg(clog("$k\'s prudence and self-regard has brought the ". + "wrath of the gods upon the realm. All your great ". + "wickedness makes you as it were heavy with lead, ". + "and to tend downwards with great weight and ". + "pressure towards hell. Therefore have you drawn ". + "yourselves 15 steps closer to that gaping maw.")); + for $player (grep { $rps{$_}{online} } keys %rps) { + my $gain = int(15 * ($opts{rppenstep}**$rps{$player}{level})); + $rps{$player}{pen_quest} += $gain; + $rps{$player}{next} += $gain; + } + undef(@{$quest{questers}}); + $quest{qtime} = time() + 43200; # 12 hours + } + } +} + +sub clog { + my $mesg = shift; + open(B,">>$opts{modsfile}") or do { + debug("Error: Cannot open $opts{modsfile}: $!"); + chanmsg("Error: Cannot open $opts{modsfile}: $!"); + return $mesg; + }; + print B ts()."$mesg\n"; + close(B); + return $mesg; +} + +sub backup() { + if (! -d ".dbbackup/") { mkdir(".dbbackup",0700); } + if ($^O ne "MSWin32") { + system("cp $opts{dbfile} .dbbackup/$opts{dbfile}".time()); + } + else { + system("copy $opts{dbfile} .dbbackup\\$opts{dbfile}".time()); + } +} + +sub penalize { + my $username = shift; + return 0 if !defined($username); + return 0 if !exists($rps{$username}); + my $type = shift; + my $pen = 0; + questpencheck($username); + if ($type eq "quit") { + $pen = int(20 * ($opts{rppenstep}**$rps{$username}{level})); + if ($opts{limitpen} && $pen > $opts{limitpen}) { + $pen = $opts{limitpen}; + } + $rps{$username}{pen_quit}+=$pen; + $rps{$username}{online}=0; + } + elsif ($type eq "nick") { + my $newnick = shift; + $pen = int(30 * ($opts{rppenstep}**$rps{$username}{level})); + if ($opts{limitpen} && $pen > $opts{limitpen}) { + $pen = $opts{limitpen}; + } + $rps{$username}{pen_nick}+=$pen; + $rps{$username}{nick} = substr($newnick,1); + substr($rps{$username}{userhost},0,length($rps{$username}{nick})) = + substr($newnick,1); + notice("Penalty of ".duration($pen)." added to your timer for ". + "nick change.",$rps{$username}{nick}); + } + elsif ($type eq "privmsg" || $type eq "notice") { + $pen = int(shift(@_) * ($opts{rppenstep}**$rps{$username}{level})); + if ($opts{limitpen} && $pen > $opts{limitpen}) { + $pen = $opts{limitpen}; + } + $rps{$username}{pen_mesg}+=$pen; + notice("Penalty of ".duration($pen)." added to your timer for ". + $type.".",$rps{$username}{nick}); + } + elsif ($type eq "part") { + $pen = int(200 * ($opts{rppenstep}**$rps{$username}{level})); + if ($opts{limitpen} && $pen > $opts{limitpen}) { + $pen = $opts{limitpen}; + } + $rps{$username}{pen_part}+=$pen; + notice("Penalty of ".duration($pen)." added to your timer for ". + "parting.",$rps{$username}{nick}); + $rps{$username}{online}=0; + } + elsif ($type eq "kick") { + $pen = int(250 * ($opts{rppenstep}**$rps{$username}{level})); + if ($opts{limitpen} && $pen > $opts{limitpen}) { + $pen = $opts{limitpen}; + } + $rps{$username}{pen_kick}+=$pen; + notice("Penalty of ".duration($pen)." added to your timer for ". + "being kicked.",$rps{$username}{nick}); + $rps{$username}{online}=0; + } + elsif ($type eq "logout") { + $pen = int(20 * ($opts{rppenstep}**$rps{$username}{level})); + if ($opts{limitpen} && $pen > $opts{limitpen}) { + $pen = $opts{limitpen}; + } + $rps{$username}{pen_logout} += $pen; + notice("Penalty of ".duration($pen)." added to your timer for ". + "LOGOUT command.",$rps{$username}{nick}); + $rps{$username}{online}=0; + } + $rps{$username}{next} += $pen; + return 1; # successfully penalized a user! woohoo! +} + +sub debug { + (my $text = shift) =~ s/[\r\n]//g; + my $die = shift; + if ($opts{debug} || $opts{verbose}) { + open(DBG,">>$opts{debugfile}") or do { + chanmsg("Error: Cannot open debug file: $!"); + return; + }; + print DBG ts()."$text\n"; + close(DBG); + } + if ($die) { die("$text\n"); } + return $text; +} + +sub finduser { + my $nick = shift; + return undef if !defined($nick); + for my $user (keys(%rps)) { + next unless $rps{$user}{online}; + if ($rps{$user}{nick} eq $nick) { return $user; } + } + return undef; +} + +sub ha { # return 0/1 if username has access + my $user = shift; + if (!defined($user) || !exists($rps{$user})) { + debug("Error: Attempted ha() for invalid username \"$user\""); + return 0; + } + return $rps{$user}{isadmin}; +} + +sub checksplits { # removed expired split hosts from the hash + my $host; + while ($host = each(%split)) { + if (time()-$split{$host}{time} > $opts{splitwait}) { + $rps{$split{$host}{account}}{online} = 0; + delete($split{$host}); + } + } +} + +sub collision_fight { + my($u,$opp) = @_; + my $mysum = itemsum($u,1); + my $oppsum = itemsum($opp,1); + my $myroll = int(rand($mysum)); + my $opproll = int(rand($oppsum)); + if ($myroll >= $opproll) { + my $gain = int($rps{$opp}{level}/4); + $gain = 7 if $gain < 7; + $gain = int(($gain/100)*$rps{$u}{next}); + chanmsg(clog("$u [$myroll/$mysum] has come upon $opp [$opproll/$oppsum". + "] and taken them in combat! ".duration($gain)." is ". + "removed from $u\'s clock.")); + $rps{$u}{next} -= $gain; + chanmsg("$u reaches next level in ".duration($rps{$u}{next})."."); + if (rand(35) < 1 && $opp ne $primnick) { + $gain = int(((5 + int(rand(20)))/100) * $rps{$opp}{next}); + chanmsg(clog("$u has dealt $opp a Critical Strike! ". + duration($gain)." is added to $opp\'s clock.")); + $rps{$opp}{next} += $gain; + chanmsg("$opp reaches next level in ".duration($rps{$opp}{next}). + "."); + } + elsif (rand(25) < 1 && $opp ne $primnick && $rps{$u}{level} > 19) { + my @items = ("ring","amulet","charm","weapon","helm","tunic", + "pair of gloves","set of leggings","shield", + "pair of boots"); + my $type = $items[rand(@items)]; + if (int($rps{$opp}{item}{$type}) > int($rps{$u}{item}{$type})) { + chanmsg("In the fierce battle, $opp dropped his level ". + int($rps{$opp}{item}{$type})." $type! $u picks it up, ". + "tossing his old level ".int($rps{$u}{item}{$type}). + " $type to $opp."); + my $tempitem = $rps{$u}{item}{$type}; + $rps{$u}{item}{$type}=$rps{$opp}{item}{$type}; + $rps{$opp}{item}{$type} = $tempitem; + } + } + } + else { + my $gain = ($opp eq $primnick)?10:int($rps{$opp}{level}/7); + $gain = 7 if $gain < 7; + $gain = int(($gain/100)*$rps{$u}{next}); + chanmsg(clog("$u [$myroll/$mysum] has come upon $opp [$opproll/$oppsum". + "] and been defeated in combat! ".duration($gain)." is ". + "added to $u\'s clock.")); + $rps{$u}{next} += $gain; + chanmsg("$u reaches next level in ".duration($rps{$u}{next})."."); + } +} + +sub writequestfile { + return unless $opts{writequestfile}; + open(QF,">$opts{questfilename}") or do { + chanmsg("Error: Cannot open $opts{questfilename}: $!"); + return; + }; + # if no active quest, just empty questfile. otherwise, write it + if (@{$quest{questers}}) { + if ($quest{type}==1) { + print QF "T $quest{text}\n". + "Y 1\n". + "S $quest{qtime}\n". + "P1 $quest{questers}->[0]\n". + "P2 $quest{questers}->[1]\n". + "P3 $quest{questers}->[2]\n". + "P4 $quest{questers}->[3]\n"; + } + elsif ($quest{type}==2) { + print QF "T $quest{text}\n". + "Y 2\n". + "S $quest{stage}\n". + "P $quest{p1}->[0] $quest{p1}->[1] $quest{p2}->[0] ". + "$quest{p2}->[1]\n". + "P1 $quest{questers}->[0] $rps{$quest{questers}->[0]}{x} ". + "$rps{$quest{questers}->[0]}{y}\n". + "P2 $quest{questers}->[1] $rps{$quest{questers}->[1]}{x} ". + "$rps{$quest{questers}->[1]}{y}\n". + "P3 $quest{questers}->[2] $rps{$quest{questers}->[2]}{x} ". + "$rps{$quest{questers}->[2]}{y}\n". + "P4 $quest{questers}->[3] $rps{$quest{questers}->[3]}{x} ". + "$rps{$quest{questers}->[3]}{y}\n"; + } + } + close(QF); +} + +sub goodness { + my @players = grep { $rps{$_}{alignment} eq "g" && + $rps{$_}{online} } keys(%rps); + return unless @players > 1; + splice(@players,int(rand(@players)),1) while @players > 2; + my $gain = 5 + int(rand(8)); + chanmsg(clog("$players[0] and $players[1] have not let the iniquities of ". + "evil men poison them. Together have they prayed to their ". + "god, and it is his light that now shines upon them. $gain\% ". + "of their time is removed from their clocks.")); + $rps{$players[0]}{next} = int($rps{$players[0]}{next}*(1 - ($gain/100))); + $rps{$players[1]}{next} = int($rps{$players[1]}{next}*(1 - ($gain/100))); + chanmsg("$players[0] reaches next level in ". + duration($rps{$players[0]}{next})."."); + chanmsg("$players[1] reaches next level in ". + duration($rps{$players[1]}{next})."."); +} + +sub evilness { + my @evil = grep { $rps{$_}{alignment} eq "e" && + $rps{$_}{online} } keys(%rps); + return unless @evil; + my $me = $evil[rand(@evil)]; + if (int(rand(2)) < 1) { + # evil only steals from good :^( + my @good = grep { $rps{$_}{alignment} eq "g" && + $rps{$_}{online} } keys(%rps); + my $target = $good[rand(@good)]; + my @items = ("ring","amulet","charm","weapon","helm","tunic", + "pair of gloves","set of leggings","shield", + "pair of boots"); + my $type = $items[rand(@items)]; + if (int($rps{$target}{item}{$type}) > int($rps{$me}{item}{$type})) { + my $tempitem = $rps{$me}{item}{$type}; + $rps{$me}{item}{$type} = $rps{$target}{item}{$type}; + $rps{$target}{item}{$type} = $tempitem; + chanmsg(clog("$me stole $target\'s level ". + int($rps{$me}{item}{$type})." $type while they were ". + "sleeping! $me leaves his old level ". + int($rps{$target}{item}{$type})." $type behind, ". + "which $target then takes.")); + } + else { + notice("You made to steal $target\'s $type, but realized it was ". + "lower level than your own. You creep back into the ". + "shadows.",$rps{$me}{nick}); + } + } + else { # being evil only pays about half of the time... + my $gain = 1 + int(rand(5)); + chanmsg(clog("$me is forsaken by his evil god. ". + duration(int($rps{$me}{next} * ($gain/100)))." is added ". + "to his clock.")); + $rps{$me}{next} = int($rps{$me}{next} * (1 + ($gain/100))); + chanmsg("$me reaches next level in ".duration($rps{$me}{next})."."); + } +} + +sub fisher_yates_shuffle { + my $array = shift; + my $i; + for ($i = @$array; --$i; ) { + my $j = int rand ($i+1); + next if $i == $j; + @$array[$i,$j] = @$array[$j,$i]; + } +} + +sub writedb { + open(RPS,">$opts{dbfile}") or do { + chanmsg("ERROR: Cannot write $opts{dbfile}: $!"); + return 0; + }; + print RPS join("\t","# username", + "pass", + "is admin", + "level", + "class", + "next ttl", + "nick", + "userhost", + "online", + "idled", + "x pos", + "y pos", + "pen_mesg", + "pen_nick", + "pen_part", + "pen_kick", + "pen_quit", + "pen_quest", + "pen_logout", + "created", + "last login", + "amulet", + "charm", + "helm", + "boots", + "gloves", + "ring", + "leggings", + "shield", + "tunic", + "weapon", + "alignment")."\n"; + my $k; + keys(%rps); # reset internal pointer + while ($k=each(%rps)) { + if (exists($rps{$k}{next}) && defined($rps{$k}{next})) { + print RPS join("\t",$k, + $rps{$k}{pass}, + $rps{$k}{isadmin}, + $rps{$k}{level}, + $rps{$k}{class}, + $rps{$k}{next}, + $rps{$k}{nick}, + $rps{$k}{userhost}, + $rps{$k}{online}, + $rps{$k}{idled}, + $rps{$k}{x}, + $rps{$k}{y}, + $rps{$k}{pen_mesg}, + $rps{$k}{pen_nick}, + $rps{$k}{pen_part}, + $rps{$k}{pen_kick}, + $rps{$k}{pen_quit}, + $rps{$k}{pen_quest}, + $rps{$k}{pen_logout}, + $rps{$k}{created}, + $rps{$k}{lastlogin}, + $rps{$k}{item}{amulet}, + $rps{$k}{item}{charm}, + $rps{$k}{item}{helm}, + $rps{$k}{item}{"pair of boots"}, + $rps{$k}{item}{"pair of gloves"}, + $rps{$k}{item}{ring}, + $rps{$k}{item}{"set of leggings"}, + $rps{$k}{item}{shield}, + $rps{$k}{item}{tunic}, + $rps{$k}{item}{weapon}, + $rps{$k}{alignment})."\n"; + } + } + close(RPS); +} + +sub readconfig { + if (! -e ".irpg.conf") { + debug("Error: Cannot find .irpg.conf. Copy it to this directory, ". + "please.",1); + } + else { + open(CONF,"<.irpg.conf") or do { + debug("Failed to open config file .irpg.conf: $!",1); + }; + my($line,$key,$val); + while ($line=) { + next() if $line =~ /^#/; # skip comments + $line =~ s/[\r\n]//g; + $line =~ s/^\s+//g; + next() if !length($line); # skip blank lines + ($key,$val) = split(/\s+/,$line,2); + $key = lc($key); + if (lc($val) eq "on" || lc($val) eq "yes") { $val = 1; } + elsif (lc($val) eq "off" || lc($val) eq "no") { $val = 0; } + if ($key eq "die") { + die("Please edit the file .irpg.conf to setup your bot's ". + "options. Also, read the README file if you haven't ". + "yet.\n"); + } + elsif ($key eq "server") { push(@{$opts{servers}},$val); } + elsif ($key eq "okurl") { push(@{$opts{okurl}},$val); } + else { $opts{$key} = $val; } + } + } +} diff --git a/events.txt b/events.txt new file mode 100644 index 0000000..becb890 --- /dev/null +++ b/events.txt @@ -0,0 +1,71 @@ +C was bitten by drdink +C fell into a hole +C bit their tongue +C set themself on fire +C ate a poisonous fruit +C lost their mind +C died, temporarily.. +C was caught in a terrible snowstorm +C EXPLODED, somewhat.. +C got knifed in a dark alley +C saw an episode of Ally McBeal +C got turned INSIDE OUT, practically +C ate a very disagreeable fruit, getting a terrible case of heartburn +C met up with a mob hitman for not paying his bills +C has fallen ill with the black plague +C was struck by lightning +C was attacked by a rabid cow +C was attacked by a rabid wolverine +C was set on fire +C was decapitated, temporarily.. +C was tipped by a cow +C was bucked from a horse +C was bitten by a møøse +C was sat on by a giant +C ate a plate of discounted, day-old sushi +C got harassed by peer +C got lost in the woods +C misplaced his map +C broke his compass +C lost his glasses +C walked face-first into a tree +G found a pair of Nikes +G caught a unicorn +G discovered a secret, underground passage +G was taught to run quickly by a secret tribe of pygmies that know how to, among other things, run quickly +G discovered caffeinated coffee +G grew an extra leg +G was visited by a very pretty nymph +G found kitten +G learned Perl +G found an exploit in the IRPG code +G tamed a wild horse +G found a one-time-use spell of quickness +G bought a faster computer +G bribed the local IRPG administrator +G stopped using dial-up +G invented the wheel +G gained a sixth sense +G got a kiss from drwiii +G had his clothes laundered by a passing fairy +G was rejuvenated by drinking from a magic stream +G was bitten by a radioactive spider +G hit it off with a drunk sorority chick named Jenny +G was accepted into Pi Beta Phi +Q2 225 415 280 460 lay waste to the Towers of Ankh-Allor, wherein lies the terrible sorceror Croocq +Q1 locate the centuries-lost tomes of the grim prophet Haplashak Mhadhu +Q2 400 475 480 380 explore and chart the dark lands of T'rnalvph +Q1 locate the ancient writings of Ahmo, prophet of the blind god Io, namely his last and hidden work, Time as Deity, thought to answer all of mankind's greater wonders +Q2 290 65 325 270 slay the great and horrible troll, Dokt'r Wiii +Q2 480 415 325 270 return the stolen relics of Iao-Sabao to the city of Velvragh, quieting the religious riot that has sprung up from their loss +Q2 70 315 325 270 guard the secret passage to Bharash until the full moon has passed, and the evil returned to its resting place +Q2 50 350 325 270 destroy the bandits terrorizing the roads passing through the Great Shahlil mountains +Q1 locate and destroy the immensely powerful Eyeless Amulet of the evil sorceress, Ankh B'loht +Q2 167 458 325 270 rescue the beautiful princess Juliet from the grasp of the beast Grabthul +Q1 locate the herbs and brew the elixir to rid the realm of the Normonic Plague +Q2 160 480 160 380 hunt down the over-abundance of mountain wolves that are slaying the regions' cows +Q2 35 40 325 270 assassinate the general, Ronald Ashur, of the invading army of Denmark +Q2 235 125 430 60 setup a trade route through the mountains to the neighboring land of Qwok and arrange correspondence with their leader, Cuincey-Love Vikk'l +Q2 155 155 325 270 live among and learn the ancient magick of the tribe of pygmie people, the Jow Botzi +Q2 70 125 170 100 kill the resurrected Jow Botzian zombies produced by a young wizard's wayward spell +Q1 worship the sacred Cow until such time as she is satiated diff --git a/irpgdbtool b/irpgdbtool new file mode 100644 index 0000000..c339a6c --- /dev/null +++ b/irpgdbtool @@ -0,0 +1,469 @@ +# IRPG db conversion tool; converts db version 2.4 -> 3.0 +# Jon Honeycutt, jotun@idlerpg.net, http://idlerpg.net +# Free for all use, public and private, with retention of copyright notice. + +use strict; +use IO::Socket; + +my %rps = (); +my $temp; + +$|=1; + +print "\nIRPG db conversion tool; version 2.4 -> 3.0\n\n"; + +do { + print "Read from file [irpg.db]: "; + chomp($temp=); + $temp ||= "irpg.db"; + if (! -e $temp) { print "Error: No such file\n"; } +} until (-e $temp); + +loaddb($temp); + +print "Loaded ".scalar(keys(%rps))." accounts from $temp.\n"; + +do { + print "\nBackup old irpg.db file? [yes]: "; + chomp($temp=); + $temp||="yes"; + $temp=lc($temp); +} until ($temp eq "yes" || $temp eq "no"); + +if ($temp eq "yes") { + do { + print "\nBackup filename [irpg.db.old]: "; + chomp($temp=); + $temp||="irpg.db.old"; + } until (defined($temp)); + open(RPS,">$temp") or die("Cannot write $temp: $!"); + print RPS "# username\tpass\tlevel\tclass\tnext\tnick\tuserhost\tonline\t". + "idled\tpen_mesg\tpen_nick\tpen_part\tpen_kick\tpen_quit\t". + "pen_quest\tpen_logout\tcreated\tlast login\tamulet\tcharm\t". + "helm\tboots\tgloves\tring\tleggings\tshield\ttunic\tweapon\n"; + for my $k (keys %rps) { + print RPS join("\t", + $k, + $rps{$k}{pass}, + $rps{$k}{level}, + $rps{$k}{class}, + $rps{$k}{next}, + $rps{$k}{nick}||"", + $rps{$k}{userhost}||"", + $rps{$k}{online}||0, + $rps{$k}{idled}||0, + $rps{$k}{pen_mesg}||0, + $rps{$k}{pen_nick}||0, + $rps{$k}{pen_part}||0, + $rps{$k}{pen_kick}||0, + $rps{$k}{pen_quit}||0, + $rps{$k}{pen_quest}||0, + $rps{$k}{pen_logout}||0, + $rps{$k}{created}, + $rps{$k}{lastlogin}, + $rps{$k}{item}{amulet}||0, + $rps{$k}{item}{charm}||0, + $rps{$k}{item}{helm}||0, + $rps{$k}{item}{"pair of boots"}||0, + $rps{$k}{item}{"pair of gloves"}||0, + $rps{$k}{item}{ring}||0, + $rps{$k}{item}{"set of leggings"}||0, + $rps{$k}{item}{shield}||0, + $rps{$k}{item}{tunic}||0, + $rps{$k}{item}{weapon}||0)."\n"; + } + close(RPS); + print "Wrote $temp.\n"; +} + +do { + print "\nReset all user levels to 0, all times to level to 0, all items ". + "to 0, all penalties to 0, all online flags to 0, all idled times ". + "to 0, all creation dates and last login times to today (i.e., ". + "reset game)? [no]: "; + chomp($temp=); + $temp||="no"; + $temp=lc($temp); +} until ($temp eq "yes" || $temp eq "no"); + +if ($temp eq "yes") { + for my $k (keys(%rps)) { + $rps{$k}{next}=0; + $rps{$k}{level}=0; + $rps{$k}{online}=0; + $rps{$k}{idled}=0; + $rps{$k}{item}{amulet}=0; + $rps{$k}{item}{charm}=0; + $rps{$k}{item}{helm}=0; + $rps{$k}{item}{"pair of boots"}=0; + $rps{$k}{item}{"pair of gloves"}=0; + $rps{$k}{item}{ring}=0; + $rps{$k}{item}{"set of leggings"}=0; + $rps{$k}{item}{shield}=0; + $rps{$k}{item}{tunic}=0; + $rps{$k}{item}{weapon}=0; + $rps{$k}{pen_mesg}=0; + $rps{$k}{pen_nick}=0; + $rps{$k}{pen_part}=0; + $rps{$k}{pen_kick}=0; + $rps{$k}{pen_quit}=0; + $rps{$k}{pen_quest}=0; + $rps{$k}{pen_logout}=0; + $rps{$k}{created}=time(); + $rps{$k}{lastlogin}=time(); + } + print "Game reset.\n"; +} + +do { + print "\nStrip all control codes from character names and classes? [no]: "; + chomp($temp=); + $temp ||="no"; + $temp=lc($temp); +} until ($temp eq "yes" || $temp eq "no"); + +if ($temp eq "yes") { + my(@usernames,@classes); + for my $k (keys(%rps)) { + if ($k =~ /[[:cntrl:]]/) { + my $newusername = $k; + $newusername =~ s/[[:cntrl:]]//g; + if (exists($rps{$newusername}) || !defined($newusername) || + !length($newusername)) { + print "\nError: While trying to strip control codes from $k, ". + "found stripped version ($newusername) already exists ". + "in database or is undefined. Skipping this user, so ". + "sorry.\n"; + } + else { + $rps{$newusername}=delete($rps{$k}); + push(@usernames,"$k is now: $newusername"); + $k = $newusername; + } + } + if ($rps{$k}{class} =~ /[[:cntrl:]]/) { + $rps{$k}{class} =~ s/[[:cntrl:]]//g; + push(@classes,"$k is now: $rps{$k}{class}"); + } + } + if (@usernames) { + print "\nUsernames changed (would be good to alert these users):\n"; + print "User $_\n" for @usernames; + print "\n"; + } + if (@classes) { + print "\nClass names changed (might be good to alert these users):\n"; + print "User $_\n" for @classes; + print "\n"; + } +} + +do { + print "\nStrip all non-printable characters from character names and ". + "classes? [no]: "; + chomp($temp=); + $temp ||="no"; + $temp=lc($temp); +} until ($temp eq "yes" || $temp eq "no"); + +if ($temp eq "yes") { + my(@usernames,@classes); + for my $k (keys(%rps)) { + if ($k =~ /[[:^print:]]/) { + my $newusername = $k; + $newusername =~ s/[[:^print:]]//g; + if (exists($rps{$newusername}) || !defined($newusername) || + !length($newusername)) { + print "\nError: While trying to strip non-printable chars ". + "from $k, found stripped version ($newusername) already ". + "exists in database or is undefined. Skipping this ". + "user, so sorry.\n"; + } + else { + $rps{$newusername}=delete($rps{$k}); + push(@usernames,"$k is now: $newusername"); + $k = $newusername; + } + } + if ($rps{$k}{class} =~ /[[:^print:]]/) { + $rps{$k}{class} =~ s/[[:^print:]]//g; + push(@classes,"$k\'s class is now: $rps{$k}{class}"); + } + } + if (@usernames) { + print "\nUsernames changed (would be good to alert these users):\n"; + print "User $_\n" for @usernames; + print "\n"; + } + if (@classes) { + print "\nClass names changed (might be good to alert these users):\n"; + print "User $_\n" for @classes; + print "\n"; + } +} + +do { + print "\nVersion 3.0 supports 'named items,' or a method of marking ". + "unique items as being unique. Attempt to name existing items that ". + "are known uniques? [yes]: "; + chomp($temp=); + $temp ||="yes"; + $temp=lc($temp); +} until ($temp eq "yes" || $temp eq "no"); + +if ($temp eq "yes") { + for my $k (keys(%rps)) { + for my $item (keys(%{$rps{$k}{item}})) { + if ($rps{$k}{item}{$item} > int(1.5*$rps{$k}{level})) { + if ($item eq "helm") { + print "$k\'s $item named as Mattt's Omniscience.\n"; + $rps{$k}{item}{$item} .= "a"; + } + elsif ($item eq "tunic") { + print "$k\'s $item named as Res0's Protectorate.\n"; + $rps{$k}{item}{$item} .= "b"; + } + elsif ($item eq "amulet") { + print "$k\'s $item named as Dwyn's Storm.\n"; + $rps{$k}{item}{$item} .= "c"; + } + elsif ($item eq "weapon" && $rps{$k}{item}{$item} < 175) { + print "$k\'s $item named as Jotun's Fury.\n"; + $rps{$k}{item}{$item} .= "d"; + } + elsif ($item eq "weapon" && $rps{$k}{item}{$item} > 175 && + $rps{$k}{item}{$item} < 201) { + print "$k\'s $item named as Drdink's Cane of Blind Rage.\n"; + $rps{$k}{item}{$item} .= "e"; + } + else { + print "$k has unknown unique of level ". + "$rps{$k}{item}{$item}.\n"; + } + } + } + } +} + +do { + print "\nThere exist new items in version 3.0 that some of your clients ". + "may already have had the chance to find. I.E., there is a new item ". + "with a required level of 48. Simulate an item find for all users ". + "above 48 for this and other new items to make the game fair for ". + "older users? [yes]: "; + chomp($temp=); + $temp ||="yes"; + $temp=lc($temp); +} until ($temp eq "yes" || $temp eq "no"); + +if ($temp eq "yes") { + for my $k (keys(%rps)) { + if ($rps{$k}{level} >= 48) { + for (48..$rps{$k}{level}) { + # approximately equal to normal item find, i believe + if (rand(100) < 2.25) { + my $ulevel = 250+int(rand(51)); + if ($ulevel > int($rps{$k}{item}{"pair of boots"})) { + print "$k found level $ulevel Mrquick's Magical Boots ". + "of Swiftness.\n"; + $rps{$k}{item}{"pair of boots"} = $ulevel."f"; + } + } + } + } + if ($rps{$k}{level} >= 52) { + for (52..$rps{$k}{level}) { + # approximately equal to normal item find, i believe + if (rand(100) < 2.15) { + my $ulevel = 300+int(rand(51)); + if ($ulevel > int($rps{$k}{item}{weapon})) { + print "$k found level $ulevel Jeff's Cluehammer of ". + "Doom.\n"; + $rps{$k}{item}{weapon} = $ulevel."g"; + } + } + } + } + if ($rps{$k}{level} >= 25) { + for (25..$rps{$k}{level}) { + # approximately equal to normal item find, i believe + if (rand(100) < 2.43) { + my $ulevel = 50+int(rand(25)); + if ($ulevel > int($rps{$k}{item}{ring})) { + print "$k found level $ulevel Juliet's Glorious Ring ". + "of Sparkliness.\n"; + $rps{$k}{item}{ring} = $ulevel."h"; + } + } + } + } + } +} + +for my $k (keys(%rps)) { + $rps{$k}{x} = int(rand(500)); + $rps{$k}{y} = int(rand(500)); + $rps{$k}{isadmin}=0; + $rps{$k}{alignment}="n"; +} + +print "\nUsernames that you would like to have admin status (separate with ". + "commas, use proper CaSe): "; +chomp($temp=); +$temp =~ s/\s//g; +for my $k (split(/,/,$temp)) { + if (!exists($rps{$k})) { + print "\nError: Account name '$k' does not exist. Remember that ". + "account names are case sensitive. Skipping this username. Edit ". + "the database manually, or use the MKADMIN command after the ". + "bot connects to add this user.\n\n"; + } + else { + print "$k is now admin.\n"; + $rps{$k}{isadmin}=1; + } +} +print "\nYou can add more admins later with the MKADMIN command.\n"; + +do { + print "\nWrite to new db file [irpg.db]: "; + chomp($temp=); + $temp ||= "irpg.db"; +} until (defined($temp)); + +open(RPS,">$temp") or die "Cannot open $temp: $!"; + +print RPS join("\t","# username", + "pass", + "is admin", + "level", + "class", + "next ttl", + "nick", + "userhost", + "online", + "idled", + "x pos", + "y pos", + "pen_mesg", + "pen_nick", + "pen_part", + "pen_kick", + "pen_quit", + "pen_quest", + "pen_logout", + "created", + "last login", + "amulet", + "charm", + "helm", + "boots", + "gloves", + "ring", + "leggings", + "shield", + "tunic", + "weapon", + "alignment")."\n"; + +for my $k (keys(%rps)) { + print RPS join("\t", + $k, + $rps{$k}{pass}, + $rps{$k}{isadmin}, + $rps{$k}{level}, + $rps{$k}{class}, + $rps{$k}{next}, + $rps{$k}{nick}, + $rps{$k}{userhost}, + $rps{$k}{online}, + $rps{$k}{idled}, + $rps{$k}{x}, + $rps{$k}{y}, + $rps{$k}{pen_mesg}, + $rps{$k}{pen_nick}, + $rps{$k}{pen_part}, + $rps{$k}{pen_kick}, + $rps{$k}{pen_quit}, + $rps{$k}{pen_quest}, + $rps{$k}{pen_logout}, + $rps{$k}{created}, + $rps{$k}{lastlogin}, + $rps{$k}{item}{amulet}, + $rps{$k}{item}{charm}, + $rps{$k}{item}{helm}, + $rps{$k}{item}{"pair of boots"}, + $rps{$k}{item}{"pair of gloves"}, + $rps{$k}{item}{ring}, + $rps{$k}{item}{"set of leggings"}, + $rps{$k}{item}{shield}, + $rps{$k}{item}{tunic}, + $rps{$k}{item}{weapon}, + $rps{$k}{alignment})."\n"; +} +close(RPS); + +do { + print "\nDone writing $temp! Thanks for your interest in the Idle RPG. May ". + "I send an (anonymous) user count to idlerpg.net? jotun is ". + "interested in knowing how many people play his game :^) [yes]: "; + chomp($temp=); + $temp||="yes"; + $temp=lc($temp); +} until ($temp eq "yes" || $temp eq "no"); + +if ($temp eq "yes") { + print "Sending...\n"; + my $sock = IO::Socket::INET->new(PeerAddr=>"jotun.ultrazone.org:80"); + if ($sock) { + print $sock "GET /g7/count.php?converted=".scalar(keys(%rps)). + " HTTP/1.1\r\n". + "Host: jotun.ultrazone.org:80\r\n\r\n"; + 1 while <$sock>; + } + print "\nDone! Thanks a million! Enjoy Idle RPG. :^)\n"; +} +else { + print "\nI'm setting your chance of evil HoG to 100%, then. Just kidding. ". + "Thanks anyway.\n"; +} + +sub loaddb { # load the players database + open(RPS,shift(@_)) or die("loaddb() failed: $!"); + while (my $l=) { + chomp $l; + next if $l =~ /^#/; # skip comments + my @i = split("\t",$l); + print Dumper @i if @i != 28; + die("Anomaly in loaddb(); line $. of database has wrong fields (". + scalar(@i).")") if @i != 28; + ($rps{$i[0]}{pass}, + $rps{$i[0]}{level}, + $rps{$i[0]}{class}, + $rps{$i[0]}{next}, + $rps{$i[0]}{nick}, + $rps{$i[0]}{userhost}, + $rps{$i[0]}{online}, + $rps{$i[0]}{idled}, + $rps{$i[0]}{pen_mesg}, + $rps{$i[0]}{pen_nick}, + $rps{$i[0]}{pen_part}, + $rps{$i[0]}{pen_kick}, + $rps{$i[0]}{pen_quit}, + $rps{$i[0]}{pen_quest}, + $rps{$i[0]}{pen_logout}, + $rps{$i[0]}{created}, + $rps{$i[0]}{lastlogin}, + $rps{$i[0]}{item}{amulet}, + $rps{$i[0]}{item}{charm}, + $rps{$i[0]}{item}{helm}, + $rps{$i[0]}{item}{"pair of boots"}, + $rps{$i[0]}{item}{"pair of gloves"}, + $rps{$i[0]}{item}{ring}, + $rps{$i[0]}{item}{"set of leggings"}, + $rps{$i[0]}{item}{shield}, + $rps{$i[0]}{item}{tunic}, + $rps{$i[0]}{item}{weapon}) = (@i[1..$#i]); + } + close RPS; +} -- 2.43.0