Additional style cleanup
[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 self.category not 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 filename not 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 self.state is not 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(u"Sending to %s raised an exception (broken pipe?)."
913                     % account, 7)
914                 pass
915
916     def enqueue_input(self):
917         u"""Process and enqueue any new input."""
918         import telnet
919         import unicodedata
920
921         # check for some input
922         try:
923             raw_input = self.connection.recv(1024)
924         except:
925             raw_input = ""
926
927         # we got something
928         if raw_input:
929
930             # tack this on to any previous partial
931             self.partial_input += raw_input
932
933             # reply to and remove any IAC negotiation codes
934             telnet.negotiate_telnet_options(self)
935
936             # separate multiple input lines
937             new_input_lines = self.partial_input.split("\n")
938
939             # if input doesn't end in a newline, replace the
940             # held partial input with the last line of it
941             if not self.partial_input.endswith("\n"):
942                 self.partial_input = new_input_lines.pop()
943
944             # otherwise, chop off the extra null input and reset
945             # the held partial input
946             else:
947                 new_input_lines.pop()
948                 self.partial_input = ""
949
950             # iterate over the remaining lines
951             for line in new_input_lines:
952
953                 # strip off extra whitespace
954                 line = line.strip()
955
956                 # make sure it's valid unicode (probably no longer needed)
957                 try:
958                     unicode(line, u"utf-8")
959                 except UnicodeDecodeError:
960                     logline = u"Non-unicode data from "
961                     if self.account and self.account.get(u"name"):
962                         logline += self.account.get(u"name") + u": "
963                     else:
964                         logline += u"unknown user: "
965                     logline += repr(line)
966                     log(logline, 4)
967                     line = ""
968
969                 # log non-printable characters remaining
970                 if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.HIM):
971                     asciiline = filter(lambda x: " " <= x <= "~", line)
972                     if line != asciiline:
973                         logline = u"Non-ASCII characters from "
974                         if self.account and self.account.get(u"name"):
975                             logline += self.account.get(u"name") + u": "
976                         else:
977                             logline += u"unknown user: "
978                         logline += repr(line)
979                         log(logline, 4)
980                         line = asciiline
981
982                 # put on the end of the queue
983                 self.input_queue.append(
984                     unicodedata.normalize(u"NFKC", unicode(line, u"utf-8"))
985                 )
986
987     def new_avatar(self):
988         u"""Instantiate a new, unconfigured avatar for this user."""
989         counter = 0
990         while u"avatar:" + self.account.get(u"name") + u":" + unicode(
991             counter
992         ) in universe.categories[u"actor"].keys():
993             counter += 1
994         self.avatar = Element(
995             u"actor:avatar:" + self.account.get(u"name") + u":" + unicode(
996                 counter
997             ),
998             universe
999         )
1000         self.avatar.append(u"inherit", u"archetype:avatar")
1001         self.account.append(u"avatars", self.avatar.key)
1002
1003     def delete_avatar(self, avatar):
1004         u"""Remove an avatar from the world and from the user's list."""
1005         if self.avatar is universe.contents[avatar]:
1006             self.avatar = None
1007         universe.contents[avatar].destroy()
1008         avatars = self.account.getlist(u"avatars")
1009         avatars.remove(avatar)
1010         self.account.set(u"avatars", avatars)
1011
1012     def activate_avatar_by_index(self, index):
1013         u"""Enter the world with a particular indexed avatar."""
1014         self.avatar = universe.contents[
1015             self.account.getlist(u"avatars")[index]]
1016         self.avatar.owner = self
1017         self.state = u"active"
1018         self.avatar.go_home()
1019
1020     def deactivate_avatar(self):
1021         u"""Have the active avatar leave the world."""
1022         if self.avatar:
1023             current = self.avatar.get(u"location")
1024             if current:
1025                 self.avatar.set(u"default_location", current)
1026                 self.avatar.echo_to_location(
1027                     u"You suddenly wonder where " + self.avatar.get(
1028                         u"name"
1029                     ) + u" went."
1030                 )
1031                 del universe.contents[current].contents[self.avatar.key]
1032                 self.avatar.remove_facet(u"location")
1033             self.avatar.owner = None
1034             self.avatar = None
1035
1036     def destroy(self):
1037         u"""Destroy the user and associated avatars."""
1038         for avatar in self.account.getlist(u"avatars"):
1039             self.delete_avatar(avatar)
1040         self.account.destroy()
1041
1042     def list_avatar_names(self):
1043         u"""List names of assigned avatars."""
1044         return [
1045             universe.contents[avatar].get(
1046                 u"name"
1047             ) for avatar in self.account.getlist(u"avatars")
1048         ]
1049
1050
1051 def broadcast(message, add_prompt=True):
1052     u"""Send a message to all connected users."""
1053     for each_user in universe.userlist:
1054         each_user.send(u"$(eol)" + message, add_prompt=add_prompt)
1055
1056
1057 def log(message, level=0):
1058     u"""Log a message."""
1059     import codecs
1060     import os.path
1061     import syslog
1062     import time
1063
1064     # a couple references we need
1065     file_name = universe.categories[u"internal"][u"logging"].get(u"file")
1066     max_log_lines = universe.categories[u"internal"][u"logging"].getint(
1067         u"max_log_lines"
1068     )
1069     syslog_name = universe.categories[u"internal"][u"logging"].get(u"syslog")
1070     timestamp = time.asctime()[4:19]
1071
1072     # turn the message into a list of lines
1073     lines = filter(
1074         lambda x: x != u"", [(x.rstrip()) for x in message.split(u"\n")]
1075     )
1076
1077     # send the timestamp and line to a file
1078     if file_name:
1079         if not os.path.isabs(file_name):
1080             file_name = os.path.join(universe.startdir, file_name)
1081         file_descriptor = codecs.open(file_name, u"a", u"utf-8")
1082         for line in lines:
1083             file_descriptor.write(timestamp + u" " + line + u"\n")
1084         file_descriptor.flush()
1085         file_descriptor.close()
1086
1087     # send the timestamp and line to standard output
1088     if universe.categories[u"internal"][u"logging"].getboolean(u"stdout"):
1089         for line in lines:
1090             print(timestamp + u" " + line)
1091
1092     # send the line to the system log
1093     if syslog_name:
1094         syslog.openlog(
1095             syslog_name.encode("utf-8"),
1096             syslog.LOG_PID,
1097             syslog.LOG_INFO | syslog.LOG_DAEMON
1098         )
1099         for line in lines:
1100             syslog.syslog(line)
1101         syslog.closelog()
1102
1103     # display to connected administrators
1104     for user in universe.userlist:
1105         if user.state == u"active" and user.account.getboolean(
1106            u"administrator"
1107            ) and user.account.getint(u"loglevel") <= level:
1108             # iterate over every line in the message
1109             full_message = u""
1110             for line in lines:
1111                 full_message += (
1112                     u"$(bld)$(red)" + timestamp + u" "
1113                     + line.replace(u"$(", u"$_(") + u"$(nrm)$(eol)")
1114             user.send(full_message, flush=True)
1115
1116     # add to the recent log list
1117     for line in lines:
1118         while 0 < len(universe.loglines) >= max_log_lines:
1119             del universe.loglines[0]
1120         universe.loglines.append((level, timestamp + u" " + line))
1121
1122
1123 def get_loglines(level, start, stop):
1124     u"""Return a specific range of loglines filtered by level."""
1125
1126     # filter the log lines
1127     loglines = filter(lambda x: x[0] >= level, universe.loglines)
1128
1129     # we need these in several places
1130     total_count = unicode(len(universe.loglines))
1131     filtered_count = len(loglines)
1132
1133     # don't proceed if there are no lines
1134     if filtered_count:
1135
1136         # can't start before the begining or at the end
1137         if start > filtered_count:
1138             start = filtered_count
1139         if start < 1:
1140             start = 1
1141
1142         # can't stop before we start
1143         if stop > start:
1144             stop = start
1145         elif stop < 1:
1146             stop = 1
1147
1148         # some preamble
1149         message = u"There are " + unicode(total_count)
1150         message += u" log lines in memory and " + unicode(filtered_count)
1151         message += u" at or above level " + unicode(level) + u"."
1152         message += u" The matching lines from " + unicode(stop) + u" to "
1153         message += unicode(start) + u" are:$(eol)$(eol)"
1154
1155         # add the text from the selected lines
1156         if stop > 1:
1157             range_lines = loglines[-start:-(stop - 1)]
1158         else:
1159             range_lines = loglines[-start:]
1160         for line in range_lines:
1161             message += u"   (" + unicode(line[0]) + u") " + line[1].replace(
1162                 u"$(", u"$_("
1163             ) + u"$(eol)"
1164
1165     # there were no lines
1166     else:
1167         message = u"None of the " + unicode(total_count)
1168         message += u" lines in memory matches your request."
1169
1170     # pass it back
1171     return message
1172
1173
1174 def glyph_columns(character):
1175     u"""Convenience function to return the column width of a glyph."""
1176     import unicodedata
1177     if unicodedata.east_asian_width(character) in u"FW":
1178         return 2
1179     else:
1180         return 1
1181
1182
1183 def wrap_ansi_text(text, width):
1184     u"""Wrap text with arbitrary width while ignoring ANSI colors."""
1185     import unicodedata
1186
1187     # the current position in the entire text string, including all
1188     # characters, printable or otherwise
1189     abs_pos = 0
1190
1191     # the current text position relative to the begining of the line,
1192     # ignoring color escape sequences
1193     rel_pos = 0
1194
1195     # the absolute position of the most recent whitespace character
1196     last_whitespace = 0
1197
1198     # whether the current character is part of a color escape sequence
1199     escape = False
1200
1201     # normalize any potentially composited unicode before we count it
1202     text = unicodedata.normalize(u"NFKC", text)
1203
1204     # iterate over each character from the begining of the text
1205     for each_character in text:
1206
1207         # the current character is the escape character
1208         if each_character == u"\x1b" and not escape:
1209             escape = True
1210
1211         # the current character is within an escape sequence
1212         elif escape:
1213
1214             # the current character is m, which terminates the
1215             # escape sequence
1216             if each_character == u"m":
1217                 escape = False
1218
1219         # the current character is a newline, so reset the relative
1220         # position (start a new line)
1221         elif each_character == u"\n":
1222             rel_pos = 0
1223             last_whitespace = abs_pos
1224
1225         # the current character meets the requested maximum line width,
1226         # so we need to backtrack and find a space at which to wrap;
1227         # special care is taken to avoid an off-by-one in case the
1228         # current character is a double-width glyph
1229         elif each_character != u"\r" and (
1230             rel_pos >= width or (
1231                 rel_pos >= width - 1 and glyph_columns(
1232                     each_character
1233                 ) == 2
1234             )
1235         ):
1236
1237             # it's always possible we landed on whitespace
1238             if unicodedata.category(each_character) in (u"Cc", u"Zs"):
1239                 last_whitespace = abs_pos
1240
1241             # insert an eol in place of the space
1242             text = text[:last_whitespace] + \
1243                 u"\r\n" + text[last_whitespace + 1:]
1244
1245             # increase the absolute position because an eol is two
1246             # characters but the space it replaced was only one
1247             abs_pos += 1
1248
1249             # now we're at the begining of a new line, plus the
1250             # number of characters wrapped from the previous line
1251             rel_pos = 0
1252             for remaining_characters in text[last_whitespace:abs_pos]:
1253                 rel_pos += glyph_columns(remaining_characters)
1254
1255         # as long as the character is not a carriage return and the
1256         # other above conditions haven't been met, count it as a
1257         # printable character
1258         elif each_character != u"\r":
1259             rel_pos += glyph_columns(each_character)
1260             if unicodedata.category(each_character) in (u"Cc", u"Zs"):
1261                 last_whitespace = abs_pos
1262
1263         # increase the absolute position for every character
1264         abs_pos += 1
1265
1266     # return the newly-wrapped text
1267     return text
1268
1269
1270 def weighted_choice(data):
1271     u"""Takes a dict weighted by value and returns a random key."""
1272     import random
1273
1274     # this will hold our expanded list of keys from the data
1275     expanded = []
1276
1277     # create the expanded list of keys
1278     for key in data.keys():
1279         for count in range(data[key]):
1280             expanded.append(key)
1281
1282     # return one at random
1283     return random.choice(expanded)
1284
1285
1286 def random_name():
1287     u"""Returns a random character name."""
1288     import random
1289
1290     # the vowels and consonants needed to create romaji syllables
1291     vowels = [
1292         u"a",
1293         u"i",
1294         u"u",
1295         u"e",
1296         u"o"
1297     ]
1298     consonants = [
1299         u"'",
1300         u"k",
1301         u"z",
1302         u"s",
1303         u"sh",
1304         u"z",
1305         u"j",
1306         u"t",
1307         u"ch",
1308         u"ts",
1309         u"d",
1310         u"n",
1311         u"h",
1312         u"f",
1313         u"m",
1314         u"y",
1315         u"r",
1316         u"w"
1317     ]
1318
1319     # this dict will hold our weighted list of syllables
1320     syllables = {}
1321
1322     # generate the list with an even weighting
1323     for consonant in consonants:
1324         for vowel in vowels:
1325             syllables[consonant + vowel] = 1
1326
1327     # we'll build the name into this string
1328     name = u""
1329
1330     # create a name of random length from the syllables
1331     for syllable in range(random.randrange(2, 6)):
1332         name += weighted_choice(syllables)
1333
1334     # strip any leading quotemark, capitalize and return the name
1335     return name.strip(u"'").capitalize()
1336
1337
1338 def replace_macros(user, text, is_input=False):
1339     u"""Replaces macros in text output."""
1340     import codecs
1341     import data
1342     import os.path
1343
1344     # third person pronouns
1345     pronouns = {
1346         u"female": {u"obj": u"her", u"pos": u"hers", u"sub": u"she"},
1347         u"male": {u"obj": u"him", u"pos": u"his", u"sub": u"he"},
1348         u"neuter": {u"obj": u"it", u"pos": u"its", u"sub": u"it"}
1349     }
1350
1351     # a dict of replacement macros
1352     macros = {
1353         u"eol": u"\r\n",
1354         u"bld": unichr(27) + u"[1m",
1355         u"nrm": unichr(27) + u"[0m",
1356         u"blk": unichr(27) + u"[30m",
1357         u"blu": unichr(27) + u"[34m",
1358         u"cyn": unichr(27) + u"[36m",
1359         u"grn": unichr(27) + u"[32m",
1360         u"mgt": unichr(27) + u"[35m",
1361         u"red": unichr(27) + u"[31m",
1362         u"yel": unichr(27) + u"[33m",
1363     }
1364
1365     # add dynamic macros where possible
1366     if user.account:
1367         account_name = user.account.get(u"name")
1368         if account_name:
1369             macros[u"account"] = account_name
1370     if user.avatar:
1371         avatar_gender = user.avatar.get(u"gender")
1372         if avatar_gender:
1373             macros[u"tpop"] = pronouns[avatar_gender][u"obj"]
1374             macros[u"tppp"] = pronouns[avatar_gender][u"pos"]
1375             macros[u"tpsp"] = pronouns[avatar_gender][u"sub"]
1376
1377     # loop until broken
1378     while True:
1379
1380         # find and replace per the macros dict
1381         macro_start = text.find(u"$(")
1382         if macro_start == -1:
1383             break
1384         macro_end = text.find(u")", macro_start) + 1
1385         macro = text[macro_start + 2:macro_end - 1]
1386         if macro in macros.keys():
1387             replacement = macros[macro]
1388
1389         # this is how we handle local file inclusion (dangerous!)
1390         elif macro.startswith(u"inc:"):
1391             incfile = data.find_file(macro[4:], universe=universe)
1392             if os.path.exists(incfile):
1393                 incfd = codecs.open(incfile, u"r", u"utf-8")
1394                 replacement = u""
1395                 for line in incfd:
1396                     if line.endswith(u"\n") and not line.endswith(u"\r\n"):
1397                         line = line.replace(u"\n", u"\r\n")
1398                     replacement += line
1399                 # lose the trailing eol
1400                 replacement = replacement[:-2]
1401             else:
1402                 replacement = u""
1403                 log(u"Couldn't read included " + incfile + u" file.", 6)
1404
1405         # if we get here, log and replace it with null
1406         else:
1407             replacement = u""
1408             if not is_input:
1409                 log(u"Unexpected replacement macro " +
1410                     macro + u" encountered.", 6)
1411
1412         # and now we act on the replacement
1413         text = text.replace(u"$(" + macro + u")", replacement)
1414
1415     # replace the look-like-a-macro sequence
1416     text = text.replace(u"$_(", u"$(")
1417
1418     return text
1419
1420
1421 def escape_macros(text):
1422     u"""Escapes replacement macros in text."""
1423     return text.replace(u"$(", u"$_(")
1424
1425
1426 def first_word(text, separator=u" "):
1427     u"""Returns a tuple of the first word and the rest."""
1428     if text:
1429         if text.find(separator) > 0:
1430             return text.split(separator, 1)
1431         else:
1432             return text, u""
1433     else:
1434         return u"", u""
1435
1436
1437 def on_pulse():
1438     u"""The things which should happen on each pulse, aside from reloads."""
1439     import time
1440
1441     # open the listening socket if it hasn't been already
1442     if not hasattr(universe, u"listening_socket"):
1443         universe.initialize_server_socket()
1444
1445     # assign a user if a new connection is waiting
1446     user = check_for_connection(universe.listening_socket)
1447     if user:
1448         universe.userlist.append(user)
1449
1450     # iterate over the connected users
1451     for user in universe.userlist:
1452         user.pulse()
1453
1454     # add an element for counters if it doesn't exist
1455     if u"counters" not in universe.categories[u"internal"]:
1456         universe.categories[u"internal"][u"counters"] = Element(
1457             u"internal:counters", universe
1458         )
1459
1460     # update the log every now and then
1461     if not universe.categories[u"internal"][u"counters"].getint(u"mark"):
1462         log(unicode(len(universe.userlist)) + u" connection(s)")
1463         universe.categories[u"internal"][u"counters"].set(
1464             u"mark", universe.categories[u"internal"][u"time"].getint(
1465                 u"frequency_log"
1466             )
1467         )
1468     else:
1469         universe.categories[u"internal"][u"counters"].set(
1470             u"mark", universe.categories[u"internal"][u"counters"].getint(
1471                 u"mark"
1472             ) - 1
1473         )
1474
1475     # periodically save everything
1476     if not universe.categories[u"internal"][u"counters"].getint(u"save"):
1477         universe.save()
1478         universe.categories[u"internal"][u"counters"].set(
1479             u"save", universe.categories[u"internal"][u"time"].getint(
1480                 u"frequency_save"
1481             )
1482         )
1483     else:
1484         universe.categories[u"internal"][u"counters"].set(
1485             u"save", universe.categories[u"internal"][u"counters"].getint(
1486                 u"save"
1487             ) - 1
1488         )
1489
1490     # pause for a configurable amount of time (decimal seconds)
1491     time.sleep(universe.categories[u"internal"]
1492                [u"time"].getfloat(u"increment"))
1493
1494     # increase the elapsed increment counter
1495     universe.categories[u"internal"][u"counters"].set(
1496         u"elapsed", universe.categories[u"internal"][u"counters"].getint(
1497             u"elapsed"
1498         ) + 1
1499     )
1500
1501
1502 def reload_data():
1503     u"""Reload all relevant objects."""
1504     for user in universe.userlist[:]:
1505         user.reload()
1506     for element in universe.contents.values():
1507         if element.origin.is_writeable():
1508             element.reload()
1509     universe.load()
1510
1511
1512 def check_for_connection(listening_socket):
1513     u"""Check for a waiting connection and return a new user object."""
1514     import telnet
1515
1516     # try to accept a new connection
1517     try:
1518         connection, address = listening_socket.accept()
1519     except:
1520         return None
1521
1522     # note that we got one
1523     log(u"Connection from " + address[0], 2)
1524
1525     # disable blocking so we can proceed whether or not we can send/receive
1526     connection.setblocking(0)
1527
1528     # create a new user object
1529     user = User()
1530
1531     # associate this connection with it
1532     user.connection = connection
1533
1534     # set the user's ipa from the connection's ipa
1535     user.address = address[0]
1536
1537     # let the client know we WILL EOR (RFC 885)
1538     telnet.enable(user, telnet.TELOPT_EOR, telnet.US)
1539     user.negotiation_pause = 2
1540
1541     # return the new user object
1542     return user
1543
1544
1545 def get_menu(state, error=None, choices=None):
1546     u"""Show the correct menu text to a user."""
1547
1548     # make sure we don't reuse a mutable sequence by default
1549     if choices is None:
1550         choices = {}
1551
1552     # get the description or error text
1553     message = get_menu_description(state, error)
1554
1555     # get menu choices for the current state
1556     message += get_formatted_menu_choices(state, choices)
1557
1558     # try to get a prompt, if it was defined
1559     message += get_menu_prompt(state)
1560
1561     # throw in the default choice, if it exists
1562     message += get_formatted_default_menu_choice(state)
1563
1564     # display a message indicating if echo is off
1565     message += get_echo_message(state)
1566
1567     # return the assembly of various strings defined above
1568     return message
1569
1570
1571 def menu_echo_on(state):
1572     u"""True if echo is on, false if it is off."""
1573     return universe.categories[u"menu"][state].getboolean(u"echo", True)
1574
1575
1576 def get_echo_message(state):
1577     u"""Return a message indicating that echo is off."""
1578     if menu_echo_on(state):
1579         return u""
1580     else:
1581         return u"(won't echo) "
1582
1583
1584 def get_default_menu_choice(state):
1585     u"""Return the default choice for a menu."""
1586     return universe.categories[u"menu"][state].get(u"default")
1587
1588
1589 def get_formatted_default_menu_choice(state):
1590     u"""Default menu choice foratted for inclusion in a prompt string."""
1591     default_choice = get_default_menu_choice(state)
1592     if default_choice:
1593         return u"[$(red)" + default_choice + u"$(nrm)] "
1594     else:
1595         return u""
1596
1597
1598 def get_menu_description(state, error):
1599     u"""Get the description or error text."""
1600
1601     # an error condition was raised by the handler
1602     if error:
1603
1604         # try to get an error message matching the condition
1605         # and current state
1606         description = universe.categories[
1607             u"menu"][state].get(u"error_" + error)
1608         if not description:
1609             description = u"That is not a valid choice..."
1610         description = u"$(red)" + description + u"$(nrm)"
1611
1612     # there was no error condition
1613     else:
1614
1615         # try to get a menu description for the current state
1616         description = universe.categories[u"menu"][state].get(u"description")
1617
1618     # return the description or error message
1619     if description:
1620         description += u"$(eol)$(eol)"
1621     return description
1622
1623
1624 def get_menu_prompt(state):
1625     u"""Try to get a prompt, if it was defined."""
1626     prompt = universe.categories[u"menu"][state].get(u"prompt")
1627     if prompt:
1628         prompt += u" "
1629     return prompt
1630
1631
1632 def get_menu_choices(user):
1633     u"""Return a dict of choice:meaning."""
1634     menu = universe.categories[u"menu"][user.state]
1635     create_choices = menu.get(u"create")
1636     if create_choices:
1637         choices = eval(create_choices)
1638     else:
1639         choices = {}
1640     ignores = []
1641     options = {}
1642     creates = {}
1643     for facet in menu.facets():
1644         if facet.startswith(u"demand_") and not eval(
1645            universe.categories[u"menu"][user.state].get(facet)
1646            ):
1647             ignores.append(facet.split(u"_", 2)[1])
1648         elif facet.startswith(u"create_"):
1649             creates[facet] = facet.split(u"_", 2)[1]
1650         elif facet.startswith(u"choice_"):
1651             options[facet] = facet.split(u"_", 2)[1]
1652     for facet in creates.keys():
1653         if not creates[facet] in ignores:
1654             choices[creates[facet]] = eval(menu.get(facet))
1655     for facet in options.keys():
1656         if not options[facet] in ignores:
1657             choices[options[facet]] = menu.get(facet)
1658     return choices
1659
1660
1661 def get_formatted_menu_choices(state, choices):
1662     u"""Returns a formatted string of menu choices."""
1663     choice_output = u""
1664     choice_keys = choices.keys()
1665     choice_keys.sort()
1666     for choice in choice_keys:
1667         choice_output += u"   [$(red)" + choice + u"$(nrm)]  " + choices[
1668             choice
1669         ] + u"$(eol)"
1670     if choice_output:
1671         choice_output += u"$(eol)"
1672     return choice_output
1673
1674
1675 def get_menu_branches(state):
1676     u"""Return a dict of choice:branch."""
1677     branches = {}
1678     for facet in universe.categories[u"menu"][state].facets():
1679         if facet.startswith(u"branch_"):
1680             branches[
1681                 facet.split(u"_", 2)[1]
1682             ] = universe.categories[u"menu"][state].get(facet)
1683     return branches
1684
1685
1686 def get_default_branch(state):
1687     u"""Return the default branch."""
1688     return universe.categories[u"menu"][state].get(u"branch")
1689
1690
1691 def get_choice_branch(user, choice):
1692     u"""Returns the new state matching the given choice."""
1693     branches = get_menu_branches(user.state)
1694     if choice in branches.keys():
1695         return branches[choice]
1696     elif choice in user.menu_choices.keys():
1697         return get_default_branch(user.state)
1698     else:
1699         return u""
1700
1701
1702 def get_menu_actions(state):
1703     u"""Return a dict of choice:branch."""
1704     actions = {}
1705     for facet in universe.categories[u"menu"][state].facets():
1706         if facet.startswith(u"action_"):
1707             actions[
1708                 facet.split(u"_", 2)[1]
1709             ] = universe.categories[u"menu"][state].get(facet)
1710     return actions
1711
1712
1713 def get_default_action(state):
1714     u"""Return the default action."""
1715     return universe.categories[u"menu"][state].get(u"action")
1716
1717
1718 def get_choice_action(user, choice):
1719     u"""Run any indicated script for the given choice."""
1720     actions = get_menu_actions(user.state)
1721     if choice in actions.keys():
1722         return actions[choice]
1723     elif choice in user.menu_choices.keys():
1724         return get_default_action(user.state)
1725     else:
1726         return u""
1727
1728
1729 def handle_user_input(user):
1730     u"""The main handler, branches to a state-specific handler."""
1731     import telnet
1732
1733     # if the user's client echo is off, send a blank line for aesthetics
1734     if telnet.is_enabled(user, telnet.TELOPT_ECHO, telnet.US):
1735         user.send(u"", add_prompt=False, prepend_padding=False)
1736
1737     # check to make sure the state is expected, then call that handler
1738     if u"handler_" + user.state in globals():
1739         exec(u"handler_" + user.state + u"(user)")
1740     else:
1741         generic_menu_handler(user)
1742
1743     # since we got input, flag that the menu/prompt needs to be redisplayed
1744     user.menu_seen = False
1745
1746     # update the last_input timestamp while we're at it
1747     user.last_input = universe.get_time()
1748
1749
1750 def generic_menu_handler(user):
1751     u"""A generic menu choice handler."""
1752
1753     # get a lower-case representation of the next line of input
1754     if user.input_queue:
1755         choice = user.input_queue.pop(0)
1756         if choice:
1757             choice = choice.lower()
1758     else:
1759         choice = u""
1760     if not choice:
1761         choice = get_default_menu_choice(user.state)
1762     if choice in user.menu_choices:
1763         exec(get_choice_action(user, choice))
1764         new_state = get_choice_branch(user, choice)
1765         if new_state:
1766             user.state = new_state
1767     else:
1768         user.error = u"default"
1769
1770
1771 def handler_entering_account_name(user):
1772     u"""Handle the login account name."""
1773
1774     # get the next waiting line of input
1775     input_data = user.input_queue.pop(0)
1776
1777     # did the user enter anything?
1778     if input_data:
1779
1780         # keep only the first word and convert to lower-case
1781         name = input_data.lower()
1782
1783         # fail if there are non-alphanumeric characters
1784         if name != filter(
1785            lambda x: x >= u"0" and x <= u"9" or x >= u"a" and x <= u"z", name
1786            ):
1787             user.error = u"bad_name"
1788
1789         # if that account exists, time to request a password
1790         elif name in universe.categories[u"account"]:
1791             user.account = universe.categories[u"account"][name]
1792             user.state = u"checking_password"
1793
1794         # otherwise, this could be a brand new user
1795         else:
1796             user.account = Element(u"account:" + name, universe)
1797             user.account.set(u"name", name)
1798             log(u"New user: " + name, 2)
1799             user.state = u"checking_new_account_name"
1800
1801     # if the user entered nothing for a name, then buhbye
1802     else:
1803         user.state = u"disconnecting"
1804
1805
1806 def handler_checking_password(user):
1807     u"""Handle the login account password."""
1808     import password
1809
1810     # get the next waiting line of input
1811     input_data = user.input_queue.pop(0)
1812
1813     # does the hashed input equal the stored hash?
1814     if password.verify(input_data, user.account.get(u"passhash")):
1815
1816         # if so, set the username and load from cold storage
1817         if not user.replace_old_connections():
1818             user.authenticate()
1819             user.state = u"main_utility"
1820
1821     # if at first your hashes don't match, try, try again
1822     elif user.password_tries < universe.categories[
1823         u"internal"
1824     ][
1825         u"limits"
1826     ].getint(
1827         u"password_tries"
1828     ) - 1:
1829         user.password_tries += 1
1830         user.error = u"incorrect"
1831
1832     # we've exceeded the maximum number of password failures, so disconnect
1833     else:
1834         user.send(
1835             u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1836         )
1837         user.state = u"disconnecting"
1838
1839
1840 def handler_entering_new_password(user):
1841     u"""Handle a new password entry."""
1842     import password
1843
1844     # get the next waiting line of input
1845     input_data = user.input_queue.pop(0)
1846
1847     # make sure the password is strong--at least one upper, one lower and
1848     # one digit, seven or more characters in length
1849     if len(input_data) > 6 and len(
1850        filter(lambda x: x >= u"0" and x <= u"9", input_data)
1851        ) and len(
1852         filter(lambda x: x >= u"A" and x <= u"Z", input_data)
1853     ) and len(
1854         filter(lambda x: x >= u"a" and x <= u"z", input_data)
1855     ):
1856
1857         # hash and store it, then move on to verification
1858         user.account.set(u"passhash", password.create(input_data))
1859         user.state = u"verifying_new_password"
1860
1861     # the password was weak, try again if you haven't tried too many times
1862     elif user.password_tries < universe.categories[
1863         u"internal"
1864     ][
1865         u"limits"
1866     ].getint(
1867         u"password_tries"
1868     ) - 1:
1869         user.password_tries += 1
1870         user.error = u"weak"
1871
1872     # too many tries, so adios
1873     else:
1874         user.send(
1875             u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1876         )
1877         user.account.destroy()
1878         user.state = u"disconnecting"
1879
1880
1881 def handler_verifying_new_password(user):
1882     u"""Handle the re-entered new password for verification."""
1883     import password
1884
1885     # get the next waiting line of input
1886     input_data = user.input_queue.pop(0)
1887
1888     # hash the input and match it to storage
1889     if password.verify(input_data, user.account.get(u"passhash")):
1890         user.authenticate()
1891
1892         # the hashes matched, so go active
1893         if not user.replace_old_connections():
1894             user.state = u"main_utility"
1895
1896     # go back to entering the new password as long as you haven't tried
1897     # too many times
1898     elif user.password_tries < universe.categories[
1899         u"internal"
1900     ][
1901         u"limits"
1902     ].getint(
1903         u"password_tries"
1904     ) - 1:
1905         user.password_tries += 1
1906         user.error = u"differs"
1907         user.state = u"entering_new_password"
1908
1909     # otherwise, sayonara
1910     else:
1911         user.send(
1912             u"$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)"
1913         )
1914         user.account.destroy()
1915         user.state = u"disconnecting"
1916
1917
1918 def handler_active(user):
1919     u"""Handle input for active users."""
1920
1921     # get the next waiting line of input
1922     input_data = user.input_queue.pop(0)
1923
1924     # is there input?
1925     if input_data:
1926
1927         # split out the command and parameters
1928         actor = user.avatar
1929         mode = actor.get(u"mode")
1930         if mode and input_data.startswith(u"!"):
1931             command_name, parameters = first_word(input_data[1:])
1932         elif mode == u"chat":
1933             command_name = u"say"
1934             parameters = input_data
1935         else:
1936             command_name, parameters = first_word(input_data)
1937
1938         # lowercase the command
1939         command_name = command_name.lower()
1940
1941         # the command matches a command word for which we have data
1942         if command_name in universe.categories[u"command"]:
1943             command = universe.categories[u"command"][command_name]
1944         else:
1945             command = None
1946
1947         # if it's allowed, do it
1948         if actor.can_run(command):
1949             exec(command.get(u"action"))
1950
1951         # otherwise, give an error
1952         elif command_name:
1953             command_error(actor, input_data)
1954
1955     # if no input, just idle back with a prompt
1956     else:
1957         user.send(u"", just_prompt=True)
1958
1959
1960 def command_halt(actor, parameters):
1961     u"""Halt the world."""
1962     if actor.owner:
1963
1964         # see if there's a message or use a generic one
1965         if parameters:
1966             message = u"Halting: " + parameters
1967         else:
1968             message = u"User " + actor.owner.account.get(
1969                 u"name"
1970             ) + u" halted the world."
1971
1972         # let everyone know
1973         broadcast(message, add_prompt=False)
1974         log(message, 8)
1975
1976         # set a flag to terminate the world
1977         universe.terminate_flag = True
1978
1979
1980 def command_reload(actor):
1981     u"""Reload all code modules, configs and data."""
1982     if actor.owner:
1983
1984         # let the user know and log
1985         actor.send(u"Reloading all code modules, configs and data.")
1986         log(
1987             u"User " +
1988             actor.owner.account.get(u"name") + u" reloaded the world.",
1989             8
1990         )
1991
1992         # set a flag to reload
1993         universe.reload_flag = True
1994
1995
1996 def command_quit(actor):
1997     u"""Leave the world and go back to the main menu."""
1998     if actor.owner:
1999         actor.owner.state = u"main_utility"
2000         actor.owner.deactivate_avatar()
2001
2002
2003 def command_help(actor, parameters):
2004     u"""List available commands and provide help for commands."""
2005
2006     # did the user ask for help on a specific command word?
2007     if parameters and actor.owner:
2008
2009         # is the command word one for which we have data?
2010         if parameters in universe.categories[u"command"]:
2011             command = universe.categories[u"command"][parameters]
2012         else:
2013             command = None
2014
2015         # only for allowed commands
2016         if actor.can_run(command):
2017
2018             # add a description if provided
2019             description = command.get(u"description")
2020             if not description:
2021                 description = u"(no short description provided)"
2022             if command.getboolean(u"administrative"):
2023                 output = u"$(red)"
2024             else:
2025                 output = u"$(grn)"
2026             output += parameters + u"$(nrm) - " + description + u"$(eol)$(eol)"
2027
2028             # add the help text if provided
2029             help_text = command.get(u"help")
2030             if not help_text:
2031                 help_text = u"No help is provided for this command."
2032             output += help_text
2033
2034             # list related commands
2035             see_also = command.getlist(u"see_also")
2036             if see_also:
2037                 really_see_also = u""
2038                 for item in see_also:
2039                     if item in universe.categories[u"command"]:
2040                         command = universe.categories[u"command"][item]
2041                         if actor.can_run(command):
2042                             if really_see_also:
2043                                 really_see_also += u", "
2044                             if command.getboolean(u"administrative"):
2045                                 really_see_also += u"$(red)"
2046                             else:
2047                                 really_see_also += u"$(grn)"
2048                             really_see_also += item + u"$(nrm)"
2049                 if really_see_also:
2050                     output += u"$(eol)$(eol)See also: " + really_see_also
2051
2052         # no data for the requested command word
2053         else:
2054             output = u"That is not an available command."
2055
2056     # no specific command word was indicated
2057     else:
2058
2059         # give a sorted list of commands with descriptions if provided
2060         output = u"These are the commands available to you:$(eol)$(eol)"
2061         sorted_commands = universe.categories[u"command"].keys()
2062         sorted_commands.sort()
2063         for item in sorted_commands:
2064             command = universe.categories[u"command"][item]
2065             if actor.can_run(command):
2066                 description = command.get(u"description")
2067                 if not description:
2068                     description = u"(no short description provided)"
2069                 if command.getboolean(u"administrative"):
2070                     output += u"   $(red)"
2071                 else:
2072                     output += u"   $(grn)"
2073                 output += item + u"$(nrm) - " + description + u"$(eol)"
2074         output += u"$(eol)Enter \"help COMMAND\" for help on a command " \
2075             + u"named \"COMMAND\"."
2076
2077     # send the accumulated output to the user
2078     actor.send(output)
2079
2080
2081 def command_move(actor, parameters):
2082     u"""Move the avatar in a given direction."""
2083     if parameters in universe.contents[actor.get(u"location")].portals():
2084         actor.move_direction(parameters)
2085     else:
2086         actor.send(u"You cannot go that way.")
2087
2088
2089 def command_look(actor, parameters):
2090     u"""Look around."""
2091     if parameters:
2092         actor.send(u"You can't look at or in anything yet.")
2093     else:
2094         actor.look_at(actor.get(u"location"))
2095
2096
2097 def command_say(actor, parameters):
2098     u"""Speak to others in the same room."""
2099     import unicodedata
2100
2101     # check for replacement macros and escape them
2102     parameters = escape_macros(parameters)
2103
2104     # if the message is wrapped in quotes, remove them and leave contents
2105     # intact
2106     if parameters.startswith(u"\"") and parameters.endswith(u"\""):
2107         message = parameters[1:-1]
2108         literal = True
2109
2110     # otherwise, get rid of stray quote marks on the ends of the message
2111     else:
2112         message = parameters.strip(u"\"'`")
2113         literal = False
2114
2115     # the user entered a message
2116     if message:
2117
2118         # match the punctuation used, if any, to an action
2119         actions = universe.categories[u"internal"][u"language"].getdict(
2120             u"actions"
2121         )
2122         default_punctuation = (
2123             universe.categories[u"internal"][u"language"].get(
2124                 u"default_punctuation"))
2125         action = u""
2126         for mark in actions.keys():
2127             if not literal and message.endswith(mark):
2128                 action = actions[mark]
2129                 break
2130
2131         # add punctuation if needed
2132         if not action:
2133             action = actions[default_punctuation]
2134             if message and not (
2135                literal or unicodedata.category(message[-1]) == u"Po"
2136                ):
2137                 message += default_punctuation
2138
2139         # failsafe checks to avoid unwanted reformatting and null strings
2140         if message and not literal:
2141
2142             # decapitalize the first letter to improve matching
2143             message = message[0].lower() + message[1:]
2144
2145             # iterate over all words in message, replacing typos
2146             typos = universe.categories[u"internal"][u"language"].getdict(
2147                 u"typos"
2148             )
2149             words = message.split()
2150             for index in range(len(words)):
2151                 word = words[index]
2152                 while unicodedata.category(word[0]) == u"Po":
2153                     word = word[1:]
2154                 while unicodedata.category(word[-1]) == u"Po":
2155                     word = word[:-1]
2156                 if word in typos.keys():
2157                     words[index] = words[index].replace(word, typos[word])
2158             message = u" ".join(words)
2159
2160             # capitalize the first letter
2161             message = message[0].upper() + message[1:]
2162
2163     # tell the room
2164     if message:
2165         actor.echo_to_location(
2166             actor.get(u"name") + u" " + action + u"s, \"" + message + u"\""
2167         )
2168         actor.send(u"You " + action + u", \"" + message + u"\"")
2169
2170     # there was no message
2171     else:
2172         actor.send(u"What do you want to say?")
2173
2174
2175 def command_chat(actor):
2176     u"""Toggle chat mode."""
2177     mode = actor.get(u"mode")
2178     if not mode:
2179         actor.set(u"mode", u"chat")
2180         actor.send(u"Entering chat mode (use $(grn)!chat$(nrm) to exit).")
2181     elif mode == u"chat":
2182         actor.remove_facet(u"mode")
2183         actor.send(u"Exiting chat mode.")
2184     else:
2185         actor.send(u"Sorry, but you're already busy with something else!")
2186
2187
2188 def command_show(actor, parameters):
2189     u"""Show program data."""
2190     import re
2191     message = u""
2192     arguments = parameters.split()
2193     if not parameters:
2194         message = u"What do you want to show?"
2195     elif arguments[0] == u"time":
2196         message = universe.categories[u"internal"][u"counters"].get(
2197             u"elapsed"
2198         ) + u" increments elapsed since the world was created."
2199     elif arguments[0] == u"categories":
2200         message = u"These are the element categories:$(eol)"
2201         categories = universe.categories.keys()
2202         categories.sort()
2203         for category in categories:
2204             message += u"$(eol)   $(grn)" + category + u"$(nrm)"
2205     elif arguments[0] == u"files":
2206         message = u"These are the current files containing the universe:$(eol)"
2207         filenames = universe.files.keys()
2208         filenames.sort()
2209         for filename in filenames:
2210             if universe.files[filename].is_writeable():
2211                 status = u"rw"
2212             else:
2213                 status = u"ro"
2214             message += u"$(eol)   $(red)(" + status + u") $(grn)" + filename \
2215                 + u"$(nrm)"
2216     elif arguments[0] == u"category":
2217         if len(arguments) != 2:
2218             message = u"You must specify one category."
2219         elif arguments[1] in universe.categories:
2220             message = u"These are the elements in the \"" + arguments[1] \
2221                 + u"\" category:$(eol)"
2222             elements = [
2223                 (
2224                     universe.categories[arguments[1]][x].key
2225                 ) for x in universe.categories[arguments[1]].keys()
2226             ]
2227             elements.sort()
2228             for element in elements:
2229                 message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2230         else:
2231             message = u"Category \"" + arguments[1] + u"\" does not exist."
2232     elif arguments[0] == u"file":
2233         if len(arguments) != 2:
2234             message = u"You must specify one file."
2235         elif arguments[1] in universe.files:
2236             message = u"These are the elements in the \"" + arguments[1] \
2237                 + u"\" file:$(eol)"
2238             elements = universe.files[arguments[1]].data.sections()
2239             elements.sort()
2240             for element in elements:
2241                 message += u"$(eol)   $(grn)" + element + u"$(nrm)"
2242         else:
2243             message = u"Category \"" + arguments[1] + u"\" does not exist."
2244     elif arguments[0] == u"element":
2245         if len(arguments) != 2:
2246             message = u"You must specify one element."
2247         elif arguments[1] in universe.contents:
2248             element = universe.contents[arguments[1]]
2249             message = u"These are the properties of the \"" + arguments[1] \
2250                 + \
2251                 u"\" element (in \"" + \
2252                 element.origin.filename + u"\"):$(eol)"
2253             facets = element.facets()
2254             facets.sort()
2255             for facet in facets:
2256                 message += u"$(eol)   $(grn)" + facet + u": $(red)" \
2257                     + escape_macros(element.get(facet)) + u"$(nrm)"
2258         else:
2259             message = u"Element \"" + arguments[1] + u"\" does not exist."
2260     elif arguments[0] == u"result":
2261         if len(arguments) < 2:
2262             message = u"You need to specify an expression."
2263         else:
2264             try:
2265                 message = repr(eval(u" ".join(arguments[1:])))
2266             except:
2267                 message = u"Your expression raised an exception!"
2268     elif arguments[0] == u"log":
2269         if len(arguments) == 4:
2270             if re.match(u"^\d+$", arguments[3]) and int(arguments[3]) >= 0:
2271                 stop = int(arguments[3])
2272             else:
2273                 stop = -1
2274         else:
2275             stop = 0
2276         if len(arguments) >= 3:
2277             if re.match(u"^\d+$", arguments[2]) and int(arguments[2]) > 0:
2278                 start = int(arguments[2])
2279             else:
2280                 start = -1
2281         else:
2282             start = 10
2283         if len(arguments) >= 2:
2284             if (re.match(u"^\d+$", arguments[1])
2285                     and 0 <= int(arguments[1]) <= 9):
2286                 level = int(arguments[1])
2287             else:
2288                 level = -1
2289         elif 0 <= actor.owner.account.getint(u"loglevel") <= 9:
2290             level = actor.owner.account.getint(u"loglevel")
2291         else:
2292             level = 1
2293         if level > -1 and start > -1 and stop > -1:
2294             message = get_loglines(level, start, stop)
2295         else:
2296             message = u"When specified, level must be 0-9 (default 1), " \
2297                 + u"start and stop must be >=1 (default 10 and 1)."
2298     else:
2299         message = u"I don't know what \"" + parameters + u"\" is."
2300     actor.send(message)
2301
2302
2303 def command_create(actor, parameters):
2304     u"""Create an element if it does not exist."""
2305     if not parameters:
2306         message = u"You must at least specify an element to create."
2307     elif not actor.owner:
2308         message = u""
2309     else:
2310         arguments = parameters.split()
2311         if len(arguments) == 1:
2312             arguments.append(u"")
2313         if len(arguments) == 2:
2314             element, filename = arguments
2315             if element in universe.contents:
2316                 message = u"The \"" + element + u"\" element already exists."
2317             else:
2318                 message = u"You create \"" + \
2319                     element + u"\" within the universe."
2320                 logline = actor.owner.account.get(
2321                     u"name"
2322                 ) + u" created an element: " + element
2323                 if filename:
2324                     logline += u" in file " + filename
2325                     if filename not in universe.files:
2326                         message += u" Warning: \"" + filename \
2327                             + u"\" is not yet included in any other file and will " \
2328                             + \
2329                             u"not be read on startup unless this is remedied."
2330                 Element(element, universe, filename)
2331                 log(logline, 6)
2332         elif len(arguments) > 2:
2333             message = u"You can only specify an element and a filename."
2334     actor.send(message)
2335
2336
2337 def command_destroy(actor, parameters):
2338     u"""Destroy an element if it exists."""
2339     if actor.owner:
2340         if not parameters:
2341             message = u"You must specify an element to destroy."
2342         else:
2343             if parameters not in universe.contents:
2344                 message = u"The \"" + parameters + \
2345                     u"\" element does not exist."
2346             else:
2347                 universe.contents[parameters].destroy()
2348                 message = u"You destroy \"" + parameters \
2349                     + u"\" within the universe."
2350                 log(
2351                     actor.owner.account.get(
2352                         u"name"
2353                     ) + u" destroyed an element: " + parameters,
2354                     6
2355                 )
2356         actor.send(message)
2357
2358
2359 def command_set(actor, parameters):
2360     u"""Set a facet of an element."""
2361     if not parameters:
2362         message = u"You must specify an element, a facet and a value."
2363     else:
2364         arguments = parameters.split(u" ", 2)
2365         if len(arguments) == 1:
2366             message = u"What facet of element \"" + arguments[0] \
2367                 + u"\" would you like to set?"
2368         elif len(arguments) == 2:
2369             message = u"What value would you like to set for the \"" \
2370                 + arguments[1] + u"\" facet of the \"" + arguments[0] \
2371                 + u"\" element?"
2372         else:
2373             element, facet, value = arguments
2374             if element not in universe.contents:
2375                 message = u"The \"" + element + u"\" element does not exist."
2376             else:
2377                 universe.contents[element].set(facet, value)
2378                 message = u"You have successfully (re)set the \"" + facet \
2379                     + u"\" facet of element \"" + element \
2380                     + u"\". Try \"show element " + \
2381                     element + u"\" for verification."
2382     actor.send(message)
2383
2384
2385 def command_delete(actor, parameters):
2386     u"""Delete a facet from an element."""
2387     if not parameters:
2388         message = u"You must specify an element and a facet."
2389     else:
2390         arguments = parameters.split(u" ")
2391         if len(arguments) == 1:
2392             message = u"What facet of element \"" + arguments[0] \
2393                 + u"\" would you like to delete?"
2394         elif len(arguments) != 2:
2395             message = u"You may only specify an element and a facet."
2396         else:
2397             element, facet = arguments
2398             if element not in universe.contents:
2399                 message = u"The \"" + element + u"\" element does not exist."
2400             elif facet not in universe.contents[element].facets():
2401                 message = u"The \"" + element + u"\" element has no \"" + facet \
2402                     + u"\" facet."
2403             else:
2404                 universe.contents[element].remove_facet(facet)
2405                 message = u"You have successfully deleted the \"" + facet \
2406                     + u"\" facet of element \"" + element \
2407                     + u"\". Try \"show element " + \
2408                     element + u"\" for verification."
2409     actor.send(message)
2410
2411
2412 def command_error(actor, input_data):
2413     u"""Generic error for an unrecognized command word."""
2414     import random
2415
2416     # 90% of the time use a generic error
2417     if random.randrange(10):
2418         message = u"I'm not sure what \"" + input_data + u"\" means..."
2419
2420     # 10% of the time use the classic diku error
2421     else:
2422         message = u"Arglebargle, glop-glyf!?!"
2423
2424     # send the error message
2425     actor.send(message)
2426
2427
2428 def daemonize(universe):
2429     u"""Fork and disassociate from everything."""
2430     import codecs
2431     import ctypes
2432     import ctypes.util
2433     import os
2434     import os.path
2435     import sys
2436
2437     # only if this is what we're configured to do
2438     if universe.contents[u"internal:process"].getboolean(u"daemon"):
2439
2440         # if possible, we want to rename the process to the same as the script
2441         # (these will need to be byte type during 2to3 migration)
2442         new_argv = "\0".join(sys.argv) + "\0"
2443         new_short_argv0 = os.path.basename(sys.argv[0]) + "\0"
2444
2445         # attempt the linux way first
2446         try:
2447             argv_array = ctypes.POINTER(ctypes.c_char_p)
2448             ctypes.pythonapi.Py_GetArgcArgv.argtypes = (
2449                 ctypes.POINTER(ctypes.c_int),
2450                 ctypes.POINTER(argv_array)
2451             )
2452             argc = argv_array()
2453             ctypes.pythonapi.Py_GetArgcArgv(
2454                 ctypes.c_int(0),
2455                 ctypes.pointer(argc)
2456             )
2457             old_argv0_size = len(argc.contents.value)
2458             ctypes.memset(argc.contents, 0, len(new_argv) + old_argv0_size)
2459             ctypes.memmove(argc.contents, new_argv, len(new_argv))
2460             ctypes.CDLL(ctypes.util.find_library(u"c")).prctl(
2461                 15,
2462                 new_short_argv0,
2463                 0,
2464                 0,
2465                 0
2466             )
2467
2468         except:
2469
2470             # since that failed, maybe it's bsd?
2471             try:
2472
2473                 # much simpler, since bsd has a libc function call for this
2474                 ctypes.CDLL(ctypes.util.find_library(u"c")).setproctitle(
2475                     new_argv
2476                 )
2477
2478             except:
2479
2480                 # that didn't work either, so just log that we couldn't
2481                 log(u"Failed to rename the interpreter process (cosmetic).")
2482
2483         # log before we start forking around, so the terminal gets the message
2484         log(u"Disassociating from the controlling terminal.")
2485
2486         # fork off and die, so we free up the controlling terminal
2487         if os.fork():
2488             os._exit(0)
2489
2490         # switch to a new process group
2491         os.setsid()
2492
2493         # fork some more, this time to free us from the old process group
2494         if os.fork():
2495             os._exit(0)
2496
2497         # reset the working directory so we don't needlessly tie up mounts
2498         os.chdir(u"/")
2499
2500         # clear the file creation mask so we can bend it to our will later
2501         os.umask(0)
2502
2503         # redirect stdin/stdout/stderr and close off their former descriptors
2504         for stdpipe in range(3):
2505             os.close(stdpipe)
2506         sys.stdin = codecs.open(u"/dev/null", u"r", u"utf-8")
2507         sys.__stdin__ = codecs.open(u"/dev/null", u"r", u"utf-8")
2508         sys.stdout = codecs.open(u"/dev/null", u"w", u"utf-8")
2509         sys.stderr = codecs.open(u"/dev/null", u"w", u"utf-8")
2510         sys.__stdout__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2511         sys.__stderr__ = codecs.open(u"/dev/null", u"w", u"utf-8")
2512
2513
2514 def create_pidfile(universe):
2515     u"""Write a file containing the current process ID."""
2516     import codecs
2517     import os
2518     import os.path
2519     pid = unicode(os.getpid())
2520     log(u"Process ID: " + pid)
2521     file_name = universe.contents[u"internal:process"].get(u"pidfile")
2522     if file_name:
2523         if not os.path.isabs(file_name):
2524             file_name = os.path.join(universe.startdir, file_name)
2525         file_descriptor = codecs.open(file_name, u"w", u"utf-8")
2526         file_descriptor.write(pid + u"\n")
2527         file_descriptor.flush()
2528         file_descriptor.close()
2529
2530
2531 def remove_pidfile(universe):
2532     u"""Remove the file containing the current process ID."""
2533     import os
2534     import os.path
2535     file_name = universe.contents[u"internal:process"].get(u"pidfile")
2536     if file_name:
2537         if not os.path.isabs(file_name):
2538             file_name = os.path.join(universe.startdir, file_name)
2539         if os.access(file_name, os.W_OK):
2540             os.remove(file_name)
2541
2542
2543 def excepthook(excepttype, value, tracebackdata):
2544     u"""Handle uncaught exceptions."""
2545     import traceback
2546
2547     # assemble the list of errors into a single string
2548     message = u"".join(
2549         traceback.format_exception(excepttype, value, tracebackdata)
2550     )
2551
2552     # try to log it, if possible
2553     try:
2554         log(message, 9)
2555     except:
2556         pass
2557
2558     # try to write it to stderr, if possible
2559     try:
2560         sys.stderr.write(message)
2561     except:
2562         pass
2563
2564
2565 def sighook(what, where):
2566     u"""Handle external signals."""
2567     import signal
2568
2569     # a generic message
2570     message = u"Caught signal: "
2571
2572     # for a hangup signal
2573     if what == signal.SIGHUP:
2574         message += u"hangup (reloading)"
2575         universe.reload_flag = True
2576
2577     # for a terminate signal
2578     elif what == signal.SIGTERM:
2579         message += u"terminate (halting)"
2580         universe.terminate_flag = True
2581
2582     # catchall for unexpected signals
2583     else:
2584         message += unicode(what) + u" (unhandled)"
2585
2586     # log what happened
2587     log(message, 8)
2588
2589
2590 def override_excepthook():
2591     u"""Redefine sys.excepthook with our own."""
2592     import sys
2593     sys.excepthook = excepthook
2594
2595
2596 def assign_sighook():
2597     u"""Assign a customized handler for some signals."""
2598     import signal
2599     signal.signal(signal.SIGHUP, sighook)
2600     signal.signal(signal.SIGTERM, sighook)
2601
2602
2603 def setup():
2604     """This contains functions to be performed when starting the engine."""
2605     import sys
2606
2607     # see if a configuration file was specified
2608     if len(sys.argv) > 1:
2609         conffile = sys.argv[1]
2610     else:
2611         conffile = u""
2612
2613     # the big bang
2614     global universe
2615     universe = Universe(conffile, True)
2616
2617     # log an initial message
2618     log(u"Started mudpy with command line: " + u" ".join(sys.argv))
2619
2620     # fork and disassociate
2621     daemonize(universe)
2622
2623     # override the default exception handler so we get logging first thing
2624     override_excepthook()
2625
2626     # set up custom signal handlers
2627     assign_sighook()
2628
2629     # make the pidfile
2630     create_pidfile(universe)
2631
2632     # pass the initialized universe back
2633     return universe
2634
2635
2636 def finish():
2637     """This contains functions to be performed when shutting down the
2638         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)