From a4ff18076a859892cbeec1073ab5374789349a68 Mon Sep 17 00:00:00 2001 From: TommySalami Date: Fri, 12 Sep 2025 19:42:46 -0600 Subject: [PATCH] Add project files. --- IdleRPG.sln | 57 + bot.v3.1.2/.irpg.conf | 241 ++++ bot.v3.1.2/ChangeLog.txt | 651 +++++++++++ bot.v3.1.2/README | 62 + bot.v3.1.2/bot.v3.1.2.pl | 2352 ++++++++++++++++++++++++++++++++++++++ bot.v3.1.2/events.txt | 71 ++ bot.v3.1.2/irpgdbtool | 469 ++++++++ bot.v3.1.2/modifiers.txt | 0 bot.v3.1.2/questinfo.txt | 0 irpg/ChangeLog | 108 ++ irpg/README | 42 + irpg/admincomms.txt | 65 ++ irpg/commonfunctions.php | 94 ++ irpg/config.php | 39 + irpg/contact.php | 47 + irpg/db.php | 257 +++++ irpg/down.png | Bin 0 -> 86 bytes irpg/dump.php | 118 ++ irpg/footer.php | 35 + irpg/g7.css | 111 ++ irpg/head.png | Bin 0 -> 13741 bytes irpg/header.php | 47 + irpg/hits.db | 16 + irpg/idlerpg-adv.txt | 142 +++ irpg/index.php | 480 ++++++++ irpg/makemap.php | 60 + irpg/makequestmap.php | 79 ++ irpg/makeworldmap.php | 26 + irpg/maperror.png | Bin 0 -> 23314 bytes irpg/newmap.png | Bin 0 -> 22216 bytes irpg/players.php | 37 + irpg/playerview.php | 156 +++ irpg/quest.php | 118 ++ irpg/tablegrad.gif | Bin 0 -> 829 bytes irpg/up.png | Bin 0 -> 88 bytes irpg/worldmap.php | 27 + irpg/xml.php | 82 ++ 37 files changed, 6089 insertions(+) create mode 100644 IdleRPG.sln create mode 100644 bot.v3.1.2/.irpg.conf create mode 100644 bot.v3.1.2/ChangeLog.txt create mode 100644 bot.v3.1.2/README create mode 100644 bot.v3.1.2/bot.v3.1.2.pl create mode 100644 bot.v3.1.2/events.txt create mode 100644 bot.v3.1.2/irpgdbtool create mode 100644 bot.v3.1.2/modifiers.txt create mode 100644 bot.v3.1.2/questinfo.txt create mode 100644 irpg/ChangeLog create mode 100644 irpg/README create mode 100644 irpg/admincomms.txt create mode 100644 irpg/commonfunctions.php create mode 100644 irpg/config.php create mode 100644 irpg/contact.php create mode 100644 irpg/db.php create mode 100644 irpg/down.png create mode 100644 irpg/dump.php create mode 100644 irpg/footer.php create mode 100644 irpg/g7.css create mode 100644 irpg/head.png create mode 100644 irpg/header.php create mode 100644 irpg/hits.db create mode 100644 irpg/idlerpg-adv.txt create mode 100644 irpg/index.php create mode 100644 irpg/makemap.php create mode 100644 irpg/makequestmap.php create mode 100644 irpg/makeworldmap.php create mode 100644 irpg/maperror.png create mode 100644 irpg/newmap.png create mode 100644 irpg/players.php create mode 100644 irpg/playerview.php create mode 100644 irpg/quest.php create mode 100644 irpg/tablegrad.gif create mode 100644 irpg/up.png create mode 100644 irpg/worldmap.php create mode 100644 irpg/xml.php diff --git a/IdleRPG.sln b/IdleRPG.sln new file mode 100644 index 0000000..759a0c4 --- /dev/null +++ b/IdleRPG.sln @@ -0,0 +1,57 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36414.22 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bot.v3.1.2", "bot.v3.1.2", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + bot.v3.1.2\.irpg.conf = bot.v3.1.2\.irpg.conf + bot.v3.1.2\bot.v3.1.2.pl = bot.v3.1.2\bot.v3.1.2.pl + bot.v3.1.2\ChangeLog.txt = bot.v3.1.2\ChangeLog.txt + bot.v3.1.2\events.txt = bot.v3.1.2\events.txt + bot.v3.1.2\irpgdbtool = bot.v3.1.2\irpgdbtool + bot.v3.1.2\modifiers.txt = bot.v3.1.2\modifiers.txt + bot.v3.1.2\questinfo.txt = bot.v3.1.2\questinfo.txt + bot.v3.1.2\README = bot.v3.1.2\README + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "irpg", "irpg", "{440DEC4B-57C9-4361-9949-7CF55F9CFED8}" + ProjectSection(SolutionItems) = preProject + irpg\admincomms.txt = irpg\admincomms.txt + irpg\ChangeLog = irpg\ChangeLog + irpg\commonfunctions.php = irpg\commonfunctions.php + irpg\config.php = irpg\config.php + irpg\contact.php = irpg\contact.php + irpg\db.php = irpg\db.php + irpg\down.png = irpg\down.png + irpg\dump.php = irpg\dump.php + irpg\footer.php = irpg\footer.php + irpg\g7.css = irpg\g7.css + irpg\head.png = irpg\head.png + irpg\header.php = irpg\header.php + irpg\hits.db = irpg\hits.db + irpg\idlerpg-adv.txt = irpg\idlerpg-adv.txt + irpg\index.php = irpg\index.php + irpg\makemap.php = irpg\makemap.php + irpg\makequestmap.php = irpg\makequestmap.php + irpg\makeworldmap.php = irpg\makeworldmap.php + irpg\maperror.png = irpg\maperror.png + irpg\newmap.png = irpg\newmap.png + irpg\players.php = irpg\players.php + irpg\playerview.php = irpg\playerview.php + irpg\quest.php = irpg\quest.php + irpg\README = irpg\README + irpg\tablegrad.gif = irpg\tablegrad.gif + irpg\up.png = irpg\up.png + irpg\worldmap.php = irpg\worldmap.php + irpg\xml.php = irpg\xml.php + EndProjectSection +EndProject +Global + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8C959837-E00F-4F71-9966-96BB12DA3FC4} + EndGlobalSection +EndGlobal diff --git a/bot.v3.1.2/.irpg.conf b/bot.v3.1.2/.irpg.conf new file mode 100644 index 0000000..b8a4d6d --- /dev/null +++ b/bot.v3.1.2/.irpg.conf @@ -0,0 +1,241 @@ +# Configuration file for IRPG bot. Prefix comments with a #. Line must start +# with a # to be a comment (no leading spaces and no comments starting in the +# middle of a line). +# +# If you don't personally know your admins, or you're just not the trusting +# type, you may want to look at the ownerpevalonly, owneraddonly, and +# ownerdelonly options. ownerpevalonly prevents non-owner accounts from using +# the PEVAL command, which can allow admins to execute arbitrary code under the +# username that the bot runs as. owneraddonly prevents non-owner accounts from +# assigning admin status to users. ownerdelonly prevents non-owner accounts from +# removing admin status from users +# +# 'disablepeval' option was renamed to 'ownerpevalonly' +# +# Command line options override options in this file. + +# remove or comment out this line so the bot knows that you edited the config +# file +die + +# local hostname or address to bind to. leave blank or comment out if you don't +# want to use a vhost +#localaddr myvhost.domain.com + +# server name:port, enter as many as you like +server miami.fl.us.irc-network.org:6667 +server dallas.tx.us.irc-network.org:7000 +server brussels.be.eu.irc-network.org:6660 + +# bot's nickname +botnick bot + +# bot's username +botuser bot + +# real name field +botrlnm http://www.slashnet.org/~bot/ + +# channel name (followed by key, if your channel uses a key +botchan #irpg s3cr3t.p4ss + +# (identify) command to send upon successful connect. if using a privmsg +# command, you must begin the text of the message with a ":" -- see below +botident PRIVMSG NickServ :identify ilovedink + +# modes to set bot upon successful connect +botmodes +ix + +# command to send upon joining channel. %botnick% will evaluate to the bot's +# current nickname, so you don't have to worry about opping the wrong person. if +# using a privmsg command, you must begin the text of the message with a ":" -- +# see below +botopcmd PRIVMSG ChanServ :op #idlerpg %botnick% + +# command sent to recover nick if bot's primary nickname is in use if using a +# privmsg command, you must begin the text of the message with a ":" -- see +# below +botghostcmd PRIVMSG NickServ :ghost bot ilovedink + +# URL to send users to for help +helpurl http://idlerpg.net/ + +# admin commands list (for admin help) +admincommurl http://idlerpg.net/admincomms.txt + +# base time to level up, 600 = 10 minutes +rpbase 600 + +# time to next level = rpbase * (rpstep ** CURRENT_LEVEL) +rpstep 1.16 + +# penalty time = penalty * (rppenstep ** CURRENT_LEVEL) +rppenstep 1.14 + +# player database file +dbfile irpg.db + +# where quests/godsends/calamities are stored +eventsfile events.txt + +# debug mode on/off flag, merely prints what text was received, what queue +# number outgoing text is given, and what text is sent to server (to the +# debug file, no longer to STDOUT) +debug off + +# choose filename to send debug output to. text is appended to this file +# while the bot is in debug mode, in lieu of STDOUT +debugfile debug.txt + +# Use URL-type banning for non-logged-in users that have been on the channel +# less than 90 seconds? +doban on + +# URLs containing these terms will not be banned by the 'http:' +# advertisement ban (if you have it turned on). enter as many as you like +okurl ultrazone.org +okurl idlerpg.net + +# modes of silence. in mode 0, bot sends all privmsgs. in mode 1, only +# chanmsg() is disabled. in mode 2, only privmsg() to non-channels is +# disabled. in mode 3, privmsgs to users and channels are disabled +silentmode 0 + +# write quest info file? all this file does is give outside programs info +# about the active quests, its participants, their positions, and time to +# completion +writequestfile on + +# filename for the above-mentioned file +questfilename questinfo.txt + +# voice users on login (and register)? if you like, you can set your channel +# +m, then +v clients as they login, cutting down on spam. however, if your +# users generally bring in a second client to chat with, that client won't +# be able to speak in the channel +voiceonlogin on + +# disallow usernames and character classes with control codes (bold, color, +# underline, bell, etc)? +noccodes on + +# disallow usernames and character classes that contain "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 +nononp on + +# URL where users can reach the online quest map, if available. if not +# offering a map to users, leave this blank +mapurl http://idlerpg.net/quest.php + +# allow a STATUS command for users? this is a p0 command to view information +# on an irpg user. useful if you don't have a website where users can view +# their stats +statuscmd off + +# filename to write our PID to. leave blank or comment out if pidfile is +# unnecessary to you +pidfile .irpg.pid + +# attempt to reconnect if disconnected? +reconnect on + +# seconds to wait before attempting to reconnect? don't hammer your irc +# network, please; 90+ seconds is suggested +reconnect_wait 120 + +# this is what the bot considers to be an "internal clock" of sorts. some +# examples of where this is used: $freemessages lines of text from the +# message queue are sent every self_clock seconds; every self_clock seconds, +# the players move on the map (self_clock times to simulate movement every +# second); HOGs, calamities, godsends, etc. are given a chance to occur +# every self_clock seconds; and the list goes on. if you have problems with +# the bot flooding off, try increasing this number to 4 or 5. if your bot +# appears to 'lag' because it is queueing too much text, you can set this +# as low as 1. this must be an integral value (no fractions), and it must be a +# factor of 60 (or certain events will not occur, like database rewrites) +self_clock 3 + +# file into which character modifier texts are appended +modsfile modifiers.txt + +# disallow the registration of usernames already existing in a different +# case? ie, jon == Jon == JON +casematters on + +# allow rudimentary netsplit detection, and a) give no penalty and b) log +# them back in upon return? I always suggest to users that they switch to +# the server that the bot is on, but this has been a frequent request, so. +# will pick up quit messages that match /^\S+\.\S+ \S+\.\S+$/. if your +# network does not prefix quit messages with "Quit: " (or something other +# string), then users can cheat this at their whim +detectsplits on + +# time to wait for netsplit users to return? in seconds. 900 = 15 minutes, good +# for large nets +splitwait 900 + +# allow non-admin users some information on the bot, such as the server it +# is connected to and the nicknames of online admins via a p0 INFO command? +allowuserinfo on + +# ignore the new scaling features and use the old method for calculating the +# odds of events occurring? if you have a very large game and were +# comfortable with the speed that HoGs, Godsends, Calamities, etc. were +# occurring, you may want to set this +noscale off + +# allow bot to access http://jotun.ultrazone.org/g7/count.php?new=1 each +# time someone registers a new username? it only takes a second, and I'd +# really like to be able to keep up with the total player count :^) +phonehome on + +# username of the bot's owner. this account cannot be DELADMINed and has access +# to PEVAL even if it is disabled +owner jotun + +# disable the PEVAL command for non-owner accounts? this command allows the +# execution of arbitrary Perl code by bot admins, effectively giving them +# complete control of the account under which the bot runs. I prefer to leave +# this command available and choose my admins with care, but, whatever :^) +ownerpevalonly off + +# only owner account can use the MKADMIN command to assign admin status to +# users? +owneraddonly on + +# only owner account can use the DELADMIN command to remove admin status from +# users? +ownerdelonly on + +# check for newer versions each time the bot starts up? this will access the +# URL http://jotun.ultrazone.org/g7/version.php?version=$version and report on +# any updated versions and what features there are/bugs have been fixed +checkupdates on + +# send list of usernames that are automatically logged back in when we restart +# (iff that list is < 1 k)? this should, hopefully, no longer cause the bot to +# flood off. the function that sends text to the server will no longer send more +# than 768 bytes to the server every self_clock seconds. the old function would +# send as much as 5 * 512b or 2.5k each self_clock seconds, and as this was +# usually the feature that caused such a large amount of text to be sent at +# once, if your channel had a large amount of users, this would cause it to +# flood off. so, though I think the bug is fixed, I offer the option to turn +# this off +senduserlist on + +# limit maximum amount of penalty for one event? this will prevent a user from +# being penalized more than seconds for one event: part, privmsg, notice, +# kick, etc. set to 0 if you want to disable limiting. +# 604800 == 7 * 86400 == 1 week +limitpen 604800 + +# if you would like a custom-sized map, define the width of your map here: +mapx 500 + +# if you would like a custom-sized map, define the length of your map here: +mapy 500 + +# specify modes / line. the bot will override this from what it grabs from the +# server's 005 numeric, though, if anything. used only for auto-login voicing +modesperline 3 diff --git a/bot.v3.1.2/ChangeLog.txt b/bot.v3.1.2/ChangeLog.txt new file mode 100644 index 0000000..2985918 --- /dev/null +++ b/bot.v3.1.2/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/bot.v3.1.2/README b/bot.v3.1.2/README new file mode 100644 index 0000000..b07a016 --- /dev/null +++ b/bot.v3.1.2/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/bot.v3.1.2.pl b/bot.v3.1.2/bot.v3.1.2.pl new file mode 100644 index 0000000..11d0bb1 --- /dev/null +++ b/bot.v3.1.2/bot.v3.1.2.pl @@ -0,0 +1,2352 @@ +#!/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 + 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/bot.v3.1.2/events.txt b/bot.v3.1.2/events.txt new file mode 100644 index 0000000..becb890 --- /dev/null +++ b/bot.v3.1.2/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/bot.v3.1.2/irpgdbtool b/bot.v3.1.2/irpgdbtool new file mode 100644 index 0000000..c339a6c --- /dev/null +++ b/bot.v3.1.2/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; +} diff --git a/bot.v3.1.2/modifiers.txt b/bot.v3.1.2/modifiers.txt new file mode 100644 index 0000000..e69de29 diff --git a/bot.v3.1.2/questinfo.txt b/bot.v3.1.2/questinfo.txt new file mode 100644 index 0000000..e69de29 diff --git a/irpg/ChangeLog b/irpg/ChangeLog new file mode 100644 index 0000000..570b698 --- /dev/null +++ b/irpg/ChangeLog @@ -0,0 +1,108 @@ +This is the ChangeLog for the Idle RPG Website Code. I'm not as religious about +making sure my updates make it here as I am about changes to the IRPG code +making it into that ChangeLog, so there may be changes between versions that you +don't see listed here. + +Also, please note that any use of "incowrect" or other cow-related "typos" are +NOT typos. I like cattle. A lot. Please stop fixing them :^) + +2004-07-17 Jon Honeycutt + * huge modification of the entire source by the original coder, res0 + . res0 has really helped this project through his many + contributions; it wouldn't be where it is today without him. thanks, res0! + * New site layout (valid XHTML 1.1 strict) using CSS + * Cleaned up PHP + * User maps do not display unless visitor clicks 'display' link + * changed fgets() calls to add the optional second parameter (for old PHPs) + * removed incorrect header() in makequestmap() (mikegrb) + * checked to make sure key $_SESSION['time'] was set before attempting to + reference it (HarK0nNeN) + * db.php's table tag now specifies the number of rows and columns to make + drawing faster in browsers that pay attention to it (res0) + * header("Location: ") directives now give full URIs + * made better use of htmlentities() and urlencode()s as relates to display + of usernames and classes + * users on quest and world maps are now clickable, leading to their player + info page (John Nielsen) + +2004-05-31 Jon Honeycutt + * changed players.php to show offline users as gray, this style is in + header.php, patch from Chewie + * changed map-drawing PHP scripts to draw a transparent image which is then + overlayed (using CSS) over the static newmap.png image. changed the map + drawing frequency to 1/user/20s as I think this greatly decreases CPU + usage. changed the maperror.png to something more appropriate. $mapx + and $mapy in config.php describe the dimensions of your map file (Rick) + * fixed quest.php to show proper coordinates; another evil c/p error (Rick) + +2004-05-23 Jon Honeycutt + * changed most everything to make sense with the new database + * edited dump.php to display data in the same format, despite the new + database format. + * added an xml.php page that takes a 'player' argument and displays + information on that user in xml 1.0 format. should allow a better API + as far as compatibility with newer dbs goes + * changed playerview.php to use fgets() to read from modifiers.txt. + hopefully this is much faster + * edited playerview.php to show map and XML data link + * changed the counter in footer.php to strip the leading path information + from $_SERVER['PHP_SELF'] before using it to identify the page + * removed some unused $i variable from players.php + * created quest.php which shows active quest info + * created makequestmap.php which draws a quest map if quest type is 2, + redirects otherwise + * created worldmap.php which shows the active state of the irpg world + * created makeworldmap.php which draws the world map + * created makemap.php which takes a 'player' argument and draws them on the + map + * commonfunctions.php now has functions to sort by isadmin flag and player + alignment + * index.php updated with information on the new game features + * changed titles on several pages to use $irpg_chan instead of hard-coded + #G7 + * updated header.php to link to new pages and idlerpg.net forum + * changed 'time modifiers' to 'character modifiers' as item modifiers are + now stored, also + +2004-05-23 Alexander Hirzel + * submitted a series of patches to correct, among other things, the horrible + use of where was more appropriate + +2004-04-22 Jon Honeycutt + * if $_GET['alltime'] wasn't set, < and > in time modifiers were not + translated to < and > (thanks DinTx) + * attempted to fix playerview.php from showing others' stats, traded + stristr() for strstr() matching either "^name ", " name ", or "^name's " + +2004-04-23 Jonathan Attwell + * created README, detailing how users are to install and what to edit. + * created config.php, which holds all the setup variables. + * cleaned up some of the php coding. + +2004-04-15 Jon Honeycutt + * we now stripslashes() on $_GET['player'] for players.php, + playerview.php, dump.php (thanks Diab) + +2004-04-05 Jon Honeycutt + * removed some of parallax's tabs :^) + * edited players.php to forward requests for specific players to the proper + playerview.php + * changed links in sitesource.php to be fully-qualified + * removed preg_grep in playerview.php, which insecurely used tainted + data. switched to stristr. (thanks mike@ethernal.org) + * stripped < and > from modifiers.txt output (thanks mike@ethernal.org) + * changed link to idlerpg-adv.txt in players.php to be fully-qualified. + (thanks daxxar) + * may've made other changes, not sure. + +2004-02-15 Jon Honeycutt + * edited source.php to provide fully-qualified URLs to source files; + easier for users to follow license. + * playerview.php's 'View all Time Modifiers' link now displays the + number of time modifiers. + * contact.php now sends a From: field in its headers parameter. + +2004-01-31 Titus Barik + * created ChangeLog, following Debian ChangeLog format. + * modified header.php to be location independent via $BASE_URL var. + * players.php and playerview.php are now separate files. diff --git a/irpg/README b/irpg/README new file mode 100644 index 0000000..ff5b259 --- /dev/null +++ b/irpg/README @@ -0,0 +1,42 @@ +------------------------------------------ +IRPG Website Code README v1.1 (2004-06-27) +------------------------------------------ + +----------- + Notes +----------- + +Homepage: http://idlerpg.net +License : Public Domain + +----------- + Versions +----------- + +Site: v0.5 [current/changes courtesy of res0], Released July 17th, 2004. +Bot : v3.1.2 [current], Released June 6th, 2004. + +------------ +Installation +------------ + +1. Make sure you have the bot functional and running. +2. Copy all the files here to your public_html or some folder. +3. Edit config.php with your favorite editor. +4. chmod 644 * && chmod 666 hits.db +5. If you change the default settings in the IRPG bot (for example, if you turn + off the option to write quest info to file, you'll have to manually edit + some scripts to take this into account. If you disable the INFO command, + you might want to remove it from the index.php page. +6. Some code in this package requires that your system have GD 2.0+ (or have it + enabled in your php.ini, on Win32). If you don't want this functionality to + be available, edit the script playerview.php to remove the use of the map + and header.php to remove the links to the world map and the quest info page. + You can also delete the worldmap.php, makeworldmap.php, makemap.php, + quest.php, and makequestmap.php scripts. +7. Edit the website ANY WAY you see fit. You don't have to keep all of the links + to me, I just thought they might be useful or interesting to users :^) +8. The best way to receive support for these scripts is to post to the Help + section of the forum, http://idlerpg.net/forum.php, not via e-mail or IRC. + If you notice a bug, however, you may use any method to contact me. + Thanks :^) diff --git a/irpg/admincomms.txt b/irpg/admincomms.txt new file mode 100644 index 0000000..1f9e5f2 --- /dev/null +++ b/irpg/admincomms.txt @@ -0,0 +1,65 @@ +This is not the full list of commands for the Idle RPG bot, but only the list +of admin commands. For more information on the Idle RPG bot, visit +http://idlerpg.net/ + +INFO, retrieve some fairly useless stats about the bot. + +DIE, kills the bot. + +HOG, summon the Hand of God spell. See the main help file. + +RESTART, restarts the bot. + +CHPASS , change a character's pass in the IRPG. + +CHCLASS , change a character's class in the IRPG. + +CHUSER , change a character's username in the IRPG. + Please only use in very special circumstances; otherwise, have them form + a new player and DEL the old one. This should not let you overwrite an + existing account, but is untested. + +PUSH , push a player toward his goal by subtracting time + from his next time to level. Please use this only if bot has mistakenly + penalized someone. You could also use this to punish a user by setting + the number of seconds to a negative number. Don't do that. + +DEL , remove a user's account. + +JUMP , move the bot to another server. + +SILENT , switch bot between 4 modes of silence. + - mode 0, bot sends all privmsgs. + - mode 1, only chanmsg() is disabled. + - mode 2, only privmsg()/notice() to non-channels is disabled. + - mode 3, privmsgs/notices to users and channels are disabled. + +BACKUP, tell bot to copy $opts{'dbfile'} to .dbbackup/$opts{'dbfile'}TIMESTAMP + +RELOADDB, force bot to reload player database file, rewriting all memory. + RELOADDB can only be used while in pause mode. + +PAUSE, toggle pause mode. + +PEVAL , execute arbitrary argument as Perl code. Queues output > 3 lines + or >1k of text. Some useful PEVAL commands: + - Delete all accounts not logged in in 4 weeks (See also: DELOLD): + /msg bot PEVAL delete $rps{$_} for grep { time()-$rps{$_}{lastlogin} > 3600*24*7*4 && !$rps{$_}{online} } keys %rps; + - Remove one hour from everyone's clocks: + /msg bot PEVAL $rps{$_}{next} -= 3600 for keys %rps; + - List all online users, separated by commas: + /msg bot PEVAL join(', ',grep { $rps{$_}{online} } keys %rps); + - View contents of a file on remote host: + /msg bot peval `cat file` + - Turn on debug mode: + /msg bot peval $opts{debug}=1; + - Force write-out of database: + /msg bot peval writedb(); + +DELOLD , remove all non-logged-in accounts inactive in the last + days. + +CLEARQ, clear the outgoing message queue. Useful to use if someone floods the + bot with a lot of text that it plans to respond to. + +MKADMIN , set the isadmin flag for a given username. diff --git a/irpg/commonfunctions.php b/irpg/commonfunctions.php new file mode 100644 index 0000000..13a72fe --- /dev/null +++ b/irpg/commonfunctions.php @@ -0,0 +1,94 @@ + $level2) ? -1 : 1; + } + function cmp_alignment_asc($a,$b) { return cmp_alignment_desc($b,$a); } + function cmp_alignment_desc($a,$b) { + list(,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,$a1)=explode("\t",trim($a)); + list(,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,$a2)=explode("\t",trim($b)); + if ($a1 == "g" || $a2 == "e") return -1; + if ($a1 == "e" || $a2 == "g") return 1; + return 0; + } + function cmp_isadmin_asc($a,$b) { return cmp_isadmin_desc($b,$a); } + function cmp_isadmin_desc($a,$b) { + list(,,$o1)=explode("\t",trim($a)); + list(,,$o2)=explode("\t",trim($b)); + return ($o1 > $o2) ? -1 : 1; + } + function cmp_ttl_asc($a,$b) { return cmp_ttl_desc($b,$a); } + function cmp_ttl_desc($a,$b) { + list(,,,,,$time1)=explode("\t",trim($a)); + list(,,,,,$time2)=explode("\t",trim($b)); + return ($time2 < $time1) ? -1 : 1; + } + function cmp_user_asc($a,$b) { return cmp_user_desc($b,$a); } + function cmp_user_desc($a,$b) { + list($u1)=explode("\t",trim($a)); + list($u2)=explode("\t",trim($b)); + return (strtolower($u1) > strtolower($u2)) ? -1 : 1; + } + function cmp_online_asc($a,$b) { return cmp_online_desc($b,$a); } + function cmp_online_desc($a,$b) { + list(,,,,,,,,$o1)=explode("\t",trim($a)); + list(,,,,,,,,$o2)=explode("\t",trim($b)); + return ($o1 > $o2) ? -1 : 1; + } + function cmp_idled_asc($a,$b) { return cmp_idled_desc($b,$a); } + function cmp_idled_desc($a,$b) { + list(,,,,,,,,,$i1)=explode("\t",trim($a)); + list(,,,,,,,,,$i2)=explode("\t",trim($b)); + return ($i1 > $i2) ? -1 : 1; + } + function cmp_created_asc($a,$b) { return cmp_created_desc($b,$a); } + function cmp_created_desc($a,$b) { + list(,,,,,,,,,,,,,,,,,,,$i1)=explode("\t",trim($a)); + list(,,,,,,,,,,,,,,,,,,,$i2)=explode("\t",trim($b)); + return ($i1 > $i2) ? -1 : 1; + } + function cmp_lastlogin_asc($a,$b) { return cmp_lastlogin_desc($b,$a); } + function cmp_lastlogin_desc($a,$b) { + list(,,,,,,,,,,,,,,,,,,,,$i1)=explode("\t",trim($a)); + list(,,,,,,,,,,,,,,,,,,,,$i2)=explode("\t",trim($b)); + return ($i1 > $i2) ? -1 : 1; + } + function cmp_uhost_asc($a,$b) { return cmp_uhost_desc($b,$a); } + function cmp_uhost_desc($a,$b) { + list(,,,,,,,$u1)=explode("\t",trim($a)); + list(,,,,,,,$u2)=explode("\t",trim($b)); + return (strtolower($u1) > strtolower($u2)) ? -1 : 1; + } + function cmp_pen_asc($a,$b) { return cmp_pen_desc($b,$a); } + function cmp_pen_desc($a,$b) { + list(,,,,,,,,,,,,$p1[0],$p1[1],$p1[2],$p1[3],$p1[4],$p1[5], + $p1[6])=explode("\t",trim($a)); + list(,,,,,,,,,,,,$p2[0],$p2[1],$p2[2],$p2[3],$p2[4],$p2[5], + $p2[6])=explode("\t",trim($b)); + $s1 = $s2 = 0; + foreach ($p1 as $pen) $s1 += $pen; + foreach ($p2 as $pen) $s2 += $pen; + return ($s1 > $s2) ? -1 : 1; + } + function cmp_sum_asc($a,$b) { return cmp_sum_desc($b,$a); } + function cmp_sum_desc($a,$b) { + list(,,,,,,,,,,,,,,,,,,,,,$i1[0],$i1[1],$i1[2],$i1[3],$i1[4],$i1[5], + $i1[6],$i1[7],$i1[8],$i1[9])=explode("\t",trim($a)); + list(,,,,,,,,,,,,,,,,,,,,,$i2[0],$i2[1],$i2[2],$i2[3],$i2[4],$i2[5], + $i2[6],$i2[7],$i2[8],$i2[9])=explode("\t",trim($b)); + $s1 = $s2 = 0; + foreach ($i1 as $item) { $s1 += $item; } + foreach ($i2 as $item) $s2 += $item; + return ($s1 > $s2) ? -1 : 1; + } +?> diff --git a/irpg/config.php b/irpg/config.php new file mode 100644 index 0000000..10e17f0 --- /dev/null +++ b/irpg/config.php @@ -0,0 +1,39 @@ + diff --git a/irpg/contact.php b/irpg/contact.php new file mode 100644 index 0000000..bc29914 --- /dev/null +++ b/irpg/contact.php @@ -0,0 +1,47 @@ +Contact"; + if ($_POST['from'] && $_POST['text']) { + mail($admin_email,"IRPG: ".$_POST['from'], + "Name: ".$_POST['name']."\nE-mail: ".$_POST['from']."\n\n". + $_POST['text'],"From: ".$_POST['from']."\r\n"); + echo('
Thanks for your submission.
'); + } + else { + echo(' +
+ + + + + + + + + + + + + + + +
: + +
: + +
+
+
+ +
+
+'); + } + include("footer.php"); +?> diff --git a/irpg/db.php b/irpg/db.php new file mode 100644 index 0000000..78ba930 --- /dev/null +++ b/irpg/db.php @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +",">",$class); + $user = str_replace(">",">",$user); + $sum = 0; + foreach ($item as $k => $v) $sum += $v; + $pentot = 0; + foreach ($pen as $k => $v) $pentot += $v; + echo " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n". + " \n"; + } + + echo(' +
+ User + ( + + + / + + + ) + + Level + ( + + + / + + + ) + + Admin + ( + + + / + + + ) + Class + TTL + ( + + + / + + + ) + + Nick!User@Host + ( + + + / + + + ) + + Online + ( + + + / + + + ) + + Total Time Idled + ( + + + / + + + ) + X PosY PosMesg Pen.Nick Pen.Part Pen.Kick Pen.Quit Pen.Quest Pen.LOGOUT Pen. + Total Pen. + ( + + + / + + + ) + + Acct. Created + ( + + + / + + + ) + + Last Login + ( + + + / + + + ) + AmuletCharmHelmBootsGlovesRingLeggingsShieldTunicWeapon + Sum + ( + + + / + + + ) + + Alignment + ( + + + / + + + ) +
$user$level".($isadmin?"Yes":"No")."$class".duration($secs)."$uhost".(($online == 1) ? "Yes" : "No")."".duration($idled)."$x$y".duration($pen['mesg'])."".duration($pen['nick'])."".duration($pen['part'])."".duration($pen['kick'])."".duration($pen['quit'])."".duration($pen['quest'])."".duration($pen['logout'])."".duration($pentot)."".date("D M j H:i:s Y",$created)."".date("D M j H:i:s Y",$lastlogin)."".$item['amulet']."".$item['charm']."".$item['helm']."".$item['boots']."".$item['gloves']."".$item['ring']."".$item['leggings']."".$item['shield']."".$item['tunic']."".$item['weapon']."$sum".($alignment=='e'?"Evil":($alignment=='n'?"Neutral":"Good"))."
+

