Support clients using CR+NUL to signal EOL
[mudpy.git] / mudpy / misc.py
index 439e00a..4e43353 100644 (file)
@@ -64,8 +64,9 @@ class Element:
 
     def reload(self):
         """Create a new element and replace this one."""
-        Element(self.key, self.universe, self.origin)
-        del(self)
+        args = (self.key, self.universe, self.origin)
+        self.destroy()
+        Element(*args)
 
     def destroy(self):
         """Remove an element from the universe and destroy it."""
@@ -363,23 +364,9 @@ class Universe:
         # it's possible for this to enter before logging configuration is read
         pending_loglines = []
 
-        # the files dict must exist and filename needs to be read-only
-        if not hasattr(
-           self, "files"
-           ) or not (
-            self.filename in self.files and self.files[
-                self.filename
-            ].is_writeable()
-        ):
-
-            # clear out all read-only files
-            if hasattr(self, "files"):
-                for data_filename in list(self.files.keys()):
-                    if not self.files[data_filename].is_writeable():
-                        del self.files[data_filename]
-
-            # start loading from the initial file
-            mudpy.data.Data(self.filename, self)
+        # start populating the (re)files dict from the base config
+        self.files = {}
+        mudpy.data.Data(self.filename, self)
 
         # load default storage locations for groups
         if hasattr(self, "contents") and "mudpy.filing" in self.contents:
@@ -404,17 +391,6 @@ class Universe:
             if user.avatar in inactive_avatars:
                 inactive_avatars.remove(user.avatar)
 
-        # go through all elements to clear out inactive avatar locations
-        for element in self.contents.values():
-            area = element.get("location")
-            if element in inactive_avatars and area:
-                if area in self.contents and element.key in self.contents[
-                   area
-                   ].contents:
-                    del self.contents[area].contents[element.key]
-                element.set("default_location", area)
-                element.remove_facet("location")
-
         # another pass to straighten out all the element contents
         for element in self.contents.values():
             element.update_location()
@@ -525,7 +501,7 @@ class User:
         self.output_queue = []
         self.partial_input = b""
         self.password_tries = 0
-        self.state = "initial"
+        self.state = "telopt_negotiation"
         self.telopts = {}
 
     def quit(self):
@@ -584,26 +560,30 @@ class User:
     def reload(self):
         """Save, load a new user and relocate the connection."""
 
+        # copy old attributes
+        attributes = self.__dict__
+
         # get out of the list
         self.remove()
 
+        # get rid of the old user object
+        del(self)
+
         # create a new user object
         new_user = User()
 
         # set everything equivalent
-        for attribute in vars(self).keys():
-            exec("new_user." + attribute + " = self." + attribute)
+        new_user.__dict__ = attributes
 
         # the avatar needs a new owner
         if new_user.avatar:
+            new_user.account = universe.contents[new_user.account.key]
+            new_user.avatar = universe.contents[new_user.avatar.key]
             new_user.avatar.owner = new_user
 
         # add it to the list
         universe.userlist.append(new_user)
 
-        # get rid of the old user object
-        del(self)
-
     def replace_old_connections(self):
         """Disconnect active users with the same name."""
 
@@ -806,7 +786,7 @@ class User:
             self.check_idle()
 
         # if output is paused, decrement the counter
-        if self.state == "initial":
+        if self.state == "telopt_negotiation":
             if self.negotiation_pause:
                 self.negotiation_pause -= 1
             else:
@@ -863,11 +843,15 @@ class User:
             mudpy.telnet.negotiate_telnet_options(self)
 
             # separate multiple input lines
-            new_input_lines = self.partial_input.split(b"\n")
+            new_input_lines = self.partial_input.split(b"\r\0")
+            if len(new_input_lines) == 1:
+                new_input_lines = new_input_lines[0].split(b"\r\n")
 
             # if input doesn't end in a newline, replace the
             # held partial input with the last line of it
-            if not self.partial_input.endswith(b"\n"):
+            if not (
+                    self.partial_input.endswith(b"\r\0") or
+                    self.partial_input.endswith(b"\r\n")):
                 self.partial_input = new_input_lines.pop()
 
             # otherwise, chop off the extra null input and reset
@@ -883,8 +867,8 @@ class User:
                 line = line.strip()
 
                 # log non-printable characters remaining
-                if mudpy.telnet.is_enabled(self, mudpy.telnet.TELOPT_BINARY,
-                                           mudpy.telnet.HIM):
+                if not mudpy.telnet.is_enabled(
+                        self, mudpy.telnet.TELOPT_BINARY, mudpy.telnet.HIM):
                     asciiline = bytes([x for x in line if 32 <= x <= 126])
                     if line != asciiline:
                         logline = "Non-ASCII characters from "
@@ -1143,18 +1127,17 @@ def wrap_ansi_text(text, width):
                 # escape sequence
                 escape = False
 
-        # track the most recent whitespace we've seen
-        # TODO(fungi) exclude non-breaking spaces (\x0a)
-        elif unicodedata.category(each_character) in ("Cc", "Zs"):
-            if each_character == "\n":
-                # the current character is a newline, so reset the relative
-                # position too (start a new line)
-                rel_pos = 0
-            if each_character != "\r":
-                # the current character is not a carriage return, so mark it as
-                # whitespace (we don't want to break and wrap between CR+LF)
-                last_abs_whitespace = abs_pos
-                last_rel_whitespace = rel_pos
+        # the current character is a space
+        elif each_character == " ":
+            last_abs_whitespace = abs_pos
+            last_rel_whitespace = rel_pos
+
+        # the current character is a newline, so reset the relative
+        # position too (start a new line)
+        elif each_character == "\n":
+            rel_pos = 0
+            last_abs_whitespace = abs_pos
+            last_rel_whitespace = rel_pos
 
         # the current character meets the requested maximum line width, so we
         # need to wrap unless the current word is wider than the terminal (in
@@ -1180,8 +1163,7 @@ def wrap_ansi_text(text, width):
         # printable character
         elif each_character != "\r":
             rel_pos += glyph_columns(each_character)
-            if unicodedata.category(each_character) in ("Cc", "Zs"):
-                # TODO(fungi) exclude non-breaking spaces (\x0a)
+            if each_character in (" ", "\n"):
                 last_abs_whitespace = abs_pos
                 last_rel_whitespace = rel_pos
 
@@ -1416,12 +1398,13 @@ def on_pulse():
 
 def reload_data():
     """Reload all relevant objects."""
-    for user in universe.userlist[:]:
-        user.reload()
-    for element in universe.contents.values():
-        if element.origin.is_writeable():
-            element.reload()
+    universe.save()
+    old_userlist = universe.userlist[:]
+    for element in list(universe.contents.values()):
+        element.destroy()
     universe.load()
+    for user in old_userlist:
+        user.reload()
 
 
 def check_for_connection(listening_socket):