# HG changeset patch # User Emmanuel Gil Peyrot # Date 1325266626 -3600 # Node ID 5afc75f71fed186d228ceee1b9439e81e10a458a # Parent 8f4cd1c01d226c317ec0bb6b27acdda12ab47577 Add “SHT” support to EoSD, and do a little cleanup. diff --git a/pytouhou/formats/exe.py b/pytouhou/formats/exe.py new file mode 100644 --- /dev/null +++ b/pytouhou/formats/exe.py @@ -0,0 +1,157 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Thibaut Girka +## Copyright (C) 2011 Emmanuel Gil Peyrot +## +## 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('= 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 diff --git a/pytouhou/game/item.py b/pytouhou/game/item.py --- 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) diff --git a/pytouhou/game/itemtype.py b/pytouhou/game/itemtype.py --- 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 diff --git a/pytouhou/games/eosd.py b/pytouhou/games/eosd.py --- 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)) diff --git a/pytouhou/resource/loader.py b/pytouhou/resource/loader.py --- 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) diff --git a/pytouhou/utils/pe.py b/pytouhou/utils/pe.py new file mode 100644 --- /dev/null +++ b/pytouhou/utils/pe.py @@ -0,0 +1,137 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Thibaut Girka +## +## 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('