+ * Accounts created before Aug 29, 2003 may have incowrect data fields. + '); + include("footer.php"); +?> diff --git a/irpg/down.png b/irpg/down.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a249d956087ad03e9ece01d1fc91ab1a422fda GIT binary patch literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xkYHHq`AGms$$GjthE&{2{!!Dw*w-a3(IY9L iW@#W}ao~f13KPQ>CEl4QcN7MLG diff --git a/irpg/footer.php b/irpg/footer.php new file mode 100644 index 0000000..c7a33bb --- /dev/null +++ b/irpg/footer.php @@ -0,0 +1,35 @@ + + + + diff --git a/irpg/g7.css b/irpg/g7.css new file mode 100644 index 0000000..8dc6836 --- /dev/null +++ b/irpg/g7.css @@ -0,0 +1,111 @@ +body { + font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; + background-color: #FFFFF0; + color: black; + margin: 10px; + text-align: justify; +} + +h1 { margin-bottom: 0px; margin-top: 0px; } +h2 { margin-bottom: 0px; } + +p.small { margin: 0px; font-size: smaller; } + +a { text-decoration: none; color: #C69500; } +a:hover { text-decoration: underline; color: black; } + +div.head { + border: 1px solid #c0c0c0; + text-align: left; + width: 100%; + margin-bottom: 10px; + background: #FFFFFF; +} + +div.menu { + width: 120px; + position: relative; + float: left; + border: 1px solid #c0c0c0; + background: #FFFFFF; +} +div#menu a.home { color: #000000; background: #c0c0c0; border: 1px solid #000000; } +div#menu a.home:hover { background: #c0c0c0; border: 1px solid #000000; } +div#menu a { + text-align: center; + width: 118px; + margin: 0px; + display: block; + border: 1px solid #FFFFFF +} + +div#menu a:hover { border: 1px solid #000000; background: #c0c0c0; text-decoration: none; } + +div.content { + border: 1px solid #c0c0c0; + margin-left: 130px; + margin-top: 0px; + padding: 10px; + background: #FFFFFF; +} + +div.footer { + border: 1px solid #c0c0c0; + margin-left: 130px; + margin-top: 10px; + padding: 10px; + background: #FFFFFF; +} + + +table.uniques { + border: 1px solid #c0c0c0; + padding: 5px; + text-align: left; +} +table.uniques td { + padding-left: 10px; +} + +table.penalty { + border: 1px solid #c0c0c0; + padding: 5px; + text-align: left; +} +table.penalty th { + text-align: right; +} +table.penalty td { + +} + +/* Styles for the userlist */ +li.online { font-weight: bold; } +li.offline { color: #c0c0c0; } +a.offline { color: #707070; } +#map { + width: 500px; + height: 500px; + background-image: url(newmap.png); +} + + +table.forum { + border: 1px solid #c0c0c0; + table-layout: fixed; + overflow: auto; +} + +table.forum td,tr,caption,thead,tfoot,th { + padding-left: 10px; + padding-right: 10px; +} +.tdblue { background-color: #ffffdf; } +.tdgray { background-color: #eeeee0; } +.tdred { + border: 1px solid red; + background-color: #FFCCCC; +} +.smallest { + font-size: 11px; +} diff --git a/irpg/head.png b/irpg/head.png new file mode 100644 index 0000000000000000000000000000000000000000..5d6a7c3bc772f4e824a65025c9edd23fac164c96 GIT binary patch literal 13741 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 literal 0 HcmV?d00001 diff --git a/irpg/header.php b/irpg/header.php new file mode 100644 index 0000000..8b05113 --- /dev/null +++ b/irpg/header.php @@ -0,0 +1,47 @@ + + + + + <?echo $irpg_chan;?> Idle RPG: <?echo $irpg_page_title;?> + + + + + + + +
+'); +?> +
+ + + +
diff --git a/irpg/hits.db b/irpg/hits.db new file mode 100644 index 0000000..f05175c --- /dev/null +++ b/irpg/hits.db @@ -0,0 +1,16 @@ +contact.php 3992 Apr 22, 2003 +db.php 5869 Jun 13, 2003 +index.php 66845 Apr 22, 2003 +playerview.php 144029 Apr 5, 2004 +players.php 51366 Apr 5, 2004 +source.php 13036 Apr 5, 2004 +sitesource.php 7956 Apr 5, 2004 +others.php 13649 Apr 5, 2004 +donate.php 1333 Apr 5, 2004 +donated.php 4 Apr 6, 2004 +forum.php 1799 Jul 13, 2004 +quest.php 242 Jul 13, 2004 +worldmap.php 235 Jul 13, 2004 +1 Jul 15, 2004 +g7.css 1 Jul 15, 2004 +idlerpg.png 1 Jul 15, 2004 diff --git a/irpg/idlerpg-adv.txt b/irpg/idlerpg-adv.txt new file mode 100644 index 0000000..f6bf0f0 --- /dev/null +++ b/irpg/idlerpg-adv.txt @@ -0,0 +1,142 @@ +#!/usr/bin/perl -w +# idlerpg-adv (11-22-2003) by daxxar (http://mental.mine.nu) +# Usage: ./idlerpg-adv.pl [playernames] +# +# Call this script from the command line, or your login profile. +use strict; +use LWP::Simple; + +# Use cookies: +# %user - username, %class - class, %level - level, +# %next - time to next level, %status - online status, +# %uhost - nick!ident@host, %ca - created at, +# %llo - last logged on, %ti - total idletime, +# %items - list of items, %penalties - list of penalties (special; multiline) +# +# Each comma-separated element is printed with a newline at the end :) + +my @string = ( + '[User] %user', '[Class] %class', '[Level] %level', '[Next level] %next', + '[Status] %status', '[User@host] %uhost', '[Created at] %ca', + '[Last logged on] %llo', '[Total idle] %ti', '[Items] %items', + '[Penalties] %penalties' + ); + +### No need to change below ### +# For printing things in a columnized view +# print_col(\@list_of_entries, \@value_of_entries, $number_per_line) +sub make_col { + my $entryname = shift; + my $entryvalue = shift; + my $count = shift; + my @len; + my $ret; + # Find maximum length for each of the $count columns + for my $x (0 .. $#{$entryvalue}) { + my $col = $x % $count; + if (!defined($len[$col]) || $len[$col] < length($entryvalue->[$x] . $entryname->[$x])) { + $len[$col] = length("$entryvalue->[$x]"."$entryname->[$x]"); + } + } + for my $t (0 .. $#$entryvalue) { + if (!($t % $count)) { $ret .= "\n "; } + $ret .= "$entryname->[$t]\($entryvalue->[$t]\)"; + $ret .= ' ' x ($len[$t % $count] - length($entryname->[$t] . $entryvalue->[$t]) + 1); + } + return $ret; +} +sub time_to { + my @timeunits = ('yr', 'month', 'week', 'day', 'hr', 'min', 'sec'); + my @timecalc = (31104000, 2592000, 604800, 86400, 3600, 60, 1); + my $seconds = shift; my $output; + if ($seconds == 0) { return "0 seconds"; } + for my $x (0 .. $#timecalc) { + my $y = int($seconds / $timecalc[$x]); + if ($y != 0 && $seconds != 0 && $seconds >= $timecalc[$x]) { + $seconds = ($seconds % $timecalc[$x]); + $output .= "$y $timeunits[$x]"; + $output .= 's' unless $y == 1; + if ($seconds == 0) { last; } + $output .= ', ' if $x < $#timecalc - 1 && ($seconds % $timecalc[$x+1]) && $seconds != 0; + $output .= ' and ' if !($seconds % $timecalc[$x+1]); + } + } + return $output; +} +sub time_from { + my $seconds = shift; + my ($sec, $min, $hr, $day, $mo, $yr, $wday) = localtime($seconds); + $mo = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Oct', 'Sep', 'Nov', 'Dec')[$mo]; + $wday = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat')[$wday]; + $yr += 1900; + if ($hr < 10) { $hr = "0$hr"; } + if ($min < 10) { $min = "0$min"; } + if ($sec < 10) { $sec = "0$sec"; } + if ($day < 10) { $day .= ' '; } + return "$wday $mo $day $hr:$min:$sec $yr"; +} +die "Usage: $0 \n" . + "Example: $0 daxxar cyb\n" if @ARGV == 0; + +start: +my $username = shift(@ARGV); +my $page = get "http://jotun.ultrazone.org/g7/dump.php?player=$username"; + +# Only line is commented if there is no such user +if ($page =~ /^#[^\n]+$/) { print "$username: no such user\n"; exit 1; } +($page) = ($page =~ /\n([^#].*)/); # Remove the comment + +# @ent = entries on page, \t separated. +my @ent = split(/\t/, $page); + +# Assign each tab-separated entry to its hash-key +my %values = ( + 'user' => $ent[0], 'level' => $ent[1], + 'class' => $ent[2], 'next' => $ent[3], + 'host' => $ent[4], 'status' => $ent[5], + 'totalidle'=> $ent[6], 'created'=> $ent[14], + 'lastlog' => $ent[15], + 'penaltynames' => [ qw(msg nick part kick quit quest logout) ], + 'penaltytimes' => [ @ent[7 .. 13] ], + 'itemnames' => [ qw(amulet charm helm boots gloves ring leggings shield tunic weapon sum) ], + 'itemlvls' => [ @ent[16 .. 25] ] +); +$values{'next'} = time_to ($values{'next'}); +$values{'totalidle'} = time_to ($values{'totalidle'}); +$values{'lastlog'} = time_from($values{'lastlog'}); +$values{'created'} = time_from($values{'created'}); +if ($values{'status'}) {$values{'status'} = 'Online'} +else {$values{'status'} = 'Offline'} + +foreach my $str (@{[ @string ]}) { + $str =~ s/%user/$values{'user'}/g; + $str =~ s/%class/$values{'class'}/g; + $str =~ s/%level/$values{'level'}/g; + $str =~ s/%next/$values{'next'}/g; + $str =~ s/%status/$values{'status'}/g; + $str =~ s/%uhost/$values{'host'}/g; + $str =~ s/%ca/$values{'created'}/g; + $str =~ s/%llo/$values{'lastlog'}/g; + $str =~ s/%ti/$values{'totalidle'}/g; + if ($str =~ /%penalties/) { + my @penaltyname = qw(msg nick part kick quit quest logout); + my @penaltytime = @ent[7 .. 13]; + for my $t (0 .. $#penaltytime) { $penaltytime[$t] = time_to($penaltytime[$t]); } + my $cols = make_col(\@penaltyname, \@penaltytime, 3); + $str =~ s/%penalties/$cols/g; + } + if ($str =~ /%items/) { + my @itemname = qw(amulet charm helm boots gloves ring leggings shield tunic weapon sum); + my @itemlvls = @ent[16 .. 25]; + # Yay, lets get a nice sum(sum) in output! :D + my $sum; map($sum += $_, @itemlvls); + my $cols = make_col(\@itemname, [ @itemlvls, $sum ], 3); + $str =~ s/%items/$cols/g; + } + $str =~ s///g; + $str =~ s///g; + print "$str\n"; +} + +print "\n" if @ARGV != 0; +goto start if @ARGV != 0; diff --git a/irpg/index.php b/irpg/index.php new file mode 100644 index 0000000..4ec015e --- /dev/null +++ b/irpg/index.php @@ -0,0 +1,480 @@ + +

