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