changeset 229:5afc75f71fed

Add “SHT” support to EoSD, and do a little cleanup.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Fri, 30 Dec 2011 18:37:06 +0100
parents 8f4cd1c01d22
children 1c24a6d93c1b
files pytouhou/formats/exe.py pytouhou/formats/sht.py pytouhou/game/game.py pytouhou/game/item.py pytouhou/game/itemtype.py pytouhou/games/eosd.py pytouhou/resource/loader.py pytouhou/utils/pe.py
diffstat 8 files changed, 348 insertions(+), 221 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/pytouhou/formats/exe.py
@@ -0,0 +1,157 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
+## Copyright (C) 2011 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+
+from struct import Struct, unpack
+from pytouhou.utils.pe import PEFile
+
+from pytouhou.utils.helpers import get_logger
+
+logger = get_logger(__name__)
+
+
+SQ2 = 2. ** 0.5 / 2.
+
+
+class Shot(object):
+    def __init__(self):
+        self.interval = 0
+        self.delay = 0
+        self.pos = (0., 0.)
+        self.hitbox = (0., 0.)
+        self.angle = 0.
+        self.speed = 0.
+        self.damage = 0
+        self.orb = 0
+        self.shot_type = 0
+        self.sprite = 0
+        self.unknown1 = None
+
+    def __repr__(self):
+        return '(%d, %d, %f, %f, %d, %d, %d, %d, %d)' % (self.interval, self.delay, self.angle, self.speed, self.damage, self.orb, self.shot_type, self.sprite, self.unknown1)
+
+
+class SHT(object):
+    def __init__(self):
+        #self.unknown1 = None
+        #self.bombs = 0.
+        #self.unknown2 = None
+        self.hitbox = 4.
+        self.graze_hitbox = 42.
+        self.autocollection_speed = 8.
+        self.item_hitbox = 38.
+        # No percentage_of_cherry_loss_on_die
+        self.point_of_collection = 128 #TODO: find the real default.
+        self.horizontal_vertical_speed = 0.
+        self.horizontal_vertical_focused_speed = 0.
+        self.diagonal_speed = 0.
+        self.diagonal_focused_speed = 0.
+        self.shots = {}
+
+
+    @classmethod
+    def find_character_records(self, file, pe_file):
+        format = Struct('<4f2I')
+        data_section = [section for section in pe_file.sections if section.Name.startswith('.data')][0]
+        text_section = [section for section in pe_file.sections if section.Name.startswith('.text')][0]
+        data_va = pe_file.image_base + data_section.VirtualAddress
+        text_va = pe_file.image_base + text_section.VirtualAddress
+
+        for addr in xrange(data_va, data_va + data_section.SizeOfRawData, 4):
+            for character_id in xrange(4):
+                pe_file.seek_to_va(addr + character_id * 24)
+                speed1, speed2, speed3, speed4, ptr1, ptr2 = format.unpack(file.read(format.size))
+
+                if not (all(0. < x < 8. for x in (speed1, speed2, speed3, speed4))
+                        and speed2 <= speed1
+                        and 0 <= ptr1 - text_va < text_section.SizeOfRawData - 8
+                        and 0 <= ptr2 - text_va < text_section.SizeOfRawData - 8):
+                    break
+
+                pe_file.seek_to_va(ptr1 + 4)
+                shtptr1, = unpack('<I', file.read(4))
+                pe_file.seek_to_va(ptr2 + 4)
+                shtptr2, = unpack('<I', file.read(4))
+
+                if not (0 <= shtptr1 - data_va < data_section.SizeOfRawData
+                        and 0 <= shtptr2 - data_va < data_section.SizeOfRawData):
+                    break
+            else: # XXX: Obscure python feature! This gets executed if there were no break!
+                yield addr
+
+
+    @classmethod
+    def read(cls, file):
+        pe_file = PEFile(file)
+
+        character_records_va = list(cls.find_character_records(file, pe_file))[0]
+
+        characters = []
+        shots_offsets = []
+        for character in xrange(4):
+            sht = cls()
+
+            pe_file.seek_to_va(character_records_va + 6*4*character)
+
+            data = unpack('<4f2I', file.read(6*4))
+            (speed, speed_focused, speed_unknown1, speed_unknown2,
+             shots_func_offset, shots_func_offset_focused) = data
+
+            sht.horizontal_vertical_speed = speed
+            sht.horizontal_vertical_focused_speed = speed_focused
+            sht.diagonal_speed = speed * SQ2
+            sht.diagonal_focused_speed = speed_focused * SQ2
+
+            # Read from “push” operand
+            pe_file.seek_to_va(shots_func_offset + 4)
+            offset = unpack('<I', file.read(4))[0]
+            shots_offsets.append(offset)
+
+            characters.append(sht)
+
+        character = 0
+        for shots_offset in shots_offsets:
+            pe_file.seek_to_va(shots_offset)
+
+            level_count = 9
+            levels = []
+            for i in xrange(level_count):
+                shots_count, power, offset = unpack('<III', file.read(3*4))
+                levels.append((shots_count, power, offset))
+
+            sht = characters[character]
+            sht.shots = {}
+
+            for shots_count, power, offset in levels:
+                sht.shots[power] = []
+                pe_file.seek_to_va(offset)
+
+                for i in xrange(shots_count):
+                    shot = Shot()
+
+                    data = unpack('<HH6fHBBhh', file.read(36))
+                    (shot.interval, shot.delay, x, y, hitbox_x, hitbox_y,
+                     shot.angle, shot.speed, shot.damage, shot.orb, shot.shot_type,
+                     shot.sprite, shot.unknown1) = data
+
+                    shot.pos = (x, y)
+                    shot.hitbox = (hitbox_x, hitbox_y)
+
+                    sht.shots[power].append(shot)
+
+            character += 1
+
+
+        return characters
+
--- a/pytouhou/formats/sht.py
+++ b/pytouhou/formats/sht.py
@@ -22,20 +22,20 @@ logger = get_logger(__name__)
 class Shot(object):
     def __init__(self):
         self.interval = 0
