Can read enemy contact data (targets), minor tweaks, unicode support
3 files changed, 75 insertions(+), 19 deletions(-)

M common.py
M player.py
M result.py
M common.py +16 -8
@@ 34,12 34,6 @@ class PlanetsData(object):
                 setattr(self, field, None)
         self.id = None
     
-    def __setattr__(self, name, value):
-        """__setattr__() is modified to rstrip() space padded BASIC strings"""
-        if isinstance(value, str):
-            value = value.rstrip()
-        object.__setattr__(self, name, value)
-
     def unpack(self, data):
         """Unpacks binary data into class instance variables"""
         if len(data) is not self.PACK_LENGTH:

          
@@ 54,6 48,14 @@ class PlanetsData(object):
             else:
                 setattr(self, name, value)
 
+    def fix(self):
+        """Fix object values such padded spaces on BASIC strings"""
+        # Iterate through instance variables
+        for name, value in self.__dict__.items():
+            if isinstance(value, str):
+                object.__setattr__(self, name, value.decode('latin1')
+                                   .encode('utf8').rstrip())
+
     @classmethod
     def load(cls, data, count=None):
         """Unpacks binary data into a list of object instances"""

          
@@ 70,7 72,9 @@ class PlanetsData(object):
             instance.unpack(data[start:end])
             if instance.id is None:
                 instance.id = i
-            result[instance.id] = instance
+            # Some data may be zeroed out (id = 0), so only add when id > 0
+            if instance.id:
+                result[instance.id] = instance
         return result
     
     @classmethod

          
@@ 86,7 90,11 @@ class PlanetsData(object):
             data = f.read(cls.PACK_LENGTH * cls.COUNT)
         else:
             data = f.read()
-        return cls.load(data)
+        result = cls.load(data, count)
+        # Run fix() on returned objects
+        for key in result.keys():
+            result[key].fix()
+        return result
     
     @classmethod
     def open(cls, name, has_count=False, count=None):

          
M player.py +53 -8
@@ 2,6 2,9 @@ 
 Classes for reading player data such as found in RST files.
 """
 
+# Disable pylint warnings caused by fields not being explicity defined
+# pylint: disable=W0201
+
 import struct
 from common import PlanetsData
 

          
@@ 73,8 76,8 @@ class Planet(PlanetsData):
               'temperature',
               'build_base')
     
-    def unpack(self, data):
-        PlanetsData.unpack(self, data)
+    def fix(self):
+        PlanetsData.fix(self)
         # Fix temperature, which is inverted (value 0 = temp 100)
         self.temperature = 100 - self.temperature
     

          
@@ 128,11 131,51 @@ class Ship(PlanetsData):
               'transfer_id',
               'intercept_id',
               'credits')
-    
+
+class Target(PlanetsData):
+    """List of of visual enemy ships (contacts)"""
+    # No FILENAME, but typically: TARGETx.DAT (x=player)
+    # No COUNT, but obviously limited by number of ships
+    PACK_LENGTH = 34
+    PACK_FORMAT = '< h h h h h h h 20s'
+    FIELDS = ('id',
+              'owner',
+              'warp',
+              'x',
+              'y',
+              'hull',
+              'heading',
+              'name')
+
+    @classmethod
+    def read_result(cls, f, extra_offset=None):
+        """Reads visual enemy ship data from a RST file"""
+        (count,) = struct.unpack('< h', f.read(2))
+        data = f.read(cls.PACK_LENGTH * count)
+        result = cls.load(data, count)
+        # Load extra data if present in WinPlay-style RST file
+        if extra_offset:
+            f.seek(extra_offset)
+            (count,) = struct.unpack('< i', f.read(4))
+            data = f.read(cls.PACK_LENGTH * count)
+            extra = cls.load(data, count)
+            # Extra data has encrypted ship name
+            for k in extra.keys():
+                name = ''
+                for i in range(0, len(extra[k].name)):
+                    name += chr(ord(extra[k].name[i]) ^ (154 - i))
+                extra[k].name = name
+            # Merge results
+            result = dict(result.items() + extra.items())
+        # Run fix() on returned objects
+        for key in result.keys():
+            result[key].fix()
+        return result
+
 def read_messages(f):
-    """Reads and returns list of messages found in RST or MDATAx.DAT files"""
+    """Reads and returns list of messages found in RST and MDATAx.DAT files"""
     result = {}
-    # Message count should preceed messages (arguments are ignored)
+    # Message count should preceed messages
     (count,) = struct.unpack('< h', f.read(2))
     # Get pointer + size of each message (if count > 0)
     if count:

          
@@ 142,14 185,16 @@ def read_messages(f):
             (pointers[i],) = struct.unpack('< i', f.read(4))
             pointers[i] = int(pointers[i]) - 1 # Fix BASIC-style pointer
             (sizes[i],) = struct.unpack('< h', f.read(2))
-    for key, value in pointers.items():
-        f.seek(value)
+    # Read actual messages
+    for key, pointer in pointers.items():
+        f.seek(pointer)
         data = f.read(sizes[key])
+        # Message data is encoded: to decode subtract 13 from each character
         message = ''
         for i in range(0, len(data)):
             char = chr(ord(data[i]) - 13)
             if ord(char) == 13:
                 char = chr(10) # Unix-style newline
             message += char
-        result[key] = message
+        result[key] = message.decode('latin1').encode('utf8').rstrip()
     return result
  No newline at end of file

          
M result.py +6 -3
@@ 3,7 3,7 @@ Functions for reading and parsing RST (r
 """
 
 import struct
-from player import Base, Planet, Ship, read_messages
+from player import Base, Planet, Ship, Target, read_messages
 
 class InvalidResultFile(Exception):
     """Exception raised when the RST file is invalid or corrupt"""

          
@@ 12,10 12,15 @@ class InvalidResultFile(Exception):
 def read(f, filesize):
     """Reads from file and parses RST data"""
     result = {}
+    if not filesize:
+        raise InvalidResultFile('RST file size zero or not defined')
     pointers = _get_sections(f, filesize)
     # Read standard sections (should always exist, even if 0 items in section)
     f.seek(pointers['ships'])
     result['ships'] = Ship.read(f, has_count=True)
+    f.seek(pointers['targets'])
+    result['targets'] = Target.read_result(f,
+                            extra_offset=pointers['extratargets'])
     f.seek(pointers['planets'])
     result['planets'] = Planet.read(f, has_count=True)
     f.seek(pointers['bases'])

          
@@ 26,8 31,6 @@ def read(f, filesize):
     
 def _valid_pointer(pointer, filesize, datasize):
     """Checks if pointer seek location is valid (smaller than filesize)"""
-    if not filesize:
-        raise InvalidResultFile('RST file size zero or not defined')
     if pointer < 0:
         raise InvalidResultFile('Pointer should be positive integer')
     if pointer + datasize <= filesize: