comparison pytouhou/formats/ecl.py @ 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 2f3665a77f11
children f037bca24f2d
comparison
equal deleted inserted replaced
203:df8b2ab54639 204:88361534c77e
10 ## but WITHOUT ANY WARRANTY; without even the implied warranty of 10 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 ## GNU General Public License for more details. 12 ## GNU General Public License for more details.
13 ## 13 ##
14 14
15 """ECL files handling.
16
17 This module provides classes for handling the ECL file format.
18 The ECL format is a format used in Touhou 6: EoSD to script most of the gameplay
19 aspects of the game, such as enemy movements, attacks, and so on.
20 """
21
15 import struct 22 import struct
16 from struct import pack, unpack, calcsize 23 from struct import pack, unpack, calcsize
17 24
18 from pytouhou.utils.helpers import get_logger 25 from pytouhou.utils.helpers import get_logger
19 26
20 logger = get_logger(__name__) 27 logger = get_logger(__name__)
21 28
22 class ECL(object): 29 class ECL(object):
30 """Handle Touhou 6 ECL files.
31
32 ECL files are binary script files used to describe the behavior of enemies.
33 They are basically composed of a header and two additional sections.
34 The first section is a list of subroutines, each composed of a list of timed
35 instructions.
36 The second section is a list of a different set of instructions describing
37 enemy waves, triggering dialogs and level completion.
38
39 Instance variables:
40 main -- list of instructions describing waves and triggering dialogs
41 subs -- list of subroutines
42 """
43
23 _instructions = {0: ('', 'noop?'), 44 _instructions = {0: ('', 'noop?'),
24 1: ('I', 'delete?'), 45 1: ('I', 'delete?'),
25 2: ('Ii', 'relative_jump'), 46 2: ('Ii', 'relative_jump'),
26 3: ('Iii', 'relative_jump_ex'), 47 3: ('Iii', 'relative_jump_ex'),
27 4: ('ii', 'set_int'), 48 4: ('ii', 'set_int'),
38 18: ('i', 'increment'), 59 18: ('i', 'increment'),
39 20: ('iff', 'add_float'), 60 20: ('iff', 'add_float'),
40 21: ('iff', 'substract_float'), 61 21: ('iff', 'substract_float'),
41 23: ('iff', 'divide_float'), 62 23: ('iff', 'divide_float'),
42 25: ('iffff', 'get_direction'), 63 25: ('iffff', 'get_direction'),
43 26: ('i', None), 64 26: ('i', 'float_to_unit_circle'), #TODO: find a better name
44 27: ('ii', 'compare_ints'), 65 27: ('ii', 'compare_ints'),
45 28: ('ff', 'compare_floats'), 66 28: ('ff', 'compare_floats'),
46 29: ('ii', 'relative_jump_if_lower_than'), 67 29: ('ii', 'relative_jump_if_lower_than'),
47 30: ('ii', 'relative_jump_if_lower_or_equal'), 68 30: ('ii', 'relative_jump_if_lower_or_equal'),
48 31: ('ii', 'relative_jump_if_equal'), 69 31: ('ii', 'relative_jump_if_equal'),
49 32: ('ii', 'relative_jump_if_greater_than'), 70 32: ('ii', 'relative_jump_if_greater_than'),
50 33: ('ii', 'relative_jump_if_greater_or_equal'), 71 33: ('ii', 'relative_jump_if_greater_or_equal'),
51 34: ('ii', 'relative_jump_if_not_equal'), 72 34: ('ii', 'relative_jump_if_not_equal'),
52 35: ('iif', 'call'), 73 35: ('iif', 'call'),
53 36: ('', 'return?'), 74 36: ('', 'return'),
54 39: ('iifii', 'call_if_equal'), 75 39: ('iifii', 'call_if_equal'),
55 43: ('fff', 'set_position'), 76 43: ('fff', 'set_position'),
56 45: ('ff', 'set_angle_and_speed'), 77 45: ('ff', 'set_angle_and_speed'),
57 46: ('f', 'set_rotation_speed'), 78 46: ('f', 'set_rotation_speed'),
58 47: ('f', 'set_speed'), 79 47: ('f', 'set_speed'),
79 77: ('i', 'set_bullet_interval_ex'), 100 77: ('i', 'set_bullet_interval_ex'),
80 78: ('', 'delay_attack'), 101 78: ('', 'delay_attack'),
81 79: ('', 'no_delay_attack'), 102 79: ('', 'no_delay_attack'),
82 81: ('fff', 'set_bullet_launch_offset'), 103 81: ('fff', 'set_bullet_launch_offset'),
83 82: ('iiiiffff', 'set_extended_bullet_attributes'), 104 82: ('iiiiffff', 'set_extended_bullet_attributes'),
84 83: ('', None), 105 83: ('', 'change_bullets_in_star_bonus'),
85 84: ('i', None), 106 84: ('i', None),
86 85: ('hhffffffiiiiii', 'laser'), 107 85: ('hhffffffiiiiii', 'laser'),
87 86: ('hhffffffiiiiii', 'laser2'), 108 86: ('hhffffffiiiiii', 'laser2'),
88 87: ('i', 'set_upcoming_id'), 109 87: ('i', 'set_upcoming_id'),
89 88: ('if','alter_laser_angle'), 110 88: ('if','alter_laser_angle'),
90 90: ('iiii', None), 111 90: ('iiii', 'translate_laser'),
91 92: ('i', None), 112 92: ('i', 'cancel_laser'),
92 93: ('hhs', 'set_spellcard'), 113 93: ('hhs', 'set_spellcard'),
93 94: ('', 'end_spellcard'), 114 94: ('', 'end_spellcard'),
94 95: ('ifffhhi', None), 115 95: ('ifffhhi', 'spawn_enemy'),
95 96: ('', None), 116 96: ('', 'kill_all_enemies'),
96 97: ('i', 'set_anim'), 117 97: ('i', 'set_anim'),
97 98: ('hhhhhxx', 'set_multiple_anims'), 118 98: ('hhhhhxx', 'set_multiple_anims'),
98 99: ('ii', None), 119 99: ('ii', None),
99 100: ('i', 'set_death_anim'), 120 100: ('i', 'set_death_anim'),
100 101: ('i', 'set_boss_mode?'), 121 101: ('i', 'set_boss_mode?'),
121 123: ('i', 'skip_frames'), 142 123: ('i', 'skip_frames'),
122 124: ('i', 'drop_specific_bonus'), 143 124: ('i', 'drop_specific_bonus'),
123 125: ('', None), 144 125: ('', None),
124 126: ('i', 'set_remaining_lives'), 145 126: ('i', 'set_remaining_lives'),
125 127: ('i', None), 146 127: ('i', None),
126 128: ('i', None), 147 128: ('i', 'set_smooth_disappear'),
127 129: ('ii', None), 148 129: ('ii', None),
128 130: ('i', None), 149 130: ('i', None),
129 131: ('ffiiii', None), 150 131: ('ffiiii', 'set_difficulty_coeffs'),
130 132: ('i', None), 151 132: ('i', 'set_invisible'),
131 133: ('', None), 152 133: ('', None),
132 134: ('', None), 153 134: ('', None),
133 135: ('i', None)} #TODO 154 135: ('i', 'enable_spellcard_bonus')} #TODO
134 155
135 _main_instructions = {0: ('fffhhI', 'spawn_enemy'), 156 _main_instructions = {0: ('fffhhI', 'spawn_enemy'),
136 2: ('fffhhI', 'spawn_enemy_mirrored'), 157 2: ('fffhhI', 'spawn_enemy_mirrored'),
137 4: ('fffhhI', 'spawn_enemy_random'), 158 4: ('fffhhI', 'spawn_enemy_random'),
138 6: ('fffhhI', 'spawn_enemy_mirrored_random'), 159 6: ('fffhhI', 'spawn_enemy_mirrored_random'),
147 self.subs = [[]] 168 self.subs = [[]]
148 169
149 170
150 @classmethod 171 @classmethod
151 def read(cls, file): 172 def read(cls, file):
173 """Read an ECL file.
174
175 Raise an exception if the file is invalid.
176 Return a ECL instance otherwise.
177 """
178
152 sub_count, main_offset = unpack('<II', file.read(8)) 179 sub_count, main_offset = unpack('<II', file.read(8))
153 if file.read(8) != b'\x00\x00\x00\x00\x00\x00\x00\x00': 180 if file.read(8) != b'\x00\x00\x00\x00\x00\x00\x00\x00':
154 raise Exception #TODO 181 raise Exception #TODO
155 sub_offsets = unpack('<%dI' % sub_count, file.read(4 * sub_count)) 182 sub_offsets = unpack('<%dI' % sub_count, file.read(4 * sub_count))
156 183
169 instruction_offsets.append(file.tell() - sub_offset) 196 instruction_offsets.append(file.tell() - sub_offset)
170 197
171 time, opcode = unpack('<IH', file.read(6)) 198 time, opcode = unpack('<IH', file.read(6))
172 if time == 0xffffffff or opcode == 0xffff: 199 if time == 0xffffffff or opcode == 0xffff:
173 break 200 break
201
174 size, rank_mask, param_mask = unpack('<HHH', file.read(6)) 202 size, rank_mask, param_mask = unpack('<HHH', file.read(6))
175 data = file.read(size - 12) 203 data = file.read(size - 12)
176 if opcode in cls._instructions: 204 if opcode in cls._instructions:
177 fmt = '<%s' % cls._instructions[opcode][0] 205 fmt = '<%s' % cls._instructions[opcode][0]
178 if fmt.endswith('s'): 206 if fmt.endswith('s'):
186 logger.warn('unknown opcode %d', opcode) 214 logger.warn('unknown opcode %d', opcode)
187 215
188 ecl.subs[-1].append((time, opcode, rank_mask, param_mask, args)) 216 ecl.subs[-1].append((time, opcode, rank_mask, param_mask, args))
189 217
190 218
191 # Translate offsets to instruction pointers 219 # Translate offsets to instruction pointers.
220 # Indeed, jump instructions are relative and byte-based.
221 # Since our representation doesn't conserve offsets, we have to
222 # keep trace of where the jump is supposed to end up.
192 for instr_offset, (i, instr) in zip(instruction_offsets, enumerate(ecl.subs[-1])): 223 for instr_offset, (i, instr) in zip(instruction_offsets, enumerate(ecl.subs[-1])):
193 time, opcode, rank_mask, param_mask, args = instr 224 time, opcode, rank_mask, param_mask, args = instr
194 if opcode in (2, 29, 30, 31, 32, 33, 34): # relative_jump 225 if opcode in (2, 29, 30, 31, 32, 33, 34): # relative_jump
195 frame, relative_offset = args 226 frame, relative_offset = args
196 args = frame, instruction_offsets.index(instr_offset + relative_offset) 227 args = frame, instruction_offsets.index(instr_offset + relative_offset)
220 251
221 return ecl 252 return ecl
222 253
223 254
224 def write(self, file): 255 def write(self, file):
256 """Write to an ECL file."""
257
225 sub_count = len(self.subs) 258 sub_count = len(self.subs)
226 sub_offsets = [] 259 sub_offsets = []
227 main_offset = 0 260 main_offset = 0
228 261
229 # Skip header, it will be written later 262 # Skip header, it will be written later