Game Info

+

The Idle RPG is just what it sounds like: an RPG in which the players + idle. In addition to merely gaining levels, players can find items and + battle other players. However, this is all done for you; you just idle. + There are no set classes; you can name your character anything you like, and + have its class be anything you like, as well.

+ +

Location

+

+ The Idle RPG can be played on the + SlashNET IRC Network in the + channel #G7. See this + link for a list of servers. +

+ + +

Registering

+ +

To register, simply:

+ + + /msg REGISTER <char name> <password> + <char class> + + +

Where 'char name' can be up to 16 chars long, 'password' can be up + to 8 characters, and 'char class' can be up to 30 chars.

+ + +

Logging In

+ +

To login, simply:

+ + + /msg LOGIN <char name> <password> + + +

This is a p0 (see Penalties) command.

+ + +

Logging Out

+ +

To logout, simply:

+ + + /msg LOGOUT + + +

This is a p20 (see Penalties) command.

+ + +

Changing Your Password

+ +

To change your password, simply:

+ + + /msg NEWPASS <new password> + + +

This is a p0 (see Penalties) command.

+

If you have forgotten your password, please use the + INFO command to find an online admin to help you. If your + administrator does not have the INFO command enabled, then just message + an op in the channel. They can probably help you.

+ + +

Removing Your Account

