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