Imported from archive.
[mudpy.git] / mudpy.py
1 """Core objects for the mudpy engine."""
2
3 # Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
4 # Licensed per terms in the LICENSE file distributed with this software.
5
6 # import some things we need
7 from ConfigParser import RawConfigParser
8 from md5 import new as new_md5
9 from os import R_OK, access, chmod, makedirs, stat
10 from os.path import abspath, dirname, exists, isabs, join as path_join
11 from random import choice, randrange
12 from re import match
13 from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
14 from stat import S_IMODE, ST_MODE
15 from syslog import LOG_PID, LOG_INFO, LOG_DAEMON, closelog, openlog, syslog
16 from telnetlib import DO, DONT, ECHO, EOR, GA, IAC, LINEMODE, SB, SE, SGA, WILL, WONT
17 from time import asctime, sleep
18
19 class Element:
20         """An element of the universe."""
21         def __init__(self, key, universe, origin=""):
22                 """Default values for the in-memory element variables."""
23                 self.owner = None
24                 self.contents = {}
25                 self.key = key
26                 if self.key.find(":") > 0:
27                         self.category, self.subkey = self.key.split(":", 1)
28                 else:
29                         self.category = "other"
30                         self.subkey = self.key
31                 if not self.category in universe.categories: self.category = "other"
32                 universe.categories[self.category][self.subkey] = self
33                 self.origin = origin
34                 if not self.origin: self.origin = universe.default_origins[self.category]
35                 if not isabs(self.origin):
36                         self.origin = abspath(self.origin)
37                 universe.contents[self.key] = self
38                 if not self.origin in universe.files:
39                         DataFile(self.origin, universe)
40                 if not universe.files[self.origin].data.has_section(self.key):
41                         universe.files[self.origin].data.add_section(self.key)
42         def destroy(self):
43                 """Remove an element from the universe and destroy it."""
44                 log("Destroying: " + self.key + ".")
45                 universe.files[self.origin].data.remove_section(self.key)
46                 del universe.categories[self.category][self.subkey]
47                 del universe.contents[self.key]
48                 del self
49         def delete(self, facet):
50                 """Delete a facet from the element."""
51                 if universe.files[self.origin].data.has_option(self.key, facet):
52                         universe.files[self.origin].data.remove_option(self.key, facet)
53         def facets(self):
54                 """Return a list of non-inherited facets for this element."""
55                 return universe.files[self.origin].data.options(self.key)
56         def has_facet(self, facet):
57                 """Return whether the non-inherited facet exists."""
58                 return facet in self.facets()
59         def remove_facet(self, facet):
60                 """Remove a facet from the element."""
61                 if self.has_facet(facet): universe.files[self.origin].data.remove_option(self.key, facet)
62         def ancestry(self):
63                 """Return a list of the element's inheritance lineage."""
64                 if self.has_facet("inherit"):
65                         ancestry = self.getlist("inherit")
66                         for parent in ancestry[:]:
67                                 ancestors = universe.contents[parent].ancestry()
68                                 for ancestor in ancestors:
69                                         if ancestor not in ancestry: ancestry.append(ancestor)
70                         return ancestry
71                 else: return []
72         def get(self, facet, default=None):
73                 """Retrieve values."""
74                 if default is None: default = ""
75                 if universe.files[self.origin].data.has_option(self.key, facet):
76                         return universe.files[self.origin].data.get(self.key, facet)
77                 elif self.has_facet("inherit"):
78                         for ancestor in self.ancestry():
79                                 if universe.contents[ancestor].has_facet(facet):
80                                         return universe.contents[ancestor].get(facet)
81                 else: return default
82         def getboolean(self, facet, default=None):
83                 """Retrieve values as boolean type."""
84                 if default is None: default=False
85                 if universe.files[self.origin].data.has_option(self.key, facet):
86                         return universe.files[self.origin].data.getboolean(self.key, facet)
87                 elif self.has_facet("inherit"):
88                         for ancestor in self.ancestry():
89                                 if universe.contents[ancestor].has_facet(facet):
90                                         return universe.contents[ancestor].getboolean(facet)
91                 else: return default
92         def getint(self, facet, default=None):
93                 """Return values as int/long type."""
94                 if default is None: default = 0
95                 if universe.files[self.origin].data.has_option(self.key, facet):
96                         return universe.files[self.origin].data.getint(self.key, facet)
97                 elif self.has_facet("inherit"):
98                         for ancestor in self.ancestry():
99                                 if universe.contents[ancestor].has_facet(facet):
100                                         return universe.contents[ancestor].getint(facet)
101                 else: return default
102         def getfloat(self, facet, default=None):
103                 """Return values as float type."""
104                 if default is None: default = 0.0
105                 if universe.files[self.origin].data.has_option(self.key, facet):
106                         return universe.files[self.origin].data.getfloat(self.key, facet)
107                 elif self.has_facet("inherit"):
108                         for ancestor in self.ancestry():
109                                 if universe.contents[ancestor].has_facet(facet):
110                                         return universe.contents[ancestor].getfloat(facet)
111                 else: return default
112         def getlist(self, facet, default=None):
113                 """Return values as list type."""
114                 if default is None: default = []
115                 value = self.get(facet)
116                 if value: return makelist(value)
117                 else: return default
118         def getdict(self, facet, default=None):
119                 """Return values as dict type."""
120                 if default is None: default = {}
121                 value = self.get(facet)
122                 if value: return makedict(value)
123                 else: return default
124         def set(self, facet, value):
125                 """Set values."""
126                 if type(value) is long: value = str(value)
127                 elif not type(value) is str: value = repr(value)
128                 universe.files[self.origin].data.set(self.key, facet, value)
129         def append(self, facet, value):
130                 """Append value tp a list."""
131                 if type(value) is long: value = str(value)
132                 elif not type(value) is str: value = repr(value)
133                 newlist = self.getlist(facet)
134                 newlist.append(value)
135                 self.set(facet, newlist)
136         def send(self, message, eol="$(eol)"):
137                 """Convenience method to pass messages to an owner."""
138                 if self.owner: self.owner.send(message, eol)
139         def go_to(self, location):
140                 """Relocate the element to a specific location."""
141                 current = self.get("location")
142                 if current and self.key in universe.contents[current].contents:
143                         del universe.contents[current].contents[self.key]
144                 if location in universe.contents: self.set("location", location)
145                 universe.contents[location].contents[self.key] = self
146                 self.look_at(location)
147         def go_home(self):
148                 """Relocate the element to its default location."""
149                 self.go_to(self.get("default_location"))
150                 self.echo_to_location("You suddenly realize that " + self.get("name") + " is here.")
151         def move_direction(self, direction):
152                 """Relocate the element in a specified direction."""
153                 self.echo_to_location(self.get("name") + " exits " + universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
154                 self.send("You exit " + universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
155                 self.go_to(universe.contents[self.get("location")].link_neighbor(direction))
156                 self.echo_to_location(self.get("name") + " arrives from " + universe.categories["internal"]["directions"].getdict(direction)["enter"] + ".")
157         def look_at(self, key):
158                 """Show an element to another element."""
159                 if self.owner:
160                         element = universe.contents[key]
161                         message = ""
162                         name = element.get("name")
163                         if name: message += "$(cyn)" + name + "$(nrm)$(eol)"
164                         description = element.get("description")
165                         if description: message += description + "$(eol)"
166                         portal_list = element.portals().keys()
167                         if portal_list:
168                                 portal_list.sort()
169                                 message += "$(cyn)[ Exits: " + ", ".join(portal_list) + " ]$(nrm)$(eol)"
170                         for element in universe.contents[self.get("location")].contents.values():
171                                 if element.getboolean("is_actor") and element is not self:
172                                         message += "$(yel)" + element.get("name") + " is here.$(nrm)$(eol)"
173                         self.send(message)
174         def portals(self):
175                 """Map the portal directions for a room to neighbors."""
176                 portals = {}
177                 if match("""^location:-?\d+,-?\d+,-?\d+$""", self.key):
178                         coordinates = [(int(x)) for x in self.key.split(":")[1].split(",")]
179                         directions = universe.categories["internal"]["directions"]
180                         offsets = dict([(x, directions.getdict(x)["vector"]) for x in directions.facets()])
181                         for portal in self.getlist("gridlinks"):
182                                 adjacent = map(lambda c,o: c+o, coordinates, offsets[portal])
183                                 neighbor = "location:" + ",".join([(str(x)) for x in adjacent])
184                                 if neighbor in universe.contents: portals[portal] = neighbor
185                 for facet in self.facets():
186                         if facet.startswith("link_"):
187                                 neighbor = self.get(facet)
188                                 if neighbor in universe.contents:
189                                         portal = facet.split("_")[1]
190                                         portals[portal] = neighbor
191                 return portals
192         def link_neighbor(self, direction):
193                 """Return the element linked in a given direction."""
194                 portals = self.portals()
195                 if direction in portals: return portals[direction]
196         def echo_to_location(self, message):
197                 """Show a message to other elements in the current location."""
198                 for element in universe.contents[self.get("location")].contents.values():
199                         if element is not self: element.send(message)
200
201 class DataFile:
202         """A file containing universe elements."""
203         def __init__(self, filename, universe):
204                 self.data = RawConfigParser()
205                 if access(filename, R_OK): self.data.read(filename)
206                 self.filename = filename
207                 universe.files[filename] = self
208                 if self.data.has_option("__control__", "include_files"):
209                         includes = makelist(self.data.get("__control__", "include_files"))
210                 else: includes = []
211                 if self.data.has_option("__control__", "default_files"):
212                         origins = makedict(self.data.get("__control__", "default_files"))
213                         for key in origins.keys():
214                                 if not key in includes: includes.append(key)
215                                 universe.default_origins[key] = origins[key]
216                                 if not key in universe.categories:
217                                         universe.categories[key] = {}
218                 if self.data.has_option("__control__", "private_files"):
219                         for item in makelist(self.data.get("__control__", "private_files")):
220                                 if not item in includes: includes.append(item)
221                                 if not item in universe.private_files:
222                                         if not isabs(item):
223                                                 item = path_join(dirname(filename), item)
224                                         universe.private_files.append(item)
225                 for section in self.data.sections():
226                         if section != "__control__":
227                                 Element(section, universe, filename)
228                 for include_file in includes:
229                         if not isabs(include_file):
230                                 include_file = path_join(dirname(filename), include_file)
231                         DataFile(include_file, universe)
232         def save(self):
233                 """Write the data, if necessary."""
234
235                 # when there is content or the file exists, but is not read-only
236                 if ( self.data.sections() or exists(self.filename) ) and not ( self.data.has_option("__control__", "read_only") and self.data.getboolean("__control__", "read_only") ):
237
238                         # make parent directories if necessary
239                         if not exists(dirname(self.filename)):
240                                 makedirs(dirname(self.filename))
241
242                         # our data file
243                         file_descriptor = file(self.filename, "w")
244
245                         # if it's marked private, chmod it appropriately
246                         if self.filename in universe.private_files and oct(S_IMODE(stat(self.filename)[ST_MODE])) != 0600:
247                                 chmod(self.filename, 0600)
248
249                         # write it back sorted, instead of using ConfigParser
250                         sections = self.data.sections()
251                         sections.sort()
252                         for section in sections:
253                                 file_descriptor.write("[" + section + "]\n")
254                                 options = self.data.options(section)
255                                 options.sort()
256                                 for option in options:
257                                         file_descriptor.write(option + " = " + self.data.get(section, option) + "\n")
258                                 file_descriptor.write("\n")
259
260                         # flush and close the file
261                         file_descriptor.flush()
262                         file_descriptor.close()
263
264 class Universe:
265         """The universe."""
266         def __init__(self, filename=""):
267                 """Initialize the universe."""
268                 self.categories = {}
269                 self.contents = {}
270                 self.default_origins = {}
271                 self.files = {}
272                 self.private_files = []
273                 self.userlist = []
274                 self.terminate_world = False
275                 self.reload_modules = False
276                 if not filename:
277                         possible_filenames = [
278                                 ".mudpyrc",
279                                 ".mudpy/mudpyrc",
280                                 ".mudpy/mudpy.conf",
281                                 "mudpy.conf",
282                                 "etc/mudpy.conf",
283                                 "/usr/local/mudpy/mudpy.conf",
284                                 "/usr/local/mudpy/etc/mudpy.conf",
285                                 "/etc/mudpy/mudpy.conf",
286                                 "/etc/mudpy.conf"
287                                 ]
288                         for filename in possible_filenames:
289                                 if access(filename, R_OK): break
290                 if not isabs(filename):
291                         filename = abspath(filename)
292                 DataFile(filename, self)
293         def save(self):
294                 """Save the universe to persistent storage."""
295                 for key in self.files: self.files[key].save()
296
297         def initialize_server_socket(self):
298                 """Create and open the listening socket."""
299
300                 # create a new ipv4 stream-type socket object
301                 self.listening_socket = socket(AF_INET, SOCK_STREAM)
302
303                 # set the socket options to allow existing open ones to be
304                 # reused (fixes a bug where the server can't bind for a minute
305                 # when restarting on linux systems)
306                 self.listening_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
307
308                 # bind the socket to to our desired server ipa and port
309                 self.listening_socket.bind((self.categories["internal"]["network"].get("host"), self.categories["internal"]["network"].getint("port")))
310
311                 # disable blocking so we can proceed whether or not we can
312                 # send/receive
313                 self.listening_socket.setblocking(0)
314
315                 # start listening on the socket
316                 self.listening_socket.listen(1)
317
318                 # note that we're now ready for user connections
319                 log("Waiting for connection(s)...")
320
321 class User:
322         """This is a connected user."""
323
324         def __init__(self):
325                 """Default values for the in-memory user variables."""
326                 self.address = ""
327                 self.last_address = ""
328                 self.connection = None
329                 self.authenticated = False
330                 self.password_tries = 0
331                 self.state = "initial"
332                 self.menu_seen = False
333                 self.error = ""
334                 self.input_queue = []
335                 self.output_queue = []
336                 self.partial_input = ""
337                 self.echoing = True
338                 self.received_newline = True
339                 self.terminator = IAC+GA
340                 self.negotiation_pause = 0
341                 self.avatar = None
342                 self.account = None
343
344         def quit(self):
345                 """Log, close the connection and remove."""
346                 if self.account: name = self.account.get("name")
347                 else: name = ""
348                 if name: message = "User " + name
349                 else: message = "An unnamed user"
350                 message += " logged out."
351                 log(message)
352                 self.deactivate_avatar()
353                 self.connection.close()
354                 self.remove()
355
356         def reload(self):
357                 """Save, load a new user and relocate the connection."""
358
359                 # get out of the list
360                 self.remove()
361
362                 # create a new user object
363                 new_user = User()
364
365                 # set everything else equivalent
366                 for attribute in [
367                         "address",
368                         "last_address",
369                         "connection",
370                         "authenticated",
371                         "password_tries",
372                         "state",
373                         "menu_seen",
374                         "error",
375                         "input_queue",
376                         "output_queue",
377                         "partial_input",
378                         "echoing",
379                         "received_newline",
380                         "terminator",
381                         "negotiation_pause",
382                         "avatar",
383                         "account"
384                         ]:
385                         exec("new_user." + attribute + " = self." + attribute)
386
387                 # add it to the list
388                 universe.userlist.append(new_user)
389
390                 # get rid of the old user object
391                 del(self)
392
393         def replace_old_connections(self):
394                 """Disconnect active users with the same name."""
395
396                 # the default return value
397                 return_value = False
398
399                 # iterate over each user in the list
400                 for old_user in universe.userlist:
401
402                         # the name is the same but it's not us
403                         if old_user.account.get("name") == self.account.get("name") and old_user is not self:
404
405                                 # make a note of it
406                                 log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".")
407                                 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)", flush=True, add_prompt=False)
408
409                                 # close the old connection
410                                 old_user.connection.close()
411
412                                 # replace the old connection with this one
413                                 old_user.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
414                                 old_user.connection = self.connection
415                                 old_user.last_address = old_user.address
416                                 old_user.address = self.address
417                                 old_user.echoing = self.echoing
418
419                                 # take this one out of the list and delete
420                                 self.remove()
421                                 del(self)
422                                 return_value = True
423                                 break
424
425                 # true if an old connection was replaced, false if not
426                 return return_value
427
428         def authenticate(self):
429                 """Flag the user as authenticated and disconnect duplicates."""
430                 if not self.state is "authenticated":
431                         log("User " + self.account.get("name") + " logged in.")
432                         self.authenticated = True
433                         if self.account.subkey in universe.categories["internal"]["limits"].getlist("default_admins"):
434                                 self.account.set("administrator", "True")
435
436         def show_menu(self):
437                 """Send the user their current menu."""
438                 if not self.menu_seen:
439                         self.menu_choices = get_menu_choices(self)
440                         self.send(get_menu(self.state, self.error, self.echoing, self.terminator, self.menu_choices), "")
441                         self.menu_seen = True
442                         self.error = False
443                         self.adjust_echoing()
444
445         def adjust_echoing(self):
446                 """Adjust echoing to match state menu requirements."""
447                 if self.echoing and not menu_echo_on(self.state): self.echoing = False
448                 elif not self.echoing and menu_echo_on(self.state): self.echoing = True
449
450         def remove(self):
451                 """Remove a user from the list of connected users."""
452                 universe.userlist.remove(self)
453
454         def send(self, output, eol="$(eol)", raw=False, flush=False, add_prompt=True, just_prompt=False):
455                 """Send arbitrary text to a connected user."""
456
457                 # unless raw mode is on, clean it up all nice and pretty
458                 if not raw:
459
460                         # strip extra $(eol) off if present
461                         while output.startswith("$(eol)"): output = output[6:]
462                         while output.endswith("$(eol)"): output = output[:-6]
463
464                         # we'll take out GA or EOR and add them back on the end
465                         if output.endswith(IAC+GA) or output.endswith(IAC+EOR):
466                                 terminate = True
467                                 output = output[:-2]
468                         else: terminate = False
469
470                         # start with a newline, append the message, then end
471                         # with the optional eol string passed to this function
472                         # and the ansi escape to return to normal text
473                         if not just_prompt: output = "$(eol)$(eol)" + output
474                         output += eol + chr(27) + "[0m"
475
476                         # tack on a prompt if active
477                         if self.state == "active":
478                                 if not just_prompt: output += "$(eol)"
479                                 if add_prompt: output += "> "
480
481                         # find and replace macros in the output
482                         output = replace_macros(self, output)
483
484                         # wrap the text at 80 characters
485                         output = wrap_ansi_text(output, 80)
486
487                         # tack the terminator back on
488                         if terminate: output += self.terminator
489
490                 # drop the output into the user's output queue
491                 self.output_queue.append(output)
492
493                 # if this is urgent, flush all pending output
494                 if flush: self.flush()
495
496         def pulse(self):
497                 """All the things to do to the user per increment."""
498
499                 # if the world is terminating, disconnect
500                 if universe.terminate_world:
501                         self.state = "disconnecting"
502                         self.menu_seen = False
503
504                 # if output is paused, decrement the counter
505                 if self.state == "initial":
506                         if self.negotiation_pause: self.negotiation_pause -= 1
507                         else: self.state = "entering_account_name"
508
509                 # show the user a menu as needed
510                 elif not self.state == "active": self.show_menu()
511
512                 # flush any pending output in teh queue
513                 self.flush()
514
515                 # disconnect users with the appropriate state
516                 if self.state == "disconnecting": self.quit()
517
518                 # check for input and add it to the queue
519                 self.enqueue_input()
520
521                 # there is input waiting in the queue
522                 if self.input_queue: handle_user_input(self)
523
524         def flush(self):
525                 """Try to send the last item in the queue and remove it."""
526                 if self.output_queue:
527                         if self.received_newline:
528                                 self.received_newline = False
529                                 if self.output_queue[0].startswith("\r\n"):
530                                         self.output_queue[0] = self.output_queue[0][2:]
531                         try:
532                                 self.connection.send(self.output_queue[0])
533                                 del self.output_queue[0]
534                         except:
535                                 pass
536
537
538         def enqueue_input(self):
539                 """Process and enqueue any new input."""
540
541                 # check for some input
542                 try:
543                         input_data = self.connection.recv(1024)
544                 except:
545                         input_data = ""
546
547                 # we got something
548                 if input_data:
549
550                         # tack this on to any previous partial
551                         self.partial_input += input_data
552
553                         # reply to and remove any IAC negotiation codes
554                         self.negotiate_telnet_options()
555
556                         # separate multiple input lines
557                         new_input_lines = self.partial_input.split("\n")
558
559                         # if input doesn't end in a newline, replace the
560                         # held partial input with the last line of it
561                         if not self.partial_input.endswith("\n"):
562                                 self.partial_input = new_input_lines.pop()
563
564                         # otherwise, chop off the extra null input and reset
565                         # the held partial input
566                         else:
567                                 new_input_lines.pop()
568                                 self.partial_input = ""
569
570                         # iterate over the remaining lines
571                         for line in new_input_lines:
572
573                                 # remove a trailing carriage return
574                                 if line.endswith("\r"): line = line.rstrip("\r")
575
576                                 # log non-printable characters remaining
577                                 removed = filter(lambda x: (x < " " or x > "~"), line)
578                                 if removed:
579                                         logline = "Non-printable characters from "
580                                         if self.account and self.account.get("name"): logline += self.account.get("name") + ": "
581                                         else: logline += "unknown user: "
582                                         logline += repr(removed)
583                                         log(logline)
584
585                                 # filter out non-printables
586                                 line = filter(lambda x: " " <= x <= "~", line)
587
588                                 # strip off extra whitespace
589                                 line = line.strip()
590
591                                 # put on the end of the queue
592                                 self.input_queue.append(line)
593
594         def negotiate_telnet_options(self):
595                 """Reply to/remove partial_input telnet negotiation options."""
596
597                 # start at the begining of the input
598                 position = 0
599
600                 # make a local copy to play with
601                 text = self.partial_input
602
603                 # as long as we haven't checked it all
604                 while position < len(text):
605
606                         # jump to the first IAC you find
607                         position = text.find(IAC, position)
608
609                         # if there wasn't an IAC in the input, skip to the end
610                         if position < 0: position = len(text)
611
612                         # replace a double (literal) IAC if there's an LF later
613                         elif len(text) > position+1 and text[position+1] == IAC:
614                                 if text.find("\n", position) > 0: text = text.replace(IAC+IAC, IAC)
615                                 else: position += 1
616                                 position += 1
617
618                         # this must be an option negotiation
619                         elif len(text) > position+2 and text[position+1] in (DO, DONT, WILL, WONT):
620
621                                 negotiation = text[position+1:position+3]
622
623                                 # if we turned echo off, ignore the confirmation
624                                 if not self.echoing and negotiation == DO+ECHO: pass
625
626                                 # allow LINEMODE
627                                 elif negotiation == WILL+LINEMODE: self.send(IAC+DO+LINEMODE, raw=True)
628
629                                 # if the client likes EOR instead of GA, make a note of it
630                                 elif negotiation == DO+EOR: self.terminator = IAC+EOR
631                                 elif negotiation == DONT+EOR and self.terminator == IAC+EOR:
632                                         self.terminator = IAC+GA
633
634                                 # if the client doesn't want GA, oblige
635                                 elif negotiation == DO+SGA and self.terminator == IAC+GA:
636                                         self.terminator = ""
637                                         self.send(IAC+WILL+SGA, raw=True)
638
639                                 # we don't want to allow anything else
640                                 elif text[position+1] == DO: self.send(IAC+WONT+text[position+2], raw=True)
641                                 elif text[position+1] == WILL: self.send(IAC+DONT+text[position+2], raw=True)
642
643                                 # strip the negotiation from the input
644                                 text = text.replace(text[position:position+3], "")
645
646                         # get rid of IAC SB .* IAC SE
647                         elif len(text) > position+4 and text[position:position+2] == IAC+SB:
648                                 end_subnegotiation = text.find(IAC+SE, position)
649                                 if end_subnegotiation > 0: text = text[:position] + text[end_subnegotiation+2:]
650                                 else: position += 1
651
652                         # otherwise, strip out a two-byte IAC command
653                         elif len(text) > position+2: text = text.replace(text[position:position+2], "")
654
655                         # and this means we got the begining of an IAC
656                         else: position += 1
657
658                 # replace the input with our cleaned-up text
659                 self.partial_input = text
660
661         def can_run(self, command):
662                 """Check if the user can run this command object."""
663
664                 # has to be in the commands category
665                 if command not in universe.categories["command"].values(): result = False
666
667                 # administrators can run any command
668                 elif self.account.getboolean("administrator"): result = True
669
670                 # everyone can run non-administrative commands
671                 elif not command.getboolean("administrative"): result = True
672
673                 # otherwise the command cannot be run by this user
674                 else: result = False
675
676                 # pass back the result
677                 return result
678
679         def new_avatar(self):
680                 """Instantiate a new, unconfigured avatar for this user."""
681                 counter = 0
682                 while "avatar:" + self.account.get("name") + ":" + str(counter) in universe.categories["actor"].keys(): counter += 1
683                 self.avatar = Element("actor:avatar:" + self.account.get("name") + ":" + str(counter), universe)
684                 self.avatar.append("inherit", "template:actor")
685                 self.account.append("avatars", self.avatar.key)
686
687         def delete_avatar(self, avatar):
688                 """Remove an avatar from the world and from the user's list."""
689                 if self.avatar is universe.contents[avatar]: self.avatar = None
690                 universe.contents[avatar].destroy()
691                 avatars = self.account.getlist("avatars")
692                 avatars.remove(avatar)
693                 self.account.set("avatars", avatars)
694
695         def activate_avatar_by_index(self, index):
696                 """Enter the world with a particular indexed avatar."""
697                 self.avatar = universe.contents[self.account.getlist("avatars")[index]]
698                 self.avatar.owner = self
699                 self.state = "active"
700                 self.avatar.go_home()
701
702         def deactivate_avatar(self):
703                 """Have the active avatar leave the world."""
704                 if self.avatar:
705                         current = self.avatar.get("location")
706                         self.avatar.set("default_location", current)
707                         self.avatar.echo_to_location("You suddenly wonder where " + self.avatar.get("name") + " went.")
708                         del universe.contents[current].contents[self.avatar.key]
709                         self.avatar.remove_facet("location")
710                         self.avatar.owner = None
711                         self.avatar = None
712
713         def destroy(self):
714                 """Destroy the user and associated avatars."""
715                 for avatar in self.account.getlist("avatars"): self.delete_avatar(avatar)
716                 self.account.destroy()
717
718         def list_avatar_names(self):
719                 """List names of assigned avatars."""
720                 return [ universe.contents[avatar].get("name") for avatar in self.account.getlist("avatars") ]
721
722 def makelist(value):
723         """Turn string into list type."""
724         if value[0] + value[-1] == "[]": return eval(value)
725         else: return [ value ]
726
727 def makedict(value):
728         """Turn string into dict type."""
729         if value[0] + value[-1] == "{}": return eval(value)
730         elif value.find(":") > 0: return eval("{" + value + "}")
731         else: return { value: None }
732
733 def broadcast(message, add_prompt=True):
734         """Send a message to all connected users."""
735         for each_user in universe.userlist: each_user.send("$(eol)" + message, add_prompt=add_prompt)
736
737 def log(message):
738         """Log a message."""
739
740         # the time in posix log timestamp format
741         timestamp = asctime()[4:19]
742
743         file_name = universe.categories["internal"]["logging"].get("file")
744         if file_name:
745                 file_descriptor = file(file_name, "a")
746                 file_descriptor.write(timestamp + " " + message + "\n")
747                 file_descriptor.flush()
748                 file_descriptor.close()
749
750         # send the timestamp and message to standard output
751         if universe.categories["internal"]["logging"].getboolean("stdout"):
752                 print(timestamp + " " + message)
753
754         # send the message to the system log
755         syslog_name = universe.categories["internal"]["logging"].get("syslog")
756         if syslog_name:
757                 openlog(syslog_name, LOG_PID, LOG_INFO | LOG_DAEMON)
758                 syslog(message)
759                 closelog()
760
761 def wrap_ansi_text(text, width):
762         """Wrap text with arbitrary width while ignoring ANSI colors."""
763
764         # the current position in the entire text string, including all
765         # characters, printable or otherwise
766         absolute_position = 0
767
768         # the current text position relative to the begining of the line,
769         # ignoring color escape sequences
770         relative_position = 0
771
772         # whether the current character is part of a color escape sequence
773         escape = False
774
775         # iterate over each character from the begining of the text
776         for each_character in text:
777
778                 # the current character is the escape character
779                 if each_character == chr(27):
780                         escape = True
781
782                 # the current character is within an escape sequence
783                 elif escape:
784
785                         # the current character is m, which terminates the
786                         # current escape sequence
787                         if each_character == "m":
788                                 escape = False
789
790                 # the current character is a newline, so reset the relative
791                 # position (start a new line)
792                 elif each_character == "\n":
793                         relative_position = 0
794
795                 # the current character meets the requested maximum line width,
796                 # so we need to backtrack and find a space at which to wrap
797                 elif relative_position == width:
798
799                         # distance of the current character examined from the
800                         # relative position
801                         wrap_offset = 0
802
803                         # count backwards until we find a space
804                         while text[absolute_position - wrap_offset] != " ":
805                                 wrap_offset += 1
806
807                         # insert an eol in place of the space
808                         text = text[:absolute_position - wrap_offset] + "\r\n" + text[absolute_position - wrap_offset + 1:]
809
810                         # increase the absolute position because an eol is two
811                         # characters but the space it replaced was only one
812                         absolute_position += 1
813
814                         # now we're at the begining of a new line, plus the
815                         # number of characters wrapped from the previous line
816                         relative_position = wrap_offset
817
818                 # as long as the character is not a carriage return and the
819                 # other above conditions haven't been met, count it as a
820                 # printable character
821                 elif each_character != "\r":
822                         relative_position += 1
823
824                 # increase the absolute position for every character
825                 absolute_position += 1
826
827         # return the newly-wrapped text
828         return text
829
830 def weighted_choice(data):
831         """Takes a dict weighted by value and returns a random key."""
832
833         # this will hold our expanded list of keys from the data
834         expanded = []
835
836         # create thee expanded list of keys
837         for key in data.keys():
838                 for count in range(data[key]):
839                         expanded.append(key)
840
841         # return one at random
842         return choice(expanded)
843
844 def random_name():
845         """Returns a random character name."""
846
847         # the vowels and consonants needed to create romaji syllables
848         vowels = [ "a", "i", "u", "e", "o" ]
849         consonants = ["'", "k", "z", "s", "sh", "z", "j", "t", "ch", "ts", "d", "n", "h", "f", "m", "y", "r", "w" ]
850
851         # this dict will hold our weighted list of syllables
852         syllables = {}
853
854         # generate the list with an even weighting
855         for consonant in consonants:
856                 for vowel in vowels:
857                         syllables[consonant + vowel] = 1
858
859         # we'll build the name into this string
860         name = ""
861
862         # create a name of random length from the syllables
863         for syllable in range(randrange(2, 6)):
864                 name += weighted_choice(syllables)
865
866         # strip any leading quotemark, capitalize and return the name
867         return name.strip("'").capitalize()
868
869 def replace_macros(user, text, is_input=False):
870         """Replaces macros in text output."""
871
872         # loop until broken
873         while True:
874
875                 # third person pronouns
876                 pronouns = {
877                         "female": { "obj": "her", "pos": "hers", "sub": "she" },
878                         "male": { "obj": "him", "pos": "his", "sub": "he" },
879                         "neuter": { "obj": "it", "pos": "its", "sub": "it" }
880                         }
881
882                 # a dict of replacement macros
883                 macros = {
884                         "$(eol)": "\r\n",
885                         "$(bld)": chr(27) + "[1m",
886                         "$(nrm)": chr(27) + "[0m",
887                         "$(blk)": chr(27) + "[30m",
888                         "$(blu)": chr(27) + "[34m",
889                         "$(cyn)": chr(27) + "[36m",
890                         "$(grn)": chr(27) + "[32m",
891                         "$(mgt)": chr(27) + "[35m",
892                         "$(red)": chr(27) + "[31m",
893                         "$(yel)": chr(27) + "[33m",
894                         }
895
896                 # add dynamic macros where possible
897                 if user.account:
898                         account_name = user.account.get("name")
899                         if account_name:
900                                 macros["$(account)"] = account_name
901                 if user.avatar:
902                         avatar_gender = user.avatar.get("gender")
903                         if avatar_gender:
904                                 macros["$(tpop)"] = pronouns[avatar_gender]["obj"]
905                                 macros["$(tppp)"] = pronouns[avatar_gender]["pos"]
906                                 macros["$(tpsp)"] = pronouns[avatar_gender]["sub"]
907
908                 # find and replace per the macros dict
909                 macro_start = text.find("$(")
910                 if macro_start == -1: break
911                 macro_end = text.find(")", macro_start) + 1
912                 macro = text[macro_start:macro_end]
913                 if macro in macros.keys():
914                         text = text.replace(macro, macros[macro])
915
916                 # if we get here, log and replace it with null
917                 else:
918                         text = text.replace(macro, "")
919                         if not is_input:
920                                 log("Unexpected replacement macro " + macro + " encountered.")
921
922         # replace the look-like-a-macro sequence
923         text = text.replace("$_(", "$(")
924
925         return text
926
927 def escape_macros(text):
928         """Escapes replacement macros in text."""
929         return text.replace("$(", "$_(")
930
931 def check_time(frequency):
932         """Check for a factor of the current increment count."""
933         if type(frequency) is str:
934                 frequency = universe.categories["internal"]["time"].getint(frequency)
935         if not "counters" in universe.categories["internal"]:
936                 Element("internal:counters", universe)
937         return not universe.categories["internal"]["counters"].getint("elapsed") % frequency
938
939 def on_pulse():
940         """The things which should happen on each pulse, aside from reloads."""
941
942         # open the listening socket if it hasn't been already
943         if not hasattr(universe, "listening_socket"):
944                 universe.initialize_server_socket()
945
946         # assign a user if a new connection is waiting
947         user = check_for_connection(universe.listening_socket)
948         if user: universe.userlist.append(user)
949
950         # iterate over the connected users
951         for user in universe.userlist: user.pulse()
952
953         # update the log every now and then
954         if check_time("frequency_log"):
955                 log(str(len(universe.userlist)) + " connection(s)")
956
957         # periodically save everything
958         if check_time("frequency_save"):
959                 universe.save()
960
961         # pause for a configurable amount of time (decimal seconds)
962         sleep(universe.categories["internal"]["time"].getfloat("increment"))
963
964         # increment the elapsed increment counter
965         universe.categories["internal"]["counters"].set("elapsed", universe.categories["internal"]["counters"].getint("elapsed") + 1)
966
967 def reload_data():
968         """Reload data into new persistent objects."""
969         for user in universe.userlist[:]: user.reload()
970
971 def check_for_connection(listening_socket):
972         """Check for a waiting connection and return a new user object."""
973
974         # try to accept a new connection
975         try:
976                 connection, address = listening_socket.accept()
977         except:
978                 return None
979
980         # note that we got one
981         log("Connection from " + address[0])
982
983         # disable blocking so we can proceed whether or not we can send/receive
984         connection.setblocking(0)
985
986         # create a new user object
987         user = User()
988
989         # associate this connection with it
990         user.connection = connection
991
992         # set the user's ipa from the connection's ipa
993         user.address = address[0]
994
995         # let the client know we WILL EOR
996         user.send(IAC+WILL+EOR, raw=True)
997         user.negotiation_pause = 2
998
999         # return the new user object
1000         return user
1001
1002 def get_menu(state, error=None, echoing=True, terminator="", choices=None):
1003         """Show the correct menu text to a user."""
1004
1005         # make sure we don't reuse a mutable sequence by default
1006         if choices is None: choices = {}
1007
1008         # begin with a telnet echo command sequence if needed
1009         message = get_echo_sequence(state, echoing)
1010
1011         # get the description or error text
1012         message += get_menu_description(state, error)
1013
1014         # get menu choices for the current state
1015         message += get_formatted_menu_choices(state, choices)
1016
1017         # try to get a prompt, if it was defined
1018         message += get_menu_prompt(state)
1019
1020         # throw in the default choice, if it exists
1021         message += get_formatted_default_menu_choice(state)
1022
1023         # display a message indicating if echo is off
1024         message += get_echo_message(state)
1025
1026         # tack on EOR or GA to indicate the prompt will not be followed by CRLF
1027         message += terminator
1028
1029         # return the assembly of various strings defined above
1030         return message
1031
1032 def menu_echo_on(state):
1033         """True if echo is on, false if it is off."""
1034         return universe.categories["menu"][state].getboolean("echo", True)
1035
1036 def get_echo_sequence(state, echoing):
1037         """Build the appropriate IAC WILL/WONT ECHO sequence as needed."""
1038
1039         # if the user has echo on and the menu specifies it should be turned
1040         # off, send: iac + will + echo + null
1041         if echoing and not menu_echo_on(state): return IAC+WILL+ECHO
1042
1043         # if echo is not set to off in the menu and the user curently has echo
1044         # off, send: iac + wont + echo + null
1045         elif not echoing and menu_echo_on(state): return IAC+WONT+ECHO
1046
1047         # default is not to send an echo control sequence at all
1048         else: return ""
1049
1050 def get_echo_message(state):
1051         """Return a message indicating that echo is off."""
1052         if menu_echo_on(state): return ""
1053         else: return "(won't echo) "
1054
1055 def get_default_menu_choice(state):
1056         """Return the default choice for a menu."""
1057         return universe.categories["menu"][state].get("default")
1058
1059 def get_formatted_default_menu_choice(state):
1060         """Default menu choice foratted for inclusion in a prompt string."""
1061         default_choice = get_default_menu_choice(state)
1062         if default_choice: return "[$(red)" + default_choice + "$(nrm)] "
1063         else: return ""
1064
1065 def get_menu_description(state, error):
1066         """Get the description or error text."""
1067
1068         # an error condition was raised by the handler
1069         if error:
1070
1071                 # try to get an error message matching the condition
1072                 # and current state
1073                 description = universe.categories["menu"][state].get("error_" + error)
1074                 if not description: description = "That is not a valid choice..."
1075                 description = "$(red)" + description + "$(nrm)"
1076
1077         # there was no error condition
1078         else:
1079
1080                 # try to get a menu description for the current state
1081                 description = universe.categories["menu"][state].get("description")
1082
1083         # return the description or error message
1084         if description: description += "$(eol)$(eol)"
1085         return description
1086
1087 def get_menu_prompt(state):
1088         """Try to get a prompt, if it was defined."""
1089         prompt = universe.categories["menu"][state].get("prompt")
1090         if prompt: prompt += " "
1091         return prompt
1092
1093 def get_menu_choices(user):
1094         """Return a dict of choice:meaning."""
1095         menu = universe.categories["menu"][user.state]
1096         create_choices = menu.get("create")
1097         if create_choices: choices = eval(create_choices)
1098         else: choices = {}
1099         ignores = []
1100         options = {}
1101         creates = {}
1102         for facet in menu.facets():
1103                 if facet.startswith("demand_") and not eval(universe.categories["menu"][user.state].get(facet)):
1104                         ignores.append(facet.split("_", 2)[1])
1105                 elif facet.startswith("create_"):
1106                         creates[facet] = facet.split("_", 2)[1]
1107                 elif facet.startswith("choice_"):
1108                         options[facet] = facet.split("_", 2)[1]
1109         for facet in creates.keys():
1110                 if not creates[facet] in ignores:
1111                         choices[creates[facet]] = eval(menu.get(facet))
1112         for facet in options.keys():
1113                 if not options[facet] in ignores:
1114                         choices[options[facet]] = menu.get(facet)
1115         return choices
1116
1117 def get_formatted_menu_choices(state, choices):
1118         """Returns a formatted string of menu choices."""
1119         choice_output = ""
1120         choice_keys = choices.keys()
1121         choice_keys.sort()
1122         for choice in choice_keys:
1123                 choice_output += "   [$(red)" + choice + "$(nrm)]  " + choices[choice] + "$(eol)"
1124         if choice_output: choice_output += "$(eol)"
1125         return choice_output
1126
1127 def get_menu_branches(state):
1128         """Return a dict of choice:branch."""
1129         branches = {}
1130         for facet in universe.categories["menu"][state].facets():
1131                 if facet.startswith("branch_"):
1132                         branches[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
1133         return branches
1134
1135 def get_default_branch(state):
1136         """Return the default branch."""
1137         return universe.categories["menu"][state].get("branch")
1138
1139 def get_choice_branch(user, choice):
1140         """Returns the new state matching the given choice."""
1141         branches = get_menu_branches(user.state)
1142         if choice in branches.keys(): return branches[choice]
1143         elif choice in user.menu_choices.keys(): return get_default_branch(user.state)
1144         else: return ""
1145
1146 def get_menu_actions(state):
1147         """Return a dict of choice:branch."""
1148         actions = {}
1149         for facet in universe.categories["menu"][state].facets():
1150                 if facet.startswith("action_"):
1151                         actions[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
1152         return actions
1153
1154 def get_default_action(state):
1155         """Return the default action."""
1156         return universe.categories["menu"][state].get("action")
1157
1158 def get_choice_action(user, choice):
1159         """Run any indicated script for the given choice."""
1160         actions = get_menu_actions(user.state)
1161         if choice in actions.keys(): return actions[choice]
1162         elif choice in user.menu_choices.keys(): return get_default_action(user.state)
1163         else: return ""
1164
1165 def handle_user_input(user):
1166         """The main handler, branches to a state-specific handler."""
1167
1168         # check to make sure the state is expected, then call that handler
1169         if "handler_" + user.state in globals():
1170                 exec("handler_" + user.state + "(user)")
1171         else:
1172                 generic_menu_handler(user)
1173
1174         # since we got input, flag that the menu/prompt needs to be redisplayed
1175         user.menu_seen = False
1176
1177         # if the user's client echo is off, send a blank line for aesthetics
1178         if user.echoing: user.received_newline = True
1179
1180 def generic_menu_handler(user):
1181         """A generic menu choice handler."""
1182
1183         # get a lower-case representation of the next line of input
1184         if user.input_queue:
1185                 choice = user.input_queue.pop(0)
1186                 if choice: choice = choice.lower()
1187         else: choice = ""
1188         if not choice: choice = get_default_menu_choice(user.state)
1189         if choice in user.menu_choices:
1190                 exec(get_choice_action(user, choice))
1191                 new_state = get_choice_branch(user, choice)
1192                 if new_state: user.state = new_state
1193         else: user.error = "default"
1194
1195 def handler_entering_account_name(user):
1196         """Handle the login account name."""
1197
1198         # get the next waiting line of input
1199         input_data = user.input_queue.pop(0)
1200
1201         # did the user enter anything?
1202         if input_data:
1203                 
1204                 # keep only the first word and convert to lower-case
1205                 name = input_data.lower()
1206
1207                 # fail if there are non-alphanumeric characters
1208                 if name != filter(lambda x: x>="0" and x<="9" or x>="a" and x<="z", name):
1209                         user.error = "bad_name"
1210
1211                 # if that account exists, time to request a password
1212                 elif name in universe.categories["account"]:
1213                         user.account = universe.categories["account"][name]
1214                         user.state = "checking_password"
1215
1216                 # otherwise, this could be a brand new user
1217                 else:
1218                         user.account = Element("account:" + name, universe)
1219                         user.account.set("name", name)
1220                         log("New user: " + name)
1221                         user.state = "checking_new_account_name"
1222
1223         # if the user entered nothing for a name, then buhbye
1224         else:
1225                 user.state = "disconnecting"
1226
1227 def handler_checking_password(user):
1228         """Handle the login account password."""
1229
1230         # get the next waiting line of input
1231         input_data = user.input_queue.pop(0)
1232
1233         # does the hashed input equal the stored hash?
1234         if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
1235
1236                 # if so, set the username and load from cold storage
1237                 if not user.replace_old_connections():
1238                         user.authenticate()
1239                         user.state = "main_utility"
1240
1241         # if at first your hashes don't match, try, try again
1242         elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1243                 user.password_tries += 1
1244                 user.error = "incorrect"
1245
1246         # we've exceeded the maximum number of password failures, so disconnect
1247         else:
1248                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1249                 user.state = "disconnecting"
1250
1251 def handler_entering_new_password(user):
1252         """Handle a new password entry."""
1253
1254         # get the next waiting line of input
1255         input_data = user.input_queue.pop(0)
1256
1257         # make sure the password is strong--at least one upper, one lower and
1258         # one digit, seven or more characters in length
1259         if len(input_data) > 6 and len(filter(lambda x: x>="0" and x<="9", input_data)) and len(filter(lambda x: x>="A" and x<="Z", input_data)) and len(filter(lambda x: x>="a" and x<="z", input_data)):
1260
1261                 # hash and store it, then move on to verification
1262                 user.account.set("passhash",  new_md5(user.account.get("name") + input_data).hexdigest())
1263                 user.state = "verifying_new_password"
1264
1265         # the password was weak, try again if you haven't tried too many times
1266         elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1267                 user.password_tries += 1
1268                 user.error = "weak"
1269
1270         # too many tries, so adios
1271         else:
1272                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1273                 user.account.destroy()
1274                 user.state = "disconnecting"
1275
1276 def handler_verifying_new_password(user):
1277         """Handle the re-entered new password for verification."""
1278
1279         # get the next waiting line of input
1280         input_data = user.input_queue.pop(0)
1281
1282         # hash the input and match it to storage
1283         if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
1284                 user.authenticate()
1285
1286                 # the hashes matched, so go active
1287                 if not user.replace_old_connections(): user.state = "main_utility"
1288
1289         # go back to entering the new password as long as you haven't tried
1290         # too many times
1291         elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1292                 user.password_tries += 1
1293                 user.error = "differs"
1294                 user.state = "entering_new_password"
1295
1296         # otherwise, sayonara
1297         else:
1298                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1299                 user.account.destroy()
1300                 user.state = "disconnecting"
1301
1302 def handler_active(user):
1303         """Handle input for active users."""
1304
1305         # get the next waiting line of input
1306         input_data = user.input_queue.pop(0)
1307
1308         # is there input?
1309         if input_data:
1310
1311                 # split out the command (first word) and parameters (everything else)
1312                 if input_data.find(" ") > 0:
1313                         command_name, parameters = input_data.split(" ", 1)
1314                 else:
1315                         command_name = input_data
1316                         parameters = ""
1317
1318                 # lowercase the command
1319                 command_name = command_name.lower()
1320
1321                 # the command matches a command word for which we have data
1322                 if command_name in universe.categories["command"]:
1323                         command = universe.categories["command"][command_name]
1324                 else: command = None
1325
1326                 # if it's allowed, do it
1327                 if user.can_run(command): exec(command.get("action"))
1328
1329                 # otherwise, give an error
1330                 elif command_name: command_error(user, input_data)
1331
1332         # if no input, just idle back with a prompt
1333         else: user.send("", just_prompt=True)
1334         
1335 def command_halt(user, parameters):
1336         """Halt the world."""
1337
1338         # see if there's a message or use a generic one
1339         if parameters: message = "Halting: " + parameters
1340         else: message = "User " + user.account.get("name") + " halted the world."
1341
1342         # let everyone know
1343         broadcast(message, add_prompt=False)
1344         log(message)
1345
1346         # set a flag to terminate the world
1347         universe.terminate_world = True
1348
1349 def command_reload(user):
1350         """Reload all code modules, configs and data."""
1351
1352         # let the user know and log
1353         user.send("Reloading all code modules, configs and data.")
1354         log("User " + user.account.get("name") + " reloaded the world.")
1355
1356         # set a flag to reload
1357         universe.reload_modules = True
1358
1359 def command_quit(user):
1360         """Leave the world and go back to the main menu."""
1361         user.deactivate_avatar()
1362         user.state = "main_utility"
1363
1364 def command_help(user, parameters):
1365         """List available commands and provide help for commands."""
1366
1367         # did the user ask for help on a specific command word?
1368         if parameters:
1369
1370                 # is the command word one for which we have data?
1371                 if parameters in universe.categories["command"]:
1372                         command = universe.categories["command"][parameters]
1373                 else: command = None
1374
1375                 # only for allowed commands
1376                 if user.can_run(command):
1377
1378                         # add a description if provided
1379                         description = command.get("description")
1380                         if not description:
1381                                 description = "(no short description provided)"
1382                         if command.getboolean("administrative"): output = "$(red)"
1383                         else: output = "$(grn)"
1384                         output += parameters + "$(nrm) - " + description + "$(eol)$(eol)"
1385
1386                         # add the help text if provided
1387                         help_text = command.get("help")
1388                         if not help_text:
1389                                 help_text = "No help is provided for this command."
1390                         output += help_text
1391
1392                 # no data for the requested command word
1393                 else:
1394                         output = "That is not an available command."
1395
1396         # no specific command word was indicated
1397         else:
1398
1399                 # give a sorted list of commands with descriptions if provided
1400                 output = "These are the commands available to you:$(eol)$(eol)"
1401                 sorted_commands = universe.categories["command"].keys()
1402                 sorted_commands.sort()
1403                 for item in sorted_commands:
1404                         command = universe.categories["command"][item]
1405                         if user.can_run(command):
1406                                 description = command.get("description")
1407                                 if not description:
1408                                         description = "(no short description provided)"
1409                                 if command.getboolean("administrative"): output += "   $(red)"
1410                                 else: output += "   $(grn)"
1411                                 output += item + "$(nrm) - " + description + "$(eol)"
1412                 output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"."
1413
1414         # send the accumulated output to the user
1415         user.send(output)
1416
1417 def command_move(user, parameters):
1418         """Move the avatar in a given direction."""
1419         if parameters in universe.contents[user.avatar.get("location")].portals():
1420                 user.avatar.move_direction(parameters)
1421         else: user.send("You cannot go that way.")
1422
1423 def command_look(user, parameters):
1424         """Look around."""
1425         if parameters: user.send("You look at or in anything yet.")
1426         else: user.avatar.look_at(user.avatar.get("location"))
1427
1428 def command_say(user, parameters):
1429         """Speak to others in the same room."""
1430
1431         # check for replacement macros
1432         if replace_macros(user, parameters, True) != parameters:
1433                 user.send("You cannot speak $_(replacement macros).")
1434
1435         # the user entered a message
1436         elif parameters:
1437
1438                 # get rid of quote marks on the ends of the message and
1439                 # capitalize the first letter
1440                 message = parameters.strip("\"'`").capitalize()
1441
1442                 # a dictionary of punctuation:action pairs
1443                 actions = {}
1444                 for facet in universe.categories["internal"]["language"].facets():
1445                         if facet.startswith("punctuation_"):
1446                                 action = facet.split("_")[1]
1447                                 for mark in universe.categories["internal"]["language"].getlist(facet):
1448                                                 actions[mark] = action
1449
1450                 # match the punctuation used, if any, to an action
1451                 default_punctuation = universe.categories["internal"]["language"].get("default_punctuation")
1452                 action = actions[default_punctuation]
1453                 for mark in actions.keys():
1454                         if message.endswith(mark) and mark != default_punctuation:
1455                                 action = actions[mark]
1456                                 break
1457
1458                 # if the action is default and there is no mark, add one
1459                 if action == actions[default_punctuation] and not message.endswith(default_punctuation):
1460                         message += default_punctuation
1461
1462                 # capitalize a list of words within the message
1463                 capitalize_words = universe.categories["internal"]["language"].getlist("capitalize_words")
1464                 for word in capitalize_words:
1465                         message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
1466
1467                 # tell the room
1468                 user.avatar.echo_to_location(user.avatar.get("name") + " " + action + "s, \"" + message + "\"")
1469                 user.send("You " + action + ", \"" + message + "\"")
1470
1471         # there was no message
1472         else:
1473                 user.send("What do you want to say?")
1474
1475 def command_show(user, parameters):
1476         """Show program data."""
1477         message = ""
1478         if parameters.find(" ") < 1:
1479                 if parameters == "time":
1480                         message = universe.categories["internal"]["counters"].get("elapsed") + " increments elapsed since the world was created."
1481                 elif parameters == "categories":
1482                         message = "These are the element categories:$(eol)"
1483                         categories = universe.categories.keys()
1484                         categories.sort()
1485                         for category in categories: message += "$(eol)   $(grn)" + category + "$(nrm)"
1486                 elif parameters == "files":
1487                         message = "These are the current files containing the universe:$(eol)"
1488                         filenames = universe.files.keys()
1489                         filenames.sort()
1490                         for filename in filenames: message += "$(eol)   $(grn)" + filename + "$(nrm)"
1491                 else: message = ""
1492         else:
1493                 arguments = parameters.split()
1494                 if arguments[0] == "category":
1495                         if arguments[1] in universe.categories:
1496                                 message = "These are the elements in the \"" + arguments[1] + "\" category:$(eol)"
1497                                 elements = universe.categories[arguments[1]].keys()
1498                                 elements.sort()
1499                                 for element in elements:
1500                                         message += "$(eol)   $(grn)" + universe.categories[arguments[1]][element].key + "$(nrm)"
1501                 elif arguments[0] == "element":
1502                         if arguments[1] in universe.contents:
1503                                 message = "These are the properties of the \"" + arguments[1] + "\" element:$(eol)"
1504                                 element = universe.contents[arguments[1]]
1505                                 facets = element.facets()
1506                                 facets.sort()
1507                                 for facet in facets:
1508                                         message += "$(eol)   $(grn)" + facet + ": $(red)" + escape_macros(element.get(facet)) + "$(nrm)"
1509                 elif arguments[0] == "result":
1510                         if len(arguments) > 1:
1511                                 try:
1512                                         message = repr(eval(" ".join(arguments[1:])))
1513                                 except:
1514                                         message = "Your expression raised an exception!"
1515         if not message:
1516                 if parameters: message = "I don't know what \"" + parameters + "\" is."
1517                 else: message = "What do you want to show?"
1518         user.send(message)
1519
1520 def command_create(user, parameters):
1521         """Create an element if it does not exist."""
1522         if not parameters: message = "You must at least specify an element to create."
1523         else:
1524                 arguments = parameters.split()
1525                 if len(arguments) == 1: arguments.append("")
1526                 if len(arguments) == 2:
1527                         element, filename = arguments
1528                         if element in universe.contents: message = "The \"" + element + "\" element already exists."
1529                         else:
1530                                 message = "You create \"" + element + "\" within the universe."
1531                                 logline = user.account.get("name") + " created an element: " + element
1532                                 if filename:
1533                                         logline += " in file " + filename
1534                                         if filename not in universe.files:
1535                                                 message += " Warning: \"" + filename + "\" is not yet included in any other file and will not be read on startup unless this is remedied."
1536                                 Element(element, universe, filename)
1537                                 log(logline)
1538                 elif len(arguments) > 2: message = "You can only specify an element and a filename."
1539         user.send(message)
1540
1541 def command_destroy(user, parameters):
1542         """Destroy an element if it exists."""
1543         if not parameters: message = "You must specify an element to destroy."
1544         else:
1545                 if parameters not in universe.contents: message = "The \"" + parameters + "\" element does not exist."
1546                 else:
1547                         universe.contents[parameters].destroy()
1548                         message = "You destroy \"" + parameters + "\" within the universe."
1549                         log(user.account.get("name") + " destroyed an element: " + parameters)
1550         user.send(message)
1551
1552 def command_set(user, parameters):
1553         """Set a facet of an element."""
1554         if not parameters: message = "You must specify an element, a facet and a value."
1555         else:
1556                 arguments = parameters.split(" ", 2)
1557                 if len(arguments) == 1: message = "What facet of element \"" + arguments[0] + "\" would you like to set?"
1558                 elif len(arguments) == 2: message = "What value would you like to set for the \"" + arguments[1] + "\" facet of the \"" + arguments[0] + "\" element?"
1559                 else:
1560                         element, facet, value = arguments
1561                         if element not in universe.contents: message = "The \"" + element + "\" element does not exist."
1562                         else:
1563                                 universe.contents[element].set(facet, value)
1564                                 message = "You have successfully (re)set the \"" + facet + "\" facet of element \"" + element + "\". Try \"show element " + element + "\" for verification."
1565         user.send(message)
1566
1567 def command_delete(user, parameters):
1568         """Delete a facet from an element."""
1569         if not parameters: message = "You must specify an element and a facet."
1570         else:
1571                 arguments = parameters.split(" ")
1572                 if len(arguments) == 1: message = "What facet of element \"" + arguments[0] + "\" would you like to delete?"
1573                 elif len(arguments) != 2: message = "You may only specify an element and a facet."
1574                 else:
1575                         element, facet = arguments
1576                         if element not in universe.contents: message = "The \"" + element + "\" element does not exist."
1577                         elif facet not in universe.contents[element].facets(): message = "The \"" + element + "\" element has no \"" + facet + "\" facet."
1578                         else:
1579                                 universe.contents[element].delete(facet)
1580                                 message = "You have successfully deleted the \"" + facet + "\" facet of element \"" + element + "\". Try \"show element " + element + "\" for verification."
1581         user.send(message)
1582
1583 def command_error(user, input_data):
1584         """Generic error for an unrecognized command word."""
1585
1586         # 90% of the time use a generic error
1587         if randrange(10):
1588                 message = "I'm not sure what \"" + input_data + "\" means..."
1589
1590         # 10% of the time use the classic diku error
1591         else:
1592                 message = "Arglebargle, glop-glyf!?!"
1593
1594         # send the error message
1595         user.send(message)
1596
1597 # if there is no universe, create an empty one
1598 if not "universe" in locals(): universe = Universe()
1599