Mercurial > touhou
changeset 204:88361534c77e
Add some documentation (argh, so much left to document!)
author | Thibaut Girka <thib@sitedethib.com> |
---|---|
date | Tue, 01 Nov 2011 13:46:03 +0100 |
parents | df8b2ab54639 |
children | ee6dfd14a785 |
files | README pytouhou/__init__.py pytouhou/formats/__init__.py pytouhou/formats/anm0.py pytouhou/formats/ecl.py pytouhou/formats/pbg3.py pytouhou/formats/std.py pytouhou/formats/t6rp.py |
diffstat | 8 files changed, 199 insertions(+), 21 deletions(-) [+] |
line wrap: on
line diff
--- a/README +++ b/README @@ -28,3 +28,10 @@ Documentation: The code should be sufficiently documented for anyone interested to learn how the EoSD engine work, but additional documentation is available at: http://linkmauve.fr/doc/touhou/ + + + +Contact: +-------- + +You are welcome to join us at <xmpp:touhou@muc.linkmauve.fr> on jabber!
--- a/pytouhou/__init__.py +++ b/pytouhou/__init__.py @@ -0,0 +1,13 @@ +"""Reimplementation of the Touhou engines. + +This package provides various classes to handle parts or the whole of Touhou +file formats and gameplay. + +Sub-packages: +formats -- file formats handling +game -- game logic +games -- game-specific classes +resource -- resource loading +utils -- various helpers and non Touhou-specific functions +vm -- virtual machines for enemies, etc. +"""
--- a/pytouhou/formats/__init__.py +++ b/pytouhou/formats/__init__.py @@ -0,0 +1,5 @@ +"""Touhou games file formats handling. + +This package provides modules to handle the various proprietary file formats +used by Touhou games. +"""
--- a/pytouhou/formats/anm0.py +++ b/pytouhou/formats/anm0.py @@ -12,6 +12,14 @@ ## GNU General Public License for more details. ## +"""ANM0 files handling. + +This module provides classes for handling the ANM0 file format. +The ANM0 format is a format used in Touhou 6: EoSD to describe sprites +and animations. +Almost everything rendered in the game is described by an ANM0 file. +""" + from struct import pack, unpack from pytouhou.utils.helpers import read_string, get_logger
--- a/pytouhou/formats/ecl.py +++ b/pytouhou/formats/ecl.py @@ -12,6 +12,13 @@ ## GNU General Public License for more details. ## +"""ECL files handling. + +This module provides classes for handling the ECL file format. +The ECL format is a format used in Touhou 6: EoSD to script most of the gameplay +aspects of the game, such as enemy movements, attacks, and so on. +""" + import struct from struct import pack, unpack, calcsize @@ -20,6 +27,20 @@ from pytouhou.utils.helpers import get_l logger = get_logger(__name__) class ECL(object): + """Handle Touhou 6 ECL files. + + ECL files are binary script files used to describe the behavior of enemies. + They are basically composed of a header and two additional sections. + The first section is a list of subroutines, each composed of a list of timed + instructions. + The second section is a list of a different set of instructions describing + enemy waves, triggering dialogs and level completion. + + Instance variables: + main -- list of instructions describing waves and triggering dialogs + subs -- list of subroutines + """ + _instructions = {0: ('', 'noop?'), 1: ('I', 'delete?'), 2: ('Ii', 'relative_jump'), @@ -40,7 +61,7 @@ class ECL(object): 21: ('iff', 'substract_float'), 23: ('iff', 'divide_float'), 25: ('iffff', 'get_direction'), - 26: ('i', None), + 26: ('i', 'float_to_unit_circle'), #TODO: find a better name 27: ('ii', 'compare_ints'), 28: ('ff', 'compare_floats'), 29: ('ii', 'relative_jump_if_lower_than'), @@ -50,7 +71,7 @@ class ECL(object): 33: ('ii', 'relative_jump_if_greater_or_equal'), 34: ('ii', 'relative_jump_if_not_equal'), 35: ('iif', 'call'), - 36: ('', 'return?'), + 36: ('', 'return'), 39: ('iifii', 'call_if_equal'), 43: ('fff', 'set_position'), 45: ('ff', 'set_angle_and_speed'), @@ -81,18 +102,18 @@ class ECL(object): 79: ('', 'no_delay_attack'), 81: ('fff', 'set_bullet_launch_offset'), 82: ('iiiiffff', 'set_extended_bullet_attributes'), - 83: ('', None), + 83: ('', 'change_bullets_in_star_bonus'), 84: ('i', None), 85: ('hhffffffiiiiii', 'laser'), 86: ('hhffffffiiiiii', 'laser2'), 87: ('i', 'set_upcoming_id'), 88: ('if','alter_laser_angle'), - 90: ('iiii', None), - 92: ('i', None), + 90: ('iiii', 'translate_laser'), + 92: ('i', 'cancel_laser'), 93: ('hhs', 'set_spellcard'), 94: ('', 'end_spellcard'), - 95: ('ifffhhi', None), - 96: ('', None), + 95: ('ifffhhi', 'spawn_enemy'), + 96: ('', 'kill_all_enemies'), 97: ('i', 'set_anim'), 98: ('hhhhhxx', 'set_multiple_anims'), 99: ('ii', None), @@ -123,14 +144,14 @@ class ECL(object): 125: ('', None), 126: ('i', 'set_remaining_lives'), 127: ('i', None), - 128: ('i', None), + 128: ('i', 'set_smooth_disappear'), 129: ('ii', None), 130: ('i', None), - 131: ('ffiiii', None), - 132: ('i', None), + 131: ('ffiiii', 'set_difficulty_coeffs'), + 132: ('i', 'set_invisible'), 133: ('', None), 134: ('', None), - 135: ('i', None)} #TODO + 135: ('i', 'enable_spellcard_bonus')} #TODO _main_instructions = {0: ('fffhhI', 'spawn_enemy'), 2: ('fffhhI', 'spawn_enemy_mirrored'), @@ -149,6 +170,12 @@ class ECL(object): @classmethod def read(cls, file): + """Read an ECL file. + + Raise an exception if the file is invalid. + Return a ECL instance otherwise. + """ + sub_count, main_offset = unpack('<II', file.read(8)) if file.read(8) != b'\x00\x00\x00\x00\x00\x00\x00\x00': raise Exception #TODO @@ -171,6 +198,7 @@ class ECL(object): time, opcode = unpack('<IH', file.read(6)) if time == 0xffffffff or opcode == 0xffff: break + size, rank_mask, param_mask = unpack('<HHH', file.read(6)) data = file.read(size - 12) if opcode in cls._instructions: @@ -188,7 +216,10 @@ class ECL(object): ecl.subs[-1].append((time, opcode, rank_mask, param_mask, args)) - # Translate offsets to instruction pointers + # Translate offsets to instruction pointers. + # Indeed, jump instructions are relative and byte-based. + # Since our representation doesn't conserve offsets, we have to + # keep trace of where the jump is supposed to end up. for instr_offset, (i, instr) in zip(instruction_offsets, enumerate(ecl.subs[-1])): time, opcode, rank_mask, param_mask, args = instr if opcode in (2, 29, 30, 31, 32, 33, 34): # relative_jump @@ -222,6 +253,8 @@ class ECL(object): def write(self, file): + """Write to an ECL file.""" + sub_count = len(self.subs) sub_offsets = [] main_offset = 0
--- a/pytouhou/formats/pbg3.py +++ b/pytouhou/formats/pbg3.py @@ -12,6 +12,17 @@ ## GNU General Public License for more details. ## +"""PBG3 archive files handling. + +This module provides classes for handling the PBG3 file format. +The PBG3 format is the archive format used by Touhou: EoSD. + +PBG3 files are merely a bitstream composed of a header, +a file table, and LZSS-compressed files. +""" + +from collections import namedtuple + from pytouhou.utils.bitstream import BitStream import pytouhou.utils.lzss as lzss @@ -21,12 +32,26 @@ logger = get_logger(__name__) class PBG3BitStream(BitStream): + """Helper class to handle strings and integers in PBG3 bitstreams.""" + def read_int(self): + """Read an integer from the bitstream. + + Integers have variable sizes. They begin with a two-bit value indicating + the number of (non-aligned) bytes to read. + """ + size = self.read(2) return self.read((size + 1) * 8) def read_string(self, maxsize): + """Read a string from the bitstream. + + Strings are stored as standard NULL-termianted sequences of bytes. + The only catch is that they are not byte-aligned. + """ + string = [] for i in range(maxsize): byte = self.read(8) @@ -37,9 +62,24 @@ class PBG3BitStream(BitStream): +PBG3Entry = namedtuple('PBG3Entry', 'unknown1 unknown2 checksum offset size') + + + class PBG3(object): - def __init__(self, entries, bitstream=None): - self.entries = entries + """Handle PBG3 archive files. + + PBG3 is a file archive format used in Touhou 6: EoSD. + This class provides a representation of such files, as well as functions to + read and extract files from a PBG3 archive. + + Instance variables: + entries -- list of PBG3Entry objects describing files present in the archive + bitstream -- PBG3BitStream object + """ + + def __init__(self, entries=None, bitstream=None): + self.entries = entries or [] self.bitstream = bitstream #TODO @@ -53,6 +93,12 @@ class PBG3(object): @classmethod def read(cls, file): + """Read a PBG3 file. + + Raise an exception if the file is invalid. + Return a PBG3 instance otherwise. + """ + magic = file.read(4) if magic != b'PBG3': raise Exception #TODO @@ -70,21 +116,31 @@ class PBG3(object): offset = bitstream.read_int() size = bitstream.read_int() name = bitstream.read_string(255).decode('ascii') - entries[name] = (unknown1, unknown2, checksum, offset, size) + entries[name] = PBG3Entry(unknown1, unknown2, checksum, offset, size) return PBG3(entries, bitstream) def list_files(self): + """List files present in the archive.""" return self.entries.keys() def extract(self, filename, check=False): + """Extract a given file. + + If “filename” is in the archive, extract it and return its contents. + Otherwise, raise an exception. + + By default, the checksum of the file won't be verified, + you can however force the verification using the “check” argument. + """ + unkwn1, unkwn2, checksum, offset, size = self.entries[filename] self.bitstream.seek(offset) data = lzss.decompress(self.bitstream, size) if check: - # Checking the checksum + # Verify the checksum compressed_size = self.bitstream.io.tell() - offset self.bitstream.seek(offset) value = 0
--- a/pytouhou/formats/std.py +++ b/pytouhou/formats/std.py @@ -12,6 +12,14 @@ ## GNU General Public License for more details. ## +"""Stage Definition (STD) files handling. + +This module provides classes for handling the Stage Definition file format. +The STD file format is a format used in Touhou 6: EoSD to describe non-gameplay +aspects of a stage: its name, its music, 3D models composing its background, +and various scripted events such as camera movement. +""" + from struct import pack, unpack, calcsize from pytouhou.utils.helpers import read_string, get_logger @@ -29,6 +37,25 @@ class Model(object): class Stage(object): + """Handle Touhou 6 Stage Definition files. + + Stage Definition files are structured files describing non-gameplay aspects + aspects of a stage. They are split in a header an 3 additional sections. + + The header contains the name of the stage, the background musics (BGM) used, + as well as the number of quads and objects composing the background. + The first section describes the models composing the background, whereas + the second section dictates how they are used. + The last section describes the changes to the camera, fog, and other things. + + Instance variables: + name -- name of the stage + bgms -- list of (name, path) describing the different background musics used + models -- list of Model objects + object_instances -- list of instances of the aforementioned models + script -- stage script (camera, fog, etc.) + """ + _instructions = {0: ('fff', 'set_viewpos'), 1: ('BBBxff', 'set_fog'), 2: ('fff', 'set_viewpos2'), @@ -45,6 +72,12 @@ class Stage(object): @classmethod def read(cls, file): + """Read a Stage Definition file. + + Raise an exception if the file is invalid. + Return a STD instance otherwise. + """ + stage = Stage() nb_models, nb_faces = unpack('<HH', file.read(4)) @@ -71,9 +104,13 @@ class Stage(object): for offset in offsets: model = Model() file.seek(offset) + + # Read model header id_, unknown, x, y, z, width, height, depth = unpack('<HHffffff', file.read(28)) model.unknown = unknown model.bounding_box = x, y, z, width, height, depth #TODO: check + + # Read model quads while True: unknown, size = unpack('<HH', file.read(4)) if unknown == 0xffff: @@ -116,14 +153,15 @@ class Stage(object): def write(self, file): + """Write to a Stage Definition file.""" model_offsets = [] second_section_offset = 0 third_section_offset = 0 nb_faces = sum(len(model.quads) for model in self.models) - # Write header - file.write(pack('<HH', len(self.models), nb_faces)) #TODO: nb_faces + # Write header (offsets, number of quads, name and background musics) + file.write(pack('<HH', len(self.models), nb_faces)) file.write(pack('<II', 0, 0)) file.write(pack('<I', 0)) file.write(pack('<128s', self.name.encode('shift_jis'))) @@ -133,7 +171,7 @@ class Stage(object): file.write(pack('<128s', bgm_path.encode('ascii'))) file.write(b'\x00\x00\x00\x00' * len(self.models)) - # Write first section + # Write first section (models) for i, model in enumerate(self.models): model_offsets.append(file.tell()) file.write(pack('<HHffffff', i, model.unknown, *model.bounding_box)) @@ -142,13 +180,13 @@ class Stage(object): file.write(pack('<Hxxfffff', *quad)) file.write(pack('<HH', 0xffff, 4)) - # Write second section + # Write second section (object instances) second_section_offset = file.tell() for obj_id, x, y, z in self.object_instances: file.write(pack('<HHfff', obj_id, 256, x, y, z)) file.write(b'\xff' * 16) - # Write third section + # Write third section (script) third_section_offset = file.tell() for frame, opcode, args in self.script: size = calcsize(self._instructions[opcode][0])
--- a/pytouhou/formats/t6rp.py +++ b/pytouhou/formats/t6rp.py @@ -12,6 +12,14 @@ ## GNU General Public License for more details. ## +"""Touhou 6 Replay (T6RP) files handling. + +This module provides classes for handling the Touhou 6 Replay file format. +The T6RP file format is an encrypted format describing different aspects of +a game of EoSD. Since the EoSD engine is entirely deterministic, a small +replay file is sufficient to unfold a full game. +""" + from struct import unpack from io import BytesIO @@ -46,6 +54,16 @@ class T6RP(object): @classmethod def read(cls, file, decrypt=True, verify=True): + """Read a T6RP file. + + Raise an exception if the file is invalid. + Return a T6RP instance otherwise. + + Keyword arguments: + decrypt -- whether or not to decrypt the file (default True) + verify -- whether or not to verify the file's checksum (default True) + """ + if file.read(4) != b'T6RP': raise Exception