1 """Core objects for the mudpy engine."""
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.
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
13 from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
14 from stat import S_IMODE, ST_MODE
15 from sys import stderr
16 from syslog import LOG_PID, LOG_INFO, LOG_DAEMON, closelog, openlog, syslog
17 from telnetlib import DO, DONT, ECHO, EOR, GA, IAC, LINEMODE, SB, SE, SGA, WILL, WONT
18 from time import asctime, sleep
19 from traceback import format_exception
21 def excepthook(excepttype, value, traceback):
22 """Handle uncaught exceptions."""
24 # assemble the list of errors into a single string
25 message = "".join(format_exception(excepttype, value, traceback))
27 # try to log it, if possible
31 # try to write it to stderr, if possible
32 try: stderr.write(message)
35 # redefine sys.excepthook with ours
37 sys.excepthook = excepthook
40 """An element of the universe."""
41 def __init__(self, key, universe, origin=""):
42 """Default values for the in-memory element variables."""
46 if self.key.find(":") > 0:
47 self.category, self.subkey = self.key.split(":", 1)
49 self.category = "other"
50 self.subkey = self.key
51 if not self.category in universe.categories: self.category = "other"
52 universe.categories[self.category][self.subkey] = self
54 if not self.origin: self.origin = universe.default_origins[self.category]
55 if not isabs(self.origin):
56 self.origin = abspath(self.origin)
57 universe.contents[self.key] = self
58 if not self.origin in universe.files:
59 DataFile(self.origin, universe)
60 if not universe.files[self.origin].data.has_section(self.key):
61 universe.files[self.origin].data.add_section(self.key)
63 """Remove an element from the universe and destroy it."""
64 log("Destroying: " + self.key + ".", 2)
65 universe.files[self.origin].data.remove_section(self.key)
66 del universe.categories[self.category][self.subkey]
67 del universe.contents[self.key]
69 def delete(self, facet):
70 """Delete a facet from the element."""
71 if universe.files[self.origin].data.has_option(self.key, facet):
72 universe.files[self.origin].data.remove_option(self.key, facet)
74 """Return a list of non-inherited facets for this element."""
75 if self.key in universe.files[self.origin].data.sections():
76 return universe.files[self.origin].data.options(self.key)
78 def has_facet(self, facet):
79 """Return whether the non-inherited facet exists."""
80 return facet in self.facets()
81 def remove_facet(self, facet):
82 """Remove a facet from the element."""
83 if self.has_facet(facet): universe.files[self.origin].data.remove_option(self.key, facet)
85 """Return a list of the element's inheritance lineage."""
86 if self.has_facet("inherit"):
87 ancestry = self.getlist("inherit")
88 for parent in ancestry[:]:
89 ancestors = universe.contents[parent].ancestry()
90 for ancestor in ancestors:
91 if ancestor not in ancestry: ancestry.append(ancestor)
94 def get(self, facet, default=None):
95 """Retrieve values."""
96 if default is None: default = ""
97 if universe.files[self.origin].data.has_option(self.key, facet):
98 return universe.files[self.origin].data.get(self.key, facet)
99 elif self.has_facet("inherit"):
100 for ancestor in self.ancestry():
101 if universe.contents[ancestor].has_facet(facet):
102 return universe.contents[ancestor].get(facet)
104 def getboolean(self, facet, default=None):
105 """Retrieve values as boolean type."""
106 if default is None: default=False
107 if universe.files[self.origin].data.has_option(self.key, facet):
108 return universe.files[self.origin].data.getboolean(self.key, facet)
109 elif self.has_facet("inherit"):
110 for ancestor in self.ancestry():
111 if universe.contents[ancestor].has_facet(facet):
112 return universe.contents[ancestor].getboolean(facet)
114 def getint(self, facet, default=None):
115 """Return values as int/long type."""
116 if default is None: default = 0
117 if universe.files[self.origin].data.has_option(self.key, facet):
118 return universe.files[self.origin].data.getint(self.key, facet)
119 elif self.has_facet("inherit"):
120 for ancestor in self.ancestry():
121 if universe.contents[ancestor].has_facet(facet):
122 return universe.contents[ancestor].getint(facet)
124 def getfloat(self, facet, default=None):
125 """Return values as float type."""
126 if default is None: default = 0.0
127 if universe.files[self.origin].data.has_option(self.key, facet):
128 return universe.files[self.origin].data.getfloat(self.key, facet)
129 elif self.has_facet("inherit"):
130 for ancestor in self.ancestry():
131 if universe.contents[ancestor].has_facet(facet):
132 return universe.contents[ancestor].getfloat(facet)
134 def getlist(self, facet, default=None):
135 """Return values as list type."""
136 if default is None: default = []
137 value = self.get(facet)
138 if value: return makelist(value)
140 def getdict(self, facet, default=None):
141 """Return values as dict type."""
142 if default is None: default = {}
143 value = self.get(facet)
144 if value: return makedict(value)
146 def set(self, facet, value):
148 if type(value) is long: value = str(value)
149 elif not type(value) is str: value = repr(value)
150 universe.files[self.origin].data.set(self.key, facet, value)
151 def append(self, facet, value):
152 """Append value tp a list."""
153 if type(value) is long: value = str(value)
154 elif not type(value) is str: value = repr(value)
155 newlist = self.getlist(facet)
156 newlist.append(value)
157 self.set(facet, newlist)
158 def send(self, message, eol="$(eol)"):
159 """Convenience method to pass messages to an owner."""
160 if self.owner: self.owner.send(message, eol)
161 def go_to(self, location):
162 """Relocate the element to a specific location."""
163 current = self.get("location")
164 if current and self.key in universe.contents[current].contents:
165 del universe.contents[current].contents[self.key]
166 if location in universe.contents: self.set("location", location)
167 universe.contents[location].contents[self.key] = self
168 self.look_at(location)
170 """Relocate the element to its default location."""
171 self.go_to(self.get("default_location"))
172 self.echo_to_location("You suddenly realize that " + self.get("name") + " is here.")
173 def move_direction(self, direction):
174 """Relocate the element in a specified direction."""
175 self.echo_to_location(self.get("name") + " exits " + universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
176 self.send("You exit " + universe.categories["internal"]["directions"].getdict(direction)["exit"] + ".")
177 self.go_to(universe.contents[self.get("location")].link_neighbor(direction))
178 self.echo_to_location(self.get("name") + " arrives from " + universe.categories["internal"]["directions"].getdict(direction)["enter"] + ".")
179 def look_at(self, key):
180 """Show an element to another element."""
182 element = universe.contents[key]
184 name = element.get("name")
185 if name: message += "$(cyn)" + name + "$(nrm)$(eol)"
186 description = element.get("description")
187 if description: message += description + "$(eol)"
188 portal_list = element.portals().keys()
191 message += "$(cyn)[ Exits: " + ", ".join(portal_list) + " ]$(nrm)$(eol)"
192 for element in universe.contents[self.get("location")].contents.values():
193 if element.getboolean("is_actor") and element is not self:
194 message += "$(yel)" + element.get("name") + " is here.$(nrm)$(eol)"
197 """Map the portal directions for a room to neighbors."""
199 if match("""^location:-?\d+,-?\d+,-?\d+$""", self.key):
200 coordinates = [(int(x)) for x in self.key.split(":")[1].split(",")]
201 directions = universe.categories["internal"]["directions"]
202 offsets = dict([(x, directions.getdict(x)["vector"]) for x in directions.facets()])
203 for portal in self.getlist("gridlinks"):
204 adjacent = map(lambda c,o: c+o, coordinates, offsets[portal])
205 neighbor = "location:" + ",".join([(str(x)) for x in adjacent])
206 if neighbor in universe.contents: portals[portal] = neighbor
207 for facet in self.facets():
208 if facet.startswith("link_"):
209 neighbor = self.get(facet)
210 if neighbor in universe.contents:
211 portal = facet.split("_")[1]
212 portals[portal] = neighbor
214 def link_neighbor(self, direction):
215 """Return the element linked in a given direction."""
216 portals = self.portals()
217 if direction in portals: return portals[direction]
218 def echo_to_location(self, message):
219 """Show a message to other elements in the current location."""
220 for element in universe.contents[self.get("location")].contents.values():
221 if element is not self: element.send(message)
224 """A file containing universe elements."""
225 def __init__(self, filename, universe):
226 self.data = RawConfigParser()
227 if access(filename, R_OK): self.data.read(filename)
228 self.filename = filename
229 universe.files[filename] = self
230 if self.data.has_option("__control__", "include_files"):
231 includes = makelist(self.data.get("__control__", "include_files"))
233 if self.data.has_option("__control__", "default_files"):
234 origins = makedict(self.data.get("__control__", "default_files"))
235 for key in origins.keys():
236 if not key in includes: includes.append(key)
237 universe.default_origins[key] = origins[key]
238 if not key in universe.categories:
239 universe.categories[key] = {}
240 if self.data.has_option("__control__", "private_files"):
241 for item in makelist(self.data.get("__control__", "private_files")):
242 if not item in includes: includes.append(item)
243 if not item in universe.private_files:
245 item = path_join(dirname(filename), item)
246 universe.private_files.append(item)
247 for section in self.data.sections():
248 if section != "__control__":
249 Element(section, universe, filename)
250 for include_file in includes:
251 if not isabs(include_file):
252 include_file = path_join(dirname(filename), include_file)
253 DataFile(include_file, universe)
255 """Write the data, if necessary."""
257 # when there is content or the file exists, but is not read-only
258 if ( self.data.sections() or exists(self.filename) ) and not ( self.data.has_option("__control__", "read_only") and self.data.getboolean("__control__", "read_only") ):
260 # make parent directories if necessary
261 if not exists(dirname(self.filename)):
262 makedirs(dirname(self.filename))
265 file_descriptor = file(self.filename, "w")
267 # if it's marked private, chmod it appropriately
268 if self.filename in universe.private_files and oct(S_IMODE(stat(self.filename)[ST_MODE])) != 0600:
269 chmod(self.filename, 0600)
271 # write it back sorted, instead of using ConfigParser
272 sections = self.data.sections()
274 for section in sections:
275 file_descriptor.write("[" + section + "]\n")
276 options = self.data.options(section)
278 for option in options:
279 file_descriptor.write(option + " = " + self.data.get(section, option) + "\n")
280 file_descriptor.write("\n")
282 # flush and close the file
283 file_descriptor.flush()
284 file_descriptor.close()
288 def __init__(self, filename=""):
289 """Initialize the universe."""
292 self.default_origins = {}
294 self.private_files = []
297 self.terminate_world = False
298 self.reload_modules = False
300 possible_filenames = [
306 "/usr/local/mudpy/mudpy.conf",
307 "/usr/local/mudpy/etc/mudpy.conf",
308 "/etc/mudpy/mudpy.conf",
311 for filename in possible_filenames:
312 if access(filename, R_OK): break
313 if not isabs(filename):
314 filename = abspath(filename)
315 DataFile(filename, self)
317 """Save the universe to persistent storage."""
318 for key in self.files: self.files[key].save()
320 def initialize_server_socket(self):
321 """Create and open the listening socket."""
323 # create a new ipv4 stream-type socket object
324 self.listening_socket = socket(AF_INET, SOCK_STREAM)
326 # set the socket options to allow existing open ones to be
327 # reused (fixes a bug where the server can't bind for a minute
328 # when restarting on linux systems)
329 self.listening_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
331 # bind the socket to to our desired server ipa and port
332 self.listening_socket.bind((self.categories["internal"]["network"].get("host"), self.categories["internal"]["network"].getint("port")))
334 # disable blocking so we can proceed whether or not we can
336 self.listening_socket.setblocking(0)
338 # start listening on the socket
339 self.listening_socket.listen(1)
341 # note that we're now ready for user connections
342 log("Waiting for connection(s)...")
345 """This is a connected user."""
348 """Default values for the in-memory user variables."""
350 self.last_address = ""
351 self.connection = None
352 self.authenticated = False
353 self.password_tries = 0
354 self.state = "initial"
355 self.menu_seen = False
357 self.input_queue = []
358 self.output_queue = []
359 self.partial_input = ""
361 self.received_newline = True
362 self.terminator = IAC+GA
363 self.negotiation_pause = 0
368 """Log, close the connection and remove."""
369 if self.account: name = self.account.get("name")
371 if name: message = "User " + name
372 else: message = "An unnamed user"
373 message += " logged out."
375 self.deactivate_avatar()
376 self.connection.close()
380 """Save, load a new user and relocate the connection."""
382 # get out of the list
385 # create a new user object
388 # set everything else equivalent
408 exec("new_user." + attribute + " = self." + attribute)
411 universe.userlist.append(new_user)
413 # get rid of the old user object
416 def replace_old_connections(self):
417 """Disconnect active users with the same name."""
419 # the default return value
422 # iterate over each user in the list
423 for old_user in universe.userlist:
425 # the name is the same but it's not us
426 if old_user.account.get("name") == self.account.get("name") and old_user is not self:
429 log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".", 2)
430 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)", flush=True, add_prompt=False)
432 # close the old connection
433 old_user.connection.close()
435 # replace the old connection with this one
436 old_user.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
437 old_user.connection = self.connection
438 old_user.last_address = old_user.address
439 old_user.address = self.address
440 old_user.echoing = self.echoing
442 # take this one out of the list and delete
448 # true if an old connection was replaced, false if not
451 def authenticate(self):
452 """Flag the user as authenticated and disconnect duplicates."""
453 if not self.state is "authenticated":
454 log("User " + self.account.get("name") + " logged in.", 2)
455 self.authenticated = True
456 if self.account.subkey in universe.categories["internal"]["limits"].getlist("default_admins"):
457 self.account.set("administrator", "True")
460 """Send the user their current menu."""
461 if not self.menu_seen:
462 self.menu_choices = get_menu_choices(self)
463 self.send(get_menu(self.state, self.error, self.echoing, self.terminator, self.menu_choices), "")
464 self.menu_seen = True
466 self.adjust_echoing()
468 def adjust_echoing(self):
469 """Adjust echoing to match state menu requirements."""
470 if self.echoing and not menu_echo_on(self.state): self.echoing = False
471 elif not self.echoing and menu_echo_on(self.state): self.echoing = True
474 """Remove a user from the list of connected users."""
475 universe.userlist.remove(self)
477 def send(self, output, eol="$(eol)", raw=False, flush=False, add_prompt=True, just_prompt=False):
478 """Send arbitrary text to a connected user."""
480 # unless raw mode is on, clean it up all nice and pretty
483 # strip extra $(eol) off if present
484 while output.startswith("$(eol)"): output = output[6:]
485 while output.endswith("$(eol)"): output = output[:-6]
487 # we'll take out GA or EOR and add them back on the end
488 if output.endswith(IAC+GA) or output.endswith(IAC+EOR):
491 else: terminate = False
493 # start with a newline, append the message, then end
494 # with the optional eol string passed to this function
495 # and the ansi escape to return to normal text
496 if not just_prompt: output = "$(eol)$(eol)" + output
497 output += eol + chr(27) + "[0m"
499 # tack on a prompt if active
500 if self.state == "active":
501 if not just_prompt: output += "$(eol)"
502 if add_prompt: output += "> "
504 # find and replace macros in the output
505 output = replace_macros(self, output)
507 # wrap the text at 80 characters
508 output = wrap_ansi_text(output, 80)
510 # tack the terminator back on
511 if terminate: output += self.terminator
513 # drop the output into the user's output queue
514 self.output_queue.append(output)
516 # if this is urgent, flush all pending output
517 if flush: self.flush()
520 """All the things to do to the user per increment."""
522 # if the world is terminating, disconnect
523 if universe.terminate_world:
524 self.state = "disconnecting"
525 self.menu_seen = False
527 # if output is paused, decrement the counter
528 if self.state == "initial":
529 if self.negotiation_pause: self.negotiation_pause -= 1
530 else: self.state = "entering_account_name"
532 # show the user a menu as needed
533 elif not self.state == "active": self.show_menu()
535 # flush any pending output in teh queue
538 # disconnect users with the appropriate state
539 if self.state == "disconnecting": self.quit()
541 # check for input and add it to the queue
544 # there is input waiting in the queue
545 if self.input_queue: handle_user_input(self)
548 """Try to send the last item in the queue and remove it."""
549 if self.output_queue:
550 if self.received_newline:
551 self.received_newline = False
552 if self.output_queue[0].startswith("\r\n"):
553 self.output_queue[0] = self.output_queue[0][2:]
555 self.connection.send(self.output_queue[0])
556 del self.output_queue[0]
561 def enqueue_input(self):
562 """Process and enqueue any new input."""
564 # check for some input
566 input_data = self.connection.recv(1024)
573 # tack this on to any previous partial
574 self.partial_input += input_data
576 # reply to and remove any IAC negotiation codes
577 self.negotiate_telnet_options()
579 # separate multiple input lines
580 new_input_lines = self.partial_input.split("\n")
582 # if input doesn't end in a newline, replace the
583 # held partial input with the last line of it
584 if not self.partial_input.endswith("\n"):
585 self.partial_input = new_input_lines.pop()
587 # otherwise, chop off the extra null input and reset
588 # the held partial input
590 new_input_lines.pop()
591 self.partial_input = ""
593 # iterate over the remaining lines
594 for line in new_input_lines:
596 # remove a trailing carriage return
597 if line.endswith("\r"): line = line.rstrip("\r")
599 # log non-printable characters remaining
600 removed = filter(lambda x: (x < " " or x > "~"), line)
602 logline = "Non-printable characters from "
603 if self.account and self.account.get("name"): logline += self.account.get("name") + ": "
604 else: logline += "unknown user: "
605 logline += repr(removed)
608 # filter out non-printables
609 line = filter(lambda x: " " <= x <= "~", line)
611 # strip off extra whitespace
614 # put on the end of the queue
615 self.input_queue.append(line)
617 def negotiate_telnet_options(self):
618 """Reply to/remove partial_input telnet negotiation options."""
620 # start at the begining of the input
623 # make a local copy to play with
624 text = self.partial_input
626 # as long as we haven't checked it all
627 while position < len(text):
629 # jump to the first IAC you find
630 position = text.find(IAC, position)
632 # if there wasn't an IAC in the input, skip to the end
633 if position < 0: position = len(text)
635 # replace a double (literal) IAC if there's an LF later
636 elif len(text) > position+1 and text[position+1] == IAC:
637 if text.find("\n", position) > 0: text = text.replace(IAC+IAC, IAC)
641 # this must be an option negotiation
642 elif len(text) > position+2 and text[position+1] in (DO, DONT, WILL, WONT):
644 negotiation = text[position+1:position+3]
646 # if we turned echo off, ignore the confirmation
647 if not self.echoing and negotiation == DO+ECHO: pass
650 elif negotiation == WILL+LINEMODE: self.send(IAC+DO+LINEMODE, raw=True)
652 # if the client likes EOR instead of GA, make a note of it
653 elif negotiation == DO+EOR: self.terminator = IAC+EOR
654 elif negotiation == DONT+EOR and self.terminator == IAC+EOR:
655 self.terminator = IAC+GA
657 # if the client doesn't want GA, oblige
658 elif negotiation == DO+SGA and self.terminator == IAC+GA:
660 self.send(IAC+WILL+SGA, raw=True)
662 # we don't want to allow anything else
663 elif text[position+1] == DO: self.send(IAC+WONT+text[position+2], raw=True)
664 elif text[position+1] == WILL: self.send(IAC+DONT+text[position+2], raw=True)
666 # strip the negotiation from the input
667 text = text.replace(text[position:position+3], "")
669 # get rid of IAC SB .* IAC SE
670 elif len(text) > position+4 and text[position:position+2] == IAC+SB:
671 end_subnegotiation = text.find(IAC+SE, position)
672 if end_subnegotiation > 0: text = text[:position] + text[end_subnegotiation+2:]
675 # otherwise, strip out a two-byte IAC command
676 elif len(text) > position+2: text = text.replace(text[position:position+2], "")
678 # and this means we got the begining of an IAC
681 # replace the input with our cleaned-up text
682 self.partial_input = text
684 def can_run(self, command):
685 """Check if the user can run this command object."""
687 # has to be in the commands category
688 if command not in universe.categories["command"].values(): result = False
690 # administrators can run any command
691 elif self.account.getboolean("administrator"): result = True
693 # everyone can run non-administrative commands
694 elif not command.getboolean("administrative"): result = True
696 # otherwise the command cannot be run by this user
699 # pass back the result
702 def new_avatar(self):
703 """Instantiate a new, unconfigured avatar for this user."""
705 while "avatar:" + self.account.get("name") + ":" + str(counter) in universe.categories["actor"].keys(): counter += 1
706 self.avatar = Element("actor:avatar:" + self.account.get("name") + ":" + str(counter), universe)
707 self.avatar.append("inherit", "template:actor")
708 self.account.append("avatars", self.avatar.key)
710 def delete_avatar(self, avatar):
711 """Remove an avatar from the world and from the user's list."""
712 if self.avatar is universe.contents[avatar]: self.avatar = None
713 universe.contents[avatar].destroy()
714 avatars = self.account.getlist("avatars")
715 avatars.remove(avatar)
716 self.account.set("avatars", avatars)
718 def activate_avatar_by_index(self, index):
719 """Enter the world with a particular indexed avatar."""
720 self.avatar = universe.contents[self.account.getlist("avatars")[index]]
721 self.avatar.owner = self
722 self.state = "active"
723 self.avatar.go_home()
725 def deactivate_avatar(self):
726 """Have the active avatar leave the world."""
728 current = self.avatar.get("location")
729 self.avatar.set("default_location", current)
730 self.avatar.echo_to_location("You suddenly wonder where " + self.avatar.get("name") + " went.")
731 del universe.contents[current].contents[self.avatar.key]
732 self.avatar.remove_facet("location")
733 self.avatar.owner = None
737 """Destroy the user and associated avatars."""
738 for avatar in self.account.getlist("avatars"): self.delete_avatar(avatar)
739 self.account.destroy()
741 def list_avatar_names(self):
742 """List names of assigned avatars."""
743 return [ universe.contents[avatar].get("name") for avatar in self.account.getlist("avatars") ]
746 """Turn string into list type."""
747 if value[0] + value[-1] == "[]": return eval(value)
748 else: return [ value ]
751 """Turn string into dict type."""
752 if value[0] + value[-1] == "{}": return eval(value)
753 elif value.find(":") > 0: return eval("{" + value + "}")
754 else: return { value: None }
756 def broadcast(message, add_prompt=True):
757 """Send a message to all connected users."""
758 for each_user in universe.userlist: each_user.send("$(eol)" + message, add_prompt=add_prompt)
760 def log(message, level=0):
763 # a couple references we need
764 file_name = universe.categories["internal"]["logging"].get("file")
765 max_log_lines = universe.categories["internal"]["logging"].getint("max_log_lines")
766 syslog_name = universe.categories["internal"]["logging"].get("syslog")
767 timestamp = asctime()[4:19]
769 # turn the message into a list of lines
770 lines = filter(lambda x: x!="", [(x.rstrip()) for x in message.split("\n")])
772 # send the timestamp and line to a file
774 file_descriptor = file(file_name, "a")
775 for line in lines: file_descriptor.write(timestamp + " " + line + "\n")
776 file_descriptor.flush()
777 file_descriptor.close()
779 # send the timestamp and line to standard output
780 if universe.categories["internal"]["logging"].getboolean("stdout"):
781 for line in lines: print(timestamp + " " + line)
783 # send the line to the system log
785 openlog(syslog_name, LOG_PID, LOG_INFO | LOG_DAEMON)
786 for line in lines: syslog(line)
789 # display to connected administrators
790 for user in universe.userlist:
791 if user.state == "active" and user.account.getboolean("administrator") and user.account.getint("loglevel") <= level:
792 # iterate over every line in the message
795 full_message += "$(bld)$(red)" + timestamp + " " + line + "$(nrm)$(eol)"
796 user.send(full_message, flush=True)
798 # add to the recent log list
800 while 0 < len(universe.loglist) >= max_log_lines: del universe.loglist[0]
801 universe.loglist.append(timestamp + " " + line)
803 def wrap_ansi_text(text, width):
804 """Wrap text with arbitrary width while ignoring ANSI colors."""
806 # the current position in the entire text string, including all
807 # characters, printable or otherwise
808 absolute_position = 0
810 # the current text position relative to the begining of the line,
811 # ignoring color escape sequences
812 relative_position = 0
814 # whether the current character is part of a color escape sequence
817 # iterate over each character from the begining of the text
818 for each_character in text:
820 # the current character is the escape character
821 if each_character == chr(27):
824 # the current character is within an escape sequence
827 # the current character is m, which terminates the
828 # current escape sequence
829 if each_character == "m":
832 # the current character is a newline, so reset the relative
833 # position (start a new line)
834 elif each_character == "\n":
835 relative_position = 0
837 # the current character meets the requested maximum line width,
838 # so we need to backtrack and find a space at which to wrap
839 elif relative_position == width:
841 # distance of the current character examined from the
845 # count backwards until we find a space
846 while text[absolute_position - wrap_offset] != " ":
849 # insert an eol in place of the space
850 text = text[:absolute_position - wrap_offset] + "\r\n" + text[absolute_position - wrap_offset + 1:]
852 # increase the absolute position because an eol is two
853 # characters but the space it replaced was only one
854 absolute_position += 1
856 # now we're at the begining of a new line, plus the
857 # number of characters wrapped from the previous line
858 relative_position = wrap_offset
860 # as long as the character is not a carriage return and the
861 # other above conditions haven't been met, count it as a
862 # printable character
863 elif each_character != "\r":
864 relative_position += 1
866 # increase the absolute position for every character
867 absolute_position += 1
869 # return the newly-wrapped text
872 def weighted_choice(data):
873 """Takes a dict weighted by value and returns a random key."""
875 # this will hold our expanded list of keys from the data
878 # create thee expanded list of keys
879 for key in data.keys():
880 for count in range(data[key]):
883 # return one at random
884 return choice(expanded)
887 """Returns a random character name."""
889 # the vowels and consonants needed to create romaji syllables
890 vowels = [ "a", "i", "u", "e", "o" ]
891 consonants = ["'", "k", "z", "s", "sh", "z", "j", "t", "ch", "ts", "d", "n", "h", "f", "m", "y", "r", "w" ]
893 # this dict will hold our weighted list of syllables
896 # generate the list with an even weighting
897 for consonant in consonants:
899 syllables[consonant + vowel] = 1
901 # we'll build the name into this string
904 # create a name of random length from the syllables
905 for syllable in range(randrange(2, 6)):
906 name += weighted_choice(syllables)
908 # strip any leading quotemark, capitalize and return the name
909 return name.strip("'").capitalize()
911 def replace_macros(user, text, is_input=False):
912 """Replaces macros in text output."""
917 # third person pronouns
919 "female": { "obj": "her", "pos": "hers", "sub": "she" },
920 "male": { "obj": "him", "pos": "his", "sub": "he" },
921 "neuter": { "obj": "it", "pos": "its", "sub": "it" }
924 # a dict of replacement macros
927 "$(bld)": chr(27) + "[1m",
928 "$(nrm)": chr(27) + "[0m",
929 "$(blk)": chr(27) + "[30m",
930 "$(blu)": chr(27) + "[34m",
931 "$(cyn)": chr(27) + "[36m",
932 "$(grn)": chr(27) + "[32m",
933 "$(mgt)": chr(27) + "[35m",
934 "$(red)": chr(27) + "[31m",
935 "$(yel)": chr(27) + "[33m",
938 # add dynamic macros where possible
940 account_name = user.account.get("name")
942 macros["$(account)"] = account_name
944 avatar_gender = user.avatar.get("gender")
946 macros["$(tpop)"] = pronouns[avatar_gender]["obj"]
947 macros["$(tppp)"] = pronouns[avatar_gender]["pos"]
948 macros["$(tpsp)"] = pronouns[avatar_gender]["sub"]
950 # find and replace per the macros dict
951 macro_start = text.find("$(")
952 if macro_start == -1: break
953 macro_end = text.find(")", macro_start) + 1
954 macro = text[macro_start:macro_end]
955 if macro in macros.keys():
956 text = text.replace(macro, macros[macro])
958 # if we get here, log and replace it with null
960 text = text.replace(macro, "")
962 log("Unexpected replacement macro " + macro + " encountered.", 6)
964 # replace the look-like-a-macro sequence
965 text = text.replace("$_(", "$(")
969 def escape_macros(text):
970 """Escapes replacement macros in text."""
971 return text.replace("$(", "$_(")
973 def check_time(frequency):
974 """Check for a factor of the current increment count."""
975 if type(frequency) is str:
976 frequency = universe.categories["internal"]["time"].getint(frequency)
977 if not "counters" in universe.categories["internal"]:
978 Element("internal:counters", universe)
979 return not universe.categories["internal"]["counters"].getint("elapsed") % frequency
982 """The things which should happen on each pulse, aside from reloads."""
984 # open the listening socket if it hasn't been already
985 if not hasattr(universe, "listening_socket"):
986 universe.initialize_server_socket()
988 # assign a user if a new connection is waiting
989 user = check_for_connection(universe.listening_socket)
990 if user: universe.userlist.append(user)
992 # iterate over the connected users
993 for user in universe.userlist: user.pulse()
995 # update the log every now and then
996 if check_time("frequency_log"):
997 log(str(len(universe.userlist)) + " connection(s)")
999 # periodically save everything
1000 if check_time("frequency_save"):
1003 # pause for a configurable amount of time (decimal seconds)
1004 sleep(universe.categories["internal"]["time"].getfloat("increment"))
1006 # increment the elapsed increment counter
1007 universe.categories["internal"]["counters"].set("elapsed", universe.categories["internal"]["counters"].getint("elapsed") + 1)
1010 """Reload data into new persistent objects."""
1011 for user in universe.userlist[:]: user.reload()
1013 def check_for_connection(listening_socket):
1014 """Check for a waiting connection and return a new user object."""
1016 # try to accept a new connection
1018 connection, address = listening_socket.accept()
1022 # note that we got one
1023 log("Connection from " + address[0], 2)
1025 # disable blocking so we can proceed whether or not we can send/receive
1026 connection.setblocking(0)
1028 # create a new user object
1031 # associate this connection with it
1032 user.connection = connection
1034 # set the user's ipa from the connection's ipa
1035 user.address = address[0]
1037 # let the client know we WILL EOR
1038 user.send(IAC+WILL+EOR, raw=True)
1039 user.negotiation_pause = 2
1041 # return the new user object
1044 def get_menu(state, error=None, echoing=True, terminator="", choices=None):
1045 """Show the correct menu text to a user."""
1047 # make sure we don't reuse a mutable sequence by default
1048 if choices is None: choices = {}
1050 # begin with a telnet echo command sequence if needed
1051 message = get_echo_sequence(state, echoing)
1053 # get the description or error text
1054 message += get_menu_description(state, error)
1056 # get menu choices for the current state
1057 message += get_formatted_menu_choices(state, choices)
1059 # try to get a prompt, if it was defined
1060 message += get_menu_prompt(state)
1062 # throw in the default choice, if it exists
1063 message += get_formatted_default_menu_choice(state)
1065 # display a message indicating if echo is off
1066 message += get_echo_message(state)
1068 # tack on EOR or GA to indicate the prompt will not be followed by CRLF
1069 message += terminator
1071 # return the assembly of various strings defined above
1074 def menu_echo_on(state):
1075 """True if echo is on, false if it is off."""
1076 return universe.categories["menu"][state].getboolean("echo", True)
1078 def get_echo_sequence(state, echoing):
1079 """Build the appropriate IAC WILL/WONT ECHO sequence as needed."""
1081 # if the user has echo on and the menu specifies it should be turned
1082 # off, send: iac + will + echo + null
1083 if echoing and not menu_echo_on(state): return IAC+WILL+ECHO
1085 # if echo is not set to off in the menu and the user curently has echo
1086 # off, send: iac + wont + echo + null
1087 elif not echoing and menu_echo_on(state): return IAC+WONT+ECHO
1089 # default is not to send an echo control sequence at all
1092 def get_echo_message(state):
1093 """Return a message indicating that echo is off."""
1094 if menu_echo_on(state): return ""
1095 else: return "(won't echo) "
1097 def get_default_menu_choice(state):
1098 """Return the default choice for a menu."""
1099 return universe.categories["menu"][state].get("default")
1101 def get_formatted_default_menu_choice(state):
1102 """Default menu choice foratted for inclusion in a prompt string."""
1103 default_choice = get_default_menu_choice(state)
1104 if default_choice: return "[$(red)" + default_choice + "$(nrm)] "
1107 def get_menu_description(state, error):
1108 """Get the description or error text."""
1110 # an error condition was raised by the handler
1113 # try to get an error message matching the condition
1115 description = universe.categories["menu"][state].get("error_" + error)
1116 if not description: description = "That is not a valid choice..."
1117 description = "$(red)" + description + "$(nrm)"
1119 # there was no error condition
1122 # try to get a menu description for the current state
1123 description = universe.categories["menu"][state].get("description")
1125 # return the description or error message
1126 if description: description += "$(eol)$(eol)"
1129 def get_menu_prompt(state):
1130 """Try to get a prompt, if it was defined."""
1131 prompt = universe.categories["menu"][state].get("prompt")
1132 if prompt: prompt += " "
1135 def get_menu_choices(user):
1136 """Return a dict of choice:meaning."""
1137 menu = universe.categories["menu"][user.state]
1138 create_choices = menu.get("create")
1139 if create_choices: choices = eval(create_choices)
1144 for facet in menu.facets():
1145 if facet.startswith("demand_") and not eval(universe.categories["menu"][user.state].get(facet)):
1146 ignores.append(facet.split("_", 2)[1])
1147 elif facet.startswith("create_"):
1148 creates[facet] = facet.split("_", 2)[1]
1149 elif facet.startswith("choice_"):
1150 options[facet] = facet.split("_", 2)[1]
1151 for facet in creates.keys():
1152 if not creates[facet] in ignores:
1153 choices[creates[facet]] = eval(menu.get(facet))
1154 for facet in options.keys():
1155 if not options[facet] in ignores:
1156 choices[options[facet]] = menu.get(facet)
1159 def get_formatted_menu_choices(state, choices):
1160 """Returns a formatted string of menu choices."""
1162 choice_keys = choices.keys()
1164 for choice in choice_keys:
1165 choice_output += " [$(red)" + choice + "$(nrm)] " + choices[choice] + "$(eol)"
1166 if choice_output: choice_output += "$(eol)"
1167 return choice_output
1169 def get_menu_branches(state):
1170 """Return a dict of choice:branch."""
1172 for facet in universe.categories["menu"][state].facets():
1173 if facet.startswith("branch_"):
1174 branches[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
1177 def get_default_branch(state):
1178 """Return the default branch."""
1179 return universe.categories["menu"][state].get("branch")
1181 def get_choice_branch(user, choice):
1182 """Returns the new state matching the given choice."""
1183 branches = get_menu_branches(user.state)
1184 if choice in branches.keys(): return branches[choice]
1185 elif choice in user.menu_choices.keys(): return get_default_branch(user.state)
1188 def get_menu_actions(state):
1189 """Return a dict of choice:branch."""
1191 for facet in universe.categories["menu"][state].facets():
1192 if facet.startswith("action_"):
1193 actions[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
1196 def get_default_action(state):
1197 """Return the default action."""
1198 return universe.categories["menu"][state].get("action")
1200 def get_choice_action(user, choice):
1201 """Run any indicated script for the given choice."""
1202 actions = get_menu_actions(user.state)
1203 if choice in actions.keys(): return actions[choice]
1204 elif choice in user.menu_choices.keys(): return get_default_action(user.state)
1207 def handle_user_input(user):
1208 """The main handler, branches to a state-specific handler."""
1210 # check to make sure the state is expected, then call that handler
1211 if "handler_" + user.state in globals():
1212 exec("handler_" + user.state + "(user)")
1214 generic_menu_handler(user)
1216 # since we got input, flag that the menu/prompt needs to be redisplayed
1217 user.menu_seen = False
1219 # if the user's client echo is off, send a blank line for aesthetics
1220 if user.echoing: user.received_newline = True
1222 def generic_menu_handler(user):
1223 """A generic menu choice handler."""
1225 # get a lower-case representation of the next line of input
1226 if user.input_queue:
1227 choice = user.input_queue.pop(0)
1228 if choice: choice = choice.lower()
1230 if not choice: choice = get_default_menu_choice(user.state)
1231 if choice in user.menu_choices:
1232 exec(get_choice_action(user, choice))
1233 new_state = get_choice_branch(user, choice)
1234 if new_state: user.state = new_state
1235 else: user.error = "default"
1237 def handler_entering_account_name(user):
1238 """Handle the login account name."""
1240 # get the next waiting line of input
1241 input_data = user.input_queue.pop(0)
1243 # did the user enter anything?
1246 # keep only the first word and convert to lower-case
1247 name = input_data.lower()
1249 # fail if there are non-alphanumeric characters
1250 if name != filter(lambda x: x>="0" and x<="9" or x>="a" and x<="z", name):
1251 user.error = "bad_name"
1253 # if that account exists, time to request a password
1254 elif name in universe.categories["account"]:
1255 user.account = universe.categories["account"][name]
1256 user.state = "checking_password"
1258 # otherwise, this could be a brand new user
1260 user.account = Element("account:" + name, universe)
1261 user.account.set("name", name)
1262 log("New user: " + name, 2)
1263 user.state = "checking_new_account_name"
1265 # if the user entered nothing for a name, then buhbye
1267 user.state = "disconnecting"
1269 def handler_checking_password(user):
1270 """Handle the login account password."""
1272 # get the next waiting line of input
1273 input_data = user.input_queue.pop(0)
1275 # does the hashed input equal the stored hash?
1276 if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
1278 # if so, set the username and load from cold storage
1279 if not user.replace_old_connections():
1281 user.state = "main_utility"
1283 # if at first your hashes don't match, try, try again
1284 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1285 user.password_tries += 1
1286 user.error = "incorrect"
1288 # we've exceeded the maximum number of password failures, so disconnect
1290 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1291 user.state = "disconnecting"
1293 def handler_entering_new_password(user):
1294 """Handle a new password entry."""
1296 # get the next waiting line of input
1297 input_data = user.input_queue.pop(0)
1299 # make sure the password is strong--at least one upper, one lower and
1300 # one digit, seven or more characters in length
1301 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)):
1303 # hash and store it, then move on to verification
1304 user.account.set("passhash", new_md5(user.account.get("name") + input_data).hexdigest())
1305 user.state = "verifying_new_password"
1307 # the password was weak, try again if you haven't tried too many times
1308 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1309 user.password_tries += 1
1312 # too many tries, so adios
1314 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1315 user.account.destroy()
1316 user.state = "disconnecting"
1318 def handler_verifying_new_password(user):
1319 """Handle the re-entered new password for verification."""
1321 # get the next waiting line of input
1322 input_data = user.input_queue.pop(0)
1324 # hash the input and match it to storage
1325 if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
1328 # the hashes matched, so go active
1329 if not user.replace_old_connections(): user.state = "main_utility"
1331 # go back to entering the new password as long as you haven't tried
1333 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1334 user.password_tries += 1
1335 user.error = "differs"
1336 user.state = "entering_new_password"
1338 # otherwise, sayonara
1340 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1341 user.account.destroy()
1342 user.state = "disconnecting"
1344 def handler_active(user):
1345 """Handle input for active users."""
1347 # get the next waiting line of input
1348 input_data = user.input_queue.pop(0)
1353 # split out the command (first word) and parameters (everything else)
1354 if input_data.find(" ") > 0:
1355 command_name, parameters = input_data.split(" ", 1)
1357 command_name = input_data
1360 # lowercase the command
1361 command_name = command_name.lower()
1363 # the command matches a command word for which we have data
1364 if command_name in universe.categories["command"]:
1365 command = universe.categories["command"][command_name]
1366 else: command = None
1368 # if it's allowed, do it
1369 if user.can_run(command): exec(command.get("action"))
1371 # otherwise, give an error
1372 elif command_name: command_error(user, input_data)
1374 # if no input, just idle back with a prompt
1375 else: user.send("", just_prompt=True)
1377 def command_halt(user, parameters):
1378 """Halt the world."""
1380 # see if there's a message or use a generic one
1381 if parameters: message = "Halting: " + parameters
1382 else: message = "User " + user.account.get("name") + " halted the world."
1385 broadcast(message, add_prompt=False)
1388 # set a flag to terminate the world
1389 universe.terminate_world = True
1391 def command_reload(user):
1392 """Reload all code modules, configs and data."""
1394 # let the user know and log
1395 user.send("Reloading all code modules, configs and data.")
1396 log("User " + user.account.get("name") + " reloaded the world.", 8)
1398 # set a flag to reload
1399 universe.reload_modules = True
1401 def command_quit(user):
1402 """Leave the world and go back to the main menu."""
1403 user.deactivate_avatar()
1404 user.state = "main_utility"
1406 def command_help(user, parameters):
1407 """List available commands and provide help for commands."""
1409 # did the user ask for help on a specific command word?
1412 # is the command word one for which we have data?
1413 if parameters in universe.categories["command"]:
1414 command = universe.categories["command"][parameters]
1415 else: command = None
1417 # only for allowed commands
1418 if user.can_run(command):
1420 # add a description if provided
1421 description = command.get("description")
1423 description = "(no short description provided)"
1424 if command.getboolean("administrative"): output = "$(red)"
1425 else: output = "$(grn)"
1426 output += parameters + "$(nrm) - " + description + "$(eol)$(eol)"
1428 # add the help text if provided
1429 help_text = command.get("help")
1431 help_text = "No help is provided for this command."
1434 # no data for the requested command word
1436 output = "That is not an available command."
1438 # no specific command word was indicated
1441 # give a sorted list of commands with descriptions if provided
1442 output = "These are the commands available to you:$(eol)$(eol)"
1443 sorted_commands = universe.categories["command"].keys()
1444 sorted_commands.sort()
1445 for item in sorted_commands:
1446 command = universe.categories["command"][item]
1447 if user.can_run(command):
1448 description = command.get("description")
1450 description = "(no short description provided)"
1451 if command.getboolean("administrative"): output += " $(red)"
1452 else: output += " $(grn)"
1453 output += item + "$(nrm) - " + description + "$(eol)"
1454 output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"."
1456 # send the accumulated output to the user
1459 def command_move(user, parameters):
1460 """Move the avatar in a given direction."""
1461 if parameters in universe.contents[user.avatar.get("location")].portals():
1462 user.avatar.move_direction(parameters)
1463 else: user.send("You cannot go that way.")
1465 def command_look(user, parameters):
1467 if parameters: user.send("You look at or in anything yet.")
1468 else: user.avatar.look_at(user.avatar.get("location"))
1470 def command_say(user, parameters):
1471 """Speak to others in the same room."""
1473 # check for replacement macros
1474 if replace_macros(user, parameters, True) != parameters:
1475 user.send("You cannot speak $_(replacement macros).")
1477 # the user entered a message
1480 # get rid of quote marks on the ends of the message and
1481 # capitalize the first letter
1482 message = parameters.strip("\"'`").capitalize()
1484 # a dictionary of punctuation:action pairs
1486 for facet in universe.categories["internal"]["language"].facets():
1487 if facet.startswith("punctuation_"):
1488 action = facet.split("_")[1]
1489 for mark in universe.categories["internal"]["language"].getlist(facet):
1490 actions[mark] = action
1492 # match the punctuation used, if any, to an action
1493 default_punctuation = universe.categories["internal"]["language"].get("default_punctuation")
1494 action = actions[default_punctuation]
1495 for mark in actions.keys():
1496 if message.endswith(mark) and mark != default_punctuation:
1497 action = actions[mark]
1500 # if the action is default and there is no mark, add one
1501 if action == actions[default_punctuation] and not message.endswith(default_punctuation):
1502 message += default_punctuation
1504 # capitalize a list of words within the message
1505 capitalize_words = universe.categories["internal"]["language"].getlist("capitalize_words")
1506 for word in capitalize_words:
1507 message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
1510 user.avatar.echo_to_location(user.avatar.get("name") + " " + action + "s, \"" + message + "\"")
1511 user.send("You " + action + ", \"" + message + "\"")
1513 # there was no message
1515 user.send("What do you want to say?")
1517 def command_show(user, parameters):
1518 """Show program data."""
1520 if parameters.find(" ") < 1:
1521 if parameters == "time":
1522 message = universe.categories["internal"]["counters"].get("elapsed") + " increments elapsed since the world was created."
1523 elif parameters == "categories":
1524 message = "These are the element categories:$(eol)"
1525 categories = universe.categories.keys()
1527 for category in categories: message += "$(eol) $(grn)" + category + "$(nrm)"
1528 elif parameters == "files":
1529 message = "These are the current files containing the universe:$(eol)"
1530 filenames = universe.files.keys()
1532 for filename in filenames: message += "$(eol) $(grn)" + filename + "$(nrm)"
1535 arguments = parameters.split()
1536 if arguments[0] == "category":
1537 if arguments[1] in universe.categories:
1538 message = "These are the elements in the \"" + arguments[1] + "\" category:$(eol)"
1539 elements = universe.categories[arguments[1]].keys()
1541 for element in elements:
1542 message += "$(eol) $(grn)" + universe.categories[arguments[1]][element].key + "$(nrm)"
1543 elif arguments[0] == "element":
1544 if arguments[1] in universe.contents:
1545 message = "These are the properties of the \"" + arguments[1] + "\" element:$(eol)"
1546 element = universe.contents[arguments[1]]
1547 facets = element.facets()
1549 for facet in facets:
1550 message += "$(eol) $(grn)" + facet + ": $(red)" + escape_macros(element.get(facet)) + "$(nrm)"
1551 elif arguments[0] == "result":
1552 if len(arguments) > 1:
1554 message = repr(eval(" ".join(arguments[1:])))
1556 message = "Your expression raised an exception!"
1557 elif arguments[0] == "log":
1558 if match("^\d+$", arguments[1]) and int(arguments[1]) > 0:
1559 linecount = int(arguments[1])
1560 if linecount > len(universe.loglist): linecount = len(universe.loglist)
1561 message = "There are " + str(len(universe.loglist)) + " log lines in memory."
1562 message += " The most recent " + str(linecount) + " lines are:$(eol)$(eol)"
1563 for line in universe.loglist[-linecount:]:
1564 message += " " + line + "$(eol)"
1565 else: message = "\"" + arguments[1] + "\" is not a positive integer greater than 0."
1567 if parameters: message = "I don't know what \"" + parameters + "\" is."
1568 else: message = "What do you want to show?"
1571 def command_create(user, parameters):
1572 """Create an element if it does not exist."""
1573 if not parameters: message = "You must at least specify an element to create."
1575 arguments = parameters.split()
1576 if len(arguments) == 1: arguments.append("")
1577 if len(arguments) == 2:
1578 element, filename = arguments
1579 if element in universe.contents: message = "The \"" + element + "\" element already exists."
1581 message = "You create \"" + element + "\" within the universe."
1582 logline = user.account.get("name") + " created an element: " + element
1584 logline += " in file " + filename
1585 if filename not in universe.files:
1586 message += " Warning: \"" + filename + "\" is not yet included in any other file and will not be read on startup unless this is remedied."
1587 Element(element, universe, filename)
1589 elif len(arguments) > 2: message = "You can only specify an element and a filename."
1592 def command_destroy(user, parameters):
1593 """Destroy an element if it exists."""
1594 if not parameters: message = "You must specify an element to destroy."
1596 if parameters not in universe.contents: message = "The \"" + parameters + "\" element does not exist."
1598 universe.contents[parameters].destroy()
1599 message = "You destroy \"" + parameters + "\" within the universe."
1600 log(user.account.get("name") + " destroyed an element: " + parameters, 6)
1603 def command_set(user, parameters):
1604 """Set a facet of an element."""
1605 if not parameters: message = "You must specify an element, a facet and a value."
1607 arguments = parameters.split(" ", 2)
1608 if len(arguments) == 1: message = "What facet of element \"" + arguments[0] + "\" would you like to set?"
1609 elif len(arguments) == 2: message = "What value would you like to set for the \"" + arguments[1] + "\" facet of the \"" + arguments[0] + "\" element?"
1611 element, facet, value = arguments
1612 if element not in universe.contents: message = "The \"" + element + "\" element does not exist."
1614 universe.contents[element].set(facet, value)
1615 message = "You have successfully (re)set the \"" + facet + "\" facet of element \"" + element + "\". Try \"show element " + element + "\" for verification."
1618 def command_delete(user, parameters):
1619 """Delete a facet from an element."""
1620 if not parameters: message = "You must specify an element and a facet."
1622 arguments = parameters.split(" ")
1623 if len(arguments) == 1: message = "What facet of element \"" + arguments[0] + "\" would you like to delete?"
1624 elif len(arguments) != 2: message = "You may only specify an element and a facet."
1626 element, facet = arguments
1627 if element not in universe.contents: message = "The \"" + element + "\" element does not exist."
1628 elif facet not in universe.contents[element].facets(): message = "The \"" + element + "\" element has no \"" + facet + "\" facet."
1630 universe.contents[element].delete(facet)
1631 message = "You have successfully deleted the \"" + facet + "\" facet of element \"" + element + "\". Try \"show element " + element + "\" for verification."
1634 def command_error(user, input_data):
1635 """Generic error for an unrecognized command word."""
1637 # 90% of the time use a generic error
1639 message = "I'm not sure what \"" + input_data + "\" means..."
1641 # 10% of the time use the classic diku error
1643 message = "Arglebargle, glop-glyf!?!"
1645 # send the error message
1648 # if there is no universe, create an empty one
1649 if not "universe" in locals(): universe = Universe()