-        self.unknown1 = None
+        self.delay = 0
         self.pos = (0., 0.)
         self.hitbox = (0., 0.)
         self.angle = 0.
         self.speed = 0.
         self.damage = 0
         self.orb = 0
+        self.shot_type = 0
+        self.sprite = 0
+        self.unknown1 = None
         self.unknown2 = None
-        self.sprite = 0
         self.unknown3 = None
         self.unknown4 = None
-        self.homing = False
         self.unknown5 = None
-        self.unknown6 = None
 
 
 class SHT(object):
@@ -79,20 +79,20 @@ class SHT(object):
             file.seek(offset)
 
             while True:
-                interval, unknown1 = unpack('<HH', file.read(4))
-                if interval == 0xffff and unknown1 == 0xffff:
+                interval, delay = unpack('<HH', file.read(4))
+                if interval == 0xffff and delay == 0xffff:
                     break
 
                 shot = Shot()
 
                 shot.interval = interval
-                shot.unknown1 = unknown1
+                shot.delay = delay
 
                 data = unpack('<6fHBBhh4I', file.read(48))
                 (x, y, hitbox_x, hitbox_y, shot.angle, shot.speed,
-                 shot.damage, shot.orb, shot.unknown2, shot.sprite,
-                 shot.unknown3, shot.unknown4, shot.homing, shot.unknown5,
-                 shot.unknown6) = data
+                 shot.damage, shot.orb, shot.shot_type, shot.sprite,
+                 shot.unknown1, shot.unknown2, shot.unknown3, shot.unknown4,
+                 shot.unknown5) = data
 
                 shot.pos = (x, y)
                 shot.hitbox = (hitbox_x, hitbox_y)
--- a/pytouhou/game/game.py
+++ b/pytouhou/game/game.py
@@ -243,8 +243,8 @@ class Game(object):
             if py < 128 and player.state.power >= 128: #TODO: check py.
                 self.autocollect(player)
 
+            half_size = player.sht.item_hitbox / 2.
             for item in self.items:
-                half_size = item.hitbox_half_size
                 bx, by = item.x, item.y
                 bx1, bx2 = bx - half_size, bx + half_size
                 by1, by2 = by - half_size, by + half_size
--- a/pytouhou/game/item.py
+++ b/pytouhou/game/item.py
@@ -26,8 +26,6 @@ class Item(object):
         self._type = _type
         self._item_type = item_type
 
-        self.hitbox_half_size = item_type.hitbox_size / 2.
-
         self.frame = 0
         self.x, self.y = start_pos
         self.angle = angle
@@ -52,7 +50,7 @@ class Item(object):
 
     def autocollect(self, player):
         self.player = player
-        self.speed = player.sht.autocollection_speed if hasattr(player, 'sht') else 8.
+        self.speed = player.sht.autocollection_speed
 
 
     def on_collect(self, player):