+ +

To remove your account, simply:

+ + + /msg REMOVEME + + +

This is a p0 (see Penalties) command :^)

+ + +

Changing Your Alignment

+ +

To change your alignment, simply:

+ + + /msg ALIGN <good|neutral|evil> + + +

This is a p0 (see Penalties) command.

+

Your alignment can affect certain aspects of the game. You 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.

+

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.

+ + +

Obtaining Bot Info

+ +

To see some simple information on the bot, simply:

+ + + /msg INFO + + +

This is a p0 (see Penalties) command.

+

This command gives info such as to which server the bot is connected + and the nicknames of online bot admins.

+

This command is optional, and may be disabled by your bot admin.

+ + + +

Levelling

+ +

To gain levels, you must only be logged in and idle. The time + between levels is based on your character level, and is calculated + by the formula:

+ + 600*(1.16^YOUR_LEVEL) + +

Where ^ represents the exponentiation operator.

+

Very high levels are calculated differently as of version 3.0. Levels + after level 60 have a next time to level of:

+ + (time to level @ 60) + ((1 day) * (level - 60)) + +

The exponent method code had simply gotten to that point that levels + were taking too long to complete.

+ + +

Checking the Active Quest

+ +

To see the active quest, its users, and its time left to + completion:

+ + + /msg QUEST + + +

