Mercurial > touhou
view pytouhou/formats/std.py @ 261:2876c267be00
Add heuristics to extract character data from the demo .exe.
author | Thibaut Girka <thib@sitedethib.com> |
---|---|
date | Mon, 23 Jan 2012 00:03:06 +0100 |
parents | 88361534c77e |
children | 61adb5453e46 |
line wrap: on
line source
# -*- encoding: utf-8 -*- ## ## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published ## by the Free Software Foundation; version 3 only. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## """Stage Definition (STD) files handling. This module provides classes for handling the Stage Definition file format. The STD file format is a format used in Touhou 6: EoSD to describe non-gameplay aspects of a stage: its name, its music, 3D models composing its background, and various scripted events such as camera movement. """ from struct import pack, unpack, calcsize from pytouhou.utils.helpers import read_string, get_logger logger = get_logger(__name__) class Model(object): def __init__(self, unknown=0, bounding_box=None, quads=None): self.unknown = 0 self.bounding_box = bounding_box or (0., 0., 0., 0., 0., 0.) self.quads = quads or [] class Stage(object): """Handle Touhou 6 Stage Definition files. Stage Definition files are structured files describing non-gameplay aspects aspects of a stage. They are split in a header an 3 additional sections. The header contains the name of the stage, the background musics (BGM) used, as well as the number of quads and objects composing the background. The first section describes the models composing the background, whereas the second section dictates how they are used. The last section describes the changes to the camera, fog, and other things. Instance variables: name -- name of the stage bgms -- list of (name, path) describing the different background musics used models -- list of Model objects object_instances -- list of instances of the aforementioned models script -- stage script (camera, fog, etc.) """ _instructions = {0: ('fff', 'set_viewpos'), 1: ('BBBxff', 'set_fog'), 2: ('fff', 'set_viewpos2'), 3: ('Ixxxxxxxx', 'start_interpolating_viewpos2'), 4: ('Ixxxxxxxx', 'start_interpolating_fog')} def __init__(self): self.name = '' self.bgms = (('', ''), ('', ''), ('', ''), ('', '')) self.models = [] self.object_instances = [] self.script = [] @classmethod def read(cls, file): """Read a Stage Definition file. Raise an exception if the file is invalid. Return a STD instance otherwise. """ stage = Stage() nb_models, nb_faces = unpack('<HH', file.read(4)) object_instances_offset, script_offset = unpack('<II', file.read(8)) if file.read(4) != b'\x00\x00\x00\x00': raise Exception #TODO stage.name = read_string(file, 128, 'shift_jis') bgm_a = read_string(file, 128, 'shift_jis') bgm_b = read_string(file, 128, 'shift_jis') bgm_c = read_string(file, 128, 'shift_jis') bgm_d = read_string(file, 128, 'shift_jis') bgm_a_path = read_string(file, 128, 'ascii') bgm_b_path = read_string(file, 128, 'ascii') bgm_c_path = read_string(file, 128, 'ascii') bgm_d_path = read_string(file, 128, 'ascii') stage.bgms = [(bgm_a, bgm_a_path), (bgm_b, bgm_b_path), (bgm_c, bgm_c_path), (bgm_d, bgm_d_path)] #TODO: handle ' ' # Read model definitions offsets = unpack('<%s' % ('I' * nb_models), file.read(4 * nb_models)) for offset in offsets: model = Model() file.seek(offset) # Read model header id_, unknown, x, y, z, width, height, depth = unpack('<HHffffff', file.read(28)) model.unknown = unknown model.bounding_box = x, y, z, width, height, depth #TODO: check # Read model quads while True: unknown, size = unpack('<HH', file.read(4)) if unknown == 0xffff: break if size != 0x1c: raise Exception #TODO script_index, x, y, z, width, height = unpack('<Hxxfffff', file.read(24)) model.quads.append((script_index, x, y, z, width, height)) stage.models.append(model) # Read object usages file.seek(object_instances_offset) while True: obj_id, unknown, x, y, z = unpack('<HHfff', file.read(16)) if (obj_id, unknown) == (0xffff, 0xffff): break if unknown != 256: raise Exception #TODO stage.object_instances.append((obj_id, x, y, z)) # Read other funny things (script) file.seek(script_offset) while True: frame, opcode, size = unpack('<IHH', file.read(8)) if (frame, opcode, size) == (0xffffffff, 0xffff, 0xffff): break if size != 0x0c: raise Exception #TODO data = file.read(size) if opcode in cls._instructions: args = unpack('<%s' % cls._instructions[opcode][0], data) else: args = (data,) logger.warn('unknown opcode %d', opcode) stage.script.append((frame, opcode, args)) return stage def write(self, file): """Write to a Stage Definition file.""" model_offsets = [] second_section_offset = 0 third_section_offset = 0 nb_faces = sum(len(model.quads) for model in self.models) # Write header (offsets, number of quads, name and background musics) file.write(pack('<HH', len(self.models), nb_faces)) file.write(pack('<II', 0, 0)) file.write(pack('<I', 0)) file.write(pack('<128s', self.name.encode('shift_jis'))) for bgm_name, bgm_path in self.bgms: file.write(pack('<128s', bgm_name.encode('shift_jis'))) for bgm_name, bgm_path in self.bgms: file.write(pack('<128s', bgm_path.encode('ascii'))) file.write(b'\x00\x00\x00\x00' * len(self.models)) # Write first section (models) for i, model in enumerate(self.models): model_offsets.append(file.tell()) file.write(pack('<HHffffff', i, model.unknown, *model.bounding_box)) for quad in model.quads: file.write(pack('<HH', 0x00, 0x1c)) file.write(pack('<Hxxfffff', *quad)) file.write(pack('<HH', 0xffff, 4)) # Write second section (object instances) second_section_offset = file.tell() for obj_id, x, y, z in self.object_instances: file.write(pack('<HHfff', obj_id, 256, x, y, z)) file.write(b'\xff' * 16) # Write third section (script) third_section_offset = file.tell() for frame, opcode, args in self.script: size = calcsize(self._instructions[opcode][0]) file.write(pack('<IHH%s' % self._instructions[opcode][0], frame, opcode, size, *args)) file.write(b'\xff' * 20) # Fix offsets file.seek(4) file.write(pack('<II', second_section_offset, third_section_offset)) file.seek(16+128+128*2*4) file.write(pack('<%sI' % len(self.models), *model_offsets))