@@ -84,7 +82,7 @@ class Item(object):
 
         elif self._type == 1: # point
             player_state.points += 1
-            poc = player.sht.point_of_collection if hasattr(player, 'sht') else 128 #TODO: find the exact poc in EoSD.
+            poc = player.sht.point_of_collection
             if player_state.y < poc:
                 score = 100000
                 self._game.modify_difficulty(+30)
--- a/pytouhou/game/itemtype.py
+++ b/pytouhou/game/itemtype.py
@@ -1,10 +1,9 @@
 from pytouhou.game.sprite import Sprite
 
 class ItemType(object):
-    def __init__(self, anm_wrapper, sprite_index, indicator_sprite_index, hitbox_size=38.):
+    def __init__(self, anm_wrapper, sprite_index, indicator_sprite_index):
         self.anm_wrapper = anm_wrapper
         self.sprite = Sprite()
         self.sprite.anm, self.sprite.texcoords = anm_wrapper.get_sprite(sprite_index)
         self.indicator_sprite = Sprite()
         self.indicator_sprite.anm, self.indicator_sprite.texcoords = anm_wrapper.get_sprite(indicator_sprite_index)
-        self.hitbox_size = hitbox_size
--- a/pytouhou/games/eosd.py
+++ b/pytouhou/games/eosd.py
@@ -50,10 +50,14 @@ class EoSDGame(Game):
                       ItemType(etama3, 5, 12), #1up
                       ItemType(etama3, 6, 13)] #Star
 
-        eosd_characters = [ReimuA, ReimuB, MarisaA, MarisaB]
+        characters = resource_loader.get_eosd_characters('102h.exe')
+        print characters[0]
+
+        #eosd_characters = [ReimuA, ReimuB, MarisaA, MarisaB]
         players = []
         for player in player_states:
-            players.append(eosd_characters[player.character](player, self, resource_loader))
+            #players.append(eosd_characters[player.character](player, self, resource_loader))
+            players.append(EoSDPlayer(player, self, resource_loader, sht=characters[player.character]))
 
         Game.__init__(self, resource_loader, players, stage, rank, difficulty,
                       bullet_types, item_types, nb_bullets_max=640, **kwargs)
@@ -61,11 +65,19 @@ class EoSDGame(Game):
 
 
 class EoSDPlayer(Player):
