Fixed NetHack plugin crashing on first load.
[zzz-dywypi.git] / plugins / NetHack / plugin.py
1 ###
2 # Copyright (c) 2010, Alex "Eevee" Munroe
3 # All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are met:
7 #
8 # * Redistributions of source code must retain the above copyright notice,
9 # this list of conditions, and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above copyright notice,
11 # this list of conditions, and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
13 # * Neither the name of the author of this software nor the name of
14 # contributors to this software may be used to endorse or promote products
15 # derived from this software without specific prior written consent.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28
29 ###
30
31 import supybot.utils as utils
32 from supybot.commands import *
33 import supybot.plugins as plugins
34 import supybot.ircutils as ircutils
35 import supybot.ircmsgs as ircmsgs
36 import supybot.callbacks as callbacks
37 import supybot.schedule as schedule
38
39 from glob import glob
40 import os
41 import os.path
42 import re
43
44 # dnum, used in xlogfile
45 dungeons = [
46 'the Dungeons of Doom',
47 'Gehennom',
48 'the Gnomish Mines',
49 'the Quest',
50 'Sokoban',
51 'Fort Ludios',
52 "Vlad's Tower",
53 'the Elemental Planes',
54 ]
55
56 # achievements, used by livelog
57 achievements = [
58 'completed the Quest and obtained the Bell of Opening',
59 'entered Gehennom',
60 'obtained the Candelabrum of Invocation',
61 'obtained the Book of the Dead',
62 'performed the Invocation',
63 'obtained the Amulet of Yendor',
64 'reached the Elemental Planes',
65 'reached the Astral Plane',
66 'ascended to a higher plane of existence',
67 "completed Mine's End",
68 'completed Sokoban',
69 'slew Medusa',
70 ]
71
72 def parse_xlog(line):
73 line = line.strip()
74 data = {}
75 for keyval in line.split(':'):
76 key, val = keyval.split('=', 1)
77 data[key] = val
78
79 ### Extras for us
80 # Original and ending gender/alignment are tracked separately
81 if data['gender0'] == data['gender']:
82 data['gender_delta'] = data['gender']
83 else:
84 data['gender_delta'] = "%(gender0)s->%(gender)s" % data
85
86 if data['align0'] == data['align']:
87 data['align_delta'] = data['align']
88 else:
89 data['align_delta'] = "%(align0)s->%(align)s" % data
90
91 data['level_desc'] = "%s dlvl %s" % (dungeons[ int(data['deathdnum']) ],
92 data['deathlev'])
93 if data['deathlev'] != data['maxlvl']:
94 data['level_desc'] += " (deepest dlvl: %(maxlvl)s)" % data
95
96 # Human-readable time played
97 realtime = int(data['realtime'])
98 time_secs = realtime % 60; realtime //= 60
99 time_mins = realtime % 60; realtime //= 60
100 time_hrs = realtime % 24; realtime //= 24
101 time_days = realtime
102 # Don't need seconds
103 if time_secs >= 30:
104 time_mins += 1
105 # Construct '0d 0h 5m 12s', then lop off the 0x bits
106 data['realtime_pretty'] = re.sub(
107 "^(0. )+",
108 "",
109 "%dd %dh %dm" % (time_days, time_hrs, time_mins)
110 )
111 return data
112
113
114 def parse_livelog(line):
115 line = line.strip()
116 data = {}
117 for keyval in line.split(':'):
118 key, val = keyval.split('=', 1)
119 data[key] = val
120
121 return data
122
123 def livelog_announcement(livelog):
124 # achievement gained
125 if 'achieve_diff' in livelog:
126 # these are stored as 0xABC
127 achieve_diff = int(livelog['achieve_diff'], 16)
128
129 # each item in the achievement list is encoded as that number bit
130 for i, achievement in enumerate(achievements):
131 if achieve_diff & (1 << i):
132 return "{player} just {achievement}, on turn {turns}!".format(
133 achievement=achievement, **livelog)
134
135 # achieve_diff is zero? nothing changed? can't happen, but..
136 return "{0} just accomplished nothing!".format(player)
137
138 # wishes
139 if 'wish' in livelog:
140 return "%(player)s just wished for %(wish)s, on turn %(turns)s." % livelog
141
142 # kill a player ghost
143 if 'bones_killed' in livelog:
144 return "%(player)s just killed the %(bones_monst)s of %(bones_killed)s, " \
145 "the former %(bones_rank)s, on turn %(turns)s on dlvl %(dlev)s." % livelog
146
147 # killed a unique monster
148 # may result in spam for the three horsemen..
149 if 'killed_uniq' in livelog:
150 if livelog['killed_uniq'] == 'Medusa':
151 # Medusa is already an achievement. No need to announce twice
152 return None
153 return "%(player)s has just slain %(killed_uniq)s on turn %(turns)s!" % livelog
154
155 # stole something
156 if 'shoplifted' in livelog:
157 return "%(player)s just stole %(shoplifted)s zorkmids' worth of merchandise " \
158 "from %(shopkeeper)s's %(shop)s, on turn %(turns)s. Tut tut." % livelog
159
160 # default??
161 return "%(player)s just did something-or-other." % livelog
162
163 report_template = "{name} ({role} {race} {gender_delta} {align_delta}): " \
164 "{death} on {level_desc}. {points} points in {turns} turns, " \
165 "wasting {realtime_pretty}. {dumplog}"
166
167
168 CONFIG_PLAYGROUND = '/opt/nethack.veekun.com/nethack/var'
169 CONFIG_USERDATA_FILE = '/opt/nethack.veekun.com/dgldir/userdata'
170 CONFIG_USERDATA_WEB = 'http://nethack.veekun.com/userdata'
171 CONFIG_CHANNEL = '#cafe'
172 class NetHack(callbacks.Plugin):
173 """Add the help for "@plugin help NetHack" here
174 This should describe *how* to use this plugin."""
175 def __init__(self, irc):
176 self.__parent = super(NetHack, self)
177 self.__parent.__init__(irc)
178
179 self.xlog = open(os.path.join(CONFIG_PLAYGROUND, 'xlogfile'))
180 self.livelog = open(os.path.join(CONFIG_PLAYGROUND, 'livelog'))
181 self.xlog.seek(0, os.SEEK_END)
182 self.livelog.seek(0, os.SEEK_END)
183
184 # Remove the event first, in case this is a reload. This will fail if
185 # this is the first load, so throw it in a try
186 try:
187 schedule.removePeriodicEvent('nethack-log-ping')
188 except:
189 pass
190
191 def callback():
192 self._checkLogs(irc)
193 schedule.addPeriodicEvent(callback, 10, name='nethack-log-ping')
194
195 def _checkLogs(self, irc):
196 """Checks the files for new lines and, if there be any, prints them to
197 IRC.
198
199 Actual work is all done here.
200 """
201
202 # Check xlogfile
203 self.xlog.seek(0, os.SEEK_CUR)
204 line = self.xlog.readline()
205 if line:
206 data = parse_xlog(line)
207
208 # Find dumplog
209 dumplog_paths = glob(
210 os.path.join(CONFIG_USERDATA_FILE,
211 data['name'],
212 'dumplog',
213 data['starttime'])
214 + '*'
215 )
216 if dumplog_paths:
217 (_, dumplog_file) = os.path.split(dumplog_paths[0])
218 dumplog_url = '{base}/{name}/dumplog/{file}'.format(
219 base=CONFIG_USERDATA_WEB,
220 name=data['name'],
221 file=dumplog_file,
222 )
223 else:
224 dumplog_url = "Can't find dumplog :("
225
226 report = report_template.format(dumplog=dumplog_url, **data)
227 msg = ircmsgs.privmsg(CONFIG_CHANNEL, report)
228 irc.queueMsg(msg)
229
230 # Check livelog
231 self.livelog.seek(0, os.SEEK_CUR)
232 line = self.livelog.readline()
233 if line:
234 data = parse_livelog(line)
235 report = livelog_announcement(data)
236 if report:
237 msg = ircmsgs.privmsg(CONFIG_CHANNEL, report)
238 irc.queueMsg(msg)
239
240
241 Class = NetHack
242
243
244 # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: