summaryrefslogtreecommitdiff
path: root/pokemontools/battle_animations.py
blob: 8cc302f8e3a71038260a9a43cf92dfed3be48aa5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# coding: utf-8

from __future__ import print_function
from __future__ import absolute_import
import os
from new import classobj

from . import configuration
conf = configuration.Config()

from .crystal import (
	SingleByteParam,
	PointerLabelParam,
	DecimalParam,
	BigEndianParam,
	Command,
	load_rom
)

from .gbz80disasm import get_local_address, get_global_address
from .audio import sort_asms


from .wram import read_constants

rom = bytearray(load_rom())

sfx_constants = read_constants(os.path.join(conf.path, 'constants/sfx_constants.asm'))
class SoundEffectParam(SingleByteParam):
	def to_asm(self):
		if self.byte in sfx_constants.keys():
			sfx_constant = sfx_constants[self.byte]
			return sfx_constant
		return SingleByteParam.to_asm(self)

anim_gfx_constants = read_constants(os.path.join(conf.path, 'constants/gfx_constants.asm'))
class AnimGFXParam(SingleByteParam):
	def to_asm(self):
		if self.byte in anim_gfx_constants.keys():
			return anim_gfx_constants[self.byte]
		return SingleByteParam.to_asm(self)

anims = read_constants(os.path.join(conf.path, 'constants/animation_constants.asm'))
objs  = { k: v for k, v in anims.items() if 'ANIM_OBJ' in v }
bgs   = { k: v for k, v in anims.items() if 'ANIM_BG'  in v }
anims = { k: v.replace('ANIM_','') for k, v in anims.items() }
from .move_constants import moves
anims.update(moves)

class AnimObjParam(SingleByteParam):
	def to_asm(self):
		if self.byte in objs.keys():
			return objs[self.byte]
		return SingleByteParam.to_asm(self)

class BGEffectParam(SingleByteParam):
	def to_asm(self):
		if self.byte in bgs.keys():
			return bgs[self.byte]
		return SingleByteParam.to_asm(self)