-    def __init__(self, state, game, anm_wrapper, speeds=None, hitbox_size=2.5, graze_hitbox_size=42.):
-        Player.__init__(self, state, game, anm_wrapper, speeds=speeds)
+    def __init__(self, state, game, resource_loader, speeds=None, hitbox_size=2.5, graze_hitbox_size=42., sht=None):
+        self.sht = sht
+        anm_wrapper = resource_loader.get_anm_wrapper(('player0%d.anm' % (state.character // 2),))
+        self.anm_wrapper = anm_wrapper
 
-        self.orbs = [Orb(self.anm_wrapper, 128, self.state, self.orb_fire),
-                     Orb(self.anm_wrapper, 129, self.state, self.orb_fire)]
+        Player.__init__(self, state, game, anm_wrapper,
+                        speeds=(self.sht.horizontal_vertical_speed,
+                                self.sht.diagonal_speed,
+                                self.sht.horizontal_vertical_focused_speed,
+                                self.sht.diagonal_focused_speed))
+
+        self.orbs = [Orb(self.anm_wrapper, 128, self.state, None),
+                     Orb(self.anm_wrapper, 129, self.state, None)]
 
         self.orbs[0].offset_x = -24
         self.orbs[1].offset_x = 24
@@ -115,210 +127,27 @@ class EoSDPlayer(Player):
             orb.update()
 
 
-    def orb_fire(self, orb):
-        pass
-
-
-
-class Reimu(EoSDPlayer):
-    def __init__(self, state, game, resource_loader):
-        anm_wrapper = resource_loader.get_anm_wrapper(('player00.anm',))
-        self.bullet_angle = pi/30 #TODO: check
-
-        EoSDPlayer.__init__(self, state, game, anm_wrapper, speeds=(4., 4. * SQ2, 2., 2. * SQ2))
-
-
     def fire(self):
-        if self.fire_time % self.bullet_launch_interval == 0:
-            if self.state.power < 16:
-                bullets_per_shot = 1
-            elif self.state.power < 48:
-                bullets_per_shot = 2
-            elif self.state.power < 96:
-                bullets_per_shot = 3
-            elif self.state.power < 128:
-                bullets_per_shot = 4
-            else:
-                bullets_per_shot = 5
-
-            bullets = self._game.players_bullets
-            nb_bullets_max = self._game.nb_bullets_max
-
-            bullet_angle = self.bullet_launch_angle - self.bullet_angle * (bullets_per_shot - 1) / 2.
-            for bullet_nb in range(bullets_per_shot):
-                if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
-                    break
+        sht = self.sht
+        power = min(power for power in sht.shots if self.state.power < power)
 
-                bullets.append(Bullet((self.x, self.y), self.bullet_type, 0,
-                                      bullet_angle, self.bullet_speed,
-                                      (0, 0, 0, 0, 0., 0., 0., 0.),
-                                      0, self, self._game, damage=48, player_bullet=True))
-                bullet_angle += self.bullet_angle
-
-        for orb in self.orbs:
-            orb.fire(orb)
-
-
-
-class ReimuA(Reimu):
-    def __init__(self, state, game, resource_loader):
-        Reimu.__init__(self, state, game, resource_loader)
-
-        self.bulletA_type = BulletType(self.anm_wrapper, 65, 97, 0, 0, 0, hitbox_size=4) #TODO: verify the hitbox, damage is 14.
-        self.bulletA_speed = 12.
-
-
-    def fire(self):
-        Reimu.fire(self)
-
-        if self.state.power < 8:
-            return
-
-        else:
-            pass #TODO
-
-
-
-class ReimuB(Reimu):
-    def __init__(self, state, game, resource_loader):
-        Reimu.__init__(self, state, game, resource_loader)
-
-        self.bulletB_type = BulletType(self.anm_wrapper, 66, 98, 0, 0, 0, hitbox_size=4) #TODO: verify the hitbox.
-        self.bulletB_speed = 22.
-
-
-    def fire_spine(self, orb, offset_x):
         bullets = self._game.players_bullets
         nb_bullets_max = self._game.nb_bullets_max
 
-        if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
-            return
-
-        bullets.append(Bullet((orb.x + offset_x, orb.y), self.bulletB_type, 0,
-                              self.bullet_launch_angle, self.bulletB_speed,
-                              (0, 0, 0, 0, 0., 0., 0., 0.),
-                              0, self, self._game, damage=12, player_bullet=True))
-
-    def orb_fire(self, orb):
-        if self.state.power < 8:
-            return
-
-        elif self.state.power < 16:
-            if self.fire_time % 15 == 0:
-                self.fire_spine(orb, 0)
-
-        elif self.state.power < 32:
-            if self.fire_time % 10 == 0:
-                self.fire_spine(orb, 0)
-
-        elif self.state.power < 48:
-            if self.fire_time % 8 == 0:
-                self.fire_spine(orb, 0)
-
-        elif self.state.power < 96:
-            if self.fire_time % 8 == 0:
-                self.fire_spine(orb, -8)
-            if self.fire_time % 5 == 0:
-                self.fire_spine(orb, 8)
-
-        elif self.state.power < 128:
-            if self.fire_time % 5 == 0:
-                self.fire_spine(orb, -12)
-            if self.fire_time % 10 == 0:
-                self.fire_spine(orb, 0)
-            if self.fire_time % 3 == 0:
-                self.fire_spine(orb, 12)
-
-        else:
-            if self.fire_time % 3 == 0:
-                self.fire_spine(orb, -12)
-                self.fire_spine(orb, 12)
-            if self.fire_time % 5 == 0:
-                self.fire_spine(orb, 0)
-
-
-
-class Marisa(EoSDPlayer):
-    def __init__(self, state, game, resource_loader):
-        anm_wrapper = resource_loader.get_anm_wrapper(('player01.anm',))
-        self.bullet_angle = pi/40 #TODO: check
-
-        EoSDPlayer.__init__(self, state, game, anm_wrapper, speeds=(5., 5. * SQ2, 2.5, 2.5 * SQ2))
-
-
-    def fire(self):
-        if self.fire_time % self.bullet_launch_interval == 0:
-            if self.state.power < 32:
-                bullets_per_shot = 1
-            elif self.state.power < 96:
-                bullets_per_shot = 2
-            elif self.state.power < 128:
-                bullets_per_shot = 3
-            else:
-                bullets_per_shot = 5
-
-            bullets = self._game.players_bullets
-            nb_bullets_max = self._game.nb_bullets_max
-
-            bullet_angle = self.bullet_launch_angle - self.bullet_angle * (bullets_per_shot - 1) / 2.
-            for bullet_nb in range(bullets_per_shot):
+        for shot in sht.shots[power]:
+            if self.fire_time % shot.interval == shot.delay:
                 if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
                     break
 
-                bullets.append(Bullet((self.x, self.y), self.bullet_type, 0,
-                                      bullet_angle, self.bullet_speed,
-                                      (0, 0, 0, 0, 0., 0., 0., 0.),
-                                      0, self, self._game, damage=48, player_bullet=True))
-                bullet_angle += self.bullet_angle
-
-
-
-class MarisaA(Marisa):
-    def __init__(self, state, game, resource_loader):
-        Marisa.__init__(self, state, game, resource_loader)
-
-        #TODO: verify the hitbox and damages.
-        self.bulletA_types = [BulletType(self.anm_wrapper, 65, 0, 0, 0, 0, hitbox_size=4), # damage is 40.
-                              BulletType(self.anm_wrapper, 66, 0, 0, 0, 0, hitbox_size=4),
-                              BulletType(self.anm_wrapper, 67, 0, 0, 0, 0, hitbox_size=4),
-                              BulletType(self.anm_wrapper, 68, 0, 0, 0, 0, hitbox_size=4)]
-        self.bulletA_speed_interpolator = None
-
-
-    def fire(self):
-        Marisa.fire(self)
-
-        if self.state.power < 8:
-            return
-
-        else:
-            pass #TODO
+                origin = self.orbs[shot.orb - 1] if shot.orb else self
+                x = origin.x + shot.pos[0]
+                y = origin.y + shot.pos[1]
 
-
-
-class MarisaB(Marisa):
-    def __init__(self, state, game, resource_loader):
-        Marisa.__init__(self, state, game, resource_loader)
-
-        #TODO:  power   damages period
-        #       8       240     120
-        #       16      390     170
-        #       32      480     ???
-        #       48      510     ???
-        #       64      760     ???
-        #       80      840     ???
-        #       96      1150    270
-        #       128     1740    330
-        # The duration of the laser is period - 42.
-        # The damages are given for one laser shot on one enemy for its entire duration.
-
-
-    def fire(self):
-        Marisa.fire(self)
-
-        if self.state.power < 8:
-            return
-
-        else:
-            pass #TODO
-
+                #TODO: find a better way to do that.
+                bullet_type = BulletType(self.anm_wrapper, shot.sprite % 256,
+                                                         shot.sprite % 256 + 32, #TODO: find the real cancel anim
+                                                         0, 0, 0, 0.)
+                bullets.append(Bullet((x, y), bullet_type, 0,
+                                      shot.angle, shot.speed,
+                                      (0, 0, 0, 0, 0., 0., 0., 0.),
+                                      0, self, self._game, player_bullet=True, damage=shot.damage, hitbox=shot.hitbox))
--- a/pytouhou/resource/loader.py
+++ b/pytouhou/resource/loader.py
@@ -6,6 +6,7 @@ from pytouhou.formats.ecl import ECL
 from pytouhou.formats.anm0 import Animations
 from pytouhou.formats.msg import MSG
 from pytouhou.formats.sht import SHT
+from pytouhou.formats.exe import SHT as EoSDSHT
 
 
 from pytouhou.resource.anmwrapper import AnmWrapper
@@ -102,6 +103,12 @@ class Loader(object):
         return self.instanced_shts[name]
 
 
+    def get_eosd_characters(self, name):
+        with open(name, 'rb') as file:
+            characters = EoSDSHT.read(file) #TODO: modular
+        return characters
+
+
     def get_anm_wrapper(self, names):
         return AnmWrapper(self.get_anm(name) for name in names)
 
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/pe.py
@@ -0,0 +1,137 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+
+from struct import Struct, unpack
+from collections import namedtuple
+
+
+class PEStructs:
+    _IMAGE_FILE_HEADER = namedtuple('_IMAGE_FILE_HEADER',
+                                    ('Machine',
+                                     'NumberOfSections',
+                                     'TimeDateStamp',
+                                     'PointerToSymbolTable',
+                                     'NumberOfSymbols',
+                                     'SizeOfOptionalHeader',
+                                     'Characteristics'))
+    @classmethod
+    def read_image_file_header(cls, file):
+        format = Struct('<HHIIIHH')
+        return cls._IMAGE_FILE_HEADER(*format.unpack(file.read(format.size)))
+
+    _IMAGE_OPTIONAL_HEADER = namedtuple('_IMAGE_OPTIONAL_HEADER',
+                                        ('Magic',
+                                         'MajorLinkerVersion', 'MinorLinkerVersion',
+                                         'SizeOfCode', 'SizeOfInitializedData',
+                                         'SizeOfUninitializedData',
+                                         'AddressOfEntryPoint', 'BaseOfCode',
+                                         'BaseOfData', 'ImageBase',
+                                         'SectionAlignement', 'FileAlignement',
+                                         'MajorOperatingSystemVersion',
+                                         'MinorOperatingSystemVersion',
+                                         'MajorImageVersion',
+                                         'MinorImageVersion',
+                                         'MajorSubsystemVersion',
+                                         'MinorSubsystemVersion',
+                                         'Win32VersionValue',
+                                         'SizeOfImage',
+                                         'SizeOfHeaders',
+                                         'CheckSum',
+                                         'Subsystem',
+                                         'DllCharacteristics',
+                                         'SizeOfStackReserve',
+                                         'SizeOfStackCommit',
+                                         'SizeOfHeapReserve',
+                                         'SizeOfHeapCommit',
+                                         'LoaderFlags',
+                                         'NumberOfRvaAndSizes',
+                                         'DataDirectory'))
+    _IMAGE_DATA_DIRECTORY = namedtuple('_IMAGE_DATA_DIRECTORY',
+                                       ('VirtualAddress', 'Size'))
+    @classmethod
+    def read_image_optional_header(cls, file):
+        format = Struct('<HBBIIIIIIIIIHHHHHHIIIIHHIIIIII')
+        directory_format = Struct('<II')
+        directory = []
+        partial_header = format.unpack(file.read(format.size))
+        directory = [cls._IMAGE_DATA_DIRECTORY(*directory_format.unpack(file.read(directory_format.size))) for i in xrange(16)]
+        return cls._IMAGE_OPTIONAL_HEADER(*(partial_header + (directory,)))
+
+    _IMAGE_SECTION_HEADER = namedtuple('_IMAGE_SECTION_HEADER',
+                                       ('Name', 'VirtualSize',
+                                        'VirtualAddress',
+                                        'SizeOfRawData', 'PointerToRawData',
+                                        'PointerToRelocations',
+                                        'PointerToLinenumbers',
+                                        'NumberOfRelocations',
+                                        'NumberOfLinenumbers',
+                                        'Characteristics'))
+    @classmethod
+    def read_image_section_header(cls, file):
+        format = Struct('<8sIIIIIIHHI')
+        return cls._IMAGE_SECTION_HEADER(*format.unpack(file.read(format.size)))
+
+
+
+class PEFile(object):
+    def __init__(self, file):
+        self.file = file
+
+        self.image_base = 0
+        self.sections = []
+
+        file.seek(0x3c)
+        pe_offset, = unpack('<I', file.read(4))
+
+        file.seek(pe_offset)
+        pe_sig = file.read(4)
+        assert pe_sig == b'PE\0\0'
+
+        pe_file_header = PEStructs.read_image_file_header(file)
+        pe_optional_header = PEStructs.read_image_optional_header(file)
+
+        # Read image base
+        self.image_base = pe_optional_header.ImageBase
+
+        self.sections = [PEStructs.read_image_section_header(file)
+                            for i in xrange(pe_file_header.NumberOfSections)]
+
+
+    def seek_to_va(self, va):
+        self.file.seek(self.va_to_offset(va))
+
+
+    def offset_to_rva(self, offset):
+        for section in self.sections:
+            if 0 <= (offset - section.PointerToRawData) < section.SizeOfRawData:
+                #TODO: is that okay?
+                return offset - section.PointerToRawData + section.VirtualAddress
+        raise IndexError #TODO
+
+
+    def offset_to_va(self, offset):
+        return self.offset_to_rva(offset) + self.image_base
+
+
+    def rva_to_offset(self, rva):
+        for section in self.sections:
+            if 0 <= (rva - section.VirtualAddress) < section.SizeOfRawData:
+                #TODO: is that okay?
+                return rva - section.VirtualAddress + section.PointerToRawData
+        raise IndexError #TODO
+
+
+    def va_to_offset(self, va):
+        return self.rva_to_offset(va - self.image_base)
+