This is a p0 (see Penalties) command.

+ + +

Checking Your Online Status

+ +

To see whether you are logged on, simply:

+ + + /msg WHOAMI + + +

This is a p0 (see Penalties) command.

+ + +

Penalties

+ + +

If you do something other than idle, like part, quit, talk in the + channel, change your nick, or notice the channel, you are + penalized. The penalties are time, in seconds, added to your next + time to level and are based on your character level. The formulae + are as follows:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nick change30*(1.14^(YOUR_LEVEL))
Part200*(1.14^(YOUR_LEVEL))
Quit20*(1.14^(YOUR_LEVEL))
LOGOUT command20*(1.14^(YOUR_LEVEL))
Being Kicked250*(1.14^(YOUR_LEVEL))
Channel privmsg[message_length]*(1.14^(YOUR_LEVEL))
Channel notice[message_length]*(1.14^(YOUR_LEVEL))
+
+

So, a level 25 character changing their nick would be penalized + 20*(1.14^25)=793 seconds towards their next level.

+

Penalty shorthand is p[num]. So, a nick change is a p30 event, + parting the channel is a p200 event, and quitting IRC is a p20 event. + Messages and notices are p[length of message in characters].

+ + +

Items

+ +

Each time you level, you find an item. You can find an item as + high as 1.5*YOUR_LEVEL (unless you find a + unique item). There are 10 types of items: rings, + amulets, charms, weapons, helms, tunics, gloves, leggings, + shields, and boots. You can find one of each type. When you find + an item with a level higher than the level of the item you already + have, you toss the old item and start using the new one. As of version + 3.0, there is an optional, p0 STATUS command that your admin may have + enabled, but you cannot see which items you have over IRC (only your + total item sum). You can, however, see which items you have on the web + here.

