1 """Core objects for the mudpy engine."""
3 # Copyright (c) 2005 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 R_OK, access, chmod, makedirs, stat
10 from os.path import abspath, dirname, exists, isabs, join as path_join
11 from random import choice, randrange
12 from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
13 from stat import S_IMODE, ST_MODE
14 from telnetlib import DO, DONT, ECHO, EOR, IAC, WILL, WONT
15 from time import asctime, sleep
18 """An element of the universe."""
19 def __init__(self, key, universe, origin=""):
20 """Default values for the in-memory element variables."""
22 if self.key.find(":") > 0:
23 self.category, self.subkey = self.key.split(":", 1)
25 self.category = "other"
26 self.subkey = self.key
27 if not self.category in universe.categories: self.category = "other"
28 universe.categories[self.category][self.subkey] = self
30 if not self.origin: self.origin = universe.default_origins[self.category]
31 if not isabs(self.origin):
32 self.origin = abspath(self.origin)
33 universe.contents[self.key] = self
34 if not self.origin in universe.files:
35 DataFile(self.origin, universe)
36 if not universe.files[self.origin].data.has_section(self.key):
37 universe.files[self.origin].data.add_section(self.key)
39 """Remove an element from the universe and destroy it."""
40 log("Destroying: " + self.key + ".")
41 universe.files[self.origin].data.remove_section(self.key)
42 del universe.categories[self.category][self.subkey]
43 del universe.contents[self.key]
45 def delete(self, facet):
46 """Delete a facet from the element."""
47 if universe.files[self.origin].data.has_option(self.key, facet):
48 universe.files[self.origin].data.remove_option(self.key, facet)
50 """Return a list of facets for this element."""
51 return universe.files[self.origin].data.options(self.key)
52 def get(self, facet, default=None):
53 """Retrieve values."""
54 if default is None: default = ""
55 if universe.files[self.origin].data.has_option(self.key, facet):
56 return universe.files[self.origin].data.get(self.key, facet)
58 def getboolean(self, facet, default=None):
59 """Retrieve values as boolean type."""
60 if default is None: default=False
61 if universe.files[self.origin].data.has_option(self.key, facet):
62 return universe.files[self.origin].data.getboolean(self.key, facet)
64 def getint(self, facet, default=None):
65 """Return values as int/long type."""
66 if default is None: default = 0
67 if universe.files[self.origin].data.has_option(self.key, facet):
68 return universe.files[self.origin].data.getint(self.key, facet)
70 def getfloat(self, facet, default=None):
71 """Return values as float type."""
72 if default is None: default = 0.0
73 if universe.files[self.origin].data.has_option(self.key, facet):
74 return universe.files[self.origin].data.getfloat(self.key, facet)
76 def getlist(self, facet, default=None):
77 """Return values as list type."""
78 if default is None: default = []
79 value = self.get(facet)
80 if value: return makelist(value)
82 def getdict(self, facet, default=None):
83 """Return values as dict type."""
84 if default is None: default = {}
85 value = self.get(facet)
86 if value: return makedict(value)
88 def set(self, facet, value):
90 if type(value) is long: value = str(value)
91 elif not type(value) is str: value = repr(value)
92 universe.files[self.origin].data.set(self.key, facet, value)
95 """A file containing universe elements."""
96 def __init__(self, filename, universe):
97 self.data = RawConfigParser()
98 if access(filename, R_OK): self.data.read(filename)
99 self.filename = filename
100 universe.files[filename] = self
101 if self.data.has_option("control", "include_files"):
102 includes = makelist(self.data.get("control", "include_files"))
104 if self.data.has_option("control", "default_files"):
105 origins = makedict(self.data.get("control", "default_files"))
106 for key in origins.keys():
107 if not key in includes: includes.append(key)
108 universe.default_origins[key] = origins[key]
109 if not key in universe.categories:
110 universe.categories[key] = {}
111 if self.data.has_option("control", "private_files"):
112 for item in makelist(self.data.get("control", "private_files")):
113 if not item in includes: includes.append(item)
114 if not item in universe.private_files:
116 item = path_join(dirname(filename), item)
117 universe.private_files.append(item)
118 for section in self.data.sections():
119 if section != "control":
120 Element(section, universe, filename)
121 for include_file in includes:
122 if not isabs(include_file):
123 include_file = path_join(dirname(filename), include_file)
124 DataFile(include_file, universe)
126 if ( self.data.sections() or exists(self.filename) ) and not ( self.data.has_option("control", "read_only") and self.data.getboolean("control", "read_only") ):
127 if not exists(dirname(self.filename)): makedirs(dirname(self.filename))
128 file_descriptor = file(self.filename, "w")
129 if self.filename in universe.private_files and oct(S_IMODE(stat(self.filename)[ST_MODE])) != 0600:
130 chmod(self.filename, 0600)
131 self.data.write(file_descriptor)
132 file_descriptor.flush()
133 file_descriptor.close()
137 def __init__(self, filename=""):
138 """Initialize the universe."""
141 self.default_origins = {}
143 self.private_files = []
145 self.terminate_world = False
146 self.reload_modules = False
148 possible_filenames = [
154 "/usr/local/mudpy/mudpy.conf",
155 "/usr/local/mudpy/etc/mudpy.conf",
156 "/etc/mudpy/mudpy.conf",
159 for filename in possible_filenames:
160 if access(filename, R_OK): break
161 if not isabs(filename):
162 filename = abspath(filename)
163 DataFile(filename, self)
165 """Save the universe to persistent storage."""
166 for key in self.files: self.files[key].save()
168 def initialize_server_socket(self):
169 """Create and open the listening socket."""
171 # create a new ipv4 stream-type socket object
172 self.listening_socket = socket(AF_INET, SOCK_STREAM)
174 # set the socket options to allow existing open ones to be
175 # reused (fixes a bug where the server can't bind for a minute
176 # when restarting on linux systems)
177 self.listening_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
179 # bind the socket to to our desired server ipa and port
180 self.listening_socket.bind((self.categories["internal"]["network"].get("host"), self.categories["internal"]["network"].getint("port")))
182 # disable blocking so we can proceed whether or not we can
184 self.listening_socket.setblocking(0)
186 # start listening on the socket
187 self.listening_socket.listen(1)
189 # note that we're now ready for user connections
190 log("Waiting for connection(s)...")
193 """This is a connected user."""
196 """Default values for the in-memory user variables."""
198 self.last_address = ""
199 self.connection = None
200 self.authenticated = False
201 self.password_tries = 0
202 self.state = "entering_account_name"
203 self.menu_seen = False
205 self.input_queue = []
206 self.output_queue = []
207 self.partial_input = ""
213 """Log, close the connection and remove."""
214 if self.account: name = self.account.get("name")
216 if name: message = "User " + name
217 else: message = "An unnamed user"
218 message += " logged out."
220 self.connection.close()
224 """Save, load a new user and relocate the connection."""
226 # get out of the list
229 # create a new user object
232 # set everything else equivalent
249 exec("new_user." + attribute + " = self." + attribute)
252 universe.userlist.append(new_user)
254 # get rid of the old user object
257 def replace_old_connections(self):
258 """Disconnect active users with the same name."""
260 # the default return value
263 # iterate over each user in the list
264 for old_user in universe.userlist:
266 # the name is the same but it's not us
267 if old_user.account.get("name") == self.account.get("name") and old_user is not self:
270 log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".")
271 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
272 self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
274 # close the old connection
275 old_user.connection.close()
277 # replace the old connection with this one
278 old_user.connection = self.connection
279 old_user.last_address = old_user.address
280 old_user.address = self.address
281 old_user.echoing = self.echoing
283 # take this one out of the list and delete
289 # true if an old connection was replaced, false if not
292 def authenticate(self):
293 """Flag the user as authenticated and disconnect duplicates."""
294 if not self.state is "authenticated":
295 log("User " + self.account.get("name") + " logged in.")
296 self.authenticated = True
299 """Send the user their current menu."""
300 if not self.menu_seen:
301 self.menu_choices = get_menu_choices(self)
302 self.send(get_menu(self.state, self.error, self.echoing, self.menu_choices), "")
303 self.menu_seen = True
305 self.adjust_echoing()
307 def adjust_echoing(self):
308 """Adjust echoing to match state menu requirements."""
309 if self.echoing and not menu_echo_on(self.state): self.echoing = False
310 elif not self.echoing and menu_echo_on(self.state): self.echoing = True
313 """Remove a user from the list of connected users."""
314 universe.userlist.remove(self)
316 def send(self, output, eol="$(eol)"):
317 """Send arbitrary text to a connected user."""
319 # start with a newline, append the message, then end
320 # with the optional eol string passed to this function
321 # and the ansi escape to return to normal text
322 output = "\r\n" + output + eol + chr(27) + "[0m"
324 # find and replace macros in the output
325 output = replace_macros(self, output)
327 # wrap the text at 80 characters
328 output = wrap_ansi_text(output, 80)
330 # drop the formatted output into the output queue
331 self.output_queue.append(output)
333 # try to send the last item in the queue and remove it
335 self.connection.send(self.output_queue[0])
336 del self.output_queue[0]
338 # but if we can't, that's okay too
343 """All the things to do to the user per increment."""
345 # if the world is terminating, disconnect
346 if universe.terminate_world:
347 self.state = "disconnecting"
348 self.menu_seen = False
350 # show the user a menu as needed
353 # disconnect users with the appropriate state
354 if self.state == "disconnecting": self.quit()
356 # the user is unique and not flagged to disconnect
359 # check for input and add it to the queue
362 # there is input waiting in the queue
363 if self.input_queue: handle_user_input(self)
365 def enqueue_input(self):
366 """Process and enqueue any new input."""
368 # check for some input
370 input_data = self.connection.recv(1024)
377 # tack this on to any previous partial
378 self.partial_input += input_data
380 # reply to and remove any IAC negotiation codes
381 self.negotiate_telnet_options()
383 # separate multiple input lines
384 new_input_lines = self.partial_input.split("\n")
386 # if input doesn't end in a newline, replace the
387 # held partial input with the last line of it
388 if not self.partial_input.endswith("\n"):
389 self.partial_input = new_input_lines.pop()
391 # otherwise, chop off the extra null input and reset
392 # the held partial input
394 new_input_lines.pop()
395 self.partial_input = ""
397 # iterate over the remaining lines
398 for line in new_input_lines:
400 # remove a trailing carriage return
401 if line.endswith("\r"): line = line.rstrip("\r")
403 # log non-printable characters remaining
404 removed = filter(lambda x: (x < " " or x > "~"), line)
406 logline = "Non-printable characters from "
407 if self.account and self.account.get("name"): logline += self.account.get("name") + ": "
408 else: logline += "unknown user: "
409 logline += repr(removed)
412 # filter out non-printables
413 line = filter(lambda x: " " <= x <= "~", line)
415 # strip off extra whitespace
418 # put on the end of the queue
419 self.input_queue.append(line)
421 def negotiate_telnet_options(self):
422 """Reply to/remove partial_input telnet negotiation options."""
424 # start at the begining of the input
427 # make a local copy to play with
428 text = self.partial_input
430 # as long as we haven't checked it all
431 while position < len(text):
433 # jump to the first IAC you find
434 position = text.find(IAC, position)
436 # if there wasn't an IAC in the input, skip to the end
437 if position < 0: position = len(text)
439 # replace a double (literal) IAC and move on
440 elif len(text) > position+1 and text[position+1] == IAC:
441 text = text.replace(IAC+IAC, IAC)
444 # this must be an option negotiation
445 elif len(text) > position+2 and text[position+1] in (DO, DONT, WILL, WONT):
447 # if we turned echo off, ignore the confirmation
448 if not self.echoing and text[position+1:position+3] == DO+ECHO: pass
450 # we don't want to allow anything else
451 elif text[position+1] in (DO, WILL): self.send(IAC+WONT+text[position+2])
453 # strip the negotiation from the input
454 text = text.replace(text[position:position+3], "")
456 # otherwise, strip out a two-byte IAC command
457 else: text = text.replace(text[position:position+2], "")
459 # replace the input with our cleaned-up text
460 self.partial_input = text
462 def can_run(self, command):
463 """Check if the user can run this command object."""
465 # has to be in the commands category
466 if command not in universe.categories["command"].values(): result = False
468 # administrators can run any command
469 elif self.account.getboolean("administrator"): result = True
471 # everyone can run non-administrative commands
472 elif not command.getboolean("administrative"): result = True
474 # otherwise the command cannot be run by this user
477 # pass back the result
480 def new_avatar(self):
481 """Instantiate a new, unconfigured avatar for this user."""
483 while "avatar:" + self.account.get("name") + ":" + str(counter) in universe.categories["actor"].keys(): counter += 1
484 self.avatar = Element("actor:avatar:" + self.account.get("name") + ":" + str(counter), universe)
485 avatars = self.account.getlist("avatars")
486 avatars.append(self.avatar.key)
487 self.account.set("avatars", avatars)
489 def delete_avatar(self, avatar):
490 """Remove an avatar from the world and from the user's list."""
491 if self.avatar is universe.contents[avatar]: self.avatar = None
492 universe.contents[avatar].destroy()
493 avatars = self.account.getlist("avatars")
494 avatars.remove(avatar)
495 self.account.set("avatars", avatars)
498 """Destroy the user and associated avatars."""
499 for avatar in self.account.getlist("avatars"): self.delete_avatar(avatar)
500 self.account.destroy()
502 def list_avatar_names(self):
503 """List names of assigned avatars."""
504 return [ universe.contents[avatar].get("name") for avatar in self.account.getlist("avatars") ]
507 """Turn string into list type."""
508 if value[0] + value[-1] == "[]": return eval(value)
509 else: return [ value ]
512 """Turn string into dict type."""
513 if value[0] + value[-1] == "{}": return eval(value)
514 elif value.find(":") > 0: return eval("{" + value + "}")
515 else: return { value: None }
517 def broadcast(message):
518 """Send a message to all connected users."""
519 for each_user in universe.userlist: each_user.send("$(eol)" + message)
524 # the time in posix log timestamp format
525 timestamp = asctime()[4:19]
527 # send the timestamp and message to standard output
528 print(timestamp + " " + message)
530 def wrap_ansi_text(text, width):
531 """Wrap text with arbitrary width while ignoring ANSI colors."""
533 # the current position in the entire text string, including all
534 # characters, printable or otherwise
535 absolute_position = 0
537 # the current text position relative to the begining of the line,
538 # ignoring color escape sequences
539 relative_position = 0
541 # whether the current character is part of a color escape sequence
544 # iterate over each character from the begining of the text
545 for each_character in text:
547 # the current character is the escape character
548 if each_character == chr(27):
551 # the current character is within an escape sequence
554 # the current character is m, which terminates the
555 # current escape sequence
556 if each_character == "m":
559 # the current character is a newline, so reset the relative
560 # position (start a new line)
561 elif each_character == "\n":
562 relative_position = 0
564 # the current character meets the requested maximum line width,
565 # so we need to backtrack and find a space at which to wrap
566 elif relative_position == width:
568 # distance of the current character examined from the
572 # count backwards until we find a space
573 while text[absolute_position - wrap_offset] != " ":
576 # insert an eol in place of the space
577 text = text[:absolute_position - wrap_offset] + "\r\n" + text[absolute_position - wrap_offset + 1:]
579 # increase the absolute position because an eol is two
580 # characters but the space it replaced was only one
581 absolute_position += 1
583 # now we're at the begining of a new line, plus the
584 # number of characters wrapped from the previous line
585 relative_position = wrap_offset
587 # as long as the character is not a carriage return and the
588 # other above conditions haven't been met, count it as a
589 # printable character
590 elif each_character != "\r":
591 relative_position += 1
593 # increase the absolute position for every character
594 absolute_position += 1
596 # return the newly-wrapped text
599 def weighted_choice(data):
600 """Takes a dict weighted by value and returns a random key."""
602 # this will hold our expanded list of keys from the data
605 # create thee expanded list of keys
606 for key in data.keys():
607 for count in range(data[key]):
610 # return one at random
611 return choice(expanded)
614 """Returns a random character name."""
616 # the vowels and consonants needed to create romaji syllables
617 vowels = [ "a", "i", "u", "e", "o" ]
618 consonants = ["'", "k", "z", "s", "sh", "z", "j", "t", "ch", "ts", "d", "n", "h", "f", "m", "y", "r", "w" ]
620 # this dict will hold our weighted list of syllables
623 # generate the list with an even weighting
624 for consonant in consonants:
626 syllables[consonant + vowel] = 1
628 # we'll build the name into this string
631 # create a name of random length from the syllables
632 for syllable in range(randrange(2, 6)):
633 name += weighted_choice(syllables)
635 # strip any leading quotemark, capitalize and return the name
636 return name.strip("'").capitalize()
638 def replace_macros(user, text, is_input=False):
639 """Replaces macros in text output."""
644 # third person pronouns
646 "female": { "obj": "her", "pos": "hers", "sub": "she" },
647 "male": { "obj": "him", "pos": "his", "sub": "he" },
648 "neuter": { "obj": "it", "pos": "its", "sub": "it" }
651 # a dict of replacement macros
654 "$(bld)": chr(27) + "[1m",
655 "$(nrm)": chr(27) + "[0m",
656 "$(blk)": chr(27) + "[30m",
657 "$(grn)": chr(27) + "[32m",
658 "$(red)": chr(27) + "[31m",
661 # add dynamic macros where possible
663 account_name = user.account.get("name")
665 macros["$(account)"] = account_name
667 avatar_gender = user.avatar.get("gender")
669 macros["$(tpop)"] = pronouns[avatar_gender]["obj"]
670 macros["$(tppp)"] = pronouns[avatar_gender]["pos"]
671 macros["$(tpsp)"] = pronouns[avatar_gender]["sub"]
673 # find and replace per the macros dict
674 macro_start = text.find("$(")
675 if macro_start == -1: break
676 macro_end = text.find(")", macro_start) + 1
677 macro = text[macro_start:macro_end]
678 if macro in macros.keys():
679 text = text.replace(macro, macros[macro])
681 # if we get here, log and replace it with null
683 text = text.replace(macro, "")
685 log("Unexpected replacement macro " + macro + " encountered.")
687 # replace the look-like-a-macro sequence
688 text = text.replace("$_(", "$(")
692 def escape_macros(text):
693 """Escapes replacement macros in text."""
694 return text.replace("$(", "$_(")
696 def check_time(frequency):
697 """Check for a factor of the current increment count."""
698 if type(frequency) is str:
699 frequency = universe.categories["internal"]["time"].getint(frequency)
700 if not "counters" in universe.categories["internal"]:
701 Element("internal:counters", universe)
702 return not universe.categories["internal"]["counters"].getint("elapsed") % frequency
705 """The things which should happen on each pulse, aside from reloads."""
707 # open the listening socket if it hasn't been already
708 if not hasattr(universe, "listening_socket"):
709 universe.initialize_server_socket()
711 # assign a user if a new connection is waiting
712 user = check_for_connection(universe.listening_socket)
713 if user: universe.userlist.append(user)
715 # iterate over the connected users
716 for user in universe.userlist: user.pulse()
718 # update the log every now and then
719 if check_time("frequency_log"):
720 log(str(len(universe.userlist)) + " connection(s)")
722 # periodically save everything
723 if check_time("frequency_save"):
726 # pause for a configurable amount of time (decimal seconds)
727 sleep(universe.categories["internal"]["time"].getfloat("increment"))
729 # increment the elapsed increment counter
730 universe.categories["internal"]["counters"].set("elapsed", universe.categories["internal"]["counters"].getint("elapsed") + 1)
733 """Reload data into new persistent objects."""
734 for user in universe.userlist[:]: user.reload()
736 def check_for_connection(listening_socket):
737 """Check for a waiting connection and return a new user object."""
739 # try to accept a new connection
741 connection, address = listening_socket.accept()
745 # note that we got one
746 log("Connection from " + address[0])
748 # disable blocking so we can proceed whether or not we can send/receive
749 connection.setblocking(0)
751 # create a new user object
754 # associate this connection with it
755 user.connection = connection
757 # set the user's ipa from the connection's ipa
758 user.address = address[0]
760 # return the new user object
763 def get_menu(state, error=None, echoing=True, choices={}):
764 """Show the correct menu text to a user."""
766 # begin with a telnet echo command sequence if needed
767 message = get_echo_sequence(state, echoing)
769 # get the description or error text
770 message += get_menu_description(state, error)
772 # get menu choices for the current state
773 message += get_formatted_menu_choices(state, choices)
775 # try to get a prompt, if it was defined
776 message += get_menu_prompt(state)
778 # throw in the default choice, if it exists
779 message += get_formatted_default_menu_choice(state)
781 # display a message indicating if echo is off
782 message += get_echo_message(state)
784 # tack on IAC EOR to indicate the prompt will not be followed by CRLF
787 # return the assembly of various strings defined above
790 def menu_echo_on(state):
791 """True if echo is on, false if it is off."""
792 return universe.categories["menu"][state].getboolean("echo", True)
794 def get_echo_sequence(state, echoing):
795 """Build the appropriate IAC WILL/WONT ECHO sequence as needed."""
797 # if the user has echo on and the menu specifies it should be turned
798 # off, send: iac + will + echo + null
799 if echoing and not menu_echo_on(state): return IAC+WILL+ECHO
801 # if echo is not set to off in the menu and the user curently has echo
802 # off, send: iac + wont + echo + null
803 elif not echoing and menu_echo_on(state): return IAC+WONT+ECHO
805 # default is not to send an echo control sequence at all
808 def get_echo_message(state):
809 """Return a message indicating that echo is off."""
810 if menu_echo_on(state): return ""
811 else: return "(won't echo) "
813 def get_default_menu_choice(state):
814 """Return the default choice for a menu."""
815 return universe.categories["menu"][state].get("default")
817 def get_formatted_default_menu_choice(state):
818 """Default menu choice foratted for inclusion in a prompt string."""
819 default_choice = get_default_menu_choice(state)
820 if default_choice: return "[$(red)" + default_choice + "$(nrm)] "
823 def get_menu_description(state, error):
824 """Get the description or error text."""
826 # an error condition was raised by the handler
829 # try to get an error message matching the condition
831 description = universe.categories["menu"][state].get("error_" + error)
832 if not description: description = "That is not a valid choice..."
833 description = "$(red)" + description + "$(nrm)"
835 # there was no error condition
838 # try to get a menu description for the current state
839 description = universe.categories["menu"][state].get("description")
841 # return the description or error message
842 if description: description += "$(eol)$(eol)"
845 def get_menu_prompt(state):
846 """Try to get a prompt, if it was defined."""
847 prompt = universe.categories["menu"][state].get("prompt")
848 if prompt: prompt += " "
851 def get_menu_choices(user):
852 """Return a dict of choice:meaning."""
853 menu = universe.categories["menu"][user.state]
854 create_choices = menu.get("create")
855 if create_choices: choices = eval(create_choices)
860 for facet in menu.facets():
861 if facet.startswith("demand_") and not eval(universe.categories["menu"][user.state].get(facet)):
862 ignores.append(facet.split("_", 2)[1])
863 elif facet.startswith("create_"):
864 creates[facet] = facet.split("_", 2)[1]
865 elif facet.startswith("choice_"):
866 options[facet] = facet.split("_", 2)[1]
867 for facet in creates.keys():
868 if not creates[facet] in ignores:
869 choices[creates[facet]] = eval(menu.get(facet))
870 for facet in options.keys():
871 if not options[facet] in ignores:
872 choices[options[facet]] = menu.get(facet)
875 def get_formatted_menu_choices(state, choices):
876 """Returns a formatted string of menu choices."""
878 choice_keys = choices.keys()
880 for choice in choice_keys:
881 choice_output += " [$(red)" + choice + "$(nrm)] " + choices[choice] + "$(eol)"
882 if choice_output: choice_output += "$(eol)"
885 def get_menu_branches(state):
886 """Return a dict of choice:branch."""
888 for facet in universe.categories["menu"][state].facets():
889 if facet.startswith("branch_"):
890 branches[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
893 def get_default_branch(state):
894 """Return the default branch."""
895 return universe.categories["menu"][state].get("branch")
897 def get_choice_branch(user, choice):
898 """Returns the new state matching the given choice."""
899 branches = get_menu_branches(user.state)
900 if choice in branches.keys(): return branches[choice]
901 elif choice in user.menu_choices.keys(): return get_default_branch(user.state)
904 def get_menu_actions(state):
905 """Return a dict of choice:branch."""
907 for facet in universe.categories["menu"][state].facets():
908 if facet.startswith("action_"):
909 actions[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
912 def get_default_action(state):
913 """Return the default action."""
914 return universe.categories["menu"][state].get("action")
916 def get_choice_action(user, choice):
917 """Run any indicated script for the given choice."""
918 actions = get_menu_actions(user.state)
919 if choice in actions.keys(): return actions[choice]
920 elif choice in user.menu_choices.keys(): return get_default_action(user.state)
923 def handle_user_input(user):
924 """The main handler, branches to a state-specific handler."""
926 # check to make sure the state is expected, then call that handler
927 if "handler_" + user.state in globals():
928 exec("handler_" + user.state + "(user)")
930 generic_menu_handler(user)
932 # since we got input, flag that the menu/prompt needs to be redisplayed
933 user.menu_seen = False
935 # if the user's client echo is off, send a blank line for aesthetics
936 if not user.echoing: user.send("", "")
938 def generic_menu_handler(user):
939 """A generic menu choice handler."""
941 # get a lower-case representation of the next line of input
943 choice = user.input_queue.pop(0)
944 if choice: choice = choice.lower()
946 if not choice: choice = get_default_menu_choice(user.state)
947 if choice in user.menu_choices:
948 exec(get_choice_action(user, choice))
949 new_state = get_choice_branch(user, choice)
950 if new_state: user.state = new_state
951 else: user.error = "default"
953 def handler_entering_account_name(user):
954 """Handle the login account name."""
956 # get the next waiting line of input
957 input_data = user.input_queue.pop(0)
959 # did the user enter anything?
962 # keep only the first word and convert to lower-case
963 name = input_data.lower()
965 # fail if there are non-alphanumeric characters
966 if name != filter(lambda x: x>="0" and x<="9" or x>="a" and x<="z", name):
967 user.error = "bad_name"
969 # if that account exists, time to request a password
970 elif name in universe.categories["account"]:
971 user.account = universe.categories["account"][name]
972 user.state = "checking_password"
974 # otherwise, this could be a brand new user
976 user.account = Element("account:" + name, universe)
977 user.account.set("name", name)
978 log("New user: " + name)
979 user.state = "checking_new_account_name"
981 # if the user entered nothing for a name, then buhbye
983 user.state = "disconnecting"
985 def handler_checking_password(user):
986 """Handle the login account password."""
988 # get the next waiting line of input
989 input_data = user.input_queue.pop(0)
991 # does the hashed input equal the stored hash?
992 if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
994 # if so, set the username and load from cold storage
995 if not user.replace_old_connections():
997 user.state = "main_utility"
999 # if at first your hashes don't match, try, try again
1000 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1001 user.password_tries += 1
1002 user.error = "incorrect"
1004 # we've exceeded the maximum number of password failures, so disconnect
1006 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1007 user.state = "disconnecting"
1009 def handler_entering_new_password(user):
1010 """Handle a new password entry."""
1012 # get the next waiting line of input
1013 input_data = user.input_queue.pop(0)
1015 # make sure the password is strong--at least one upper, one lower and
1016 # one digit, seven or more characters in length
1017 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)):
1019 # hash and store it, then move on to verification
1020 user.account.set("passhash", new_md5(user.account.get("name") + input_data).hexdigest())
1021 user.state = "verifying_new_password"
1023 # the password was weak, try again if you haven't tried too many times
1024 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1025 user.password_tries += 1
1028 # too many tries, so adios
1030 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1031 user.account.destroy()
1032 user.state = "disconnecting"
1034 def handler_verifying_new_password(user):
1035 """Handle the re-entered new password for verification."""
1037 # get the next waiting line of input
1038 input_data = user.input_queue.pop(0)
1040 # hash the input and match it to storage
1041 if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
1044 # the hashes matched, so go active
1045 if not user.replace_old_connections(): user.state = "main_utility"
1047 # go back to entering the new password as long as you haven't tried
1049 elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
1050 user.password_tries += 1
1051 user.error = "differs"
1052 user.state = "entering_new_password"
1054 # otherwise, sayonara
1056 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
1057 user.account.destroy()
1058 user.state = "disconnecting"
1060 def handler_active(user):
1061 """Handle input for active users."""
1063 # get the next waiting line of input
1064 input_data = user.input_queue.pop(0)
1066 # split out the command (first word) and parameters (everything else)
1067 if input_data.find(" ") > 0:
1068 command_name, parameters = input_data.split(" ", 1)
1070 command_name = input_data
1073 # lowercase the command
1074 command_name = command_name.lower()
1076 # the command matches a command word for which we have data
1077 if command_name in universe.categories["command"]:
1078 command = universe.categories["command"][command_name]
1079 else: command = None
1081 # if it's allowed, do it
1082 if user.can_run(command): exec(command.get("action"))
1084 # otherwise, give an error
1085 elif command_name: command_error(user, input_data)
1087 def command_halt(user, parameters):
1088 """Halt the world."""
1090 # see if there's a message or use a generic one
1091 if parameters: message = "Halting: " + parameters
1092 else: message = "User " + user.account.get("name") + " halted the world."
1098 # set a flag to terminate the world
1099 universe.terminate_world = True
1101 def command_reload(user):
1102 """Reload all code modules, configs and data."""
1104 # let the user know and log
1105 user.send("Reloading all code modules, configs and data.")
1106 log("User " + user.account.get("name") + " reloaded the world.")
1108 # set a flag to reload
1109 universe.reload_modules = True
1111 def command_help(user, parameters):
1112 """List available commands and provide help for commands."""
1114 # did the user ask for help on a specific command word?
1117 # is the command word one for which we have data?
1118 if parameters in universe.categories["command"]:
1119 command = universe.categories["command"][parameters]
1120 else: command = None
1122 # only for allowed commands
1123 if user.can_run(command):
1125 # add a description if provided
1126 description = command.get("description")
1128 description = "(no short description provided)"
1129 if command.getboolean("administrative"): output = "$(red)"
1130 else: output = "$(grn)"
1131 output += parameters + "$(nrm) - " + description + "$(eol)$(eol)"
1133 # add the help text if provided
1134 help_text = command.get("help")
1136 help_text = "No help is provided for this command."
1139 # no data for the requested command word
1141 output = "That is not an available command."
1143 # no specific command word was indicated
1146 # give a sorted list of commands with descriptions if provided
1147 output = "These are the commands available to you:$(eol)$(eol)"
1148 sorted_commands = universe.categories["command"].keys()
1149 sorted_commands.sort()
1150 for item in sorted_commands:
1151 command = universe.categories["command"][item]
1152 if user.can_run(command):
1153 description = command.get("description")
1155 description = "(no short description provided)"
1156 if command.getboolean("administrative"): output += " $(red)"
1157 else: output += " $(grn)"
1158 output += item + "$(nrm) - " + description + "$(eol)"
1159 output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"."
1161 # send the accumulated output to the user
1164 def command_say(user, parameters):
1165 """Speak to others in the same room."""
1167 # check for replacement macros
1168 if replace_macros(user, parameters, True) != parameters:
1169 user.send("You cannot speak $_(replacement macros).")
1171 # the user entered a message
1174 # get rid of quote marks on the ends of the message and
1175 # capitalize the first letter
1176 message = parameters.strip("\"'`").capitalize()
1178 # a dictionary of punctuation:action pairs
1180 for facet in universe.categories["internal"]["language"].facets():
1181 if facet.startswith("punctuation_"):
1182 action = facet.split("_")[1]
1183 for mark in universe.categories["internal"]["language"].getlist(facet):
1184 actions[mark] = action
1186 # match the punctuation used, if any, to an action
1187 default_punctuation = universe.categories["internal"]["language"].get("default_punctuation")
1188 action = actions[default_punctuation]
1189 for mark in actions.keys():
1190 if message.endswith(mark) and mark != default_punctuation:
1191 action = actions[mark]
1194 # if the action is default and there is no mark, add one
1195 if action == actions[default_punctuation] and not message.endswith(default_punctuation):
1196 message += default_punctuation
1198 # capitalize a list of words within the message
1199 capitalize_words = universe.categories["internal"]["language"].getlist("capitalize_words")
1200 for word in capitalize_words:
1201 message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
1204 # TODO: we won't be using broadcast once there are actual rooms
1205 broadcast(user.avatar.get("name") + " " + action + "s, \"" + message + "\"")
1207 # there was no message
1209 user.send("What do you want to say?")
1211 def command_show(user, parameters):
1212 """Show program data."""
1214 if parameters.find(" ") < 1:
1215 if parameters == "time":
1216 message = universe.categories["internal"]["counters"].get("elapsed") + " increments elapsed since the world was created."
1217 elif parameters == "categories":
1218 message = "These are the element categories:$(eol)"
1219 categories = universe.categories.keys()
1221 for category in categories: message += "$(eol) $(grn)" + category + "$(nrm)"
1222 elif parameters == "files":
1223 message = "These are the current files containing the universe:$(eol)"
1224 filenames = universe.files.keys()
1226 for filename in filenames: message += "$(eol) $(grn)" + filename + "$(nrm)"
1229 arguments = parameters.split()
1230 if arguments[0] == "category":
1231 if arguments[1] in universe.categories:
1232 message = "These are the elements in the \"" + arguments[1] + "\" category:$(eol)"
1233 elements = universe.categories[arguments[1]].keys()
1235 for element in elements:
1236 message += "$(eol) $(grn)" + universe.categories[arguments[1]][element].key + "$(nrm)"
1237 elif arguments[0] == "element":
1238 if arguments[1] in universe.contents:
1239 message = "These are the properties of the \"" + arguments[1] + "\" element:$(eol)"
1240 element = universe.contents[arguments[1]]
1241 facets = element.facets()
1243 for facet in facets:
1244 message += "$(eol) $(grn)" + facet + ": $(red)" + escape_macros(element.get(facet)) + "$(nrm)"
1246 if parameters: message = "I don't know what \"" + parameters + "\" is."
1247 else: message = "What do you want to show?"
1250 def command_create(user, parameters):
1251 """Create an element if it does not exist."""
1252 if not parameters: message = "You must at least specify an element to create."
1254 arguments = parameters.split()
1255 if len(arguments) == 1: arguments.append("")
1256 if len(arguments) == 2:
1257 element, filename = arguments
1258 if element in universe.contents: message = "The \"" + element + "\" element already exists."
1260 message = "You create \"" + element + "\" within the universe."
1261 logline = user.account.get("name") + " created an element: " + element
1263 logline += " in file " + filename
1264 if filename not in universe.files:
1265 message += " Warning: \"" + filename + "\" is not yet included in any other file and will not be read on startup unless this is remedied."
1266 Element(element, universe, filename)
1268 elif len(arguments) > 2: message = "You can only specify an element and a filename."
1271 def command_destroy(user, parameters):
1272 """Destroy an element if it exists."""
1273 if not parameters: message = "You must specify an element to destroy."
1275 if parameters not in universe.contents: message = "The \"" + parameters + "\" element does not exist."
1277 universe.contents[parameters].destroy()
1278 message = "You destroy \"" + parameters + "\" within the universe."
1279 log(user.account.get("name") + " destroyed an element: " + parameters)
1282 def command_set(user, parameters):
1283 """Set a facet of an element."""
1284 if not parameters: message = "You must specify an element, a facet and a value."
1286 arguments = parameters.split(" ", 2)
1287 if len(arguments) == 1: message = "What facet of element \"" + arguments[0] + "\" would you like to set?"
1288 elif len(arguments) == 2: message = "What value would you like to set for the \"" + arguments[1] + "\" facet of the \"" + arguments[0] + "\" element?"
1290 element, facet, value = arguments
1291 if element not in universe.contents: message = "The \"" + element + "\" element does not exist."
1293 universe.contents[element].set(facet, value)
1294 message = "You have successfully (re)set the \"" + facet + "\" facet of element \"" + element + "\". Try \"show element " + element + "\" for verification."
1297 def command_delete(user, parameters):
1298 """Delete a facet from an element."""
1299 if not parameters: message = "You must specify an element and a facet."
1301 arguments = parameters.split(" ")
1302 if len(arguments) == 1: message = "What facet of element \"" + arguments[0] + "\" would you like to delete?"
1303 elif len(arguments) != 2: message = "You may only specify an element and a facet."
1305 element, facet = arguments
1306 if element not in universe.contents: message = "The \"" + element + "\" element does not exist."
1307 elif facet not in universe.contents[element].facets(): message = "The \"" + element + "\" element has no \"" + facet + "\" facet."
1309 universe.contents[element].delete(facet)
1310 message = "You have successfully deleted the \"" + facet + "\" facet of element \"" + element + "\". Try \"show element " + element + "\" for verification."
1313 def command_error(user, input_data):
1314 """Generic error for an unrecognized command word."""
1316 # 90% of the time use a generic error
1318 message = "I'm not sure what \"" + input_data + "\" means..."
1320 # 10% of the time use the classic diku error
1322 message = "Arglebargle, glop-glyf!?!"
1324 # send the error message
1327 # if there is no universe, create an empty one
1328 if not "universe" in locals(): universe = Universe()