Mercurial > touhou
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) +