Start with ResultFile class; added Ship player data class
2 files changed, 140 insertions(+), 1 deletions(-)

M player.py
A => result.py
M player.py +52 -1
@@ 33,4 33,55 @@ class Base(PlanetsData):
               'build_beamcount',
               'build_torptype',
               'build_torpcount',
-              'unused')
  No newline at end of file
+              'unused')
+    
+class Ship(PlanetsData):
+    """List of player's ships"""
+    # No FILENAME, but typically: SHIPx.DAT or SHIPx.DIS (x=player)
+    # No COUNT, but normally maximum 500 and absolute max of 999
+    PACK_LENGTH = 107
+    PACK_FORMAT = '<hh3shhhhhhhhhhhhhhhhhhh20shhhhhhhhhhhhhhhhhhhhh'
+    FIELDS = ('id',
+              'owner',
+              'friendlycode',
+              'warp',
+              'delta_x',
+              'delta_y',
+              'x',
+              'y',
+              'engine',
+              'hull',
+              'beamtype',
+              'beamcount',
+              'fighterbays',
+              'torptype',
+              'ordnance',
+              'torpcount',
+              'mission',
+              'primary_enemy',
+              'tow_id',
+              'damage',
+              'crew',
+              'cargo_clans',
+              'name',
+              'cargo_fuel',
+              'cargo_tritanium',
+              'cargo_duranium',
+              'cargo_molybdenum',
+              'cargo_supplies',
+              'unload_fuel',
+              'unload_tritanium',
+              'unload_duranium',
+              'unload_molybdenum',
+              'unload_clans',
+              'unload_supplies',
+              'unload_id',
+              'transfer_fuel',
+              'transfer_tritanium',
+              'transfer_duranium',
+              'transfer_molybdenum',
+              'transfer_clans',
+              'transfer_supplies',
+              'transfer_id',
+              'intercept_id',
+              'money')
  No newline at end of file

          
A => result.py +88 -0
@@ 0,0 1,88 @@ 
+"""
+Class for parsing RST (result) files.
+"""
+
+import struct
+from player import Base, Ship
+
+class InvalidResultFile(Exception):
+    """Exception raised when the RST file is invalid or corrupt"""
+    pass
+
+class ResultFile(object):
+    """Class for parsing RST (result) files"""
+    SIGNATURE = 'VER3.5'
+    VERSIONS = ('00', '01',)
+    TARGETSIG = ('1211', '1120',)
+    size = None
+    pointers = {}
+    data = {}
+    
+    @classmethod
+    def setsize(cls, size):
+        """Sets size of RST file, which is important for checking pointers"""
+        cls.size = size
+    
+    @classmethod
+    def validpointer(cls, pointer, datasize):
+        """Checks if pointer seek location is valid (smaller than filesize)"""
+        if cls.size is None:
+            raise ValueError('RST file size not defined')
+        if pointer < 0:
+            raise ValueError('Pointer should be positive integer')
+        if pointer + datasize <= cls.size:
+            return True
+        return False
+    
+    @classmethod
+    def read(cls, f):
+        """Reads from file and parses RST data"""
+        # Reset position
+        f.seek(0)
+        cls.getpointers(f)
+        # Read standard data
+        f.seek(cls.pointers['ships'])
+        cls.data['ships'] = Ship.read(f, has_count=True)
+        f.seek(cls.pointers['bases'])
+        cls.data['bases'] = Base.read(f, has_count=True)
+        # Return gathered data
+        return cls.data
+    
+    @classmethod
+    def getpointers(cls, f):
+        """Get pointers for various sections of the RST file"""
+        # Get pointers to standard data
+        (cls.pointers['ships'],) = struct.unpack('< i', f.read(4))
+        (cls.pointers['targets'],) = struct.unpack('< i', f.read(4))
+        (cls.pointers['planets'],) = struct.unpack('< i', f.read(4))
+        (cls.pointers['bases'],) = struct.unpack('< i', f.read(4))
+        (cls.pointers['messages'],) = struct.unpack('< i', f.read(4))
+        (cls.pointers['shipxy'],) = struct.unpack('< i', f.read(4))
+        (cls.pointers['general'],) = struct.unpack('< i', f.read(4))
+        (cls.pointers['vcr'],) = struct.unpack('< i', f.read(4))
+        # Subtract 1 to fix BASIC style pointer
+        for key, pointer in cls.pointers.items():
+            cls.pointers[key] = int(pointer) - 1
+        # Check if pointers are valid
+        for pointer in cls.pointers.values():
+            if not cls.validpointer(pointer, 2):
+                raise InvalidResultFile('One or more pointers is invalid')
+        
+    @classmethod
+    def getversion(cls, f):
+        """Analyses RST file to determine if it is DOS-style or WinPlan-style"""
+        # Check first signature (SIGNATURE) at location +32 of RST file
+        f.seek(32)
+        (signature, version, pointer) = struct.unpack('< 6s 2s i', f.read(12))
+        if (signature != cls.SIGNATURE) or (version not in cls.VERSIONS):
+            return False
+        cls.pointers['winplan'] = int(pointer) - 1
+        # Check if location of WinPlan data exists in file
+        if not cls.validpointer(cls.pointers['winplan'], 13281+4):
+            return False
+        # Check second signature (TARGETSIG) at location +13282 of WinPlan data
+        f.seek(cls.pointers['winplan'] + 13282)
+        (targetsig,) = struct.unpack('< 4s', f.read(4))
+        if targetsig not in cls.TARGETSIG:
+            return False
+        return True # True = WinPlan-style; False = otherwise
  No newline at end of file