comparison pytouhou/vm/eclrunner.py @ 69:a142e57218a0

Refactor. Move VMs to pytouhou.vm.
author Thibaut Girka <thib@sitedethib.com>
date Sat, 27 Aug 2011 10:58:54 +0200
parents pytouhou/game/eclrunner.py@e2cb9d434dc2
children 7c1f20407b3e
comparison
equal deleted inserted replaced
68:a2459defd4b6 69:a142e57218a0
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
16 from math import atan2, cos, sin
17
18 from pytouhou.utils.helpers import get_logger
19
20 from pytouhou.vm.common import MetaRegistry, instruction
21
22 logger = get_logger(__name__)
23
24
25
26 class ECLRunner(object):
27 __metaclass__ = MetaRegistry
28 __slots__ = ('_ecl', '_enemy', '_game_state', 'variables', 'sub', 'frame',
29 'instruction_pointer', 'comparison_reg', 'stack')
30
31 def __init__(self, ecl, sub, enemy, game_state):
32 # Things not supposed to change
33 self._ecl = ecl
34 self._enemy = enemy
35 self._game_state = game_state
36
37 # Things supposed to change (and be put in the stack)
38 self.variables = [0, 0, 0, 0,
39 0., 0., 0., 0.,
40 0, 0, 0, 0]
41 self.comparison_reg = 0
42 self.sub = sub
43 self.frame = 0
44 self.instruction_pointer = 0
45
46 self.stack = []
47
48
49 def run_iteration(self):
50 # First, if enemy is dead, return
51 if self._enemy._removed:
52 return False
53
54 # Then, check for callbacks
55 #TODO
56
57 # Now, process script
58 while True:
59 try:
60 frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer]
61 except IndexError:
62 return False
63
64 if frame > self.frame:
65 break
66 else:
67 self.instruction_pointer += 1
68
69
70 #TODO: skip bad ranks
71 if not rank_mask & (0x100 << self._game_state.rank):
72 continue
73
74
75 if frame == self.frame:
76 try:
77 callback = self._handlers[instr_type]
78 except KeyError:
79 logger.warn('unhandled opcode %d (args: %r)', instr_type, args)
80 else:
81 callback(self, *args)
82
83 self.frame += 1
84 return True
85
86
87 def _getval(self, value):
88 if -10012 <= value <= -10001:
89 return self.variables[int(-10001-value)]
90 elif -10025 <= value <= -10013:
91 if value == -10013:
92 return self._game_state.rank
93 elif value == -10014:
94 return self._game_state.difficulty
95 elif value == -10015:
96 return self._enemy.x
97 elif value == -10016:
98 return self._enemy.y
99 elif value == -10017:
100 return self._enemy.z
101 elif value == -10018:
102 player = self._enemy.select_player(self._game_state.players)
103 return player.x
104 elif value == -10019:
105 player = self._enemy.select_player(self._game_state.players)
106 return player.y
107 elif value == -10021:
108 player = self._enemy.select_player(self._game_state.players)
109 return self._enemy.get_player_angle(player)
110 elif value == -10022:
111 return self._enemy.frame
112 elif value == -10024:
113 return self._enemy.life
114 elif value == -10025:
115 return self._enemy.select_player(self._game_state.players).character
116 raise NotImplementedError(value) #TODO
117 else:
118 return value
119
120
121 def _setval(self, variable_id, value):
122 if -10012 <= variable_id <= -10001:
123 self.variables[int(-10001-variable_id)] = value
124 elif -10025 <= variable_id <= -10013:
125 if variable_id == -10015:
126 self._enemy.x = value
127 elif variable_id == -10016:
128 self._enemy.y = value
129 elif variable_id == -10017:
130 self._enemy.z = value
131 elif variable_id == -10022:
132 self._enemy.frame = value
133 elif variable_id == -10024:
134 self._enemy.life = value
135 else:
136 raise IndexError #TODO: proper exception
137 else:
138 raise IndexError #TODO: proper exception
139
140
141 @instruction(0)
142 def noop(self):
143 pass #TODO: Really?
144
145
146 @instruction(1)
147 def destroy(self, arg):
148 #TODO: arg?
149 self._enemy._removed = True
150
151
152 @instruction(2)
153 def relative_jump(self, frame, instruction_pointer):
154 """Jumps to a relative offset in the same subroutine.
155
156 Warning: the relative offset has been translated to an instruction pointer
157 by the ECL parsing code (see pytouhou.formats.ecl).
158 """
159 self.frame, self.instruction_pointer = frame, instruction_pointer
160
161
162 @instruction(3)
163 def relative_jump_ex(self, frame, instruction_pointer, variable_id):
164 """If the given variable is non-zero, decrease it by 1 and jump to a
165 relative offset in the same subroutine.
166
167 Warning: the relative offset has been translated to an instruction pointer
168 by the ECL parsing code (see pytouhou.formats.ecl).
169 """
170 counter_value = self._getval(variable_id)
171 if counter_value:
172 self._setval(variable_id, counter_value - 1)
173 self.frame, self.instruction_pointer = frame, instruction_pointer
174
175
176 @instruction(4)
177 @instruction(5)
178 def set_variable(self, variable_id, value):
179 self._setval(variable_id, self._getval(value))
180
181
182 @instruction(6)
183 def set_random_int(self, variable_id, maxval):
184 """Set the specified variable to a random int in the [0, maxval) range.
185 """
186 self._setval(variable_id, int(self._getval(maxval) * self._game_state.prng.rand_double()))
187
188
189 @instruction(8)
190 def set_random_float(self, variable_id, maxval):
191 """Set the specified variable to a random float in [0, maxval) range.
192 """
193 self._setval(variable_id, self._getval(maxval) * self._game_state.prng.rand_double())
194
195
196 @instruction(9)
197 def set_random_float2(self, variable_id, minval, amp):
198 self._setval(variable_id, self._getval(minval) + self._getval(amp) * self._game_state.prng.rand_double())
199
200
201 @instruction(13)
202 def set_random_int2(self, variable_id, minval, amp):
203 self._setval(variable_id, int(self._getval(minval)) + int(self._getval(amp)) * self._game_state.prng.rand_double())
204
205
206 @instruction(14)
207 @instruction(21)
208 def substract(self, variable_id, a, b):
209 #TODO: 14 takes only ints and 21 only floats.
210 # The original engine dereferences the variables in the type it waits for, so this isn't exactly the correct implementation, but the data don't contain such case.
211 self._setval(variable_id, self._getval(a) - self._getval(b))
212
213
214 @instruction(15)
215 def multiply_int(self, variable_id, a, b):
216 #TODO: takes only ints.
217 self._setval(variable_id, self._getval(a) * self._getval(b))
218
219
220 @instruction(16)
221 def divide_int(self, variable_id, a, b):
222 #TODO: takes only ints.
223 self._setval(variable_id, self._getval(a) // self._getval(b))
224
225
226 @instruction(17)
227 def modulo(self, variable_id, a, b):
228 self._setval(variable_id, self._getval(a) % self._getval(b))
229
230
231 @instruction(20)
232 def add_float(self, variable_id, a, b):
233 #TODO: takes only floats.
234 self._setval(variable_id, self._getval(a) + self._getval(b))
235
236
237 @instruction(23)
238 def divide_float(self, variable_id, a, b):
239 #TODO: takes only floats.
240 self._setval(variable_id, self._getval(a) / self._getval(b))
241
242
243 @instruction(27)
244 @instruction(28)
245 def compare(self, a, b):
246 #TODO: 27 takes only ints and 28 only floats.
247 a, b = self._getval(a), self._getval(b)
248 if a < b:
249 self.comparison_reg = -1
250 elif a == b:
251 self.comparison_reg = 0
252 else:
253 self.comparison_reg = 1
254
255
256 @instruction(29)
257 def relative_jump_if_lower_than(self, frame, instruction_pointer):
258 if self.comparison_reg == -1:
259 self.relative_jump(frame, instruction_pointer)
260
261
262 @instruction(30)
263 def relative_jump_if_lower_or_equal(self, frame, instruction_pointer):
264 if self.comparison_reg != 1:
265 self.relative_jump(frame, instruction_pointer)
266
267
268 @instruction(31)
269 def relative_jump_if_equal(self, frame, instruction_pointer):
270 if self.comparison_reg == 0:
271 self.relative_jump(frame, instruction_pointer)
272
273
274 @instruction(32)
275 def relative_jump_if_greater_than(self, frame, instruction_pointer):
276 if self.comparison_reg == 1:
277 self.relative_jump(frame, instruction_pointer)
278
279
280 @instruction(33)
281 def relative_jump_if_greater_or_equal(self, frame, instruction_pointer):
282 if self.comparison_reg != -1:
283 self.relative_jump(frame, instruction_pointer)
284
285
286 @instruction(34)
287 def relative_jump_if_not_equal(self, frame, instruction_pointer):
288 if self.comparison_reg != 0:
289 self.relative_jump(frame, instruction_pointer)
290
291
292 @instruction(35)
293 def call(self, sub, param1, param2):
294 self.stack.append((self.sub, self.frame, self.instruction_pointer,
295 self.variables, self.comparison_reg))
296 self.sub = sub
297 self.frame = 0
298 self.instruction_pointer = 0
299 self.variables = [param1, 0, 0, 0,
300 param2, 0., 0., 0.,
301 0, 0, 0, 0]
302
303
304 @instruction(36)
305 def ret(self):
306 self.sub, self.frame, self.instruction_pointer, self.variables, self.comparison_reg = self.stack.pop()
307
308
309 @instruction(39)
310 def call_if_equal(self, sub, param1, param2, a, b):
311 if self._getval(a) == self._getval(b):
312 self.call(sub, param1, param2)
313
314
315 @instruction(43)
316 def set_pos(self, x, y, z):
317 self._enemy.set_pos(x, y, z)
318
319
320 @instruction(45)
321 def set_angle_speed(self, angle, speed):
322 self._enemy.angle, self._enemy.speed = angle, speed
323
324
325 @instruction(46)
326 def set_rotation_speed(self, speed):
327 self._enemy.rotation_speed = speed
328
329
330 @instruction(47)
331 def set_speed(self, speed):
332 self._enemy.speed = speed
333
334
335 @instruction(48)
336 def set_acceleration(self, acceleration):
337 self._enemy.acceleration = acceleration
338
339
340 @instruction(50)
341 def set_random_angle(self, min_angle, max_angle):
342 if self._enemy.screen_box:
343 minx, miny, maxx, maxy = self._enemy.screen_box
344 else:
345 minx, miny, maxx, maxy = (0., 0., 0., 0.)
346
347 angle = self._game_state.prng.rand_double() * (max_angle - min_angle) + min_angle
348 sa, ca = sin(angle), cos(angle)
349
350 distx = min(96.0, (maxx - minx) / 2.)
351 disty = min(96.0, (maxy - miny) / 2.)
352
353 if self._enemy.x > maxx - 96.0:
354 ca = -abs(ca)
355 elif self._enemy.x < minx + 96.0:
356 ca = abs(ca)
357
358 if self._enemy.y > maxy - 48.0:
359 sa = -abs(sa)
360 elif self._enemy.y < miny + 48.0:
361 sa = abs(sa)
362 self._enemy.angle = atan2(sa, ca)
363
364
365 @instruction(51)
366 def target_player(self, unknown, speed):
367 #TODO: unknown
368 self._enemy.speed = speed
369 self._enemy.angle = self._enemy.get_player_angle(self._enemy.select_player(self._game_state.players))
370
371
372 @instruction(57)
373 def move_to(self, duration, x, y, z):
374 self._enemy.move_to(duration, x, y, z, lambda x: 2. * x - x ** 2)
375
376
377 @instruction(59)
378 def move_to2(self, duration, x, y, z):
379 #TODO: not accurate
380 self._enemy.move_to(duration, x, y, z, lambda x: 1.0014 * x ** 2 - 0.0012 * x)
381
382
383 @instruction(61)
384 def stop_in(self, duration):
385 self._enemy.stop_in(duration)
386
387
388 @instruction(65)
389 def set_screen_box(self, xmin, ymin, xmax, ymax):
390 self._enemy.screen_box = xmin, ymin, xmax, ymax
391
392
393 @instruction(66)
394 def clear_screen_box(self):
395 self._enemy.screen_box = None
396
397
398 @instruction(67)
399 def set_bullet_attributes1(self, bullet_anim, launch_anim, bullets_per_shot,
400 number_of_shots, speed, unknown, launch_angle,
401 angle, flags):
402 self._enemy.set_bullet_attributes(1, bullet_anim, launch_anim,
403 bullets_per_shot, number_of_shots,
404 speed, unknown, launch_angle, angle,
405 flags)
406
407
408 @instruction(77)
409 def set_bullet_interval(self, value):
410 self._enemy.bullet_launch_interval = value
411
412
413 @instruction(78)
414 def set_delay_attack(self):
415 self._enemy.delay_attack = True
416
417
418 @instruction(79)
419 def set_no_delay_attack(self):
420 self._enemy.delay_attack = False
421
422
423 @instruction(81)
424 def set_bullet_launch_offset(self, x, y, z):
425 self._enemy.bullet_launch_offset = (x, y)
426
427
428 @instruction(97)
429 def set_anim(self, sprite_index):
430 self._enemy.set_anim(sprite_index)
431
432
433 @instruction(98)
434 def set_multiple_anims(self, default, end_left, end_right, left, right):
435 self._enemy.movement_dependant_sprites = end_left, end_right, left, right
436 self._enemy.set_anim(default)
437
438
439 @instruction(100)
440 def set_death_anim(self, sprite_index):
441 self._enemy.death_anim = sprite_index % 256 #TODO
442
443
444 @instruction(101)
445 def set_boss_mode(self, unknown):
446 #TODO: unknown
447 self._game_state.boss = self._enemy
448
449
450 @instruction(103)
451 def set_hitbox(self, width, height, depth):
452 self._enemy.hitbox = (width, height)
453
454
455 @instruction(105)
456 def set_damageable(self, vulnerable):
457 self._enemy.damageable = bool(vulnerable & 1)
458
459
460 @instruction(108)
461 def set_death_callback(self, sub):
462 self._enemy.death_callback = sub
463
464
465 @instruction(109)
466 def memory_write(self, value, index):
467 #TODO
468 #XXX: this is a hack to display bosses although we don't handle MSG :)
469 if index == 0:
470 self.sub = value
471 self.frame = 0
472 self.instruction_pointer = 0
473
474
475 @instruction(111)
476 def set_life(self, value):
477 self._enemy.life = value
478
479
480 @instruction(112)
481 def set_ellapsed_time(self, value):
482 """Sets the enemy's frame counter.
483 This is used for timeouts, where the framecounter is compared to the
484 timeout value (what's displayed is (enemy.timeout - enemy.frame) // 60).
485 """
486 self._enemy.frame = value
487
488
489 @instruction(113)
490 def set_low_life_trigger(self, value):
491 self._enemy.low_life_trigger = value
492
493
494 @instruction(114)
495 def set_low_life_callback(self, sub):
496 self._enemy.low_life_callback = sub
497
498
499 @instruction(115)
500 def set_timeout(self, timeout):
501 self._enemy.timeout = timeout
502
503
504 @instruction(116)
505 def set_timeout_callback(self, sub):
506 self._enemy.timeout_callback = sub
507
508
509 @instruction(117)
510 def set_touchable(self, value):
511 """Defines whether the enemy is “touchable”.
512 Bullets only collide with an enemy if it is “touchable”.
513 Likewise, ReimuA's homing attacks only target “touchable” enemies.
514 """
515 self._enemy.touchable = bool(value)
516
517
518 @instruction(126)
519 def set_remaining_lives(self, lives):
520 self._enemy.remaining_lives = lives
521