PEP 8 conformance for monolithic library
[mudpy.git] / lib / mudpy / misc.py
1 # -*- coding: utf-8 -*-
2 u"""Miscellaneous functions for the mudpy engine."""
3
4 # Copyright (c) 2004-2011 Jeremy Stanley <fungi@yuggoth.org>. Permission
5 # to use, copy, modify, and distribute this software is granted under
6 # terms provided in the LICENSE file distributed with this software.
7
8
9 class Element:
10
11     u"""An element of the universe."""
12
13     def __init__(self, key, universe, filename=None):
14         u"""Set up a new element."""
15         import data
16         import os.path
17
18         # keep track of our key name
19         self.key = key
20
21         # keep track of what universe it's loading into
22         self.universe = universe
23
24         # clone attributes if this is replacing another element
25         if self.key in self.universe.contents:
26             old_element = self.universe.contents[self.key]
27             for attribute in vars(old_element).keys():
28                 exec(u"self." + attribute + u" = old_element." + attribute)
29             if self.owner:
30                 self.owner.avatar = self
31
32         # i guess this is a new element then
33         else:
34
35             # not owned by a user by default (used for avatars)
36             self.owner = None
37
38             # no contents in here by default
39             self.contents = {}
40
41             # parse out appropriate category and subkey names, add to list
42             if self.key.find(u":") > 0:
43                 self.category, self.subkey = self.key.split(u":", 1)
44             else:
45                 self.category = u"other"
46                 self.subkey = self.key
47             if not self.category in self.universe.categories:
48                 self.category = u"other"
49                 self.subkey = self.key
50
51             # get an appropriate filename for the origin
52             if not filename:
53                 filename = self.universe.default_origins[self.category]
54             if not os.path.isabs(filename):
55                 filename = os.path.abspath(filename)
56
57             # add the file if it doesn't exist yet
58             if not filename in self.universe.files:
59                 data.DataFile(filename, self.universe)
60
61         # record or reset a pointer to the origin file
62         self.origin = self.universe.files[filename]
63
64         # add a data section to the origin if necessary
65         if not self.origin.data.has_section(self.key):
66             self.origin.data.add_section(self.key)
67
68         # add or replace this element in the universe
69         self.universe.contents[self.key] = self
70         self.universe.categories[self.category][self.subkey] = self
71
72     def reload(self):
73         u"""Create a new element and replace this one."""
74         new_element = Element(self.key, self.universe, self.origin.filename)
75         del(self)
76
77     def destroy(self):
78         u"""Remove an element from the universe and destroy it."""
79         self.origin.data.remove_section(self.key)
80         del self.universe.categories[self.category][self.subkey]
81         del self.universe.contents[self.key]
82         del self
83
84     def facets(self):
85         u"""Return a list of non-inherited facets for this element."""
86         if self.key in self.origin.data.sections():
87             return self.origin.data.options(self.key)
88         else:
89             return []
90
91     def has_facet(self, facet):
92         u"""Return whether the non-inherited facet exists."""
93         return facet in self.facets()
94
95     def remove_facet(self, facet):
96         u"""Remove a facet from the element."""
97         if self.has_facet(facet):
98             self.origin.data.remove_option(self.key, facet)
99             self.origin.modified = True
100
101     def ancestry(self):
102         u"""Return a list of the element's inheritance lineage."""
103         if self.has_facet(u"inherit"):
104             ancestry = self.getlist(u"inherit")
105             for parent in ancestry[:]:
106                 ancestors = self.universe.contents[parent].ancestry()
107                 for ancestor in ancestors:
108                     if ancestor not in ancestry:
109                         ancestry.append(ancestor)
110             return ancestry
111         else:
112             return []
113
114     def get(self, facet, default=None):
115         u"""Retrieve values."""
116         if default is None:
117             default = u""
118         if self.origin.data.has_option(self.key, facet):
119             raw_data = self.origin.data.get(self.key, facet)
120             if raw_data.startswith(u"u\"") or raw_data.startswith(u"u'"):
121                 raw_data = raw_data[1:]
122             raw_data.strip(u"\"'")
123             if type(raw_data) == str:
124                 return unicode(raw_data, "utf-8")
125             else:
126                 return raw_data
127         elif self.has_facet(u"inherit"):
128             for ancestor in self.ancestry():
129                 if self.universe.contents[ancestor].has_facet(facet):
130                     return self.universe.contents[ancestor].get(facet)
131         else:
132             return default
133
134     def getboolean(self, facet, default=None):
135         u"""Retrieve values as boolean type."""
136         if default is None:
137             default = False
138         if self.origin.data.has_option(self.key, facet):
139             return self.origin.data.getboolean(self.key, facet)
140         elif self.has_facet(u"inherit"):
141             for ancestor in self.ancestry():
142                 if self.universe.contents[ancestor].has_facet(facet):
143                     return self.universe.contents[ancestor].getboolean(facet)
144         else:
145             return default
146
147     def getint(self, facet, default=None):
148         u"""Return values as int/long type."""
149         if default is None:
150             default = 0
151         if self.origin.data.has_option(self.key, facet):
152             return self.origin.data.getint(self.key, facet)
153         elif self.has_facet(u"inherit"):
154             for ancestor in self.ancestry():
155                 if self.universe.contents[ancestor].has_facet(facet):
156                     return self.universe.contents[ancestor].getint(facet)
157         else:
158             return default
159
160     def getfloat(self, facet, default=None):
161         u"""Return values as float type."""
162         if default is None:
163             default = 0.0
164         if self.origin.data.has_option(self.key, facet):
165             return self.origin.data.getfloat(self.key, facet)
166         elif self.has_facet(u"inherit"):
167             for ancestor in self.ancestry():
168                 if self.universe.contents[ancestor].has_facet(facet):
169                     return self.universe.contents[ancestor].getfloat(facet)
170         else:
171             return default
172
173     def getlist(self, facet, default=None):
174         u"""Return values as list type."""
175         import data
176         if default is None:
177             default = []
178         value = self.get(facet)
179         if value:
180             return data.makelist(value)
181         else:
182             return default
183
184     def getdict(self, facet, default=None):
185         u"""Return values as dict type."""
186         import data
187         if default is None:
188             default = {}
189         value = self.get(facet)
190         if value:
191             return data.makedict(value)
192         else:
193             return default
194
195     def set(self, facet, value):
196         u"""Set values."""
197         if not self.has_facet(facet) or not self.get(facet) == value:
198             if type(value) is long:
199                 value = unicode(value)
200             elif not type(value) is unicode:
201                 value = repr(value)
202             self.origin.data.set(self.key, facet, value)
203             self.origin.modified = True
204
205     def append(self, facet, value):
206         u"""Append value tp a list."""
207         if type(value) is long:
208             value = unicode(value)
209         elif not type(value) is unicode:
210             value = repr(value)
211         newlist = self.getlist(facet)
212         newlist.append(value)
213         self.set(facet, newlist)
214
215     def new_event(self, action, when=None):
216         u"""Create, attach and enqueue an event element."""
217
218         # if when isn't specified, that means now
219         if not when:
220             when = self.universe.get_time()
221
222         # events are elements themselves
223         event = Element(u"event:" + self.key + u":" + counter)
224
225     def send(
226         self,
227         message,
228         eol=u"$(eol)",
229         raw=False,
230         flush=False,
231         add_prompt=True,
232         just_prompt=False,
233         add_terminator=False,
234         prepend_padding=True
235     ):
236         u"""Convenience method to pass messages to an owner."""
237         if self.owner:
238             self.owner.send(
239                 message,
240                 eol,
241                 raw,
242                 flush,
243                 add_prompt,
244                 just_prompt,
245                 add_terminator,
246                 prepend_padding
247             )
248
249     def can_run(self, command):
250         u"""Check if the user can run this command object."""
251
252         # has to be in the commands category
253         if command not in self.universe.categories[u"command"].values():
254             result = False
255
256         # avatars of administrators can run any command
257         elif self.owner and self.owner.account.getboolean(u"administrator"):
258             result = True
259
260         # everyone can run non-administrative commands
261         elif not command.getboolean(u"administrative"):
262             result = True
263
264         # otherwise the command cannot be run by this actor
265         else:
266             result = False
267
268         # pass back the result
269         return result
270
271     def update_location(self):
272         u"""Make sure the location's contents contain this element."""
273         location = self.get(u"location")
274         if location in self.universe.contents:
275             self.universe.contents[location].contents[self.key] = self
276
277     def clean_contents(self):
278         u"""Make sure the element's contents aren't bogus."""
279         for element in self.contents.values():
280             if element.get(u"location") != self.key:
281                 del self.contents[element.key]
282
283     def go_to(self, location):
284         u"""Relocate the element to a specific location."""
285         current = self.get(u"location")
286         if current and self.key in self.universe.contents[current].contents:
287             del universe.contents[current].contents[self.key]
288         if location in self.universe.contents:
289             self.set(u"location", location)
290         self.universe.contents[location].contents[self.key] = self
291         self.look_at(location)
292
293     def go_home(self):
294         u"""Relocate the element to its default location."""
295         self.go_to(self.get(u"default_location"))
296         self.echo_to_location(
297             u"You suddenly realize that " + self.get(u"name") + u" is here."
298         )
299
300     def move_direction(self, direction):
301         u"""Relocate the element in a specified direction."""
302         self.echo_to_location(
303             self.get(
304                 u"name"
305             ) + u" exits " + self.universe.categories[
306                 u"internal"
307             ][
308                 u"directions"
309             ].getdict(
310                 direction
311             )[
312                 u"exit"
313             ] + u"."
314         )
315         self.send(
316             u"You exit " + self.universe.categories[
317                 u"internal"
318             ][
319                 u"directions"
320             ].getdict(
321                 direction
322             )[
323                 u"exit"
324             ] + u".",
325             add_prompt=False
326         )
327         self.go_to(
328             self.universe.contents[
329                 self.get(u"location")].link_neighbor(direction)
330         )
331         self.echo_to_location(
332             self.get(
333                 u"name"
334             ) + u" arrives from " + self.universe.categories[
335                 u"internal"
336             ][
337                 u"directions"
338             ].getdict(
339                 direction
340             )[
341                 u"enter"
342             ] + u"."
343         )
344
345     def look_at(self, key):
346         u"""Show an element to another element."""
347         if self.owner:
348             element = self.universe.contents[key]
349             message = u""
350             name = element.get(u"name")
351             if name:
352                 message += u"$(cyn)" + name + u"$(nrm)$(eol)"
353             description = element.get(u"description")
354             if description:
355                 message += description + u"$(eol)"
356             portal_list = element.portals().keys()
357             if portal_list:
358                 portal_list.sort()
359                 message += u"$(cyn)[ Exits: " + u", ".join(
360                     portal_list
361                 ) + u" ]$(nrm)$(eol)"
362             for element in self.universe.contents[
363                 self.get(u"location")
364             ].contents.values():
365                 if element.getboolean(u"is_actor") and element is not self:
366                     message += u"$(yel)" + element.get(
367                         u"name"
368                     ) + u" is here.$(nrm)$(eol)"
369                 elif element is not self:
370                     message += u"$(grn)" + element.get(
371                         u"impression"
372                     ) + u"$(nrm)$(eol)"
373             self.send(message)
374
375     def portals(self):
376         u"""Map the portal directions for a room to neighbors."""
377         import re
378         portals = {}
379         if re.match(u"""^location:-?\d+,-?\d+,-?\d+$""", self.key):
380             coordinates = [(int(x))
381                            for x in self.key.split(u":")[1].split(u",")]
382             directions = self.universe.categories[u"internal"][u"directions"]
383             offsets = dict(
384                 [
385                     (
386                         x, directions.getdict(x)[u"vector"]
387                     ) for x in directions.facets()
388                 ]
389             )
390             for portal in self.getlist(u"gridlinks"):
391                 adjacent = map(lambda c, o: c + o,
392                                coordinates, offsets[portal])
393                 neighbor = u"location:" + u",".join(
394                     [(unicode(x)) for x in adjacent]
395                 )
396                 if neighbor in self.universe.contents:
397                     portals[portal] = neighbor
398         for facet in self.facets():
399             if facet.startswith(u"link_"):
400                 neighbor = self.get(facet)
401                 if neighbor in self.universe.contents:
402                     portal = facet.split(u"_")[1]
403                     portals[portal] = neighbor
404         return portals
405
406     def link_neighbor(self, direction):
407         u"""Return the element linked in a given direction."""
408         portals = self.portals()
409         if direction in portals:
410             return portals[direction]
411
412     def echo_to_location(self, message):
413         u"""Show a message to other elements in the current location."""
414         for element in self.universe.contents[
415             self.get(u"location")
416         ].contents.values():
417             if element is not self:
418                 element.send(message)
419
420
421 class Universe:
422
423     u"""The universe."""
424
425     def __init__(self, filename=u"", load=False):
426         u"""Initialize the universe."""
427         import os
428         import os.path
429         self.categories = {}
430         self.contents = {}
431         self.default_origins = {}
432         self.loglines = []
433         self.pending_events_long = {}
434         self.pending_events_short = {}
435         self.private_files = []
436         self.reload_flag = False
437         self.startdir = os.getcwd()
438         self.terminate_flag = False
439         self.userlist = []
440         if not filename:
441             possible_filenames = [
442                 u".mudpyrc",
443                 u".mudpy/mudpyrc",
444                 u".mudpy/mudpy.conf",
445                 u"mudpy.conf",
446                 u"etc/mudpy.conf",
447                 u"/usr/local/mudpy/mudpy.conf",
448                 u"/usr/local/mudpy/etc/mudpy.conf",
449                 u"/etc/mudpy/mudpy.conf",
450                 u"/etc/mudpy.conf"
451             ]
452             for filename in possible_filenames:
453                 if os.access(filename, os.R_OK):
454                     break
455         if not os.path.isabs(filename):
456             filename = os.path.join(self.startdir, filename)
457         self.filename = filename
458         if load:
459             self.load()
460
461     def load(self):
462         u"""Load universe data from persistent storage."""
463         import data
464
465         # the files dict must exist and filename needs to be read-only
466         if not hasattr(
467            self, u"files"
468            ) or not (
469             self.filename in self.files and self.files[
470                 self.filename
471             ].is_writeable()
472         ):
473
474             # clear out all read-only files
475             if hasattr(self, u"files"):
476                 for data_filename in self.files.keys():
477                     if not self.files[data_filename].is_writeable():
478                         del self.files[data_filename]
479
480             # start loading from the initial file
481             data.DataFile(self.filename, self)
482
483         # make a list of inactive avatars
484         inactive_avatars = []
485         for account in self.categories[u"account"].values():
486             inactive_avatars += [
487                 (self.contents[x]) for x in account.getlist(u"avatars")
488             ]
489         for user in self.userlist:
490             if user.avatar in inactive_avatars:
491                 inactive_avatars.remove(user.avatar)
492
493         # go through all elements to clear out inactive avatar locations
494         for element in self.contents.values():
495             location = element.get(u"location")
496             if element in inactive_avatars and location:
497                 if location in self.contents and element.key in self.contents[
498                    location
499                    ].contents:
500                     del self.contents[location].contents[element.key]
501                 element.set(u"default_location", location)
502                 element.remove_facet(u"location")
503
504         # another pass to straighten out all the element contents
505         for element in self.contents.values():
506             element.update_location()
507             element.clean_contents()
508
509     def new(self):
510         u"""Create a new, empty Universe (the Big Bang)."""
511         new_universe = Universe()
512         for attribute in vars(self).keys():
513             exec(u"new_universe." + attribute + u" = self." + attribute)
514         new_universe.reload_flag = False
515         del self
516         return new_universe
517
518     def save(self):
519         u"""Save the universe to persistent storage."""
520         for key in self.files:
521             self.files[key].save()
522
523     def initialize_server_socket(self):
524         u"""Create and open the listening socket."""
525         import socket
526
527         # need to know the local address and port number for the listener
528         host = self.categories[u"internal"][u"network"].get(u"host")
529         port = self.categories[u"internal"][u"network"].getint(u"port")
530
531         # if no host was specified, bind to all local addresses (preferring
532         # ipv6)
533         if not host:
534             if socket.has_ipv6:
535                 host = u"::"
536             else:
537                 host = u"0.0.0.0"
538
539         # figure out if this is ipv4 or v6
540         family = socket.getaddrinfo(host, port)[0][0]
541         if family is socket.AF_INET6 and not socket.has_ipv6:
542             log(u"No support for IPv6 address %s (use IPv4 instead)." % host)
543
544         # create a new stream-type socket object
545         self.listening_socket = socket.socket(family, socket.SOCK_STREAM)
546
547         # set the socket options to allow existing open ones to be
548         # reused (fixes a bug where the server can't bind for a minute
549         # when restarting on linux systems)
550         self.listening_socket.setsockopt(
551             socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
552         )
553
554         # bind the socket to to our desired server ipa and port
555         self.listening_socket.bind((host, port))
556
557         # disable blocking so we can proceed whether or not we can
558         # send/receive
559         self.listening_socket.setblocking(0)
560
561         # start listening on the socket
562         self.listening_socket.listen(1)
563
564         # note that we're now ready for user connections
565         log(
566             u"Listening for Telnet connections on: " +
567             host + u":" + unicode(port)
568         )
569
570     def get_time(self):
571         u"""Convenience method to get the elapsed time counter."""
572         return self.categories[u"internal"][u"counters"].getint(u"elapsed")
573
574
575 class User:
576
577     u"""This is a connected user."""
578
579     def __init__(self):
580         u"""Default values for the in-memory user variables."""
581         import telnet
582         self.account = None
583         self.address = u""
584         self.authenticated = False
585         self.avatar = None
586         self.columns = 79
587         self.connection = None
588         self.error = u""
589         self.input_queue = []
590         self.last_address = u""
591         self.last_input = universe.get_time()
592         self.menu_choices = {}
593         self.menu_seen = False
594         self.negotiation_pause = 0
595         self.output_queue = []
596         self.partial_input = ""
597         self.password_tries = 0
598         self.state = u"initial"
599         self.telopts = {}
600
601     def quit(self):
602         u"""Log, close the connection and remove."""
603         if self.account:
604             name = self.account.get(u"name")
605         else:
606             name = u""
607         if name:
608             message = u"User " + name
609         else:
610             message = u"An unnamed user"
611         message += u" logged out."
612         log(message, 2)
613         self.deactivate_avatar()
614         self.connection.close()
615         self.remove()
616
617     def check_idle(self):
618         u"""Warn or disconnect idle users as appropriate."""
619         idletime = universe.get_time() - self.last_input
620         linkdead_dict = universe.categories[u"internal"][u"time"].getdict(
621             u"linkdead"
622         )
623         if self.state in linkdead_dict:
624             linkdead_state = self.state
625         else:
626             linkdead_state = u"default"
627         if idletime > linkdead_dict[linkdead_state]:
628             self.send(
629                 u"$(eol)$(red)You've done nothing for far too long... goodbye!"
630                 + u"$(nrm)$(eol)",
631                 flush=True,
632                 add_prompt=False
633             )
634             logline = u"Disconnecting "
635             if self.account and self.account.get(u"name"):
636                 logline += self.account.get(u"name")
637             else:
638                 logline += u"an unknown user"
639             logline += u" after idling too long in the " + \
640                 self.state + u" state."
641             log(logline, 2)
642             self.state = u"disconnecting"
643             self.menu_seen = False
644         idle_dict = universe.categories[u"internal"][u"time"].getdict(u"idle")
645         if self.state in idle_dict:
646             idle_state = self.state
647         else:
648             idle_state = u"default"
649         if idletime == idle_dict[idle_state]:
650             self.send(
651                 u"$(eol)$(red)If you continue to be unproductive, "
652                 + u"you'll be shown the door...$(nrm)$(eol)"
653             )
654
655     def reload(self):
656         u"""Save, load a new user and relocate the connection."""
657
658         # get out of the list
659         self.remove()
660
661         # create a new user object
662         new_user = User()
663
664         # set everything equivalent
665         for attribute in vars(self).keys():
666             exec(u"new_user." + attribute + u" = self." + attribute)
667
668         # the avatar needs a new owner
669         if new_user.avatar:
670             new_user.avatar.owner = new_user
671
672         # add it to the list
673         universe.userlist.append(new_user)
674
675         # get rid of the old user object
676         del(self)
677
678     def replace_old_connections(self):
679         u"""Disconnect active users with the same name."""
680
681         # the default return value
682         return_value = False
683
684         # iterate over each user in the list
685         for old_user in universe.userlist:
686
687             # the name is the same but it's not us
688             if hasattr(
689                old_user, u"account"
690                ) and old_user.account and old_user.account.get(
691                 u"name"
692             ) == self.account.get(
693                 u"name"
694             ) and old_user is not self:
695
696                 # make a note of it
697                 log(
698                     u"User " + self.account.get(
699                         u"name"
700                     ) + u" reconnected--closing old connection to "
701                     + old_user.address + u".",
702                     2
703                 )
704                 old_user.send(
705                     u"$(eol)$(red)New connection from " + self.address
706                     + u". Terminating old connection...$(nrm)$(eol)",
707                     flush=True,
708                     add_prompt=False
709                 )
710
711                 # close the old connection
712                 old_user.connection.close()
713
714                 # replace the old connection with this one
715                 old_user.send(
716                     u"$(eol)$(red)Taking over old connection from "
717                     + old_user.address + u".$(nrm)"
718                 )
719                 old_user.connection = self.connection
720                 old_user.last_address = old_user.address
721                 old_user.address = self.address
722
723                 # take this one out of the list and delete
724                 self.remove()
725                 del(self)
726                 return_value = True
727                 break
728
729         # true if an old connection was replaced, false if not
730         return return_value
731
732     def authenticate(self):
733         u"""Flag the user as authenticated and disconnect duplicates."""
734         if not self.state is u"authenticated":
735             log(u"User " + self.account.get(u"name") + u" logged in.", 2)
736             self.authenticated = True
737             if self.account.subkey in universe.categories[
738                u"internal"
739                ][
740                 u"limits"
741             ].getlist(
742                 u"default_admins"
743             ):
744                 self.account.set(u"administrator", u"True")
745
746     def show_menu(self):
747         u"""Send the user their current menu."""
748         if not self.menu_seen:
749             self.menu_choices = get_menu_choices(self)
750             self.send(
751                 get_menu(self.state, self.error, self.menu_choices),
752                 u"",
753                 add_terminator=True
754             )
755             self.menu_seen = True
756             self.error = False
757             self.adjust_echoing()
758
759     def adjust_echoing(self):
760         u"""Adjust echoing to match state menu requirements."""
761         import telnet
762         if telnet.is_enabled(self, telnet.TELOPT_ECHO, telnet.US):
763             if menu_echo_on(self.state):
764                 telnet.disable(self, telnet.TELOPT_ECHO, telnet.US)
765         elif not menu_echo_on(self.state):
766             telnet.enable(self, telnet.TELOPT_ECHO, telnet.US)
767
768     def remove(self):
769         u"""Remove a user from the list of connected users."""
770         universe.userlist.remove(self)
771
772     def send(
773         self,
774         output,
775         eol=u"$(eol)",
776         raw=False,
777         flush=False,
778         add_prompt=True,
779         just_prompt=False,
780         add_terminator=False,
781         prepend_padding=True
782     ):
783         u"""Send arbitrary text to a connected user."""
784         import telnet
785
786         # unless raw mode is on, clean it up all nice and pretty
787         if not raw:
788
789             # strip extra $(eol) off if present
790             while output.startswith(u"$(eol)"):
791                 output = output[6:]
792             while output.endswith(u"$(eol)"):
793                 output = output[:-6]
794             extra_lines = output.find(u"$(eol)$(eol)$(eol)")
795             while extra_lines > -1:
796                 output = output[:extra_lines] + output[extra_lines + 6:]
797                 extra_lines = output.find(u"$(eol)$(eol)$(eol)")
798
799             # start with a newline, append the message, then end
800             # with the optional eol string passed to this function
801             # and the ansi escape to return to normal text
802             if not just_prompt and prepend_padding:
803                 if not self.output_queue \
804                    or not self.output_queue[-1].endswith("\r\n"):
805                     output = u"$(eol)" + output
806                 elif not self.output_queue[-1].endswith(
807                     "\r\n\x1b[0m\r\n"
808                 ) and not self.output_queue[-1].endswith(
809                     "\r\n\r\n"
810                 ):
811                     output = u"$(eol)" + output
812             output += eol + unichr(27) + u"[0m"
813
814             # tack on a prompt if active
815             if self.state == u"active":
816                 if not just_prompt:
817                     output += u"$(eol)"
818                 if add_prompt:
819                     output += u"> "
820                     mode = self.avatar.get(u"mode")
821                     if mode:
822                         output += u"(" + mode + u") "
823
824             # find and replace macros in the output
825             output = replace_macros(self, output)
826
827             # wrap the text at the client's width (min 40, 0 disables)
828             if self.columns:
829                 if self.columns < 40:
830                     wrap = 40
831                 else:
832                     wrap = self.columns
833                 output = wrap_ansi_text(output, wrap)
834
835             # if supported by the client, encode it utf-8
836             if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.US):
837                 encoded_output = output.encode(u"utf-8")
838
839             # otherwise just send ascii
840             else:
841                 encoded_output = output.encode(u"ascii", u"replace")
842
843             # end with a terminator if requested
844             if add_prompt or add_terminator:
845                 if telnet.is_enabled(self, telnet.TELOPT_EOR, telnet.US):
846                     encoded_output += telnet.telnet_proto(telnet.IAC,
847                                                           telnet.EOR)
848                 elif not telnet.is_enabled(self, telnet.TELOPT_SGA, telnet.US):
849                     encoded_output += telnet.telnet_proto(telnet.IAC,
850                                                           telnet.GA)
851
852             # and tack it onto the queue
853             self.output_queue.append(encoded_output)
854
855             # if this is urgent, flush all pending output
856             if flush:
857                 self.flush()
858
859         # just dump raw bytes as requested
860         else:
861             self.output_queue.append(output)
862             self.flush()
863
864     def pulse(self):
865         u"""All the things to do to the user per increment."""
866
867         # if the world is terminating, disconnect
868         if universe.terminate_flag:
869             self.state = u"disconnecting"
870             self.menu_seen = False
871
872         # check for an idle connection and act appropriately
873         else:
874             self.check_idle()
875
876         # if output is paused, decrement the counter
877         if self.state == u"initial":
878             if self.negotiation_pause:
879                 self.negotiation_pause -= 1
880             else:
881                 self.state = u"entering_account_name"
882
883         # show the user a menu as needed
884         elif not self.state == u"active":
885             self.show_menu()
886
887         # flush any pending output in the queue
888         self.flush()
889
890         # disconnect users with the appropriate state
891         if self.state == u"disconnecting":
892             self.quit()
893
894         # check for input and add it to the queue
895         self.enqueue_input()
896
897         # there is input waiting in the queue
898         if self.input_queue:
899             handle_user_input(self)
900
901     def flush(self):
902         u"""Try to send the last item in the queue and remove it."""
903         if self.output_queue:
904             try:
905                 self.connection.send(self.output_queue[0])
906                 del self.output_queue[0]
907             except:
908                 if self.account and self.account.get(u"name"):
909                     account = self.account.get(u"name")
910                 else:
911                     account = u"an unknown user"
912                 log(
913                     u"Sending to %s raised an exception (broken pipe?)." % account,
914                     7
915                 )
916                 pass
917
918     def enqueue_input(self):
919         u"""Process and enqueue any new input."""
920         import telnet
921         import unicodedata
922
923         # check for some input
924         try:
925             raw_input = self.connection.recv(1024)
926         except:
927             raw_input = ""
928
929         # we got something
930         if raw_input:
931
932             # tack this on to any previous partial
933             self.partial_input += raw_input
934
935             # reply to and remove any IAC negotiation codes
936             telnet.negotiate_telnet_options(self)
937
938             # separate multiple input lines
939             new_input_lines = self.partial_input.split("\n")
940
941             # if input doesn't end in a newline, replace the
942             # held partial input with the last line of it
943             if not self.partial_input.endswith("\n"):
944                 self.partial_input = new_input_lines.pop()
945
946             # otherwise, chop off the extra null input and reset
947             # the held partial input
948             else:
949                 new_input_lines.pop()
950                 self.partial_input = ""
951
952             # iterate over the remaining lines
953             for line in new_input_lines:
954
955                 # strip off extra whitespace
956                 line = line.strip()
957
958                 # make sure it's valid unicode (probably no longer needed)
959                 try:
960                     unicode(line, u"utf-8")
961                 except UnicodeDecodeError:
962                     logline = u"Non-unicode data from "
963                     if self.account and self.account.get(u"name"):
964                         logline += self.account.get(u"name") + u": "
965                     else:
966                         logline += u"unknown user: "
967                     logline += repr(line)
968                     log(logline, 4)
969                     line = ""
970
971                 # log non-printable characters remaining
972                 if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.HIM):
973                     asciiline = filter(lambda x: " " <= x <= "~", line)
974                     if line != asciiline:
975                         logline = u"Non-ASCII characters from "
976                         if self.account and self.account.get(u"name"):
977                             logline += self.account.get(u"name") + u": "
978                         else:
979                             logline += u"unknown user: "
980                         logline += repr(line)
981                         log(logline, 4)
982                         line = asciiline
983
984                 # put on the end of the queue
985                 self.input_queue.append(
986                     unicodedata.normalize(u"NFKC", unicode(line, u"utf-8"))
987                 )
988
989     def new_avatar(self):
990         u"""Instantiate a new, unconfigured avatar for this user."""
991         counter = 0
992         while u"avatar:" + self.account.get(u"name") + u":" + unicode(
993             counter
994         ) in universe.categories[u"actor"].keys():
995             counter += 1
996         self.avatar = Element(
997             u"actor:avatar:" + self.account.get(u"name") + u":" + unicode(
998                 counter
999             ),
1000             universe
1001         )
1002         self.avatar.append(u"inherit", u"archetype:avatar")
1003         self.account.append(u"avatars", self.avatar.key)
1004
1005     def delete_avatar(self, avatar):
1006         u"""Remove an avatar from the world and from the user's list."""
1007         if self.avatar is universe.contents[avatar]:
1008             self.avatar = None
1009         universe.contents[avatar].destroy()
1010         avatars = self.account.getlist(u"avatars")
1011         avatars.remove(avatar)
1012         self.account.set(u"avatars", avatars)
1013
1014     def activate_avatar_by_index(self, index):
1015         u"""Enter the world with a particular indexed avatar."""
1016         self.avatar = universe.contents[
1017             self.account.getlist(u"avatars")[index]]
1018         self.avatar.owner = self
1019         self.state = u"active"
1020         self.avatar.go_home()
1021
1022     def deactivate_avatar(self):
1023         u"""Have the active avatar leave the world."""
1024         if self.avatar:
1025             current = self.avatar.get(u"location")
1026             if current:
1027                 self.avatar.set(u"default_location", current)
1028                 self.avatar.echo_to_location(
1029                     u"You suddenly wonder where " + self.avatar.get(
1030                         u"name"
1031                     ) + u" went."
1032                 )
1033                 del universe.contents[current].contents[self.avatar.key]
1034                 self.avatar.remove_facet(u"location")
1035             self.avatar.owner = None
1036             self.avatar = None
1037
1038     def destroy(self):
1039         u"""Destroy the user and associated avatars."""
1040         for avatar in self.account.getlist(u"avatars"):
1041             self.delete_avatar(avatar)
1042         self.account.destroy()
1043
1044     def list_avatar_names(self):
1045         u"""List names of assigned avatars."""
1046         return [
1047             universe.contents[avatar].get(
1048                 u"name"
1049             ) for avatar in self.account.getlist(u"avatars")
1050         ]
1051
1052
1053 def broadcast(message, add_prompt=True):
1054     u"""Send a message to all connected users."""
1055     for each_user in universe.userlist:
1056         each_user.send(u"$(eol)" + message, add_prompt=add_prompt)
1057
1058
1059 def log(message, level=0):
1060     u"""Log a message."""
1061     import codecs
1062     import os.path
1063     import syslog
1064     import time
1065
1066     # a couple references we need
1067     file_name = universe.categories[u"internal"][u"logging"].get(u"file")
1068     max_log_lines = universe.categories[u"internal"][u"logging"].getint(
1069         u"max_log_lines"
1070     )
1071     syslog_name = universe.categories[u"internal"][u"logging"].get(u"syslog")
1072     timestamp = time.asctime()[4:19]
1073
1074     # turn the message into a list of lines
1075     lines = filter(
1076         lambda x: x != u"", [(x.rstrip()) for x in message.split(u"\n")]
1077     )
1078
1079     # send the timestamp and line to a file
1080     if file_name:
1081         if not os.path.isabs(file_name):
1082             file_name = os.path.join(universe.startdir, file_name)
1083         file_descriptor = codecs.open(file_name, u"a", u"utf-8")
1084         for line in lines:
1085             file_descriptor.write(timestamp + u" " + line + u"\n")
1086         file_descriptor.flush()
1087         file_descriptor.close()
1088
1089     # send the timestamp and line to standard output
1090     if universe.categories[u"internal"][u"logging"].getboolean(u"stdout"):
1091         for line in lines:
1092             print(timestamp + u" " + line)
1093
1094     # send the line to the system log
1095     if syslog_name:
1096         syslog.openlog(
1097             syslog_name.encode("utf-8"),
1098             syslog.LOG_PID,
1099             syslog.LOG_INFO | syslog.LOG_DAEMON
1100         )
1101         for line in lines:
1102             syslog.syslog(line)
1103         syslog.closelog()
1104
1105     # display to connected administrators
1106     for user in universe.userlist:
1107         if user.state == u"active" and user.account.getboolean(
1108            u"administrator"
1109            ) and user.account.getint(u"loglevel") <= level:
1110             # iterate over every line in the message
1111             full_message = u""
1112             for line in lines:
1113                 full_message += u"$(bld)$(red)" + timestamp + u" " + line.replace(
1114                     u"$(", u"$_("
1115                 ) + u"$(nrm)$(eol)"
1116             user.send(full_message, flush=True)
1117
1118     # add to the recent log list
1119     for line in lines:
1120         while 0 < len(universe.loglines) >= max_log_lines:
1121             del universe.loglines[0]
1122         universe.loglines.append((level, timestamp + u" " + line))
1123
1124
1125 def get_loglines(level, start, stop):
1126     u"""Return a specific range of loglines filtered by level."""
1127
1128     # filter the log lines
1129     loglines = filter(lambda x: x[0] >= level, universe.loglines)
1130
1131     # we need these in several places
1132     total_count = unicode(len(universe.loglines))
1133     filtered_count = len(loglines)
1134
1135     # don't proceed if there are no lines
1136     if filtered_count:
1137
1138         # can't start before the begining or at the end
1139         if start > filtered_count:
1140             start = filtered_count
1141         if start < 1:
1142             start = 1
1143
1144         # can't stop before we start
1145         if stop > start:
1146             stop = start
1147         elif stop < 1:
1148             stop = 1
1149
1150         # some preamble
1151         message = u"There are " + unicode(total_count)
1152         message += u" log lines in memory and " + unicode(filtered_count)
1153         message += u" at or above level " + unicode(level) + u"."
1154         message += u" The matching lines from " + unicode(stop) + u" to "
1155         message += unicode(start) + u" are:$(eol)$(eol)"
1156
1157         # add the text from the selected lines
1158         if stop > 1:
1159             range_lines = loglines[-start:-(stop - 1)]
1160         else:
1161             range_lines = loglines[-start:]
1162         for line in range_lines:
1163             message += u"   (" + unicode(line[0]) + u") " + line[1].replace(
1164                 u"$(", u"$_("
1165             ) + u"$(eol)"
1166
1167     # there were no lines
1168     else:
1169         message = u"None of the " + unicode(total_count)
1170         message += u" lines in memory matches your request."
1171
1172     # pass it back
1173     return message
1174
1175
1176 def glyph_columns(character):
1177     u"""Convenience function to return the column width of a glyph."""
1178     import unicodedata
1179     if unicodedata.east_asian_width(character) in u"FW":
1180         return 2
1181     else:
1182         return 1
1183
1184
1185 def wrap_ansi_text(text, width):
1186     u"""Wrap text with arbitrary width while ignoring ANSI colors."""
1187     import unicodedata
1188
1189     # the current position in the entire text string, including all
1190     # characters, printable or otherwise
1191     absolute_position = 0
1192
1193     # the current text position relative to the begining of the line,
1194     # ignoring color escape sequences
1195     relative_position = 0
1196
1197     # the absolute position of the most recent whitespace character
1198     last_whitespace = 0
1199
1200     # whether the current character is part of a color escape sequence
1201     escape = False
1202
1203     # normalize any potentially composited unicode before we count it
1204     text = unicodedata.normalize(u"NFKC", text)
1205
1206     # iterate over each character from the begining of the text
1207     for each_character in text:
1208
1209         # the current character is the escape character
1210         if each_character == u"\x1b" and not escape:
1211             escape = True
1212
1213         # the current character is within an escape sequence
1214         elif escape:
1215
1216             # the current character is m, which terminates the
1217             # escape sequence
1218             if each_character == u"m":
1219                 escape = False
1220
1221         # the current character is a newline, so reset the relative
1222         # position (start a new line)
1223         elif each_character == u"\n":
1224             relative_position = 0
1225             last_whitespace = absolute_position
1226
1227         # the current character meets the requested maximum line width,
1228         # so we need to backtrack and find a space at which to wrap;
1229         # special care is taken to avoid an off-by-one in case the
1230         # current character is a double-width glyph
1231         elif each_character != u"\r" and (
1232             relative_position >= width or (
1233                 relative_position >= width - 1 and glyph_columns(
1234                     each_character
1235                 ) == 2
1236             )
1237         ):
1238
1239             # it's always possible we landed on whitespace
1240             if unicodedata.category(each_character) in (u"Cc", u"Zs"):
1241                 last_whitespace = absolute_position
1242
1243             # insert an eol in place of the space
1244             text = text[:last_whitespace] + \
1245                 u"\r\n" + text[last_whitespace + 1:]
1246
1247             # increase the absolute position because an eol is two
1248             # characters but the space it replaced was only one
1249             absolute_position += 1
1250
1251             # now we're at the begining of a new line, plus the
1252             # number of characters wrapped from the previous line
1253             relative_position = 0
1254             for remaining_characters in text[last_whitespace:absolute_position]:
1255                 relative_position += glyph_columns(remaining_characters)
1256
1257         # as long as the character is not a carriage return and the
1258         # other above conditions haven't been met, count it as a
1259         # printable character
1260         elif each_character != u"\r":
1261             relative_position += glyph_columns(each_character)
1262             if unicodedata.category(each_character) in (u"Cc", u"Zs"):
1263                 last_whitespace = absolute_position
1264
1265         # increase the absolute position for every character
1266         absolute_position += 1
1267
1268     # return the newly-wrapped text
1269     return text
1270
1271
1272 def weighted_choice(data):
1273     u"""Takes a dict weighted by value and returns a random key."""
1274     import random
1275
1276     # this will hold our expanded list of keys from the data
1277     expanded = []
1278
1279     # create the expanded list of keys
1280     for key in data.keys():
1281         for count in range(data[key]):
1282             expanded.append(key)
1283
1284     # return one at random
1285     return random.choice(expanded)
1286
1287
1288 def random_name():
1289     u"""Returns a random character name."""
1290     import random
1291
1292     # the vowels and consonants needed to create romaji syllables
1293     vowels = [
1294         u"a",
1295         u"i",
1296         u"u",
1297         u"e",
1298         u"o"
1299     ]
1300     consonants = [
1301         u"'",
1302         u"k",
1303         u"z",
1304         u"s",
1305         u"sh",
1306         u"z",
1307         u"j",
1308         u"t",
1309         u"ch",
1310         u"ts",
1311         u"d",
1312         u"n",
1313         u"h",
1314         u"f",
1315         u"m",
1316         u"y",
1317         u"r",
1318         u"w"
1319     ]
1320
1321     # this dict will hold our weighted list of syllables
1322     syllables = {}
1323
1324     # generate the list with an even weighting
1325     for consonant in consonants:
1326         for vowel in vowels:
1327             syllables[consonant + vowel] = 1
1328
1329     # we'll build the name into this string
1330     name = u""
1331
1332     # create a name of random length from the syllables
1333     for syllable in range(random.randrange(2, 6)):
1334         name += weighted_choice(syllables)
1335
1336     # strip any leading quotemark, capitalize and return the name
1337     return name.strip(u"'").capitalize()
1338
1339
1340 def replace_macros(user, text, is_input=False):
1341     u"""Replaces macros in text output."""
1342     import codecs
1343     import data
1344     import os.path
1345
1346     # third person pronouns
1347     pronouns = {
1348         u"female": {u"obj": u"her", u"pos": u"hers", u"sub": u"she"},
1349         u"male": {u"obj": u"him", u"pos": u"his", u"sub": u"he"},
1350         u"neuter": {u"obj": u"it", u"pos": u"its", u"sub": u"it"}
1351     }
1352
1353     # a dict of replacement macros
1354     macros = {
1355         u"eol": u"\r\n",
1356         u"bld": unichr(27) + u"[1m",
1357         u"nrm": unichr(27) + u"[0m",
1358         u"blk": unichr(27) + u"[30m",
1359         u"blu": unichr(27) + u"[34m",
1360         u"cyn": unichr(27) + u"[36m",
1361         u"grn": unichr(27) + u"[32m",
1362         u"mgt": unichr(27) + u"[35m",
1363         u"red": unichr(27) + u"[31m",
1364         u"yel": unichr(27) + u"[33m",
1365     }
1366
1367     # add dynamic macros where possible
1368     if user.account:
1369         account_name = user.account.get(u"name")
1370         if account_name:
1371             macros[u"account"] = account_name
1372     if user.avatar:
1373         avatar_gender = user.avatar.get(u"gender")
1374         if avatar_gender:
1375             macros[u"tpop"] = pronouns[avatar_gender][u"obj"]
1376             macros[u"tppp"] = pronouns[avatar_gender][u"pos"]
1377             macros[u"tpsp"] = pronouns[avatar_gender][u"sub"]
1378
1379     # loop until broken
1380     while True:
1381
1382         # find and replace per the macros dict
1383         macro_start = text.find(u"$(")
1384         if macro_start == -1:
1385             break
1386         macro_end = text.find(u")", macro_start) + 1
1387         macro = text[macro_start + 2:macro_end - 1]
1388         if macro in macros.keys():
1389             replacement = macros[macro]
1390
1391         # this is how we handle local file inclusion (dangerous!)
1392         elif macro.startswith(u"inc:"):
1393             incfile = data.find_file(macro[4:], universe=universe)
1394             if os.path.exists(incfile):
1395                 incfd = codecs.open(incfile, u"r", u"utf-8")
1396                 replacement = u""
1397                 for line in incfd:
1398                     if line.endswith(u"\n") and not line.endswith(u"\r\n"):
1399                         line = line.replace(u"\n", u"\r\n")
1400                     replacement += line
1401                 # lose the trailing eol
1402                 replacement = replacement[:-2]
1403             else:
1404                 replacement = u""
1405                 log(u"Couldn't read included " + incfile + u" file.", 6)
1406
1407         # if we get here, log and replace it with null
1408         else:
1409             replacement = u""
1410             if not is_input:
1411                 log(u"Unexpected replacement macro " +
1412                     macro + u" encountered.", 6)
1413
1414         # and now we act on the replacement
1415         text = text.replace(u"$(" + macro + u")", replacement)
1416
1417     # replace the look-like-a-macro sequence
1418     text = text.replace(u"$_(", u"$(")
1419
1420     return text
1421
1422
1423 def escape_macros(text):
1424     u"""Escapes replacement macros in text."""
1425     return text.replace(u"$(", u"$_(")
1426
1427
1428 def first_word(text, separator=u" "):
1429     u"""Returns a tuple of the first word and the rest."""
1430     if text:
1431         if text.find(separator) > 0:
1432             return text.split(separator, 1)
1433         else:
1434             return text, u""
1435     else:
1436         return u"", u""
1437
1438
1439 def on_pulse():
1440     u"""The things which should happen on each pulse, aside from reloads."""
1441     import time
1442
1443     # open the listening socket if it hasn't been already
1444     if not hasattr(universe, u"listening_socket"):
1445         universe.initialize_server_socket()
1446
1447     # assign a user if a new connection is waiting
1448     user = check_for_connection(universe.listening_socket)
1449     if user:
1450         universe.userlist.append(user)
1451
1452     # iterate over the connected users
1453     for user in universe.userlist:
1454         user.pulse()
1455
1456     # add an element for counters if it doesn't exist
1457     if not u"counters" in universe.categories[u"internal"]:
1458         universe.categories[u"internal"][u"counters"] = Element(
1459             u"internal:counters", universe
1460         )
1461
1462     # update the log every now and then
1463     if not universe.categories[u"internal"][u"counters"].getint(u"mark"):
1464         log(unicode(len(universe.userlist)) + u" connection(s)")
1465         universe.categories[u"internal"][u"counters"].set(
1466             u"mark", universe.categories[u"internal"][u"time"].getint(
1467                 u"frequency_log"
1468             )
1469         )
1470     else:
1471         universe.categories[u"internal"][u"counters"].set(
1472             u"mark", universe.categories[u"internal"][u"counters"].getint(
1473                 u"mark"
1474             ) - 1
1475         )
1476
1477     # periodically save everything
1478     if not universe.categories[u"internal"][u"counters"].getint(u"save"):
1479         universe.save()
1480         universe.categories[u"internal"][u"counters"].set(
1481             u"save", universe.categories[u"internal"][u"time"].getint(
1482                 u"frequency_save"
1483             )
1484         )
1485     else:
1486         universe.categories[u"internal"][u"counters"].set(
1487             u"save", universe.categories[u"internal"][u"counters"].getint(
1488                 u"save"
1489             ) - 1
1490         )
1491
1492     # pause for a configurable amount of time (decimal seconds)
1493     time.sleep(universe.categories[u"internal"]
1494                [u"time"].getfloat(u"increment"))
1495
1496     # increase the elapsed increment counter
1497     universe.categories[u"internal"][u"counters"].set(
1498         u"elapsed", universe.categories[u"internal"][u"counters"].getint(
1499             u"elapsed"
1500         ) + 1
1501     )
1502
1503
1504 def reload_data():
1505     u"""Reload all relevant objects."""
1506     for user in universe.userlist[:]:
1507         user.reload()
1508     for element in universe.contents.values():
1509         if element.origin.is_writeable():
1510             element.reload()
1511     universe.load()
1512
1513
1514 def check_for_connection(listening_socket):
1515     u"""Check for a waiting connection and return a new user object."""
1516     import telnet
1517
1518     # try to accept a new connection
1519     try:
1520         connection, address = listening_socket.accept()
1521     except:
1522         return None
1523
1524     # note that we got one
1525     log(u"Connection from " + address[0], 2)
1526
1527     # disable blocking so we can proceed whether or not we can send/receive
1528     connection.setblocking(0)
1529
1530     # create a new user object
1531     user = User()
1532
1533     # associate this connection with it
1534     user.connection = connection
1535
1536     # set the user's ipa from the connection's ipa
1537     user.address = address[0]
1538
1539     # let the client know we WILL EOR (RFC 885)
1540     telnet.enable(user, telnet.TELOPT_EOR, telnet.US)
1541     user.negotiation_pause = 2
1542
1543     # return the new user object
1544     return user
1545
1546
1547 def get_menu(state, error=None, choices=None):
1548     u"""Show the correct menu text to a user."""
1549
1550     # make sure we don't reuse a mutable sequence by default
1551     if choices is None:
1552         choices = {}
1553
1554     # get the description or error text
1555     message = get_menu_description(state, error)
1556
1557     # get menu choices for the current state
1558     message += get_formatted_menu_choices(state, choices)
1559
1560     # try to get a prompt, if it was defined
1561     message += get_menu_prompt(state)
1562
1563     # throw in the default choice, if it exists
1564     message += get_formatted_default_menu_choice(state)
1565
1566     # display a message indicating if echo is off
1567     message += get_echo_message(state)
1568
1569     # return the assembly of various strings defined above
1570     return message
1571
1572
1573 def menu_echo_on(state):
1574     u"""True if echo is on, false if it is off."""
1575     return universe.categories[u"menu"][state].getboolean(u"echo", True)
1576
1577
1578 def get_echo_message(state):
1579     u"""Return a message indicating that echo is off."""
1580     if menu_echo_on(state):
1581         return u""
1582     else:
1583         return u"(won't echo) "
1584
1585
1586 def get_default_menu_choice(state):
1587     u"""Return the default choice for a menu."""
1588     return universe.categories[u"menu"][state].get(u"default")
1589
1590
1591 def get_formatted_default_menu_choice(state):
1592     u"""Default menu choice foratted for inclusion in a prompt string."""
1593     default_choice = get_default_menu_choice(state)
1594     if default_choice:
1595         return u"[$(red)" + default_choice + u"$(nrm)] "
1596     else:
1597         return u""
1598
1599
1600 def get_menu_description(state, error):
1601     u"""Get the description or error text."""
1602
1603     # an error condition was raised by the handler
1604     if error:
1605
1606         # try to get an error message matching the condition
1607         # and current state
1608         description = universe.categories[
1609             u"menu"][state].get(u"error_" + error)
1610         if not description:
1611             description = u"That is not a valid choice..."
1612         description = u"$(red)" + description + u"$(nrm)"
1613
1614     # there was no error condition
1615     else:
1616
1617         # try to get a menu description for the current state
1618         description = universe.categories[u"menu"][state].get(u"description")
1619
1620     # return the description or error message
1621     if description:
1622         description += u"$(eol)$(eol)"
1623     return description
1624
1625
1626 def get_menu_prompt(state):
1627     u"""Try to get a prompt, if it was defined."""
1628     prompt = universe.categories[u"menu"][state].get(u"prompt")
1629     if prompt:
1630         prompt += u" "
1631     return prompt
1632
1633
1634 def get_menu_choices(user):
1635     u"""Return a dict of choice:meaning."""
1636     menu = universe.categories[u"menu"][user.state]
1637     create_choices = menu.get(u"create")
1638     if create_choices:
1639         choices = eval(create_choices)
1640     else:
1641         choices = {}
1642     ignores = []
1643     options = {}
1644     creates = {}
1645     for facet in menu.facets():
1646         if facet.startswith(u"demand_") and not eval(
1647            universe.categories[u"menu"][user.state].get(facet)
1648            ):
1649             ignores.append(facet.split(u"_", 2)[1])
1650         elif facet.startswith(u"create_"):
1651             creates[facet] = facet.split(u"_", 2)[1]
1652         elif facet.startswith(u"choice_"):
1653             options[facet] = facet.split(u"_", 2)[1]
1654     for facet in creates.keys():
1655         if not creates[facet] in ignores:
1656             choices[creates[facet]] = eval(menu.get(facet))
1657     for facet in options.keys():
1658         if not options[facet] in ignores:
1659             choices[options[facet]] = menu.get(facet)
1660     return choices
1661
1662
1663 def get_formatted_menu_choices(state, choices):
1664     u"""Returns a formatted string of menu choices."""
1665     choice_output = u""
1666     choice_keys = choices.keys()
1667     choice_keys.sort()
1668     for choice in choice_keys:
1669         choice_output += u"   [$(red)" + choice + u"$(nrm)]  " + choices[
1670             choice
1671         ] + u"$(eol)"
1672     if choice_output:
1673         choice_output += u"$(eol)"
1674     return choice_output
1675
1676
1677 def get_menu_branches(state):
1678     u"""Return a dict of choice:branch."""
1679     branches = {}
1680     for facet in universe.categories[u"menu"][state].facets():
1681         if facet.startswith(u"branch_"):
1682             branches[
1683                 facet.split(u"_", 2)[1]
1684             ] = universe.categories[u"menu"][state].get(facet)
1685     return branches
1686
1687
1688 def get_default_branch(state):
1689     u"""Return the default branch."""
1690     return universe.categories[u"menu"][state].get(u"branch")
1691
1692
1693 def get_choice_branch(user, choice):
1694     u"""Returns the new state matching the given choice."""
1695     branches = get_menu_branches(user.state)
1696     if choice in branches.keys():
1697         return branches[choice]
1698     elif choice in user.menu_choices.keys():
1699         return get_default_branch(user.state)
1700     else:
1701         return u""
1702
1703
1704 def get_menu_actions(state):
1705     u"""Return a dict of choice:branch."""
1706     actions = {}
1707     for facet in universe.categories[u"menu"][state].facets():
1708         if facet.startswith(u"action_"):
1709             actions[
1710                 facet.split(u"_", 2)[1]
1711             ] = universe.categories[u"menu"][state].get(facet)
1712     return actions
1713
1714
1715 def get_default_action(state):
1716     u"""Return the default action."""
1717     return universe.categories[u"menu"][state].get(u"action")
1718
1719
1720 def get_choice_action(user, choice):
1721     u"""Run any indicated script for the given choice."""
1722     actions = get_menu_actions(user.state)
1723     if choice in actions.keys():
1724         return actions[choice]
1725     elif choice in user.menu_choices.keys():
1726         return get_default_action(user.state)
1727     else:
1728         return u""
1729
1730
1731 def handle_user_input(user):
1732     u"""The main handler, branches to a state-specific handler."""
1733     import telnet
1734
1735     # if the user's client echo is off, send a blank line for aesthetics
1736     if telnet.is_enabled(user, telnet.TELOPT_ECHO, telnet.US):
1737         user.send(u"", add_prompt=False, prepend_padding=False)
1738
1739     # check to make sure the state is expected, then call that handler
1740     if u"handler_" + user.state in globals():
1741         exec(u"handler_" + user.state + u"(user)")
1742     else:
1743         generic_menu_handler(user)
1744
1745     # since we got input, flag that the menu/prompt needs to be redisplayed
1746     user.menu_seen = False
1747
1748     # update the last_input timestamp while we're at it
1749     user.last_input = universe.get_time()
1750
1751
1752 def generic_menu_handler(user):
1753     u"""A generic menu choice handler."""
1754
1755     # get a lower-case representation of the next line of input
1756     if user.input_queue:
1757         choice = user.input_queue.pop(0)
1758         if choice:
1759             choice = choice.lower()
1760     else:
1761         choice = u""
1762     if not choice:
1763         choice = get_default_menu_choice(user.state)
1764     if choice in user.menu_choices:
1765         exec(get_choice_action(user, choice))
1766         new_state = get_choice_branch(user, choice)
1767         if new_state:
1768             user.state = new_state
1769     else:
1770         user.error = u"default"
1771
1772
1773 def handler_entering_account_name(user):
1774     u"""Handle the login account name."""
1775
1776     # get the next waiting line of input
1777     input_data = user.input_queue.pop(0)
1778
1779     # did the user enter anything?
1780     if input_data:
1781
1782         # keep only the first word and convert to lower-case
1783         name = input_data.lower()
1784
1785         # fail if there are non-alphanumeric characters
1786         if name != filter(
1787            lambda x: x >= u"0" and x <= u"9" or x >= u"a" and x <= u"z", name
1788            ):
1789             user.error = u"bad_name"
1790
1791         # if that account exists, time to request a password
1792         elif name in universe.categories[u"account"]:
1793             user.account = universe.categories[u"account"][name]
1794             user.state = u"checking_password"
1795
1796         # otherwise, this could be a brand new user
1797         else:
1798             user.account = Element(u"account:" + name, universe)
1799             user.account.set(u"name", name)
1800             log(u"New user: " + name, 2)
1801             user.state = u"checking_new_account_name"
1802
1803     # if the user entered nothing for a name, then buhbye
1804     else:
1805         user.state = u"disconnecting"
1806
1807
1808 def handler_checking_password(user):
1809     u"""Handle the login account password."""
1810     import password
1811
1812     # get the next waiting line of input
1813     input_data = user.input_queue.pop(0)
1814
1815     # does the hashed input equal the stored hash?
1816     if password.verify(input_data, user.account.get(u"passhash")):
1817
1818         # if so, set the username and load from cold storage
1819         if not user.replace_old_connections():
1820             user.authenticate()
1821             user.state = u"main_utility"
1822
1823     # if at first your hashes don't match, try, try again
1824     elif user.password_tries < universe.categories[
1825         u"internal"
1826     ][
1827         u"limits"
1828     ].getint(
1829         u"password_tries"
1830     ) - 1:
1831         user.password_tries += 1
1832         user.error = u"incorrect"
1833
1834     # we've exceeded the maximum number of password failures, so disconnect
1835     else:
1836         user.send(
1837             u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1838         )
1839         user.state = u"disconnecting"
1840
1841
1842 def handler_entering_new_password(user):
1843     u"""Handle a new password entry."""
1844     import password
1845
1846     # get the next waiting line of input
1847     input_data = user.input_queue.pop(0)
1848
1849     # make sure the password is strong--at least one upper, one lower and
1850     # one digit, seven or more characters in length
1851     if len(input_data) > 6 and len(
1852        filter(lambda x: x >= u"0" and x <= u"9", input_data)
1853        ) and len(
1854         filter(lambda x: x >= u"A" and x <= u"Z", input_data)
1855     ) and len(
1856         filter(lambda x: x >= u"a" and x <= u"z", input_data)
1857     ):
1858
1859         # hash and store it, then move on to verification
1860         user.account.set(u"passhash", password.create(input_data))
1861         user.state = u"verifying_new_password"
1862
1863     # the password was weak, try again if you haven't tried too many times
1864     elif user.password_tries < universe.categories[
1865         u"internal"
1866     ][
1867         u"limits"
1868     ].getint(
1869         u"password_tries"
1870     ) - 1:
1871         user.password_tries += 1
1872         user.error = u"weak"
1873
1874     # too many tries, so adios
1875     else:
1876         user.send(
1877             u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1878         )
1879         user.account.destroy()
1880         user.state = u"disconnecting"
1881
1882
1883 def handler_verifying_new_password(user):
1884     u"""Handle the re-entered new password for verification."""
1885     import password
1886
1887     # get the next waiting line of input
1888     input_data = user.input_queue.pop(0)
1889
1890     # hash the input and match it to storage
1891     if password.verify(input_data, user.account.get(u"passhash")):
1892         user.authenticate()
1893
1894         # the hashes matched, so go active
1895         if not user.replace_old_connections():
1896             user.state = u"main_utility"
1897
1898     # go back to entering the new password as long as you haven't tried
1899     # too many times
1900     elif user.password_tries < universe.categories[
1901         u"internal"
1902     ][
1903         u"limits"
1904     ].getint(
1905         u"password_tries"
1906     ) - 1:
1907         user.password_tries += 1
1908         user.error = u"differs"
1909         user.state = u"entering_new_password"
1910
1911     # otherwise, sayonara
1912     else:
1913         user.send(
1914             u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1915         )
1916         user.account.destroy()
1917         user.state = u"disconnecting"
1918
1919
1920 def handler_active(user):
1921     u"""Handle input for active users."""
1922
1923     # get the next waiting line of input
1924     input_data = user.input_queue.pop(0)
1925
1926     # is there input?
1927     if input_data:
1928
1929         # split out the command and parameters
1930         actor = user.avatar
1931         mode = actor.get(u"mode")
1932         if mode and input_data.startswith(u"!"):
1933             command_name, parameters = first_word(input_data[1:])
1934         elif mode == u"chat":
1935             command_name = u"say"
1936             parameters = input_data
1937         else:
1938             command_name, parameters = first_word(input_data)
1939
1940         # lowercase the command
1941         command_name = command_name.lower()
1942
1943         # the command matches a command word for which we have data
1944         if command_name in universe.categories[u"command"]:
1945             command = universe.categories[u"command"][command_name]
1946         else:
1947             command = None
1948
1949         # if it's allowed, do it
1950         if actor.can_run(command):
1951             exec(command.get(u"action"))
1952
1953         # otherwise, give an error
1954         elif command_name:
1955             command_error(actor, input_data)
1956
1957     # if no input, just idle back with a prompt
1958     else:
1959         user.send(u"", just_prompt=True)
1960
1961
1962 def command_halt(actor, parameters):
1963     u"""Halt the world."""
1964     if actor.owner:
1965
1966         # see if there's a message or use a generic one
1967         if parameters:
1968             message = u"Halting: " + parameters
1969         else:
1970             message = u"User " + actor.owner.account.get(
1971                 u"name"
1972             ) + u" halted the world."
1973
1974         # let everyone know
1975         broadcast(message, add_prompt=False)
1976         log(message, 8)
1977
1978         # set a flag to terminate the world
1979         universe.terminate_flag = True
1980
1981
1982 def command_reload(actor):
1983     u"""Reload all code modules, configs and data."""
1984     if actor.owner:
1985
1986         # let the user know and log
1987         actor.send(u"Reloading all code modules, configs and data.")
1988         log(
1989             u"User " +
1990             actor.owner.account.get(u"name") + u" reloaded the world.",
1991             8
1992         )
1993
1994         # set a flag to reload
1995         universe.reload_flag = True
1996
1997
1998 def command_quit(actor):
1999     u"""Leave the world and go back to the main menu."""
2000     if actor.owner:
2001         actor.owner.state = u"main_utility"
2002         actor.owner.deactivate_avatar()
2003
2004
2005 def command_help(actor, parameters):
2006     u"""List available commands and provide help for commands."""
2007
2008     # did the user ask for help on a specific command word?
2009     if parameters and actor.owner:
2010
2011         # is the command word one for which we have data?
2012         if parameters in universe.categories[u"command"]:
2013             command = universe.categories[u"command"][parameters]
2014         else:
2015             command = None
2016
2017         # only for allowed commands
2018         if actor.can_run(command):
2019
2020             # add a description if provided
2021             description = command.get(u"description")
2022             if not description:
2023                 description = u"(no short description provided)"
2024             if command.getboolean(u"administrative"):
2025                 output = u"$(red)"
2026             else:
2027                 output = u"$(grn)"
2028             output += parameters + u"$(nrm) - " + description + u"$(eol)$(eol)"
2029
2030             # add the help text if provided
2031             help_text = command.get(u"help")
2032             if not help_text:
2033                 help_text = u"No help is provided for this command."
2034             output += help_text
2035
2036             # list related commands
2037             see_also = command.getlist(u"see_also")
2038             if see_also:
2039                 really_see_also = u""
2040                 for item in see_also:
2041                     if item in universe.categories[u"command"]:
2042                         command = universe.categories[u"command"][item]
2043                         if actor.can_run(command):
2044                             if really_see_also:
2045                                 really_see_also += u", "
2046                             if command.getboolean(u"administrative"):
2047                                 really_see_also += u"$(red)"
2048                             else:
2049                                 really_see_also += u"$(grn)"
2050                             really_see_also += item + u"$(nrm)"
2051                 if really_see_also:
2052                     output += u"$(eol)$(eol)See also: " + really_see_also
2053
2054         # no data for the requested command word
2055         else:
2056             output = u"That is not an available command."
2057
2058     # no specific command word was indicated
2059     else:
2060
2061         # give a sorted list of commands with descriptions if provided
2062         output = u"These are the commands available to you:$(eol)$(eol)"
2063         sorted_commands = universe.categories[u"command"].keys()
2064         sorted_commands.sort()
2065         for item in sorted_commands:
2066             command = universe.categories[u"command"][item]
2067             if actor.can_run(command):
2068                 description = command.get(u"description")
2069                 if not description:
2070                     description = u"(no short description provided)"
2071                 if command.getboolean(u"administrative"):
2072                     output += u"   $(red)"
2073                 else:
2074                     output += u"   $(grn)"
2075                 output += item + u"$(nrm) - " + description + u"$(eol)"
2076         output += u"$(eol)Enter \"help COMMAND\" for help on a command " \
2077             + u"named \"COMMAND\"."
2078
2079     # send the accumulated output to the user
2080     actor.send(output)
2081
2082
2083 def command_move(actor, parameters):
2084     u"""Move the avatar in a given direction."""
2085     if parameters in universe.contents[actor.get(u"location")].portals():
2086         actor.move_direction(parameters)
2087     else:
2088         actor.send(u"You cannot go that way.")
2089
2090
2091 def command_look(actor, parameters):
2092     u"""Look around."""
2093     if parameters:
2094         actor.send(u"You can't look at or in anything yet.")
2095     else:
2096         actor.look_at(actor.get(u"location"))
2097
2098
2099 def command_say(actor, parameters):
2100     u"""Speak to others in the same room."""
2101     import unicodedata
2102
2103     # check for replacement macros and escape them
2104     parameters = escape_macros(parameters)
2105
2106     # if the message is wrapped in quotes, remove them and leave contents
2107     # intact
2108     if parameters.startswith(u"\"") and parameters.endswith(u"\""):
2109         message = parameters[1:-1]
2110         literal = True
2111
2112     # otherwise, get rid of stray quote marks on the ends of the message
2113     else:
2114         message = parameters.strip(u"\"'`")
2115         literal = False
2116
2117     # the user entered a message
2118     if message:
2119
2120         # match the punctuation used, if any, to an action
2121         actions = universe.categories[u"internal"][u"language"].getdict(
2122             u"actions"
2123         )
2124         default_punctuation = universe.categories[u"internal"][u"language"].get(
2125             u"default_punctuation"
2126         )
2127         action = u""
2128         for mark in actions.keys():
2129             if not literal and message.endswith(mark):
2130                 action = actions[mark]
2131                 break
2132
2133         # add punctuation if needed
2134         if not action:
2135             action = actions[default_punctuation]
2136             if message and not (
2137                literal or unicodedata.category(message[-1]) == u"Po"
2138                ):
2139                 message += default_punctuation
2140
2141         # failsafe checks to avoid unwanted reformatting and null strings
2142         if message and not literal:
2143
2144             # decapitalize the first letter to improve matching
2145             message = message[0].lower() + message[1:]
2146
2147             # iterate over all words in message, replacing typos
2148             typos = universe.categories[u"internal"][u"language"].getdict(
2149                 u"typos"
2150             )
2151             words = message.split()
2152             for index in range(len(words)):
2153                 word = words[index]
2154                 while unicodedata.category(word[0]) == u"Po":
2155                     word = word[1:]
2156                 while unicodedata.category(word[-1]) == u"Po":
2157                     word = word[:-1]
2158                 if word in typos.keys():
2159                     words[index] = words[index].replace(word, typos[word])
2160             message = u" ".join(words)
2161
2162             # capitalize the first letter
2163             message = message[0].upper() + message[1:]
2164
2165     # tell the room
2166     if message:
2167         actor.echo_to_location(
2168             actor.get(u"name") + u" " + action + u"s, \"" + message + u"\""
2169         )
2170         actor.send(u"You " + action + u", \"" + message + u"\"")
2171
2172     # there was no message
2173     else:
2174         actor.send(u"What do you want to say?")
2175
2176
2177 def command_chat(actor):
2178     u"""Toggle chat mode."""
2179     mode = actor.get(u"mode")
2180     if not mode:
2181         actor.set(u"mode", u"chat")
2182         actor.send(u"Entering chat mode (use $(grn)!chat$(nrm) to exit).")
2183     elif mode == u"chat":
2184         actor.remove_facet(u"mode")
2185         actor.send(u"Exiting chat mode.")
2186     else:
2187         actor.send(u"Sorry, but you're already busy with something else!")
2188
2189
2190 def command_show(actor, parameters):
2191     u"""Show program data."""
2192     import re
2193     message = u""
2194     arguments = parameters.split()
2195     if not parameters:
2196         message = u"What do you want to show?"
2197     elif arguments[0] == u"time":
2198         message = universe.categories[u"internal"][u"counters"].get(
2199             u"elapsed"
2200         ) + u" increments elapsed since the world was created."
2201     elif arguments[0] == u"categories":
2202         message = u"These are the element categories:$(eol)"
2203         categories = universe.categories.keys()
2204         categories.sort()
2205         for category in categories:
2206             message += u"$(eol)   $(grn)" + category + u"$(nrm)"
2207     elif arguments[0] == u"files":
2208         message = u"These are the current files containing the universe:$(eol)"
2209         filenames = universe.files.keys()
2210         filenames.sort()
2211         for filename in filenames:
2212             if universe.files[filename].is_writeable():
2213                 status = u"rw"
2214             else:
2215                 status = u"ro"
2216             message += u"$(eol)   $(red)(" + status + u") $(grn)" + filename \
2217                 + u"$(nrm)"
2218     elif arguments[0] == u"category":
2219         if len(arguments) != 2:
2220             message = u"You must specify one category."
2221         elif arguments[1] in universe.categories:
2222             message = u"These are the elements in the \"" + arguments[1] \
2223                 + u"\" category:$(eol)"
2224             elements = [
2225                 (
2226                     universe.categories[arguments[1]][x].key
2227                 ) for x in universe.categories[arguments[1]].keys()
2228             ]
2229             elements.sort()
2230             for element in elements:
2231                 message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2232         else:
2233             message = u"Category \"" + arguments[1] + u"\" does not exist."
2234     elif arguments[0] == u"file":
2235         if len(arguments) != 2:
2236             message = u"You must specify one file."
2237         elif arguments[1] in universe.files:
2238             message = u"These are the elements in the \"" + arguments[1] \
2239                 + u"\" file:$(eol)"
2240             elements = universe.files[arguments[1]].data.sections()
2241             elements.sort()
2242             for element in elements:
2243                 message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2244         else:
2245             message = u"Category \"" + arguments[1] + u"\" does not exist."
2246     elif arguments[0] == u"element":
2247         if len(arguments) != 2:
2248             message = u"You must specify one element."
2249         elif arguments[1] in universe.contents:
2250             element = universe.contents[arguments[1]]
2251             message = u"These are the properties of the \"" + arguments[1] \
2252                 + \
2253                 u"\" element (in \"" + \
2254                 element.origin.filename + u"\"):$(eol)"
2255             facets = element.facets()
2256             facets.sort()
2257             for facet in facets:
2258                 message += u"$(eol)   $(grn)" + facet + u": $(red)" \
2259                     + escape_macros(element.get(facet)) + u"$(nrm)"
2260         else:
2261             message = u"Element \"" + arguments[1] + u"\" does not exist."
2262     elif arguments[0] == u"result":
2263         if len(arguments) < 2:
2264             message = u"You need to specify an expression."
2265         else:
2266             try:
2267                 message = repr(eval(u" ".join(arguments[1:])))
2268             except:
2269                 message = u"Your expression raised an exception!"
2270     elif arguments[0] == u"log":
2271         if len(arguments) == 4:
2272             if re.match(u"^\d+$", arguments[3]) and int(arguments[3]) >= 0:
2273                 stop = int(arguments[3])
2274             else:
2275                 stop = -1
2276         else:
2277             stop = 0
2278         if len(arguments) >= 3:
2279             if re.match(u"^\d+$", arguments[2]) and int(arguments[2]) > 0:
2280                 start = int(arguments[2])
2281             else:
2282                 start = -1
2283         else:
2284             start = 10
2285         if len(arguments) >= 2:
2286             if re.match(u"^\d+$", arguments[1]) and 0 <= int(arguments[1]) <= 9:
2287                 level = int(arguments[1])
2288             else:
2289                 level = -1
2290         elif 0 <= actor.owner.account.getint(u"loglevel") <= 9:
2291             level = actor.owner.account.getint(u"loglevel")
2292         else:
2293             level = 1
2294         if level > -1 and start > -1 and stop > -1:
2295             message = get_loglines(level, start, stop)
2296         else:
2297             message = u"When specified, level must be 0-9 (default 1), " \
2298                 + u"start and stop must be >=1 (default 10 and 1)."
2299     else:
2300         message = u"I don't know what \"" + parameters + u"\" is."
2301     actor.send(message)
2302
2303
2304 def command_create(actor, parameters):
2305     u"""Create an element if it does not exist."""
2306     if not parameters:
2307         message = u"You must at least specify an element to create."
2308     elif not actor.owner:
2309         message = u""
2310     else:
2311         arguments = parameters.split()
2312         if len(arguments) == 1:
2313             arguments.append(u"")
2314         if len(arguments) == 2:
2315             element, filename = arguments
2316             if element in universe.contents:
2317                 message = u"The \"" + element + u"\" element already exists."
2318             else:
2319                 message = u"You create \"" + \
2320                     element + u"\" within the universe."
2321                 logline = actor.owner.account.get(
2322                     u"name"
2323                 ) + u" created an element: " + element
2324                 if filename:
2325                     logline += u" in file " + filename
2326                     if filename not in universe.files:
2327                         message += u" Warning: \"" + filename \
2328                             + u"\" is not yet included in any other file and will " \
2329                             + \
2330                             u"not be read on startup unless this is remedied."
2331                 Element(element, universe, filename)
2332                 log(logline, 6)
2333         elif len(arguments) > 2:
2334             message = u"You can only specify an element and a filename."
2335     actor.send(message)
2336
2337
2338 def command_destroy(actor, parameters):
2339     u"""Destroy an element if it exists."""
2340     if actor.owner:
2341         if not parameters:
2342             message = u"You must specify an element to destroy."
2343         else:
2344             if parameters not in universe.contents:
2345                 message = u"The \"" + parameters + \
2346                     u"\" element does not exist."
2347             else:
2348                 universe.contents[parameters].destroy()
2349                 message = u"You destroy \"" + parameters \
2350                     + u"\" within the universe."
2351                 log(
2352                     actor.owner.account.get(
2353                         u"name"
2354                     ) + u" destroyed an element: " + parameters,
2355                     6
2356                 )
2357         actor.send(message)
2358
2359
2360 def command_set(actor, parameters):
2361     u"""Set a facet of an element."""
2362     if not parameters:
2363         message = u"You must specify an element, a facet and a value."
2364     else:
2365         arguments = parameters.split(u" ", 2)
2366         if len(arguments) == 1:
2367             message = u"What facet of element \"" + arguments[0] \
2368                 + u"\" would you like to set?"
2369         elif len(arguments) == 2:
2370             message = u"What value would you like to set for the \"" \
2371                 + arguments[1] + u"\" facet of the \"" + arguments[0] \
2372                 + u"\" element?"
2373         else:
2374             element, facet, value = arguments
2375             if element not in universe.contents:
2376                 message = u"The \"" + element + u"\" element does not exist."
2377             else:
2378                 universe.contents[element].set(facet, value)
2379                 message = u"You have successfully (re)set the \"" + facet \
2380                     + u"\" facet of element \"" + element \
2381                     + u"\". Try \"show element " + \
2382                     element + u"\" for verification."
2383     actor.send(message)
2384
2385
2386 def command_delete(actor, parameters):
2387     u"""Delete a facet from an element."""
2388     if not parameters:
2389         message = u"You must specify an element and a facet."
2390     else:
2391         arguments = parameters.split(u" ")
2392         if len(arguments) == 1:
2393             message = u"What facet of element \"" + arguments[0] \
2394                 + u"\" would you like to delete?"
2395         elif len(arguments) != 2:
2396             message = u"You may only specify an element and a facet."
2397         else:
2398             element, facet = arguments
2399             if element not in universe.contents:
2400                 message = u"The \"" + element + u"\" element does not exist."
2401             elif facet not in universe.contents[element].facets():
2402                 message = u"The \"" + element + u"\" element has no \"" + facet \
2403                     + u"\" facet."
2404             else:
2405                 universe.contents[element].remove_facet(facet)
2406                 message = u"You have successfully deleted the \"" + facet \
2407                     + u"\" facet of element \"" + element \
2408                     + u"\". Try \"show element " + \
2409                     element + u"\" for verification."
2410     actor.send(message)
2411
2412
2413 def command_error(actor, input_data):
2414     u"""Generic error for an unrecognized command word."""
2415     import random
2416
2417     # 90% of the time use a generic error
2418     if random.randrange(10):
2419         message = u"I'm not sure what \"" + input_data + u"\" means..."
2420
2421     # 10% of the time use the classic diku error
2422     else:
2423         message = u"Arglebargle, glop-glyf!?!"
2424
2425     # send the error message
2426     actor.send(message)
2427
2428
2429 def daemonize(universe):
2430     u"""Fork and disassociate from everything."""
2431     import codecs
2432     import ctypes
2433     import ctypes.util
2434     import os
2435     import os.path
2436     import sys
2437
2438     # only if this is what we're configured to do
2439     if universe.contents[u"internal:process"].getboolean(u"daemon"):
2440
2441         # if possible, we want to rename the process to the same as the script
2442         # (these will need to be byte type during 2to3 migration)
2443         new_argv = "\0".join(sys.argv) + "\0"
2444         new_short_argv0 = os.path.basename(sys.argv[0]) + "\0"
2445
2446         # attempt the linux way first
2447         try:
2448             argv_array = ctypes.POINTER(ctypes.c_char_p)
2449             ctypes.pythonapi.Py_GetArgcArgv.argtypes = (
2450                 ctypes.POINTER(ctypes.c_int),
2451                 ctypes.POINTER(argv_array)
2452             )
2453             argc = argv_array()
2454             ctypes.pythonapi.Py_GetArgcArgv(
2455                 ctypes.c_int(0),
2456                 ctypes.pointer(argc)
2457             )
2458             old_argv0_size = len(argc.contents.value)
2459             ctypes.memset(argc.contents, 0, len(new_argv) + old_argv0_size)
2460             ctypes.memmove(argc.contents, new_argv, len(new_argv))
2461             ctypes.CDLL(ctypes.util.find_library(u"c")).prctl(
2462                 15,
2463                 new_short_argv0,
2464                 0,
2465                 0,
2466                 0
2467             )
2468
2469         except:
2470
2471             # since that failed, maybe it's bsd?
2472             try:
2473
2474                 # much simpler, since bsd has a libc function call for this
2475                 ctypes.CDLL(ctypes.util.find_library(u"c")).setproctitle(
2476                     new_argv
2477                 )
2478
2479             except:
2480
2481                 # that didn't work either, so just log that we couldn't
2482                 log(u"Failed to rename the interpreter process (cosmetic).")
2483
2484         # log before we start forking around, so the terminal gets the message
2485         log(u"Disassociating from the controlling terminal.")
2486
2487         # fork off and die, so we free up the controlling terminal
2488         if os.fork():
2489             os._exit(0)
2490
2491         # switch to a new process group
2492         os.setsid()
2493
2494         # fork some more, this time to free us from the old process group
2495         if os.fork():
2496             os._exit(0)
2497
2498         # reset the working directory so we don't needlessly tie up mounts
2499         os.chdir(u"/")
2500
2501         # clear the file creation mask so we can bend it to our will later
2502         os.umask(0)
2503
2504         # redirect stdin/stdout/stderr and close off their former descriptors
2505         for stdpipe in range(3):
2506             os.close(stdpipe)
2507         sys.stdin = codecs.open(u"/dev/null", u"r", u"utf-8")
2508         sys.__stdin__ = codecs.open(u"/dev/null", u"r", u"utf-8")
2509         sys.stdout = codecs.open(u"/dev/null", u"w", u"utf-8")
2510         sys.stderr = codecs.open(u"/dev/null", u"w", u"utf-8")
2511         sys.__stdout__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2512         sys.__stderr__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2513
2514
2515 def create_pidfile(universe):
2516     u"""Write a file containing the current process ID."""
2517     import codecs
2518     import os
2519     import os.path
2520     pid = unicode(os.getpid())
2521     log(u"Process ID: " + pid)
2522     file_name = universe.contents[u"internal:process"].get(u"pidfile")
2523     if file_name:
2524         if not os.path.isabs(file_name):
2525             file_name = os.path.join(universe.startdir, file_name)
2526         file_descriptor = codecs.open(file_name, u"w", u"utf-8")
2527         file_descriptor.write(pid + u"\n")
2528         file_descriptor.flush()
2529         file_descriptor.close()
2530
2531
2532 def remove_pidfile(universe):
2533     u"""Remove the file containing the current process ID."""
2534     import os
2535     import os.path
2536     file_name = universe.contents[u"internal:process"].get(u"pidfile")
2537     if file_name:
2538         if not os.path.isabs(file_name):
2539             file_name = os.path.join(universe.startdir, file_name)
2540         if os.access(file_name, os.W_OK):
2541             os.remove(file_name)
2542
2543
2544 def excepthook(excepttype, value, tracebackdata):
2545     u"""Handle uncaught exceptions."""
2546     import traceback
2547
2548     # assemble the list of errors into a single string
2549     message = u"".join(
2550         traceback.format_exception(excepttype, value, tracebackdata)
2551     )
2552
2553     # try to log it, if possible
2554     try:
2555         log(message, 9)
2556     except:
2557         pass
2558
2559     # try to write it to stderr, if possible
2560     try:
2561         sys.stderr.write(message)
2562     except:
2563         pass
2564
2565
2566 def sighook(what, where):
2567     u"""Handle external signals."""
2568     import signal
2569
2570     # a generic message
2571     message = u"Caught signal: "
2572
2573     # for a hangup signal
2574     if what == signal.SIGHUP:
2575         message += u"hangup (reloading)"
2576         universe.reload_flag = True
2577
2578     # for a terminate signal
2579     elif what == signal.SIGTERM:
2580         message += u"terminate (halting)"
2581         universe.terminate_flag = True
2582
2583     # catchall for unexpected signals
2584     else:
2585         message += unicode(what) + u" (unhandled)"
2586
2587     # log what happened
2588     log(message, 8)
2589
2590
2591 def override_excepthook():
2592     u"""Redefine sys.excepthook with our own."""
2593     import sys
2594     sys.excepthook = excepthook
2595
2596
2597 def assign_sighook():
2598     u"""Assign a customized handler for some signals."""
2599     import signal
2600     signal.signal(signal.SIGHUP, sighook)
2601     signal.signal(signal.SIGTERM, sighook)
2602
2603
2604 def setup():
2605     """This contains functions to be performed when starting the engine."""
2606     import sys
2607
2608     # see if a configuration file was specified
2609     if len(sys.argv) > 1:
2610         conffile = sys.argv[1]
2611     else:
2612         conffile = u""
2613
2614     # the big bang
2615     global universe
2616     universe = Universe(conffile, True)
2617
2618     # log an initial message
2619     log(u"Started mudpy with command line: " + u" ".join(sys.argv))
2620
2621     # fork and disassociate
2622     daemonize(universe)
2623
2624     # override the default exception handler so we get logging first thing
2625     override_excepthook()
2626
2627     # set up custom signal handlers
2628     assign_sighook()
2629
2630     # make the pidfile
2631     create_pidfile(universe)
2632
2633     # pass the initialized universe back
2634     return universe
2635
2636
2637 def finish():
2638     """This contains functions to be performed when shutting down the engine."""
2639
2640     # the loop has terminated, so save persistent data
2641     universe.save()
2642
2643     # log a final message
2644     log(u"Shutting down now.")
2645
2646     # get rid of the pidfile
2647     remove_pidfile(universe)