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