+ +

As you may guess, you have a higher chance of rolling an item of a + lower value than you do of rolling one of a higher value level. The exact + formula is as follows:

+ + + for each 'number' from 1 to YOUR_LEVEL*1.5
+   you have a 1 / ((1.4)^number) chance to find an + item at this level
+ end for + + +

As for item type, you have an equal chance to roll any type.

+ + + +

Battle

+ +

Each time you level, if your level is less than 25, you have a 25% + chance to challenge someone to combat. If your level is greater than or + equal to 25, you have a 100% chance to challenge someone. A pool of + opponents is chosen of all online players, and one is chosen randomly. If + there are no other online players, you fight no one. However, if you do + challenge someone, this is how the victor is decided:

+ +
    +
  • Your item levels are summed.
  • +
  • Their item levels are summed.
  • +
  • A random number between zero and your sum is taken.
  • +
  • A random number between zero and their sum is taken.
  • +
  • If your roll is larger than theirs, you win.
  • +
+ +

If you win, your time towards your next level is lowered. The amount + that it is lowered is based on your opponent's level. The formula is:

+ + + ((the larger number of (OPPONENT_LEVEL/4) and 7) / 100) * + YOUR_NEXT_TIME_TO_LEVEL + + +

This means that you lose no less than 7% from your next time to level. + If you win, your opponent is not penalized any time, unless you land a + Critical Strike.

+ +

If you lose, you will be penalized time. The penalty is calculated + using the formula:

+ + + ((the larger number of (OPPONENT_LEVEL/7) and 7) / 100) * + YOUR_NEXT_TIME_TO_LEVEL + + +

This means that you gain no less than 7% of your next time to level. + If you lose, your opponent is not awarded any time.

+ +

Battling the IRPG bot is a special case. The bot has an item sum of + 1+[highest item sum of all players]. The percent awarded if you win is a + constant 20%, and the percent penalized if you lose is a constant 10%.

+ +

As of version 3.0, if more than 15% of online players are level 45 or + higher, then a random level 45+ user will battle another random player + every hour. This is to speed up levelling among higher level players.

+ +

Also as of version 3.0, there is a grid system. The grid is a 500 x 500 + area in which players may walk. If you encounter another player on the + grid, you have a 1 / (NUMBER_OF_ONLINE_PLAYERS) chance to battle them. + Battle awards are calculated using the above formulae. More information + on the grid system is available here.

+ +

Also as of version 3.0, a successful battle may result an item being + stolen.

+ + +

Unique Items

+ +

As of v2.1.2, after level 25, you have a chance to roll items + significantly higher than items you would normally find at that level. + These are unique items, and have the following stats:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameItem Level RangeRequired User LevelChance to Roll
Mattt's Omniscience Grand Crown50-7425 or greater1 / 40
Juliet's Glorious Ring of Sparkliness50-7425 or greater1 / 40
Res0's Protectorate Plate Mail75-9930 or greater1 / 40
Dwyn's Storm Magic Amulet100-12435 or greater1 / 40
Jotun's Fury Colossal Sword150-17440 or greater1 / 40
Drdink's Cane of Blind Rage175-20045 or greater1 / 40
Mrquick's Magical Boots of Swiftness250-30048 or greater1 / 40
Jeff's Cluehammer of Doom300-35052 or greater1 / 40
+ +

The Hand of God

+ +

As of v3.0, every online user has a (roughly) 1/20 chance per day + of a "Hand of God" affecting them. A HoG can help or hurt your character + by carrying it between 5 and 75 percent towards or away from its next time + to level. The odds are in your favor, however, with an 80% chance to help + your character, and only a 20% chance of your character being smitten.

+ +

In addition to occurring randomly, admins may summon the HoG at their + whim.

+ + +

Critical Strike

+ +

As of v2.0.4, if a challenger beats his opponent in battle, he has a + 1/35 chance of landing a Critical Strike. If this occurs, his opponent + is penalized time towards his next time to level. This amount is + calculated by the formula:

+ + ((random number from 5 to 25) / 100) * OPPONENT'S_NEXT_TIME_TO_LEVEL + +

Meaning he gains no less than 5% and no more than 25% of his next time + to level.

+ + +

Team Battles

+ +

As of v3.0, every online user has (roughly) 1/4 chance per day of + being involved in a 'team battle.' Team battles pit three online + players against three other online players. Each side's items are summed, + and a winner is chosen as in regular battling. If the first group bests + the second group in combat, 20% of the lowest of the three's TTL is + removed from their clocks. If the first group loses, 20% of their lowest + member's TTL is added to their TTL.

+ + +

Calamities

+ +

As of v3.0, every online user has a (roughly) 1/8 chance per day of a + calamity occurring to them. A calamity is a bit of extremely bad luck that + either:
+ + a) slows a player 5-12% of their next time to level
+ b) lowers one of their item's value by 10% + +

+ + +

Godsends

+ +

As of v3.0, every online user has a (roughly) 1/8 chance per day of a + godsend occurring to them. A godsend is a bit of extremely good luck that + either:
+ + a) accelerates a player 5-12% of their next time to level
+ b) increases one of their item's value by 10% + +

+ + +

Quests

+ +

As of v2.3, there are Quests. Four level 40+ users that have been + online for more than ten hours are chosen to represent and assist the + Realm by going on a quest. If all four 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. As of v3.0, there are two kinds of quests: grid-based quests + and time-based quests. Time-based quests last a random time between 12 and + 24 hours. Grid-based quests are based on the grid + system and do not have a set time to completion. Rather, the questers + must reach certain points on the map for their quest to be complete. If + the quest is not completed, ALL online users are penalized a p15 as + punishment.

+ + +

Grid System

+ +

As of v3.0, the IRPG has a grid system. The grid can be considered + a 500 x 500 point map on which the players may walk. Every second, each + player has an equal chance to step up, down, or neither, and an equal + chance to step left, right, or neither. If a user encounters another + player, there is a 1/(NUMBER_OF_ONLINE_PLAYERS) chance that they will + battle one another. Normal battling rules apply.

+ +

Some quests require that users walk to certain points on the map. In + the spirit of IRPG, of course, the trek is made for you. Your character + will automatically walk in the direction that it is supposed to, although + at a much slower than normal pace (to avoid accidents, of course. you + don't want to fall down and risk a Realm-wide p15!).

+ + +

Item Stealing

+ +

As of v3.0, the IRPG has item stealing. After each battle, if the + challenger wins, he has a slightly less than 2% chance of stealing an + item from the challengee. Only items of a higher value are stolen, and + the challenger's old item is given to the challengee in a moment of pity. +

+ + + +

Credits

+ +

Many thanks to version 3.0's map creators, res0 and Jeb! The game + wouldn't be the same without you.

+

+ The IRPG would not be possible without help from a lot of people. + To jwbozzy, yawnwraith, Tosirap, res0, dwyn, Parallax, protomek, + Bert, clavicle, drdink, jeff, rasher, Sticks, Nerje, Asterax, + emad, inkblot(!), schmolli, mikegrb, mumkin, sean, Minhiriath, + and Dan, I give many thanks. Unfortunately, this list has grown too + large to maintain. More user contributions can be seen in the + ChangeLog. +

+ + + + diff --git a/irpg/makemap.php b/irpg/makemap.php new file mode 100644 index 0000000..d5ace6a --- /dev/null +++ b/irpg/makemap.php @@ -0,0 +1,60 @@ + 500) { + $stringx = $x - ((strlen($user)+1)*$width)-12; + } + if ($y+$height > 500) { + $stringy = $y - ($height/2)-2; + } + $magenta = imageColorAllocate($map,255,0,255); + imageColorTransparent($map,$magenta); + $brown = imagecolorallocate($map, 102, 51, 0); + $parchment = imagecolorallocate($map, 255, 255, 204); + + // Avoid drawing a brown dot on a brown area + $rgb = imageColorAt($map, $x, $y); + if ($rgb > 0) { // $rgb is 0 on our parchment-colored areas + $temp = $brown; + $brown = $parchment; + $parchment = $temp; + } + // YOU ARE HERE + imageFilledEllipse($map, $x, $y, 6, 6, $brown); + // background for text + imageFilledRectangle($map,$stringx+6,$stringy-($height/2),$stringx+6+$width*(strlen($user)+1),$stringy+($height/2),$brown); + // text itself + imageString($map,5,$stringx+7+($width/2),$stringy-($height/2)-1,$user,$parchment); + } + header("Content-type: image/png"); + imagePNG($map); + imageDestroy($map); +?> diff --git a/irpg/makequestmap.php b/irpg/makequestmap.php new file mode 100644 index 0000000..50bf488 --- /dev/null +++ b/irpg/makequestmap.php @@ -0,0 +1,79 @@ + diff --git a/irpg/makeworldmap.php b/irpg/makeworldmap.php new file mode 100644 index 0000000..69011a1 --- /dev/null +++ b/irpg/makeworldmap.php @@ -0,0 +1,26 @@ + diff --git a/irpg/maperror.png b/irpg/maperror.png new file mode 100644 index 0000000000000000000000000000000000000000..11cc12bbad7f7839d63f4c5a14161db8f4051614 GIT binary patch literal 23314 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
v7Dl zQ*#{XUOmrXxx)>}d^ry~k3aAuF&Lzt3(WCg=(JNDM1!~D=nG-) zf%p8`0Pg_i*HSnVNRBOtsibWWWrL0fm=mtj`3{Uo;N&w2`$){$B)B>;r|iICJy$tl zItF@_IZ4ImApeGJVWBopMu(X5I6F?f^493SG30Nx?KwTCZ_mV>C4U|RU0_Zhr1y+J za|C?6#^-Wj#S@_)Me|wSvy84HHF^ZM3o(cACyx2Lo~s<@n=tY`&J~!8P%N!ZEDIKT z(Ox1zw1EdAH;sGyDJWSK4~nEJ^(bJ&tVQLn5TX8_{RYAESeX_=h3aj=n`xdC;G0*5ZBr(q?YA*tvGnykI$18M~dDR|v!h$fDUztP&`~$^J2gh8vfXJnn zV!eSmj-3M;C>4$HLCmj#oTMZ-(32!CkmxpfaFch8ZuE5U>Sq>FG4s!K2z z@{FdEGnDI8YCb1#H6bQDF+VQ^3S%+mY6%~mFb8SLgCq(A8H8R!nrmbV@Me%!U~fX0 z&j`mszs@LNe~24tJr6NwM*W?b<8&%h;vketVow0cadWxnZXH03Wm2lB@w6i;^-Fg6 zYRqBdw@A+sO%8|RA35esav-t{JApfoId(?6ax%&0f#sMhGTAr|8Q(ECA&E|q6vEIv zWx4}@9i+0omJVW$rhuk?$sxMLTYzU{PQiq<)_!0z9Va|48VR>`%%I~uoigzdeHU5X z_(zn?)iYm(d+4lk%rTbr5F>{ObJt3MF`fKW%>7(0F{iO);x6TbE#GS|DmW}O^C^QZ z=6!iIWfn;Pnel=5fzHgJioz}>^j?u>#9}JU1Tt<9q7ORqs6i~<;PoksId`boJS-fQ zAlgsrulOc_P23F_rzz$*2GwPlW5*By5=@CX_kiUr#oRm!0C&ymk7EA+0a$-?(Udf< Q`~Uy|07*qoM6N<$g29>;k^lez literal 0 HcmV?d00001 diff --git a/irpg/newmap.png b/irpg/newmap.png new file mode 100644 index 0000000000000000000000000000000000000000..46f947b362dfa7fbba94ffd1be3da5e4cf17c056 GIT binary patch literal 22216 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 b#QgsOrc#C1Pki*C00000NkvXXu0mjf3l~%` literal 0 HcmV?d00001 diff --git a/irpg/players.php b/irpg/players.php new file mode 100644 index 0000000..106bafb --- /dev/null +++ b/irpg/players.php @@ -0,0 +1,37 @@ + + +

Players

+

Pick a player to view

+

[gray=offline]

+
    +".htmlentities($user). + ", the level $level $class. Next level in $next_level.\n"; + + } +?> +
+

For a script to view player stats from a terminal, try this perl script by + daxxar.

+ +

See player stats in table format.

+ + diff --git a/irpg/playerview.php b/irpg/playerview.php new file mode 100644 index 0000000..ca201f3 --- /dev/null +++ b/irpg/playerview.php @@ -0,0 +1,156 @@ +Player Info"; + $file = fopen($irpg_db,"r"); + fgets($file,1024); // skip top comment + $found=0; + while ($line=fgets($file,1024)) { + if (substr($line,0,strlen($_GET['player'])+1) == $_GET['player']."\t") { + list($user,,$isadmin,$level,$class,$secs,,$uhost,$online,$idled, + $x,$y, + $pen['mesg'], + $pen['nick'], + $pen['part'], + $pen['kick'], + $pen['quit'], + $pen['quest'], + $pen['logout'], + $created, + $lastlogin, + $item['amulet'], + $item['charm'], + $item['helm'], + $item['boots'], + $item['gloves'], + $item['ring'], + $item['leggings'], + $item['shield'], + $item['tunic'], + $item['weapon'], + $alignment, + ) = explode("\t",trim($line)); + $found=1; + break; + } + } + if (!$found) echo "

