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