changeset 292:3a81b607f974

Add TH6 score.dat support.
author Thibaut Girka <thib@sitedethib.com>
date Fri, 17 Feb 2012 21:19:57 +0100
parents f6b8483a990d
children ab618c2bbce8
files pytouhou/formats/score.py
diffstat 1 files changed, 149 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/pytouhou/formats/score.py
@@ -0,0 +1,149 @@
+# -*- 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, calcsize, Struct
+from collections import namedtuple
+from io import BytesIO
+
+
+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?
+            if checksum != sum(ord(c) for c in file.read()) & 0xFFFF:
+                raise Exception
+            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))
+