Error

No such user.

\n"; + else { + $class=htmlentities($class); + /* if we htmlentities($user), then we cannot use links with it. */ + echo "

User: ".htmlentities($user)."
\n". + " Class: $class
\n". + " Admin?: ".($isadmin?"Yes":"No")."
\n". + " Level: $level
\n". + " Next level: ".duration($secs)."
\n". + " Status: O".($online?"n":"ff")."line
\n". + " Host: ".($uhost?$uhost:"Unknown")."
\n". + " Account Created: ".date("D M j H:i:s Y",$created)."
\n". + " Last login: ".date("D M j H:i:s Y",$lastlogin)."
\n". + " Total time idled: ".duration($idled)."
\n". + " Current position: [$x,$y]
\n". + " Alignment: ".($alignment=='e'?"Evil":($alignment=='n'?"Neutral":"Good"))."
\n". + " XML: [link]

\n". + "

Map

\n". + " ".($showmap?"
\n\n":"

Show map

\n\n")."". + "

Items

\n

"; + ksort($item); + $sum = 0; + foreach ($item as $key => $val) { + $uniquecolor="#be9256"; + if ($key == "helm" && substr($val,-1,1) == "a") { + $val = intval($val)." [Mattt's Omniscience Grand Crown]"; + } + if ($key == "tunic" && substr($val,-1,1) == "b") { + $val = intval($val)." [Res0's Protectorate Plate Mail]"; + } + if ($key == "amulet" && substr($val,-1,1) == "c") { + $val = intval($val)." [Dwyn's Storm Magic Amulet]"; + } + if ($key == "weapon" && substr($val,-1,1) == "d") { + $val = intval($val)." [Jotun's Fury Colossal Sword]"; + } + if ($key == "weapon" && substr($val,-1,1) == "e") { + $val = intval($val)." [Drdink's Cane of Blind Rage]"; + } + if ($key == "boots" && substr($val,-1,1) == "f") { + $val = intval($val)." [Mrquick's Magical Boots of Swiftness]"; + } + if ($key == "weapon" && substr($val,-1,1) == "g") { + $val = intval($val)." [Jeff's Cluehammer of Doom]"; + } + if ($key == "ring" && substr($val,-1,1) == "h") { + $val = intval($val)." [Juliet's Glorious Ring of Sparkliness]"; + } + echo " $key: $val
\n"; + $sum += $val; + } + echo "
\n sum: $sum
\n". + "

". + "

Penalties

\n". + "

\n"; + + ksort($pen); + $sum = 0; + foreach ($pen as $key => $val) { + echo " $key: ".duration($val)."
\n"; + $sum += $val; + } + echo "
\n total: ".duration($sum)."

\n"; + + $file = fopen($irpg_mod,"r"); + $temp = array(); + while ($line=fgets($file,1024)) { + if (strstr($line," ".$_GET['player']." ") || + strstr($line," ".$_GET['player'].", ") || + substr($line,0,strlen($_GET['player'])+1) == + $_GET['player']." " || + substr($line,0,strlen($_GET['player'])+3) == + $_GET['player']."'s ") { + array_push($temp,$line); + } + } + fclose($file); + if (!is_null($temp) && count($temp)) { + echo('

'); + echo $_GET['allmods']!=1?"Recent ":""; + echo('Character Modifiers

'); + if ($_GET['allmods'] == 1 || count($temp) < 6) { + foreach ($temp as $line) { + $line=htmlentities(trim($line)); + echo " $line
\n"; + } + echo "
\n"; + } + else { + end($temp); + for ($i=0;$i<4;++$i) prev($temp); + for ($line=trim(current($temp));$line;$line=trim(next($temp))) { + $line=htmlentities(trim($line)); + echo " $line
\n"; + } + } + } + if ($_GET['allmods'] != 1 && count($temp) > 5) { +?> +
+ [View all Character Modifiers ()] +

+ + diff --git a/irpg/quest.php b/irpg/quest.php new file mode 100644 index 0000000..e6aab1a --- /dev/null +++ b/irpg/quest.php @@ -0,0 +1,118 @@ +Current Quest\n"; + include("commonfunctions.php"); + $file = fopen($irpg_qfile,"r"); + $type=0; + while ($line=fgets($file,1024)) { + $arg = explode(" ",trim($line)); + if ($arg[0] == "T") { + unset($arg[0]); + $text = implode(" ",$arg); + } + elseif ($arg[0] == "Y") { + $type = $arg[1]; + } + elseif ($arg[0] == "P") { + $p1[0] = $arg[1]; + $p1[1] = $arg[2]; + $p2[0] = $arg[3]; + $p2[1] = $arg[4]; + } + elseif ($arg[0] == "S") { + if ($type == 1) $time = $arg[1]; + elseif ($type == 2) $stage = $arg[1]; + } + elseif ($arg[0] == "P1") { + $player[1]['name'] = $arg[1]; + if ($type == 2) { + $player[1]['x'] = $arg[2]; + $player[1]['y'] = $arg[3]; + } + } + elseif ($arg[0] == "P2") { + $player[2]['name'] = $arg[1]; + if ($type == 2) { + $player[2]['x'] = $arg[2]; + $player[2]['y'] = $arg[3]; + } + } + elseif ($arg[0] == "P3") { + $player[3]['name'] = $arg[1]; + if ($type == 2) { + $player[3]['x'] = $arg[2]; + $player[3]['y'] = $arg[3]; + } + } + elseif ($arg[0] == "P4") { + $player[4]['name'] = $arg[1]; + if ($type == 2) { + $player[4]['x'] = $arg[2]; + $player[4]['y'] = $arg[3]; + } + } + } + if (!$type) { + echo "

Sorry, there is no active quest.

\n"; + } + else { + echo "

Quest: To $text.

\n"; + if ($type == 1) { + echo "

Time to completion: ".duration($time-time()). + "

\n"; + } + elseif ($type == 2) { + if ($stage == 1) { + echo "

Current goal: [$p1[0],$p1[1]]

\n"; + } + else { + echo "

Current goal: [$p2[0],$p2[1]]

>\n"; + } + } + echo "

Participant 1: ".htmlentities($player[1]['name']). + "
\n"; + if ($type == 2) { + echo " Position: [".$player[1]['x'].",".$player[1]['y']."]

\n"; + } + else echo "
\n"; + echo "

Participant 2: ".htmlentities($player[2]['name']). + "
\n"; + if ($type == 2) { + echo " Position: [".$player[2]['x'].",".$player[2]['y']."]

\n"; + } + else echo "
\n"; + echo "

Participant 3: ".htmlentities($player[3]['name']). + "
\n"; + if ($type == 2) { + echo " Position: [".$player[3]['x'].",".$player[3]['y']."]

\n"; + } + else echo "
\n"; + echo "

Participant 4: ".htmlentities($player[4]['name']). + "
\n"; + if ($type == 2) { + echo " Position: [".$player[4]['x'].",".$player[4]['y']."]

\n". + "

Quest Map:

\n". + "

[Questers are shown in blue, current goal in red]

\n". + "
\"Idle
\n". + " \n". + " \"".htmlentities($player[1]['name']).\n". + " \"".htmlentities($player[2]['name']).\n". + " \"".htmlentities($player[3]['name']).\n". + " \"".htmlentities($player[4]['name']).\n". + " \n"; + } + else echo "
\n"; + } + echo "
\n"; + include("footer.php"); +?> diff --git a/irpg/tablegrad.gif b/irpg/tablegrad.gif new file mode 100644 index 0000000000000000000000000000000000000000..71d43cad16faa998b4b39409f98bb516d6c540a7 GIT binary patch literal 829 zcmZ?wbhEHb(*OVf literal 0 HcmV?d00001 diff --git a/irpg/worldmap.php b/irpg/worldmap.php new file mode 100644 index 0000000..33e5b6b --- /dev/null +++ b/irpg/worldmap.php @@ -0,0 +1,27 @@ + + +

World Map

+

[offline users are red, online users are blue]

+ + +
+ IdleRPG World Map + +\n"; + } + fclose($file); +?> + +
+ + diff --git a/irpg/xml.php b/irpg/xml.php new file mode 100644 index 0000000..4fbc158 --- /dev/null +++ b/irpg/xml.php @@ -0,0 +1,82 @@ +"; +?> + + + + + + + + + + + + + + + $val) { + echo " <$key>$val\n"; + $sum += $val; + } + echo " $sum\n"; +?> + + + $val) { + echo " <$key>$val\n"; + $sum += $val; + } + echo " $sum\n"; +?> + +