comparison pytouhou/formats/anm0.py @ 608:725bd24235a2

Make ANM0 pure-python again, by moving the Cython-dependent ANM class into its own module.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Wed, 26 Nov 2014 14:00:17 +0100
parents pytouhou/formats/anm0.pyx@43ecf0f98f4d
children d18c0bf11138
comparison
equal deleted inserted replaced
607:9dbc234ea087 608:725bd24235a2
1 # -*- encoding: utf-8 -*-
2 ##
3 ## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
4 ##
5 ## This program is free software; you can redistribute it and/or modify
6 ## it under the terms of the GNU General Public License as published
7 ## by the Free Software Foundation; version 3 only.
8 ##
9 ## This program is distributed in the hope that it will be useful,
10 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 ## GNU General Public License for more details.
13 ##
14
15 """ANM0 files handling.
16
17 This module provides classes for handling the ANM0 file format.
18 The ANM0 format is a format used in Touhou 6: EoSD to describe sprites
19 and animations.
20 Almost everything rendered in the game is described by an ANM0 file.
21 """
22
23 from struct import pack, unpack
24 from pytouhou.utils.helpers import read_string, get_logger
25
26 from pytouhou.formats import WrongFormatError
27 from pytouhou.formats.animation import Animation
28 from pytouhou.formats.thtx import Texture
29
30
31 logger = get_logger(__name__)
32
33 #TODO: refactor/clean up
34
35
36 class Script(list):
37 def __init__(self):
38 list.__init__(self)
39 self.interrupts = {}
40
41
42
43 class ANM0(Animation):
44 _instructions = {0: {0: ('', 'delete'),
45 1: ('I', 'set_sprite'),
46 2: ('ff', 'set_scale'),
47 3: ('I', 'set_alpha'),
48 4: ('BBBx', 'set_color'),
49 5: ('I', 'jump'),
50 7: ('', 'toggle_mirrored'),
51 9: ('fff', 'set_3d_rotations'),
52 10: ('fff', 'set_3d_rotations_speed'),
53 11: ('ff', 'set_scale_speed'),
54 12: ('ii', 'fade'),
55 13: ('', 'set_blendmode_add'),
56 14: ('', 'set_blendmode_alphablend'),
57 15: ('', 'keep_still'),
58 16: ('ii', 'set_random_sprite'),
59 17: ('fff', 'set_3d_translation'),
60 18: ('fffi', 'move_to_linear'),
61 19: ('fffi', 'move_to_decel'),
62 20: ('fffi', 'move_to_accel'),
63 21: ('', 'wait'),
64 22: ('i', 'interrupt_label'),
65 23: ('', 'set_corner_relative_placement'),
66 24: ('', 'wait_ex'),
67 25: ('i', 'set_allow_offset'), #TODO: better name
68 26: ('i', 'set_automatic_orientation'),
69 27: ('f', 'shift_texture_x'),
70 28: ('f', 'shift_texture_y'),
71 29: ('i', 'set_visible'),
72 30: ('ffi', 'scale_in'),
73 31: ('i', None)},
74
75 2: {0: ('', 'noop'),
76 1: ('', 'delete'),
77 2: ('', 'keep_still'),
78 3: ('I', 'set_sprite'),
79 4: ('II', 'jump_bis'),
80 5: ('III', 'jump_ex'),
81 6: ('fff', 'set_3d_translation'),
82 7: ('ff', 'set_scale'),
83 8: ('I', 'set_alpha'),
84 9: ('BBBx', 'set_color'),
85 10: ('', 'toggle_mirrored'),
86 12: ('fff', 'set_3d_rotations'),
87 13: ('fff', 'set_3d_rotations_speed'),
88 14: ('ff', 'set_scale_speed'),
89 15: ('ii', 'fade'),
90 16: ('I', 'set_blendmode'),
91 17: ('fffi', 'move_to_linear'),
92 18: ('fffi', 'move_to_decel'),
93 19: ('fffi', 'move_to_accel'),
94 20: ('', 'wait'),
95 21: ('i', 'interrupt_label'),
96 22: ('', 'set_corner_relative_placement'),
97 23: ('', 'wait_ex'),
98 24: ('i', 'set_allow_offset'), #TODO: better name
99 25: ('i', 'set_automatic_orientation'),
100 26: ('f', 'shift_texture_x'),
101 27: ('f', 'shift_texture_y'),
102 28: ('i', 'set_visible'),
103 29: ('ffi', 'scale_in'),
104 30: ('i', None),
105 31: ('I', None),
106 32: ('IIfff', 'move_in_linear_bis'),
107 33: ('IIBBBx', 'change_color_in'),
108 34: ('III', 'fade_bis'),
109 35: ('IIfff', 'rotate_in_bis'),
110 36: ('IIff', 'scale_in_bis'),
111 37: ('II', 'set_int'),
112 38: ('ff', 'set_float'),
113 42: ('ff', 'decrement_float'),
114 50: ('fff', 'add_float'),
115 52: ('fff', 'substract_float'),
116 55: ('III', 'divide_int'),
117 59: ('II', 'set_random_int'),
118 60: ('ff', 'set_random_float'),
119 69: ('IIII', 'branch_if_not_equal'),
120 79: ('I', 'wait_duration'),
121 80: ('I', None)}}
122
123
124 @classmethod
125 def read(cls, file):
126 anm_list = []
127 start_offset = 0
128 while True:
129 file.seek(start_offset)
130 nb_sprites, nb_scripts, zero1 = unpack('<III', file.read(12))
131 width, height, fmt, unknown1 = unpack('<IIII', file.read(16))
132 first_name_offset, unused, secondary_name_offset = unpack('<III', file.read(12))
133 version, unknown2, texture_offset, has_data, next_offset, unknown3 = unpack('<IIIIII', file.read(24))
134
135 if version == 0:
136 assert zero1 == 0
137 assert unknown3 == 0
138 assert has_data == 0
139 elif version == 2:
140 assert zero1 == 0
141 assert secondary_name_offset == 0
142 assert has_data == 1 # Can be false but we don’t support that yet.
143 else:
144 raise WrongFormatError(version)
145
146 instructions = cls._instructions[version]
147
148 sprite_offsets = [unpack('<I', file.read(4))[0] for i in range(nb_sprites)]
149 script_offsets = [unpack('<II', file.read(8)) for i in range(nb_scripts)]
150
151 self = cls()
152
153 self.size = (width, height)
154 self.version = version
155
156 # Names
157 if first_name_offset:
158 file.seek(start_offset + first_name_offset)
159 self.first_name = read_string(file, 32, 'ascii') #TODO: 32, really?
160 if secondary_name_offset:
161 file.seek(start_offset + secondary_name_offset)
162 self.secondary_name = read_string(file, 32, 'ascii') #TODO: 32, really?
163
164
165 # Sprites
166 for offset in sprite_offsets:
167 file.seek(start_offset + offset)
168 idx, x, y, width, height = unpack('<Iffff', file.read(20))
169 self.sprites[idx] = x, y, width, height
170
171
172 # Scripts
173 for i, offset in script_offsets:
174 self.scripts[i] = Script()
175 instruction_offsets = []
176 file.seek(start_offset + offset)
177 while True:
178 instruction_offsets.append(file.tell() - (start_offset + offset))
179 if version == 0:
180 time, opcode, size = unpack('<HBB', file.read(4))
181 elif version == 2:
182 opcode, size, time, mask = unpack('<HHHH', file.read(8))
183 if opcode == 0xffff:
184 break
185 size -= 8
186 data = file.read(size)
187 if opcode in instructions:
188 args = unpack('<%s' % instructions[opcode][0], data)
189 else:
190 args = (data,)
191 logger.warn('unknown opcode %d', opcode)
192
193 self.scripts[i].append((time, opcode, args))
194 if version == 0 and opcode == 0:
195 break
196
197 # Translate offsets to instruction pointers and register interrupts
198 for instr_offset, (j, instr) in zip(instruction_offsets, enumerate(self.scripts[i])):
199 time, opcode, args = instr
200 if version == 0:
201 if opcode == 5:
202 args = (instruction_offsets.index(args[0]),)
203 elif opcode == 22:
204 interrupt = args[0]
205 self.scripts[i].interrupts[interrupt] = j + 1
206 elif version == 2:
207 if opcode == 4:
208 args = (instruction_offsets.index(args[0]), args[1])
209 elif opcode == 5:
210 args = (args[0], instruction_offsets.index(args[1]), args[2])
211 elif opcode == 21:
212 interrupt = args[0]
213 self.scripts[i].interrupts[interrupt] = j + 1
214 elif opcode == 69:
215 args = (args[0], args[1], instruction_offsets.index(args[2]), args[3])
216 self.scripts[i][j] = time, opcode, args
217
218 # Texture
219 if has_data:
220 file.seek(start_offset + texture_offset)
221 magic = file.read(4)
222 assert magic == b'THTX'
223 zero, fmt, width, height, size = unpack('<HHHHI', file.read(12))
224 assert zero == 0
225 data = file.read(size)
226 self.texture = Texture(width, height, fmt, data)
227
228 anm_list.append(self)
229
230 if next_offset:
231 start_offset += next_offset
232 else:
233 break
234
235 return anm_list