view pytouhou/formats/score.py @ 400:7aa70f0def38

Add support for MoF’s hint format.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 12 Feb 2013 19:27:10 +0100
parents 70e2ed71b09c
children d1f0bb0b7a17
line wrap: on
line source

# -*- encoding: utf-8 -*-
##
## Copyright (C) 2012 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.
##


from struct import pack, unpack, Struct
from collections import namedtuple
from io import BytesIO

from pytouhou.formats import ChecksumError


class TH6Score(object):
    entry_types = {
        b'TH6K': (Struct('<I'),
                  namedtuple('TH6K', ('unknown',))),
        b'HSCR': (Struct('<IIBBB8sx'),
                  namedtuple('HSCR', ('unknown', 'score', 'character',
                                      'rank', 'stage', 'name'))),
        b'PSCR': (Struct('<IIBBBx'),
                  namedtuple('PSCR', ('unknown', 'score', 'character',
                                      'rank', 'stage'))),
        b'CLRD': (Struct('<I5B5BBx'),
                  namedtuple('CLRD', ('unknown',
                                      'easy', 'normal', 'hard', 'lunatic',
                                      'extra',
                                      'easy_continue', 'normal_continue',
                                      'hard_continue', 'lunatic_continue',
                                      'extra_continue',
                                      'character'))),
        b'CATK': (Struct('<I I HH I 34s H HH'),
                  namedtuple('CATK', ('unknown', 'unknown2', 'num',
                                      'unknown3', 'padding',
                                      'name', 'padding2',
                                      'seen',
                                      'defeated'))),
    }

    def __init__(self):
        self.key1 = 0
        self.key2 = 0
        self.unknown1 = 0
        self.unknown2 = 16
        self.unknown3 = 0
        self.unknown4 = 0
        self.entries = []


    @classmethod
    def read(cls, file, decrypt=True, verify=True):
        self = cls()

        # Decrypt data
        if decrypt:
            decrypted_file = BytesIO()
            decrypted_file.write(file.read(1))
            key = 0
            for c in file.read():
                encrypted = ord(c)
                key = ((key << 3) & 0xFF) | ((key >> 5) & 7)
                clear = encrypted ^ key
                key += clear
                decrypted_file.write(chr(clear))
            file = decrypted_file

        # Read first-part header
        file.seek(0)
        self.unknown1, self.key1, checksum = unpack('<BBH', file.read(4))

        # Verify checksum
        if verify:
            #TODO: is there more to it?
            real_sum = sum(ord(c) for c in file.read()) & 0xFFFF
            if checksum != real_sum:
                raise ChecksumError(checksum, real_sum)
            file.seek(4)

        # Read second-part header
        data = unpack('<HBBIII', file.read(16))
        self.unknown2, self.key2, self.unknown3, offset, self.unknown4, size = data

        #TODO: verify size

        # Read tags
        file.seek(offset)
        while True:
            tag = file.read(4)
            if not tag:
                break
            size, size2 = unpack('<HH', file.read(4))
            assert size == size2
            assert size >= 8
            data = file.read(size-8)
            data = cls.entry_types[tag][0].unpack(data)
            data = cls.entry_types[tag][1](*data)
            self.entries.append((tag, data))

        return self


    def write(self, file, encrypt=True):
        if encrypt:
            clearfile = BytesIO()
        else:
            clearfile = file

        # Write data
        clearfile.seek(20)
        for entry in self.entries:
            #TODO
            tag, data = entry
            format = TH6Score.entry_types[tag][0]
            clearfile.write(tag)
            clearfile.write(pack('<H', format.size + 8) * 2)
            clearfile.write(format.pack(*data))

        # Patch header
        size = clearfile.tell()
        clearfile.seek(0)
        clearfile.write(pack('<BBHHBBIII',
                             self.unknown1, self.key1, 0, self.unknown2,
                             self.key2, self.unknown3, 20, self.unknown4,
                             size))

        # Patch checksum
        clearfile.seek(4)
        checksum = sum(ord(c) for c in clearfile.read()) & 0xFFFF
        clearfile.seek(2)
        clearfile.write(pack('<H', checksum))

        # Encrypt
        if encrypt:
            clearfile.seek(0)
            file.write(clearfile.read(1))
            key = 0
            for c in clearfile.read():
                clear = ord(c)
                key = ((key << 3) & 0xFF) | ((key >> 5) & 7)
                encrypted = clear ^ key
                key += clear
                file.write(chr(encrypted))