battle_animation_commands = {
	0xd0: ['anim_obj', ['obj', AnimObjParam], ['x', DecimalParam], ['y', DecimalParam], ['param', SingleByteParam]],
	0xd1: ['anim_1gfx', ['gfx1', AnimGFXParam]],
	0xd2: ['anim_2gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam]],
	0xd3: ['anim_3gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam]],
	0xd4: ['anim_4gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam], ['gfx4', AnimGFXParam]],
	0xd5: ['anim_5gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam], ['gfx4', AnimGFXParam], ['gfx5', AnimGFXParam]],
	0xd6: ['anim_incobj', ['id', SingleByteParam]],
	0xd7: ['anim_setobj', ['id', SingleByteParam], ['obj', AnimObjParam]], # bug: second param is interpreted as a command if not found in the object array
	0xd8: ['anim_incbgeffect', ['effect', BGEffectParam]],
	0xd9: ['anim_enemyfeetobj'],
	0xda: ['anim_playerheadobj'],
	0xdb: ['anim_checkpokeball'],
	0xdc: ['anim_transform'],
	0xdd: ['anim_raisesub'],
	0xde: ['anim_dropsub'],
	0xdf: ['anim_resetobp0'],
	0xe0: ['anim_sound', ['tracks', SingleByteParam], ['id', SoundEffectParam]],
	0xe1: ['anim_cry', ['pitch', SingleByteParam]],
	0xe2: ['anim_minimizeopp'], # unused
	0xe3: ['anim_oamon'],
	0xe4: ['anim_oamoff'],
	0xe5: ['anim_clearobjs'],
	0xe6: ['anim_beatup'],
	0xe7: ['anim_0xe7'], # nothing
	0xe8: ['anim_updateactorpic'],
	0xe9: ['anim_minimize'],
	0xea: ['anim_0xea'], # nothing
	0xeb: ['anim_0xeb'], # nothing
	0xec: ['anim_0xec'], # nothing
	0xed: ['anim_0xed'], # nothing
	0xee: ['anim_jumpand', ['value', SingleByteParam], ['address', PointerLabelParam]],
	0xef: ['anim_jumpuntil', ['address', PointerLabelParam]],
	0xf0: ['anim_bgeffect', ['effect', BGEffectParam], ['unknown', SingleByteParam], ['unknown', SingleByteParam], ['unknown', SingleByteParam]],
	0xf1: ['anim_bgp', ['colors', SingleByteParam]],
	0xf2: ['anim_obp0', ['colors', SingleByteParam]],
	0xf3: ['anim_obp1', ['colors', SingleByteParam]],
	0xf4: ['anim_clearsprites'],
	0xf5: ['anim_0xf5'], # nothing
	0xf6: ['anim_0xf6'], # nothing
	0xf7: ['anim_0xf7'], # nothing
	0xf8: ['anim_jumpif', ['value', SingleByteParam], ['address', PointerLabelParam]],
	0xf9: ['anim_setvar', ['value', SingleByteParam]],
	0xfa: ['anim_incvar'],
	0xfb: ['anim_jumpvar', ['value', SingleByteParam], ['address', PointerLabelParam]],
	0xfc: ['anim_jump', ['address', PointerLabelParam]],
	0xfd: ['anim_loop', ['count', SingleByteParam], ['address', PointerLabelParam]],
	0xfe: ['anim_call', ['address', PointerLabelParam]],
	0xff: ['anim_ret'],
}

battle_animation_enders = [
	'anim_jump',
	'anim_ret',
]

def create_battle_animation_classes():
	classes = []
	for cmd, command in battle_animation_commands.items():
		cmd_name = command[0]
		params = {
			'id': cmd,
			'size': 1,
			'end': cmd_name in battle_animation_enders,
			'macro_name': cmd_name,
			'param_types': {},
		}
		for i, (name, class_) in enumerate(command[1:]):
			params['param_types'][i] = {'name': name, 'class': class_}
			params['size'] += class_.size
		class_name = cmd_name + 'Command'
		class_ = classobj(class_name, (Command,), params)
		globals()[class_name] = class_
		classes += [class_]
	return classes

battle_animation_classes = create_battle_animation_classes()


class BattleAnimWait(Command):
	macro_name = 'anim_wait'
	size = 1
	end = macro_name in battle_animation_enders
	param_types = {
		0: {'name': 'duration', 'class': DecimalParam},
	}
	override_byte_check = True


class BattleAnim:
	"""
	A list of battle animation commands read from a given address.

	Results in a list of commands (self.output) and a list of labels (self.labels).
	Format is (address, asm, last_address). Includes any subroutines and their output.

	To convert to text, use self.to_asm().

	For combining multiple BattleAnims, take self.output + self.labels from each
	and sort with sort_asms.
	"""

	def __init__(self, address, base_label=None, label=None, used_labels=[], macros=[]):
		self.start_address = address
		self.address = address

		self.base_label = base_label
		if self.base_label == None:
			self.base_label = 'BattleAnim_' + hex(self.start_address)

		self.label = label
		if self.label == None:
			self.label = self.base_label

		self.used_labels = used_labels

		self.output = []
		self.labels = []
		self.label_asm = (
			self.start_address,
			'%s: ; %x' % (self.label, self.start_address),
			self.start_address
		)
		self.labels += [self.label_asm]
		self.used_labels += [self.label_asm]

		self.macros = macros

		self.parse()

	def parse(self):

		done = False
		while not done:
			cmd = rom[self.address]
			class_ = self.get_command_class(cmd)(address=self.address)
			asm = class_.to_asm()

			# label jumps/calls
			for key, param in class_.param_types.items():
				if param['class'] == PointerLabelParam:
					label_address = class_.params[key].parsed_address
					label = '%s_branch_%x' % (self.base_label, label_address)
					label_def = '%s: ; %x' % (label, label_address)
					label_asm = (label_address, label_def, label_address)
					if label_asm not in self.used_labels:
						self.labels += [label_asm]
					asm = asm.replace('$%x' % get_local_address(label_address), label)

			self.output += [(self.address, '\t' + asm, self.address + class_.size)]
			self.address += class_.size

			done = class_.end
			# infinite loops are enders
			if class_.macro_name == 'anim_loop':
				if class_.params[0].byte == 0:
					done = True

		# last_address comment
		self.output += [(self.address, '; %x\n' % self.address, self.address)]

		# parse any other branches too
		self.labels = list(set(self.labels))
		for address, asm, last_address in self.labels:
			if not (self.start_address <= address < self.address) and (address, asm, last_address) not in self.used_labels:
				self.used_labels += [(address, asm, last_address)]
				sub = BattleAnim(
					address=address,
					base_label=self.base_label,
					label=asm.split(':')[0],
					used_labels=self.used_labels,
					macros=self.macros
				)
				self.output += sub.output
				self.labels += sub.labels

		self.output = list(set(self.output))
		self.labels = list(set(self.labels))

	def to_asm(self):
		output = sort_asms(self.output + self.labels)
		text = ''
		for (address, asm, last_address) in output:
			text += asm + '\n'
		return text

	def get_command_class(self, cmd):
		if cmd < 0xd0:
			return BattleAnimWait
		for class_ in self.macros:
			if class_.id == cmd:
				return class_
		return None


def battle_anim_label(i):
	"""
	Return a label matching the name of a battle animation by id.
	"""
	if i in anims.keys():
		base_label = 'BattleAnim_%s' % anims[i].title().replace('_','')
	else:
		base_label = 'BattleAnim_%d' % i
	return base_label

def dump_battle_anims(table_address=0xc906f, num_anims=278, macros=battle_animation_classes):
	"""
	Dump each battle animation from a pointer table.
	"""

	asms = []

	asms += [(table_address, 'BattleAnimations: ; %x' % table_address, table_address)]

	address = table_address
	bank = address / 0x4000

	for i in xrange(num_anims):
		pointer_address = address
		anim_address = rom[pointer_address] + rom[pointer_address + 1] * 0x100
		anim_address = get_global_address(anim_address, bank)
		base_label = battle_anim_label(i)
		address += 2

		# anim pointer
		asms += [(pointer_address, '\tdw %s' % base_label, address)]

		# anim script
		anim = BattleAnim(
			address=anim_address,
			base_label=base_label,
			macros=macros
		)
		asms += anim.output + anim.labels

	asms += [(address, '; %x\n' % address, address)]

	# jp sonicboom
	anim = BattleAnim(
		address=0xc9c00,
		base_label='BattleAnim_Sonicboom_JP',
		macros=macros
	)
	asms += anim.output + anim.labels

	asms = sort_asms(asms)
	return asms

def asm_list_to_text(asms):
	output = ''
	last = asms[0][0]
	for addr, asm, last_addr in asms:
		if addr > last:
			# incbin any unknown areas
			output += '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n\n\n' % (last, addr, last)
		if addr >= last:
			output += asm + '\n'
		last = last_addr
	return output

if __name__ == '__main__':
	asms = dump_battle_anims()
	print(asm_list_to_text(asms))