changeset 18:ca26a84916cb

Add preliminary ECL viewer/interpreter.
author Thibaut Girka <thib@sitedethib.com>
date Tue, 09 Aug 2011 11:40:48 +0200
parents d940d004b840
children ca7886296d4a
files eclviewer.py pytouhou/formats/ecl.py pytouhou/game/enemymanager.py
diffstat 3 files changed, 328 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/eclviewer.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+
+import sys
+import os
+
+import struct
+from math import degrees, radians
+from io import BytesIO
+from itertools import chain
+
+import pygame
+
+from pytouhou.formats.pbg3 import PBG3
+from pytouhou.formats.std import Stage
+from pytouhou.formats.ecl import ECL
+from pytouhou.formats.anm0 import Animations
+from pytouhou.game.background import Background
+from pytouhou.game.enemymanager import EnemyManager
+from pytouhou.opengl.texture import TextureManager
+
+import OpenGL
+OpenGL.FORWARD_COMPATIBLE_ONLY = True
+from OpenGL.GL import *
+from OpenGL.GLU import *
+
+
+def main(path, stage_num):
+    # Initialize pygame
+    pygame.init()
+    window = pygame.display.set_mode((384, 448), pygame.OPENGL | pygame.DOUBLEBUF)
+
+    # Initialize OpenGL
+    glMatrixMode(GL_PROJECTION)
+    glLoadIdentity()
+    gluPerspective(30, float(window.get_width())/window.get_height(), 101010101./2010101., 101010101./10101.)
+
+    glEnable(GL_BLEND)
+    glEnable(GL_TEXTURE_2D)
+    glEnable(GL_FOG)
+    glHint(GL_FOG_HINT, GL_NICEST)
+    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+    glEnableClientState(GL_VERTEX_ARRAY)
+    glEnableClientState(GL_TEXTURE_COORD_ARRAY)
+
+    # Load data
+    with open(path, 'rb') as file:
+        archive = PBG3.read(file)
+        texture_manager = TextureManager(archive)
+
+        stage = Stage.read(BytesIO(archive.extract('stage%d.std' % stage_num)), stage_num)
+
+        ecl = ECL.read(BytesIO(archive.extract('ecldata%d.ecl' % stage_num)))
+        enemies_anim = Animations.read(BytesIO(archive.extract('stg%denm.anm' % stage_num)))
+        anims = [enemies_anim]
+        try:
+            enemies2_anim = Animations.read(BytesIO(archive.extract('stg%denm2.anm' % stage_num)))
+        except KeyError:
+            pass
+        else:
+            anims.append(enemies_anim)
+        enemy_manager = EnemyManager(stage, anims, ecl)
+
+        background_anim = Animations.read(BytesIO(archive.extract('stg%dbg.anm' % stage_num)))
+        background = Background(stage, background_anim)
+
+        print(enemy_manager.stage.name)
+
+        frame = 0
+
+        # Main loop
+        clock = pygame.time.Clock()
+        while True:
+            # Check events
+            for event in pygame.event.get():
+                if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key in (pygame.K_ESCAPE, pygame.K_q)):
+                    sys.exit(0)
+                elif event.type == pygame.KEYDOWN:
+                    if event.key == pygame.K_RETURN and event.mod & pygame.KMOD_ALT:
+                        pygame.display.toggle_fullscreen()
+
+            # Update game
+            enemy_manager.update(frame)
+            background.update(frame)
+
+            # Draw everything
+            glClear(GL_COLOR_BUFFER_BIT)
+
+            fog_b, fog_g, fog_r, _, fog_start, fog_end = background.fog_interpolator.values
+            x, y, z = background.position_interpolator.values
+            unknownx, dy, dz = background.position2_interpolator.values
+
+            glFogi(GL_FOG_MODE, GL_LINEAR)
+            glFogf(GL_FOG_START, fog_start)
+            glFogf(GL_FOG_END,  fog_end)
+            glFogfv(GL_FOG_COLOR, (fog_r / 255., fog_g / 255., fog_b / 255., 1.))
+
+            #TODO
+            glMatrixMode(GL_MODELVIEW)
+            glLoadIdentity()
+            # Some explanations on the magic constants:
+            # 192. = 384. / 2. = width / 2.
+            # 224. = 448. / 2. = height / 2.
+            # 835.979370 = 224./math.tan(math.radians(15)) = (height/2.)/math.tan(math.radians(fov/2))
+            # This is so that objects on the (O, x, y) plane use pixel coordinates
+            gluLookAt(192., 224., - 835.979370 * dz,
+                      192., 224. - dy, 750 - 835.979370 * dz, 0., -1., 0.) #TODO: 750 might not be accurate
+            #print(glGetFloat(GL_MODELVIEW_MATRIX))
+            glTranslatef(-x, -y, -z)
+
+            for texture_key, (nb_vertices, vertices, uvs) in background.objects_by_texture.items():
+                glBindTexture(GL_TEXTURE_2D, texture_manager[texture_key])
+                glVertexPointer(3, GL_FLOAT, 0, vertices)
+                glTexCoordPointer(2, GL_FLOAT, 0, uvs)
+                glDrawArrays(GL_QUADS, 0, nb_vertices)
+
+            #TODO
+            glMatrixMode(GL_MODELVIEW)
+            glLoadIdentity()
+            # Some explanations on the magic constants:
+            # 192. = 384. / 2. = width / 2.
+            # 224. = 448. / 2. = height / 2.
+            # 835.979370 = 224./math.tan(math.radians(15)) = (height/2.)/math.tan(math.radians(fov/2))
+            # This is so that objects on the (O, x, y) plane use pixel coordinates
+            gluLookAt(192., 224., - 835.979370,
+                      192., 224., 750 - 835.979370, 0., -1., 0.) #TODO: 750 might not be accurate
+
+            glDisable(GL_FOG)
+            for texture_key, (nb_vertices, vertices, uvs) in enemy_manager.objects_by_texture.items():
+                glBindTexture(GL_TEXTURE_2D, texture_manager[texture_key])
+                glVertexPointer(3, GL_FLOAT, 0, vertices)
+                glTexCoordPointer(2, GL_FLOAT, 0, uvs)
+                glDrawArrays(GL_QUADS, 0, nb_vertices)
+            glEnable(GL_FOG)
+
+            pygame.display.flip()
+            clock.tick(120)
+            frame += 1
+
+
+
+try:
+    file_path, stage_num = sys.argv[1:]
+    stage_num = int(stage_num)
+except ValueError:
+    print('Usage: %s std_dat_path stage_num' % sys.argv[0])
+else:
+    main(file_path, stage_num)
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/formats/ecl.py
@@ -0,0 +1,55 @@
+from struct import pack, unpack
+from pytouhou.utils.helpers import read_string
+
+from collections import namedtuple
+
+
+class ECL(object):
+    def __init__(self):
+        self.main = []
+        self.subs = [[]]
+
+
+    @classmethod
+    def read(cls, file):
+        sub_count, main_offset = unpack('<II', file.read(8))
+        if file.read(8) != b'\x00\x00\x00\x00\x00\x00\x00\x00':
+            raise Exception #TODO
+        sub_offsets = unpack('<%s' % ('I' * sub_count), file.read(4 * sub_count))
+
+        ecl = cls()
+        ecl.subs = []
+        ecl.main = []
+
+        # Read subs
+        for offset in sub_offsets:
+            file.seek(offset)
+            ecl.subs.append([])
+            while True:
+                time, opcode = unpack('<IH', file.read(6))
+                if time == 0xffffffff or opcode == 0xffff:
+                    break
+                size, rank_mask, param_mask = unpack('<HHH', file.read(6))
+                data = file.read(size - 12)
+                #TODO: unpack data
+                ecl.subs[-1].append((time, opcode, rank_mask, param_mask, data))
+
+        # Read main
+        file.seek(main_offset)
+        while True:
+            time, = unpack('<H', file.read(2))
+            if time == 0xffff:
+                break
+            sub, instr_type, size = unpack('<HHH', file.read(6))
+            data = file.read(size - 8)
+            if instr_type == 0: # Normal enemy
+                args = unpack('<ffIhHHH', data)
+            elif instr_type == 2: # Mirrored enemy
+                args = unpack('<ffIhHHH', data)
+            else:
+                print('ECL: Warning: unknown opcode %d (%r)' % (instr_type, data)) #TODO
+                args = (data,)
+            ecl.main.append((time, sub, instr_type, args))
+
+        return ecl
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/enemymanager.py
@@ -0,0 +1,124 @@
+from itertools import chain
+from io import BytesIO
+import os
+from struct import unpack, pack
+from pytouhou.game.sprite import Sprite
+from math import cos, sin
+
+
+class Enemy(object):
+    def __init__(self, pos, life, _type, script, anms):
+        self.anms = tuple(anms)
+        self.anm = None
+        self.script = list(script)
+        self.x, self.y = pos
+        self.life = life
+        self.type = _type
+        self.frame = 0
+        self.sprite = None
+
+        self.angle = 0.
+        self.speed = 0.
+        self.rotation_speed = 0.
+        self.acceleration = 0.
+
+
+    def update(self, frame):
+        if not self.script:
+            return True
+        if self.script[0][0] == self.frame:
+            for instr_type, rank_mask, param_mask, args  in self.script.pop(0)[1]:
+                if instr_type == 1: # delete
+                    return False
+                elif instr_type == 97: # set_enemy_sprite
+                    script_index, = unpack('<I', args)
+                    if script_index in self.anms[0].scripts:
+                        self.sprite = Sprite(self.anms[0], script_index)
+                        self.anm = self.anms[0]
+                    else:
+                        self.sprite = Sprite(self.anms[1], script_index)
+                        self.anm = self.anms[1]
+                elif instr_type == 45: # set_angle_speed
+                    self.angle, self.speed = unpack('<ff', args)
+                elif instr_type == 46: # set_angle
+                    self.rotation_speed, = unpack('<f', args)
+                elif instr_type == 47: # set_speed
+                    self.speed, = unpack('<f', args)
+                elif instr_type == 48: # set_acceleration
+                    self.acceleration, = unpack('<f', args)
+        if self.sprite:
+            self.sprite.update()
+
+        self.speed += self.acceleration #TODO: units? Execution order?
+        self.angle += self.rotation_speed #TODO: units? Execution order?
+
+        dx, dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed
+        if self.type == 2:
+            self.x -= dx
+        else:
+            self.x += dx
+        self.y += dy
+
+        self.frame += 1
+        return True
+
+
+
+class EnemyManager(object):
+    def __init__(self, stage, anims, ecl):
+        self.stage = stage
+        self.anims = tuple(anims)
+        self.main = []
+        self.subs = {}
+        self.objects_by_texture = {}
+        self.enemies = []
+
+        # Populate main
+        for frame, sub, instr_type, args in ecl.main:
+            if not self.main or self.main[-1][0] < frame:
+                self.main.append((frame, [(sub, instr_type, args)]))
+            elif self.main[-1][0] == frame:
+                self.main[-1][1].append((sub, instr_type, args))
+
+
+        # Populate subs
+        for i, sub in enumerate(ecl.subs):
+            for frame, instr_type, rank_mask, param_mask, args in sub:
+                if i not in self.subs:
+                    self.subs[i] = []
+                if not self.subs[i] or self.subs[i][-1][0] < frame:
+                    self.subs[i].append((frame, [(instr_type, rank_mask, param_mask, args)]))
+                elif self.subs[i][-1][0] == frame:
+                    self.subs[i][-1][1].append((instr_type, rank_mask, param_mask, args))
+
+
+    def update(self, frame):
+        if self.main and self.main[0][0] == frame:
+            for sub, instr_type, args in self.main.pop(0)[1]:
+                if instr_type in (0, 2): # Normal/mirrored enemy
+                    x, y, z, life, unknown1, unknown2, unknown3 = args
+                    self.enemies.append(Enemy((x, y), life, instr_type, self.subs[sub], self.anims))
+
+        # Update enemies
+        for enemy in tuple(self.enemies):
+            if not enemy.update(frame):
+                self.enemies.remove(enemy)
+                continue
+
+        # Add enemies to vertices/uvs
+        self.objects_by_texture = {}
+        for enemy in self.enemies:
+            ox, oy = enemy.x, enemy.y
+            if enemy.sprite:
+                key = enemy.anm.first_name, enemy.anm.secondary_name
+                if not key in self.objects_by_texture:
+                    self.objects_by_texture[key] = (0, [], [])
+                vertices = tuple((x + ox, y + oy, z) for x, y, z in enemy.sprite._vertices)
+                self.objects_by_texture[key][2].extend(enemy.sprite._uvs)
+                self.objects_by_texture[key][1].extend(vertices)
+        for key, (nb_vertices, vertices, uvs) in self.objects_by_texture.items():
+            nb_vertices = len(vertices)
+            vertices = pack('f' * (3 * nb_vertices), *chain(*vertices))
+            uvs = pack('f' * (2 * nb_vertices), *chain(*uvs))
+            self.objects_by_texture[key] = (nb_vertices, vertices, uvs)
+