1 """Core objects for the mudpy engine."""
3 # Copyright (c) 2006 mudpy, The Fungi <fungi@yuggoth.org>, all rights reserved.
4 # Licensed per terms in the LICENSE file distributed with this software.
6 # import some things we need
7 from ConfigParser import RawConfigParser
8 from md5 import new as new_md5
9 from os import _exit, R_OK, W_OK, access, chmod, close, fork, getpid, makedirs, remove, setsid, stat
10 from os.path import abspath, dirname, exists, isabs, join as path_join
11 from random import choice, randrange
13 from signal import SIGHUP, SIGTERM, signal
14 from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
15 from stat import S_IMODE, ST_MODE
16 from sys import stderr
17 from syslog import LOG_PID, LOG_INFO, LOG_DAEMON, closelog, openlog, syslog
18 from telnetlib import DO, DONT, ECHO, EOR, GA, IAC, LINEMODE, SB, SE, SGA, WILL, WONT
19 from time import asctime, sleep
20 from traceback import format_exception
22 def excepthook(excepttype, value, traceback):
23 """Handle uncaught exceptions."""
25 # assemble the list of errors into a single string
26 message = "".join(format_exception(excepttype, value, traceback))
28 # try to log it, if possible
32 # try to write it to stderr, if possible
33 try: stderr.write(message)
36 # redefine sys.excepthook with ours
38 sys.excepthook = excepthook
40 def sighook(what, where):
41 """Handle external signals."""
44 message = "Caught signal: "
48 message += "hangup (reloading)"
49 universe.reload_modules = True
51 # for a terminate signal
53 message += "terminate (halting)"
54 universe.terminate_world = True
56 # catchall for unexpected signals
57 else: message += str(what) + " (unhandled)"
62 # assign the sgnal handlers
63 signal(SIGHUP, sighook)
64 signal(SIGTERM, sighook)
67 """An element of the universe."""
68 def __init__(self, key, universe, filename=None):
69 """Set up a new element."""
71 # keep track of our key name
74 # keep track of what universe it's loading` into
75 self.universe = universe
77 # clone attributes if this is replacing another element
78 if self.key in self.universe.contents:
79 old_element = self.universe.contents[self.key]
80 for attribute in vars(old_element).keys():
81 exec("self." + attribute + " = old_element." + attribute)
82 if self.owner: self.owner.avatar = self
84 # i guess this is a new element then
87 # not owned by a user by default (used for avatars)
90 # no contents in here by default
93 # parse out appropriate category and subkey names, add to list
94 if self.key.find(":") > 0:
95 self.category, self.subkey = self.key.split(":", 1)
97 self.category = "other"
98 self.subkey = self.key
99 if not self.category in self.universe.categories:
100 self.category = "other"
101 self.subkey = self.key
103 # get an appropriate filename for the origin
104 if not filename: filename = self.universe.default_origins[self.category]
105 if not isabs(filename): filename = abspath(filename)
107 # add the file if it doesn't exist yet
108 if not filename in self.universe.files: DataFile(filename, self.universe)
110 # record or reset a pointer to the origin file
111 self.origin = self.universe.files[filename]
113 # add a data section to the origin if necessary
114 if not self.origin.data.has_section(self.key):
115 self.origin.data.add_section(self.key)
117 # add or replace this element in the universe
118 self.universe.contents[self.key] = self
119 self.universe.categories[self.category][self.subkey] = self
122 """Create a new element and replace this one."""
123 new_element = Element(self.key, self.universe, self.origin.filename)
126 """Remove an element from the universe and destroy it."""
127 self.origin.data.remove_section(self.key)
128 del self.universe.categories[self.category][self.subkey]
129 del self.universe.contents[self.key]
132 """Return a list of non-inherited facets for this element."""
133 if self.key in self.origin.data.sections():
134 return self.origin.data.options(self.key)
136 def has_facet(self, facet):
137 """Return whether the non-inherited facet exists."""
138 return facet in self.facets()
139 def remove_facet(self, facet):
140 """Remove a facet from the element."""
141 if self.has_facet(facet):
142 self.origin.data.remove_option(self.key, facet)
143 self.origin.modified = True
145 """Return a list of the element's inheritance lineage."""
146 if self.has_facet("inherit"):
147 ancestry = self.getlist("inherit")
148 for parent in ancestry[:]:
149 ancestors = self.universe.contents[parent].ancestry()
150 for ancestor in ancestors:
151 if ancestor not in ancestry: ancestry.append(ancestor)
154 def get(self, facet, default=None):
155 """Retrieve values."""
156 if default is None: default = ""
157 if self.origin.data.has_option(self.key, facet):
158 return self.origin.data.get(self.key, facet)
159 elif self.has_facet("inherit"):
160 for ancestor in self.ancestry():
161 if self.universe.contents[ancestor].has_facet(facet):
162 return self.universe.contents[ancestor].get(facet)
164 def getboolean(self, facet, default=None):
165 """Retrieve values as boolean type."""
166 if default is None: default=False
167 if self.origin.data.has_option(self.key, facet):
168 return self.origin.data.getboolean(self.key, facet)
169 elif self.has_facet("inherit"):
170 for ancestor in self.ancestry():
171 if self.universe.contents[ancestor].has_facet(facet):
172 return self.universe.contents[ancestor].getboolean(facet)
174 def getint(self, facet, default=None):
175 """Return values as int/long type."""
176 if default is None: default = 0
177 if self.origin.data.has_option(self.key, facet):
178 return self.origin.data.getint(self.key, facet)
179 elif self.has_facet("inherit"):
180 for ancestor in self.ancestry():
181 if self.universe.contents[ancestor].has_facet(facet):
182 return self.universe.contents[ancestor].getint(facet)
184 def getfloat(self, facet, default=None):
185 """Return values as float type."""
186 if default is None: default = 0.0
187 if self.origin.data.has_option(self.key, facet):
188 return self.origin.data.getfloat(self.key, facet)
189 elif self.has_facet("inherit"):
190 for ancestor in self.ancestry():
191 if self.universe.contents[ancestor].has_facet(facet):
192 return self.universe.contents[ancestor].getfloat(facet)
194 def getlist(self, facet, default=None):
195 """Return values as list type."""
196 if default is None: default = []
197 value = self.get(facet)
198 if value: return makelist(value)
200 def getdict(self, facet, default=None):
201 """Return values as dict type."""
202 if default is None: default = {}
203 value = self.get(facet)
204 if value: return makedict(value)
206 def set(self, facet, value):
208 if not self.has_facet(facet) or not self.get(facet) == value:
209 if type(value) is long: value = str(value)
210 elif not type(value) is str: value = repr(value)
211 self.origin.data.set(self.key, facet, value)
212 self.origin.modified = True
213 def append(self, facet, value):
214 """Append value tp a list."""
215 if type(value) is long: value = str(value)
216 elif not type(value) is str: value = repr(value)
217 newlist = self.getlist(facet)
218 newlist.append(value)
219 self.set(facet, newlist)
221 def new_event(self, action, when=None):
222 """Create, attach and enqueue an event element."""
224 # if when isn't specified, that means now
225 if not when: when = self.universe.get_time()
227 # events are elements themselves
228 event = Element("event:" + self.key + ":" + counter)
230 def send(self, message, eol="$(eol)"):
231 """Convenience method to pass messages to an owner."""
232 if self.owner: self.owner.send(message, eol)
234 def can_run(self, command):
235 """Check if the user can run this command object."""
237 # has to be in the commands category
238 if command not in self.universe.categories["command"].values(): result = False
240 # avatars of administrators can run any command
241 elif self.owner and self.owner.account.getboolean("administrator"): result = True
243 # everyone can run non-administrative commands
244 elif not command.getboolean("administrative"): result = True
246 # otherwise the command cannot be run by this actor
249 # pass back the result
252 def update_location(self):
253 """Make sure the location's contents contain this element."""
254 location = self.get("location")
255 if location in self.universe.contents:
256 self.universe.contents[location].contents[self.key] = self
257 def clean_contents(self):
258 """Make sure the element's contents aren't bogus."""
259 for element in self.contents.values():
260 if element.get("location") != self.key:
261 del self.contents[element.key]
262 def go_to(self, location):
263 """Relocate the element to a specific location."""
264 current = self.get("location")
265 if current and self.key in self.universe.contents[current].contents:
266 del universe.contents[current].contents[self.key]
267 if location in self.universe.contents: self.set("location", location)
268 self.universe.contents[location].contents[self.key] = self
269 self.look_at(location)
271 """Relocate the element to its default location."""
272 self.go_to(self.get("default_location"))
273 self.echo_to_location("You suddenly realize that " + self.get("name") + " is here.")
274 def move_direction(self, direction):
275 """Relocate the element in a specified direction."""
276 self.echo_to_location(self.get("name") + " exits " + self.universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
277 self.send("You exit " + self.universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
278 self.go_to(self.universe.contents[self.get("location")].link_neighbor(direction))
279 self.echo_to_location(self.get("name") + " arrives from " + self.universe.categories["internal"]["directions"].getdict(direction)["enter"] + ".")
280 def look_at(self, key):
281 """Show an element to another element."""
283 element = self.universe.contents[key]
285 name = element.get("name")
286 if name: message += "$(cyn)" + name + "$(nrm)$(eol)"
287 description = element.get("description")
288 if description: message += description + "$(eol)"
289 portal_list = element.portals().keys()
292 message += "$(cyn)[ Exits: " + ", ".join(portal_list) + " ]$(nrm)$(eol)"
293 for element in self.universe.contents[self.get("location")].contents.values():
294 if element.getboolean("is_actor") and element is not self:
295 message += "$(yel)" + element.get("name") + " is here.$(nrm)$(eol)"
296 elif element is not self:
297 message += "$(grn)" + element.get("impression") + "$(nrm)$(eol)"
300 """Map the portal directions for a room to neighbors."""
302 if match("""^location:-?\d+,-?\d+,-?\d+$""", self.key):
303 coordinates = [(int(x)) for x in self.key.split(":")[1].split(",")]
304 directions = self.universe.categories["internal"]["directions"]
305 offsets = dict([(x, directions.getdict(x)["vector"]) for x in directions.facets()])
306 for portal in self.getlist("gridlinks"):
307 adjacent = map(lambda c,o: c+o, coordinates, offsets[portal])
308 neighbor = "location:" + ",".join([(str(x)) for x in adjacent])
309 if neighbor in self.universe.contents: portals[portal] = neighbor
310 for facet in self.facets():
311 if facet.startswith("link_"):
312 neighbor = self.get(facet)
313 if neighbor in self.universe.contents:
314 portal = facet.split("_")[1]
315 portals[portal] = neighbor
317 def link_neighbor(self, direction):
318 """Return the element linked in a given direction."""
319 portals = self.portals()
320 if direction in portals: return portals[direction]
321 def echo_to_location(self, message):
322 """Show a message to other elements in the current location."""
323 for element in self.universe.contents[self.get("location")].contents.values():
324 if element is not self: element.send(message)
327 """A file containing universe elements."""
328 def __init__(self, filename, universe):
329 self.filename = filename
330 self.universe = universe
333 """Read a file and create elements accordingly."""
334 self.modified = False
335 self.data = RawConfigParser()
336 if access(self.filename, R_OK): self.data.read(self.filename)
337 if not hasattr(self.universe, "files"): self.universe.files = {}
338 self.universe.files[self.filename] = self
339 if self.data.has_option("__control__", "include_files"):
340 includes = makelist(self.data.get("__control__", "include_files"))
342 if self.data.has_option("__control__", "default_files"):
343 origins = makedict(self.data.get("__control__", "default_files"))
344 for key in origins.keys():
345 if not key in includes: includes.append(key)
346 self.universe.default_origins[key] = origins[key]
347 if not key in self.universe.categories:
348 self.universe.categories[key] = {}
349 if self.data.has_option("__control__", "private_files"):
350 for item in makelist(self.data.get("__control__", "private_files")):
351 if not item in includes: includes.append(item)
352 if not item in self.universe.private_files:
354 item = path_join(dirname(self.filename), item)
355 self.universe.private_files.append(item)
356 for section in self.data.sections():
357 if section != "__control__":
358 Element(section, self.universe, self.filename)
359 for include_file in includes:
360 if not isabs(include_file):
361 include_file = path_join(dirname(self.filename), include_file)
362 if include_file not in self.universe.files or not self.universe.files[include_file].is_writeable():
363 DataFile(include_file, self.universe)
365 """Write the data, if necessary."""
367 # when modified, writeable and has content or the file exists
368 if self.modified and self.is_writeable() and ( self.data.sections() or exists(self.filename) ):
370 # make parent directories if necessary
371 if not exists(dirname(self.filename)):
372 makedirs(dirname(self.filename))
375 file_descriptor = file(self.filename, "w")
377 # if it's marked private, chmod it appropriately
378 if self.filename in self.universe.private_files and oct(S_IMODE(stat(self.filename)[ST_MODE])) != 0600:
379 chmod(self.filename, 0600)
381 # write it back sorted, instead of using ConfigParser
382 sections = self.data.sections()
384 for section in sections:
385 file_descriptor.write("[" + section + "]\n")
386 options = self.data.options(section)
388 for option in options:
389 file_descriptor.write(option + " = " + self.data.get(section, option) + "\n")
390 file_descriptor.write("\n")
392 # flush and close the file
393 file_descriptor.flush()
394 file_descriptor.close()
396 # unset the modified flag
397 self.modified = False
398 def is_writeable(self):
399 """Returns True if the __control__ read_only is False."""
400 return not self.data.has_option("__control__", "read_only") or not self.data.getboolean("__control__", "read_only")
405 def __init__(self, filename=""):
406 """Initialize the universe."""
409 self.default_origins = {}
410 self.private_files = []
412 self.pending_events_long = {}
413 self.pending_events_short = {}
415 self.terminate_world = False
416 self.reload_modules = False
418 possible_filenames = [
424 "/usr/local/mudpy/mudpy.conf",
425 "/usr/local/mudpy/etc/mudpy.conf",
426 "/etc/mudpy/mudpy.conf",
429 for filename in possible_filenames:
430 if access(filename, R_OK): break
431 if not isabs(filename):
432 filename = abspath(filename)
433 self.filename = filename
437 """Load universe data from persistent storage."""
439 # the files dict must exist and filename needs to be read-only
440 if not hasattr(self, "files") or not ( self.filename in self.files and self.files[self.filename].is_writeable() ):
442 # clear out all read-only files
443 if hasattr(self, "files"):
444 for data_filename in self.files.keys():
445 if not self.files[data_filename].is_writeable():
446 del self.files[data_filename]
448 # start loading from the initial file
449 DataFile(self.filename, self)
451 # make a list of inactive avatars
452 inactive_avatars = []
453 for account in self.categories["account"].values():
454 inactive_avatars += [ (self.contents[x]) for x in account.getlist("avatars") ]
455 for user in self.userlist:
456 if user.avatar in inactive_avatars:
457 inactive_avatars.remove(user.avatar)
459 # go through all elements to clear out inactive avatar locations
460 for element in self.contents.values():
461 location = element.get("location")
462 if element in inactive_avatars and location:
463 if location in self.contents and element.key in self.contents[location].contents:
464 del self.contents[location].contents[element.key]
465 element.set("default_location", location)
466 element.remove_facet("location")
468 # another pass to straighten out all the element contents
469 for element in self.contents.values():
470 element.update_location()
471 element.clean_contents()
474 """Save the universe to persistent storage."""
475 for key in self.files: self.files[key].save()
477 def initialize_server_socket(self):
478 """Create and open the listening socket."""
480 # create a new ipv4 stream-type socket object
481 self.listening_socket = socket(AF_INET, SOCK_STREAM)
483 # set the socket options to allow existing open ones to be
484 # reused (fixes a bug where the server can't bind for a minute
485 # when restarting on linux systems)
486 self.listening_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
488 # bind the socket to to our desired server ipa and port
489 host = self.categories["internal"]["network"].get("host")
490 port = self.categories["internal"]["network"].getint("port")
491 self.listening_socket.bind((host, port))
493 # disable blocking so we can proceed whether or not we can
495 self.listening_socket.setblocking(0)
497 # start listening on the socket
498 self.listening_socket.listen(1)
500 # note that we're now ready for user connections
501 if not host: host = "0.0.0.0"
502 log("Listening for Telnet connections on: " + host + ":" + str(port))
505 """Convenience method to get the elapsed time counter."""
506 return self.categories["internal"]["counters"].getint("elapsed")
509 """This is a connected user."""
512 """Default values for the in-memory user variables."""
515 self.authenticated = False
517 self.connection = None
520 self.input_queue = []
521 self.last_address = ""
522 self.menu_choices = {}
523 self.menu_seen = False
524 self.negotiation_pause = 0
525 self.output_queue = []
526 self.partial_input = ""
527 self.password_tries = 0
528 self.received_newline = True
529 self.state = "initial"
530 self.terminator = IAC+GA
533 """Log, close the connection and remove."""
534 if self.account: name = self.account.get("name")
536 if name: message = "User " + name
537 else: message = "An unnamed user"
538 message += " logged out."
540 self.deactivate_avatar()
541 self.connection.close()
545 """Save, load a new user and relocate the connection."""
547 # get out of the list
550 # create a new user object
553 # set everything equivalent
554 for attribute in vars(self).keys():
555 exec("new_user." + attribute + " = self." + attribute)
557 # the avatar needs a new owner
558 if new_user.avatar: new_user.avatar.owner = new_user
561 universe.userlist.append(new_user)
563 # get rid of the old user object
566 def replace_old_connections(self):
567 """Disconnect active users with the same name."""
569 # the default return value
572 # iterate over each user in the list
573 for old_user in universe.userlist:
575 # the name is the same but it's not us
576 if old_user.account.get("name") == self.account.get("name") and old_user is not self:
579 log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".", 2)
580 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)", flush=True, add_prompt=False)
582 # close the old connection
583 old_user.connection.close()
585 # replace the old connection with this one
586 old_user.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
587 old_user.connection = self.connection
588 old_user.last_address = old_user.address
589 old_user.address = self.address
591 # may need to tell the new connection to echo
593 old_user.send(get_echo_sequence(old_user.state, self.echoing), raw=True)
595 # take this one out of the list and delete
601 # true if an old connection was replaced, false if not
604 def authenticate(self):
605 """Flag the user as authenticated and disconnect duplicates."""
606 if not self.state is "authenticated":
607 log("User " + self.account.get("name") + " logged in.", 2)
608 self.authenticated = True
609 if self.account.subkey in universe.categories["internal"]["limits"].getlist("default_admins"):
610 self.account.set("administrator", "True")
613 """Send the user their current menu."""
614 if not self.menu_seen:
615 self.menu_choices = get_menu_choices(self)
616 self.send(get_menu(self.state, self.error, self.echoing, self.terminator, self.menu_choices), "")
617 self.menu_seen = True
619 self.adjust_echoing()
621 def adjust_echoing(self):
622 """Adjust echoing to match state menu requirements."""
623 if self.echoing and not menu_echo_on(self.state): self.echoing = False
624 elif not self.echoing and menu_echo_on(self.state): self.echoing = True
627 """Remove a user from the list of connected users."""
628 universe.userlist.remove(self)
630 def send(self, output, eol="$(eol)", raw=False, flush=False, add_prompt=True, just_prompt=False):
631 """Send arbitrary text to a connected user."""
633 # unless raw mode is on, clean it up all nice and pretty
636 # strip extra $(eol) off if present
637 while output.startswith("$(eol)"): output = output[6:]
638 while output.endswith("$(eol)"): output = output[:-6]
640 # we'll take out GA or EOR and add them back on the end
641 if output.endswith(IAC+GA) or output.endswith(IAC+EOR):
644 else: terminate = False
646 # start with a newline, append the message, then end
647 # with the optional eol string passed to this function
648 # and the ansi escape to return to normal text
649 if not just_prompt: output = "$(eol)$(eol)" + output
650 output += eol + chr(27) + "[0m"
652 # tack on a prompt if active
653 if self.state == "active":
654 if not just_prompt: output += "$(eol)"
655 if add_prompt: output += "> "
657 # find and replace macros in the output
658 output = replace_macros(self, output)
660 # wrap the text at 80 characters
661 output = wrap_ansi_text(output, 80)
663 # tack the terminator back on
664 if terminate: output += self.terminator
666 # drop the output into the user's output queue
667 self.output_queue.append(output)
669 # if this is urgent, flush all pending output
670 if flush: self.flush()
673 """All the things to do to the user per increment."""
675 # if the world is terminating, disconnect
676 if universe.terminate_world:
677 self.state = "disconnecting"
678 self.menu_seen = False
680 # if output is paused, decrement the counter
681 if self.state == "initial":
682 if self.negotiation_pause: self.negotiation_pause -= 1
683 else: self.state = "entering_account_name"
685 # show the user a menu as needed
686 elif not self.state == "active": self.show_menu()
688 # flush any pending output in teh queue
691 # disconnect users with the appropriate state
692 if self.state == "disconnecting": self.quit()
694 # check for input and add it to the queue
697 # there is input waiting in the queue
698 if self.input_queue: handle_user_input(self)
701 """Try to send the last item in the queue and remove it."""
702 if self.output_queue:
703 if self.received_newline:
704 self.received_newline = False
705 if self.output_queue[0].startswith("\r\n"):
706 self.output_queue[0] = self.output_queue[0][2:]
708 self.connection.send(self.output_queue[0])
709 del self.output_queue[0]
714 def enqueue_input(self):
715 """Process and enqueue any new input."""
717 # check for some input
719 input_data = self.connection.recv(1024)
726 # tack this on to any previous partial
727 self.partial_input += input_data
729 # reply to and remove any IAC negotiation codes
730 self.negotiate_telnet_options()
732 # separate multiple input lines
733 new_input_lines = self.partial_input.split("\n")
735 # if input doesn't end in a newline, replace the
736 # held partial input with the last line of it
737 if not self.partial_input.endswith("\n"):
738 self.partial_input = new_input_lines.pop()
740 # otherwise, chop off the extra null input and reset
741 # the held partial input
743 new_input_lines.pop()
744 self.partial_input = ""
746 # iterate over the remaining lines
747 for line in new_input_lines:
749 # remove a trailing carriage return
750 if line.endswith("\r"): line = line.rstrip("\r")
752 # log non-printable characters remaining
753 removed = filter(lambda x: (x < " " or x > "~"), line)
755 logline = "Non-printable characters from "
756 if self.account and self.account.get("name"): logline += self.account.get("name") + ": "
757 else: logline += "unknown user: "
758 logline += repr(removed)
761 # filter out non-printables
762 line = filter(lambda x: " " <= x <= "~", line)
764 # strip off extra whitespace
767 # put on the end of the queue
768 self.input_queue.append(line)
770 def negotiate_telnet_options(self):
771 """Reply to/remove partial_input telnet negotiation options."""
773 # start at the begining of the input
776 # make a local copy to play with
777 text = self.partial_input
779 # as long as we haven't checked it all
780 while position < len(text):
782 # jump to the first IAC you find
783 position = text.find(IAC, position)
785 # if there wasn't an IAC in the input, skip to the end
786 if position < 0: position = len(text)
788 # replace a double (literal) IAC if there's an LF later
789 elif len(text) > position+1 and text[position+1] == IAC:
790 if text.find("\n", position) > 0: text = text.replace(IAC+IAC, IAC)
794 # this must be an option negotiation
795 elif len(text) > position+2 and text[position+1] in (DO, DONT, WILL, WONT):
797 negotiation = text[position+1:position+3]
799 # if we turned echo off, ignore the confirmation
800 if not self.echoing and negotiation == DO+ECHO: pass
803 elif negotiation == WILL+LINEMODE: self.send(IAC+DO+LINEMODE, raw=True)
805 # if the client likes EOR instead of GA, make a note of it
806 elif negotiation == DO+EOR: self.terminator = IAC+EOR
807 elif negotiation == DONT+EOR and self.terminator == IAC+EOR:
808 self.terminator = IAC+GA
810 # if the client doesn't want GA, oblige
811 elif negotiation == DO+SGA and self.terminator == IAC+GA:
813 self.send(IAC+WILL+SGA, raw=True)
815 # we don't want to allow anything else
816 elif text[position+1] == DO: self.send(IAC+WONT+text[position+2], raw=True)
817 elif text[position+1] == WILL: self.send(IAC+DONT+text[position+2], raw=True)
819 # strip the negotiation from the input
820 text = text.replace(text[position:position+3], "")
822 # get rid of IAC SB .* IAC SE
823 elif len(text) > position+4 and text[position:position+2] == IAC+SB:
824 end_subnegotiation = text.find(IAC+SE, position)
825 if end_subnegotiation > 0: text = text[:position] + text[end_subnegotiation+2:]
828 # otherwise, strip out a two-byte IAC command
829 elif len(text) > position+2: text = text.replace(text[position:position+2], "")
831 # and this means we got the begining of an IAC
834 # replace the input with our cleaned-up text
835 self.partial_input = text
837 def new_avatar(self):
838 """Instantiate a new, unconfigured avatar for this user."""
840 while "avatar:" + self.account.get("name") + ":" + str(counter) in universe.categories["actor"].keys(): counter += 1
841 self.avatar = Element("actor:avatar:" + self.account.get("name") + ":" + str(counter), universe)
842 self.avatar.append("inherit", "archetype:avatar")
843 self.account.append("avatars", self.avatar.key)
845 def delete_avatar(self, avatar):
846 """Remove an avatar from the world and from the user's list."""
847 if self.avatar is universe.contents[avatar]: self.avatar = None
848 universe.contents[avatar].destroy()
849 avatars = self.account.getlist("avatars")
850 avatars.remove(avatar)
851 self.account.set("avatars", avatars)
853 def activate_avatar_by_index(self, index):
854 """Enter the world with a particular indexed avatar."""
855 self.avatar = universe.contents[self.account.getlist("avatars")[index]]
856 self.avatar.owner = self
857 self.state = "active"
858 self.avatar.go_home()
860 def deactivate_avatar(self):
861 """Have the active avatar leave the world."""
863 current = self.avatar.get("location")
864 self.avatar.set("default_location", current)
865 self.avatar.echo_to_location("You suddenly wonder where " + self.avatar.get("name") + " went.")
866 del universe.contents[current].contents[self.avatar.key]
867 self.avatar.remove_facet("location")
868 self.avatar.owner = None
872 """Destroy the user and associated avatars."""
873 for avatar in self.account.getlist("avatars"): self.delete_avatar(avatar)
874 self.account.destroy()
876 def list_avatar_names(self):
877 """List names of assigned avatars."""
878 return [ universe.contents[avatar].get("name") for avatar in self.account.getlist("avatars") ]
880 def create_pidfile(universe):
881 """Write a file containing the current process ID."""
883 log("Process ID: " + pid)
884 file_name = universe.contents["internal:process"].get("pidfile")
886 file_descriptor = file(file_name, 'w')
887 file_descriptor.write(pid + "\n")
888 file_descriptor.flush()
889 file_descriptor.close()
891 def remove_pidfile(universe):
892 """Remove the file containing the current process ID."""
893 file_name = universe.contents["internal:process"].get("pidfile")
894 if file_name and access(file_name, W_OK): remove(file_name)
897 """Turn string into list type."""
898 if value[0] + value[-1] == "[]": return eval(value)
899 else: return [ value ]
902 """Turn string into dict type."""
903 if value[0] + value[-1] == "{}": return eval(value)
904 elif value.find(":") > 0: return eval("{" + value + "}")
905 else: return { value: None }
907 def broadcast(message, add_prompt=True):
908 """Send a message to all connected users."""
909 for each_user in universe.userlist: each_user.send("$(eol)" + message, add_prompt=add_prompt)
911 def log(message, level=0):
914 # a couple references we need
915 file_name = universe.categories["internal"]["logging"].get("file")
916 max_log_lines = universe.categories["internal"]["logging"].getint("max_log_lines")
917 syslog_name = universe.categories["internal"]["logging"].get("syslog")
918 timestamp = asctime()[4:19]
920 # turn the message into a list of lines
921 lines = filter(lambda x: x!="", [(x.rstrip()) for x in message.split("\n")])
923 # send the timestamp and line to a file
925 file_descriptor = file(file_name, "a")
926 for line in lines: file_descriptor.write(timestamp + " " + line + "\n")
927 file_descriptor.flush()
928 file_descriptor.close()
930 # send the timestamp and line to standard output
931 if universe.categories["internal"]["logging"].getboolean("stdout"):
932 for line in lines: print(timestamp + " " + line)
934 # send the line to the system log
936 openlog(syslog_name, LOG_PID, LOG_INFO | LOG_DAEMON)
937 for line in lines: syslog(line)
940 # display to connected administrators
941 for user in universe.userlist:
942 if user.state == "active" and user.account.getboolean("administrator") and user.account.getint("loglevel") <= level:
943 # iterate over every line in the message
946 full_message += "$(bld)$(red)" + timestamp + " " + line + "$(nrm)$(eol)"
947 user.send(full_message, flush=True)
949 # add to the recent log list
951 while 0 < len(universe.loglines) >= max_log_lines: del universe.loglines[0]
952 universe.loglines.append((level, timestamp + " " + line))
954 def get_loglines(level, start, stop):
955 """Return a specific range of loglines filtered by level."""
957 # filter the log lines
958 loglines = filter(lambda x: x[0]>=level, universe.loglines)
960 # we need these in several places
961 total_count = str(len(universe.loglines))
962 filtered_count = len(loglines)
964 # don't proceed if there are no lines
967 # can't start before the begining or at the end
968 if start > filtered_count: start = filtered_count
969 if start < 1: start = 1
971 # can't stop before we start
972 if stop > start: stop = start
973 elif stop < 1: stop = 1
976 message = "There are " + str(total_count)
977 message += " log lines in memory and " + str(filtered_count)
978 message += " at or above level " + str(level) + "."
979 message += " The lines from " + str(stop) + " to " + str(start)
980 message += " are:$(eol)$(eol)"
982 # add the text from the selected lines
983 if stop > 1: range_lines = loglines[-start:-(stop-1)]
984 else: range_lines = loglines[-start:]
985 for line in range_lines:
986 message += " (" + str(line[0]) + ") " + line[1] + "$(eol)"
988 # there were no lines
990 message = "None of the " + str(total_count)
991 message += " lines in memory matches your request."
996 def wrap_ansi_text(text, width):
997 """Wrap text with arbitrary width while ignoring ANSI colors."""
999 # the current position in the entire text string, including all
1000 # characters, printable or otherwise
1001 absolute_position = 0
1003 # the current text position relative to the begining of the line,
1004 # ignoring color escape sequences
1005 relative_position = 0
1007 # whether the current character is part of a color escape sequence
1010 # iterate over each character from the begining of the text
1011 for each_character in text:
1013 # the current character is the escape character
1014 if each_character == chr(27):
1017 # the current character is within an escape sequence
1020 # the current character is m, which terminates the
1021 # current escape sequence
1022 if each_character == "m":
1025 # the current character is a newline, so reset the relative
1026 # position (start a new line)
1027 elif each_character == "\n":
1028 relative_position = 0
1030 # the current character meets the requested maximum line width,
1031 # so we need to backtrack and find a space at which to wrap
1032 elif relative_position == width:
1034 # distance of the current character examined from the
1038 # count backwards until we find a space
1039 while text[absolute_position - wrap_offset] != " ":
1042 # insert an eol in place of the space
1043 text = text[:absolute_position - wrap_offset] + "\r\n" + text[absolute_position - wrap_offset + 1:]
1045 # increase the absolute position because an eol is two
1046 # characters but the space it replaced was only one
1047 absolute_position += 1
1049 # now we're at the begining of a new line, plus the
1050 # number of characters wrapped from the previous line
1051 relative_position = wrap_offset
1053 # as long as the character is not a carriage return and the
1054 # other above conditions haven't been met, count it as a
1055 # printable character
1056 elif each_character != "\r":
1057 relative_position += 1
1059 # increase the absolute position for every character
1060 absolute_position += 1
1062 # return the newly-wrapped text
1065 def weighted_choice(data):
1066 """Takes a dict weighted by value and returns a random key."""
1068 # this will hold our expanded list of keys from the data
1071 # create thee expanded list of keys
1072 for key in data.keys():
1073 for count in range(data[key]):
1074 expanded.append(key)
1076 # return one at random
1077 return choice(expanded)
1080 """Returns a random character name."""
1082 # the vowels and consonants needed to create romaji syllables
1083 vowels = [ "a", "i", "u", "e", "o" ]
1084 consonants = ["'", "k", "z", "s", "sh", "z", "j", "t", "ch", "ts", "d", "n", "h", "f", "m", "y", "r", "w" ]
1086 # this dict will hold our weighted list of syllables
1089 # generate the list with an even weighting
1090 for consonant in consonants:
1091 for vowel in vowels:
1092 syllables[consonant + vowel] = 1
1094 # we'll build the name into this string
1097 # create a name of random length from the syllables
1098 for syllable in range(randrange(2, 6)):
1099 name += weighted_choice(syllables)
1101 # strip any leading quotemark, capitalize and return the name
1102 return name.strip("'").capitalize()
1104 def replace_macros(user, text, is_input=False):
1105 """Replaces macros in text output."""
1110 # third person pronouns
1112 "female": { "obj": "her", "pos": "hers", "sub": "she" },
1113 "male": { "obj": "him", "pos": "his", "sub": "he" },
1114 "neuter": { "obj": "it", "pos": "its", "sub": "it" }
1117 # a dict of replacement macros
1120 "$(bld)": chr(27) + "[1m",
1121 "$(nrm)": chr(27) + "[0m",
1122 "$(blk)": chr(27) + "[30m",
1123 "$(blu)": chr(27) + "[34m",
1124 "$(cyn)": chr(27) + "[36m",
1125 "$(grn)": chr(27) + "[32m",
1126 "$(mgt)": chr(27) + "[35m",
1127 "$(red)": chr(27) + "[31m",
1128 "$(yel)": chr(27) + "[33m",
1131 # add dynamic macros where possible
1133 account_name = user.account.get("name")
1135 macros["$(account)"] = account_name
1137 avatar_gender = user.avatar.get("gender")
1139 macros["$(tpop)"] = pronouns[avatar_gender]["obj"]
1140 macros["$(tppp)"] = pronouns[avatar_gender]["pos"]
1141 macros["$(tpsp)"] = pronouns[avatar_gender]["sub"]
1143 # find and replace per the macros dict
1144 macro_start = text.find("$(")
1145 if macro_start == -1: break
1146 macro_end = text.find(")", macro_start) + 1
1147 macro = text[macro_start:macro_end]
1148 if macro in macros.keys():
1149 text = text.replace(macro, macros[macro])
1151 # if we get here, log and replace it with null
1153 text = text.replace(macro, "")
1155 log("Unexpected replacement macro " + macro + " encountered.", 6)
1157 # replace the look-like-a-macro sequence
1158 text = text.replace("$_(", "$(")
1162 def escape_macros(text):
1163 """Escapes replacement macros in text."""
1164 return text.replace("$(", "$_(")
1166 def check_time(frequency):
1167 """Check for a factor of the current increment count."""
1168 if type(frequency) is str:
1169 frequency = universe.categories["internal"]["time"].getint(frequency)
1170 if not "counters" in universe.categories["internal"]:
1171 Element("internal:counters", universe)
1172 return not universe.categories["internal"]["counters"].getint("elapsed") % frequency
1175 """The things which should happen on each pulse, aside from reloads."""
1177 # open the listening socket if it hasn't been already
1178 if not hasattr(universe, "listening_socket"):
1179 universe.initialize_server_socket()
1181 # assign a user if a new connection is waiting
1182 user = check_for_connection(universe.listening_socket)
1183 if user: universe.userlist.append(user)
1185 # iterate over the connected users
1186 for user in universe.userlist: user.pulse()
1188 # update the log every now and then
1189 if check_time("frequency_log"):
1190 log(str(len(universe.userlist)) + " connection(s)")
1192 # periodically save everything
1193 if check_time("frequency_save"):
1196 # pause for a configurable amount of time (decimal seconds)
1197 sleep(universe.categories["internal"]["time"].getfloat("increment"))
1199 # increase the elapsed increment counter
1200 universe.categories["internal"]["counters"].set("elapsed", universe.categories["internal"]["counters"].getint("elapsed") + 1)
1203 """Reload all relevant objects."""
1204 for user in universe.userlist[:]: user.reload()
1205 for element in universe.contents.values():
1206 if element.origin.is_writeable(): element.reload()
1209 def check_for_connection(listening_socket):
1210 """Check for a waiting connection and return a new user object."""
1212 # try to accept a new connection
1214 connection, address = listening_socket.accept()
1218 # note that we got one
1219 log("Connection from " + address[0], 2)
1221 # disable blocking so we can proceed whether or not we can send/receive
1222 connection.setblocking(0)
1224 # create a new user object
1227 # associate this connection with it
1228 user.connection = connection
1230 # set the user's ipa from the connection's ipa
1231 user.address = address[0]
1233 # let the client know we WILL EOR
1234 user.send(IAC+WILL+EOR, raw=True)
1235 user.negotiation_pause = 2
1237 # return the new user object
1240 def get_menu(state, error=None, echoing=True, terminator="", choices=None):
1241 """Show the correct menu text to a user."""
1243 # make sure we don't reuse a mutable sequence by default
1244 if choices is None: choices = {}
1246 # begin with a telnet echo command sequence if needed
1247 message = get_echo_sequence(state, echoing)
1249 # get the description or error text
1250 message += get_menu_description(state, error)
1252 # get menu choices for the current state
1253 message += get_formatted_menu_choices(state, choices)
1255 # try to get a prompt, if it was defined
1256 message += get_menu_prompt(state)
1258 # throw in the default choice, if it exists
1259 message += get_formatted_default_menu_choice(state)
1261 # display a message indicating if echo is off
1262 message += get_echo_message(state)
1264 # tack on EOR or GA to indicate the prompt will not be followed by CRLF
1265 message += terminator
1267 # return the assembly of various strings defined above
1270 def menu_echo_on(state):
1271 """True if echo is on, false if it is off."""
1272 return universe.categories["menu"][state].getboolean("echo", True)
1274 def get_echo_sequence(state, echoing):
1275 """Build the appropriate IAC WILL/WONT ECHO sequence as needed."""
1277 # if the user has echo on and the menu specifies it should be turned
1278 # off, send: iac + will + echo + null
1279 if echoing and not menu_echo_on(state): return IAC+WILL+ECHO
1281 # if echo is not set to off in the menu and the user curently has echo
1282 # off, send: iac + wont + echo + null
1283 elif not echoing and menu_echo_on(state): return IAC+WONT+ECHO
1285 # default is not to send an echo control sequence at all
1288 def get_echo_message(state):
1289 """Return a message indicating that echo is off."""
1290 if menu_echo_on(state): return ""
1291 else: return "(won't echo) "
1293 def get_default_menu_choice(state):
1294 """Return the default choice for a menu."""
1295 return universe.categories["menu"][state].get("default")
1297 def get_formatted_default_menu_choice(state):
1298 """Default menu choice foratted for inclusion in a prompt string."""
1299 default_choice = get_default_menu_choice(state)
1300 if default_choice: return "[$(red)" + default_choice + "$(nrm)] "
1303 def get_menu_description(state, error):
1304 """Get the description or error text."""
1306 # an error condition was raised by the handler
1309 # try to get an error message matching the condition
1311 description = universe.categories["menu"][state].get("error_" + error)
1312 if not description: description = "That is not a valid choice..."
1313 description = "$(red)" + description + "$(nrm)"
1315 # there was no error condition
1318 # try to get a menu description for the current state
1319 description = universe.categories["menu"][state].get("description")
1321 # return the description or error message
1322 if description: description += "$(eol)$(eol)"
1325 def get_menu_prompt(state):
1326 """Try to get a prompt, if it was defined."""
1327 prompt = universe.categories["menu"][state].get("prompt")
1328 if prompt: prompt += " "
1331 def get_menu_choices(user):
1332 """Return a dict of choice:meaning."""
1333 menu = universe.categories["menu"][user.state]
1334 create_choices = menu.get("create")
1335 if create_choices: choices = eval(create_choices)
1340 for facet in menu.facets():
1341 if facet.startswith("demand_") and not eval(universe.categories["menu"][user.state].get(facet)):
1342 ignores.append(facet.split("_", 2)[1])
1343 elif facet.startswith("create_"):
1344 creates[facet] = facet.split("_", 2)[1]
1345 elif facet.startswith("choice_"):
1346 options[facet] = facet.split("_", 2)[1]
1347 for facet in creates.keys():
1348 if not creates[facet] in ignores:
1349 choices[creates[facet]] = eval(menu.get(facet))
1350 for facet in options.keys():
1351 if not options[facet] in ignores:
1352 choices[options[facet]] = menu.get(facet)
1355 def get_formatted_menu_choices(state, choices):
1356 """Returns a formatted string of menu choices."""
1358 choice_keys = choices.keys()
1360 for choice in choice_keys:
1361 choice_output += " [$(red)" + choice + "$(nrm)] " + choices[choice] + "$(eol)"
1362 if choice_output: choice_output += "$(eol)"
1363 return choice_output
1365 def get_menu_branches(state):
1366 """Return a dict of choice:branch."""
1368 for facet in universe.categories["menu"][state].facets():
1369 if facet.startswith("branch_"):
1370 branches[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
1373 def get_default_branch(state):
1374 """Return the default branch."""
1375 return universe.categories["menu"][state].get("branch")
1377 def get_choice_branch(user, choice):
1378 """Returns the new state matching the given choice."""
1379 branches = get_menu_branches(user.state)
1380 if choice in branches.keys(): return branches[choice]
1381 elif choice in user.menu_choices.keys(): return get_default_branch(user.state)
1384 def get_menu_actions(state):
1385 """Return a dict of choice:branch."""
1387 for facet in universe.categories["menu"][state].facets():
1388 if facet.startswith("action_"):
1389 actions[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
1392 def get_default_action(state):
1393 """Return the default action."""
1394 return universe.categories["menu"][state].get("action")
1396 def get_choice_action(user, choice):
1397 """Run any indicated script for the given choice."""
1398 actions = get_menu_actions(user.state)
1399 if choice in actions.keys(): return actions[choice]
1400 elif choice in user.menu_choices.keys(): return get_default_action(user.state)
1403 def handle_user_input(user):
1404 """The main handler, branches to a state-specific handler."""
1406 # check to make sure the state is expected, then call that handler
1407 if "handler_" + user.state in globals():
1408 exec("handler_" + user.state + "(user)")
1410 generic_menu_handler(user)
1412 # since we got input, flag that the menu/prompt needs to be redisplayed
1413 user.menu_seen = False
1415 # if the user's client echo is off, send a blank line for aesthetics
1416 if user.echoing: user.received_newline = True
1418 def generic_menu_handler(user):
1419 """A generic menu choice handler."""
1421 # get a lower-case representation of the next line of input
1422 if user.input_queue:
1423 choice = user.input_queue.pop(0)
1424 if choice: choice = choice.lower()
1426 if not choice: choice = get_default_menu_choice(user.state)
1427 if choice in user.menu_choices:
1428 exec(get_choice_action(user, choice))
1429 new_state = get_choice_branch(user, choice)
1430 if new_state: user.state = new_state
1431 else: user.error = "default"
1433 def handler_entering_account_name(user):
1434 """Handle the login account name."""
1436 # get the next waiting line of input
1437 input_data = user.input_queue.pop(0)
1439 # did the user enter anything?
1442 # keep only the first word and convert to lower-case
1443 name = input_data.lower()
1445 # fail if there are non-alphanumeric characters
1446 if name != filter(lambda x: x>="0" and x<="9" or x>="a" and x<="z", name):
1447 user.error = "bad_name"
1449 # if that account exists, time to request a password
1450 elif name in universe.categories["account"]:
1451 user.account = universe.categories["account"][name]
1452 user.state = "checking_password"
1454 # otherwise, this could be a brand new user
1456 user.account = Element("account:" + name, universe)
1457 user.account.set("name", name)
1458 log("New user: " + name, 2)
1459 user.state = "checking_new_account_name"
1461 # if the user entered nothing for a name, then buhbye
1463 user.state = "disconnecting"
1465 def handler_checking_password(user):
1466 """Handle the login account password."""
1468 # get the next waiting line of input
1469 input_data = user.input_queue.pop(0)
1471 # does the hashed input equal the stored hash?
1472 if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
1474 # if so, set the username and load from cold storage
1475 if not user.replace_old_connections():
1477 user.state = "main_utility"
1479 # if at first your hashes don't match, try, try again
1480 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1481 user.password_tries += 1
1482 user.error = "incorrect"
1484 # we've exceeded the maximum number of password failures, so disconnect
1486 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1487 user.state = "disconnecting"
1489 def handler_entering_new_password(user):
1490 """Handle a new password entry."""
1492 # get the next waiting line of input
1493 input_data = user.input_queue.pop(0)
1495 # make sure the password is strong--at least one upper, one lower and
1496 # one digit, seven or more characters in length
1497 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)):
1499 # hash and store it, then move on to verification
1500 user.account.set("passhash", new_md5(user.account.get("name") + input_data).hexdigest())
1501 user.state = "verifying_new_password"
1503 # the password was weak, try again if you haven't tried too many times
1504 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1505 user.password_tries += 1
1508 # too many tries, so adios
1510 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1511 user.account.destroy()
1512 user.state = "disconnecting"
1514 def handler_verifying_new_password(user):
1515 """Handle the re-entered new password for verification."""
1517 # get the next waiting line of input
1518 input_data = user.input_queue.pop(0)
1520 # hash the input and match it to storage
1521 if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
1524 # the hashes matched, so go active
1525 if not user.replace_old_connections(): user.state = "main_utility"
1527 # go back to entering the new password as long as you haven't tried
1529 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1530 user.password_tries += 1
1531 user.error = "differs"
1532 user.state = "entering_new_password"
1534 # otherwise, sayonara
1536 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1537 user.account.destroy()
1538 user.state = "disconnecting"
1540 def handler_active(user):
1541 """Handle input for active users."""
1543 # get the next waiting line of input
1544 input_data = user.input_queue.pop(0)
1549 # split out the command (first word) and parameters (everything else)
1550 if input_data.find(" ") > 0:
1551 command_name, parameters = input_data.split(" ", 1)
1553 command_name = input_data
1556 # lowercase the command
1557 command_name = command_name.lower()
1559 # the command matches a command word for which we have data
1560 if command_name in universe.categories["command"]:
1561 command = universe.categories["command"][command_name]
1562 else: command = None
1564 # if it's allowed, do it
1566 if actor.can_run(command): exec(command.get("action"))
1568 # otherwise, give an error
1569 elif command_name: command_error(actor, input_data)
1571 # if no input, just idle back with a prompt
1572 else: user.send("", just_prompt=True)
1574 def command_halt(actor, parameters):
1575 """Halt the world."""
1578 # see if there's a message or use a generic one
1579 if parameters: message = "Halting: " + parameters
1580 else: message = "User " + actor.owner.account.get("name") + " halted the world."
1583 broadcast(message, add_prompt=False)
1586 # set a flag to terminate the world
1587 universe.terminate_world = True
1589 def command_reload(actor):
1590 """Reload all code modules, configs and data."""
1593 # let the user know and log
1594 actor.send("Reloading all code modules, configs and data.")
1595 log("User " + actor.owner.account.get("name") + " reloaded the world.", 8)
1597 # set a flag to reload
1598 universe.reload_modules = True
1600 def command_quit(actor):
1601 """Leave the world and go back to the main menu."""
1603 actor.owner.state = "main_utility"
1604 actor.owner.deactivate_avatar()
1606 def command_help(actor, parameters):
1607 """List available commands and provide help for commands."""
1609 # did the user ask for help on a specific command word?
1610 if parameters and actor.owner:
1612 # is the command word one for which we have data?
1613 if parameters in universe.categories["command"]:
1614 command = universe.categories["command"][parameters]
1615 else: command = None
1617 # only for allowed commands
1618 if actor.can_run(command):
1620 # add a description if provided
1621 description = command.get("description")
1623 description = "(no short description provided)"
1624 if command.getboolean("administrative"): output = "$(red)"
1625 else: output = "$(grn)"
1626 output += parameters + "$(nrm) - " + description + "$(eol)$(eol)"
1628 # add the help text if provided
1629 help_text = command.get("help")
1631 help_text = "No help is provided for this command."
1634 # no data for the requested command word
1636 output = "That is not an available command."
1638 # no specific command word was indicated
1641 # give a sorted list of commands with descriptions if provided
1642 output = "These are the commands available to you:$(eol)$(eol)"
1643 sorted_commands = universe.categories["command"].keys()
1644 sorted_commands.sort()
1645 for item in sorted_commands:
1646 command = universe.categories["command"][item]
1647 if actor.can_run(command):
1648 description = command.get("description")
1650 description = "(no short description provided)"
1651 if command.getboolean("administrative"): output += " $(red)"
1652 else: output += " $(grn)"
1653 output += item + "$(nrm) - " + description + "$(eol)"
1654 output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"."
1656 # send the accumulated output to the user
1659 def command_move(actor, parameters):
1660 """Move the avatar in a given direction."""
1661 if parameters in universe.contents[actor.get("location")].portals():
1662 actor.move_direction(parameters)
1663 else: actor.send("You cannot go that way.")
1665 def command_look(actor, parameters):
1667 if parameters: actor.send("You can't look at or in anything yet.")
1668 else: actor.look_at(actor.get("location"))
1670 def command_say(actor, parameters):
1671 """Speak to others in the same room."""
1673 # check for replacement macros
1674 if replace_macros(actor.owner, parameters, True) != parameters:
1675 actor.send("You cannot speak $_(replacement macros).")
1677 # the user entered a message
1680 # get rid of quote marks on the ends of the message and
1681 # capitalize the first letter
1682 message = parameters.strip("\"'`")
1683 message = message[0].capitalize() + message[1:]
1685 # a dictionary of punctuation:action pairs
1687 for facet in universe.categories["internal"]["language"].facets():
1688 if facet.startswith("punctuation_"):
1689 action = facet.split("_")[1]
1690 for mark in universe.categories["internal"]["language"].getlist(facet):
1691 actions[mark] = action
1693 # match the punctuation used, if any, to an action
1694 default_punctuation = universe.categories["internal"]["language"].get("default_punctuation")
1695 action = actions[default_punctuation]
1696 for mark in actions.keys():
1697 if message.endswith(mark) and mark != default_punctuation:
1698 action = actions[mark]
1701 # if the action is default and there is no mark, add one
1702 if action == actions[default_punctuation] and not message.endswith(default_punctuation):
1703 message += default_punctuation
1705 # capitalize a list of words within the message
1706 capitalize_words = universe.categories["internal"]["language"].getlist("capitalize_words")
1707 for word in capitalize_words:
1708 message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
1711 actor.echo_to_location(actor.get("name") + " " + action + "s, \"" + message + "\"")
1712 actor.send("You " + action + ", \"" + message + "\"")
1714 # there was no message
1716 actor.send("What do you want to say?")
1718 def command_show(actor, parameters):
1719 """Show program data."""
1721 arguments = parameters.split()
1722 if not parameters: message = "What do you want to show?"
1723 elif arguments[0] == "time":
1724 message = universe.categories["internal"]["counters"].get("elapsed") + " increments elapsed since the world was created."
1725 elif arguments[0] == "categories":
1726 message = "These are the element categories:$(eol)"
1727 categories = universe.categories.keys()
1729 for category in categories: message += "$(eol) $(grn)" + category + "$(nrm)"
1730 elif arguments[0] == "files":
1731 message = "These are the current files containing the universe:$(eol)"
1732 filenames = universe.files.keys()
1734 for filename in filenames:
1735 if universe.files[filename].is_writeable(): status = "rw"
1737 message += "$(eol) $(red)(" + status + ") $(grn)" + filename + "$(nrm)"
1738 elif arguments[0] == "category":
1739 if len(arguments) != 2: message = "You must specify one category."
1740 elif arguments[1] in universe.categories:
1741 message = "These are the elements in the \"" + arguments[1] + "\" category:$(eol)"
1742 elements = [(universe.categories[arguments[1]][x].key) for x in universe.categories[arguments[1]].keys()]
1744 for element in elements:
1745 message += "$(eol) $(grn)" + element + "$(nrm)"
1746 else: message = "Category \"" + arguments[1] + "\" does not exist."
1747 elif arguments[0] == "file":
1748 if len(arguments) != 2: message = "You must specify one file."
1749 elif arguments[1] in universe.files:
1750 message = "These are the elements in the \"" + arguments[1] + "\" file:$(eol)"
1751 elements = universe.files[arguments[1]].data.sections()
1753 for element in elements:
1754 message += "$(eol) $(grn)" + element + "$(nrm)"
1755 else: message = "Category \"" + arguments[1] + "\" does not exist."
1756 elif arguments[0] == "element":
1757 if len(arguments) != 2: message = "You must specify one element."
1758 elif arguments[1] in universe.contents:
1759 element = universe.contents[arguments[1]]
1760 message = "These are the properties of the \"" + arguments[1] + "\" element (in \"" + element.origin.filename + "\"):$(eol)"
1761 facets = element.facets()
1763 for facet in facets:
1764 message += "$(eol) $(grn)" + facet + ": $(red)" + escape_macros(element.get(facet)) + "$(nrm)"
1765 else: message = "Element \"" + arguments[1] + "\" does not exist."
1766 elif arguments[0] == "result":
1767 if len(arguments) < 2: message = "You need to specify an expression."
1770 message = repr(eval(" ".join(arguments[1:])))
1772 message = "Your expression raised an exception!"
1773 elif arguments[0] == "log":
1774 if len(arguments) == 4:
1775 if match("^\d+$", arguments[3]) and int(arguments[3]) >= 0:
1776 stop = int(arguments[3])
1779 if len(arguments) >= 3:
1780 if match("^\d+$", arguments[2]) and int(arguments[2]) > 0:
1781 start = int(arguments[2])
1784 if len(arguments) >= 2:
1785 if match("^\d+$", arguments[1]) and 0 <= int(arguments[1]) <= 9:
1786 level = int(arguments[1])
1789 if level > -1 and start > -1 and stop > -1:
1790 message = get_loglines(level, start, stop)
1791 else: message = "When specified, level must be 0-9 (default 1), start and stop must be >=1 (default 10 and 1)."
1792 else: message = "I don't know what \"" + parameters + "\" is."
1795 def command_create(actor, parameters):
1796 """Create an element if it does not exist."""
1797 if not parameters: message = "You must at least specify an element to create."
1798 elif not actor.owner: message = ""
1800 arguments = parameters.split()
1801 if len(arguments) == 1: arguments.append("")
1802 if len(arguments) == 2:
1803 element, filename = arguments
1804 if element in universe.contents: message = "The \"" + element + "\" element already exists."
1806 message = "You create \"" + element + "\" within the universe."
1807 logline = actor.owner.account.get("name") + " created an element: " + element
1809 logline += " in file " + filename
1810 if filename not in universe.files:
1811 message += " Warning: \"" + filename + "\" is not yet included in any other file and will not be read on startup unless this is remedied."
1812 Element(element, universe, filename)
1814 elif len(arguments) > 2: message = "You can only specify an element and a filename."
1817 def command_destroy(actor, parameters):
1818 """Destroy an element if it exists."""
1820 if not parameters: message = "You must specify an element to destroy."
1822 if parameters not in universe.contents: message = "The \"" + parameters + "\" element does not exist."
1824 universe.contents[parameters].destroy()
1825 message = "You destroy \"" + parameters + "\" within the universe."
1826 log(actor.owner.account.get("name") + " destroyed an element: " + parameters, 6)
1829 def command_set(actor, parameters):
1830 """Set a facet of an element."""
1831 if not parameters: message = "You must specify an element, a facet and a value."
1833 arguments = parameters.split(" ", 2)
1834 if len(arguments) == 1: message = "What facet of element \"" + arguments[0] + "\" would you like to set?"
1835 elif len(arguments) == 2: message = "What value would you like to set for the \"" + arguments[1] + "\" facet of the \"" + arguments[0] + "\" element?"
1837 element, facet, value = arguments
1838 if element not in universe.contents: message = "The \"" + element + "\" element does not exist."
1840 universe.contents[element].set(facet, value)
1841 message = "You have successfully (re)set the \"" + facet + "\" facet of element \"" + element + "\". Try \"show element " + element + "\" for verification."
1844 def command_delete(actor, parameters):
1845 """Delete a facet from an element."""
1846 if not parameters: message = "You must specify an element and a facet."
1848 arguments = parameters.split(" ")
1849 if len(arguments) == 1: message = "What facet of element \"" + arguments[0] + "\" would you like to delete?"
1850 elif len(arguments) != 2: message = "You may only specify an element and a facet."
1852 element, facet = arguments
1853 if element not in universe.contents: message = "The \"" + element + "\" element does not exist."
1854 elif facet not in universe.contents[element].facets(): message = "The \"" + element + "\" element has no \"" + facet + "\" facet."
1856 universe.contents[element].remove_facet(facet)
1857 message = "You have successfully deleted the \"" + facet + "\" facet of element \"" + element + "\". Try \"show element " + element + "\" for verification."
1860 def command_error(actor, input_data):
1861 """Generic error for an unrecognized command word."""
1863 # 90% of the time use a generic error
1865 message = "I'm not sure what \"" + input_data + "\" means..."
1867 # 10% of the time use the classic diku error
1869 message = "Arglebargle, glop-glyf!?!"
1871 # send the error message
1875 """Fork and disassociate from everything."""
1876 if universe.contents["internal:process"].getboolean("daemon"):
1878 log("Disassociating from the controlling terminal.")
1881 for stdpipe in range(3): close(stdpipe)
1882 sys.stdin = sys.__stdin__ = file("/dev/null", "r")
1883 sys.stdout = sys.stderr = sys.__stdout__ = sys.__stderr__ = file("/dev/null", "w")
1885 # if there is no universe, create an empty one
1886 if not "universe" in locals(): universe = Universe()