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