๐Ÿ“ฆ cyrozap / tools-for-tdc

๐Ÿ“„ pack.py ยท 124 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later

# pack.py - A tool for packing unpacked .tdc data back into a .tdc file
# Copyright (C) 2025  Forest Crossman <cyrozap@gmail.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, either version 3 of the License, or
# (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.


import argparse
import json
import sys
from pathlib import Path
from typing import BinaryIO

from crc32_bzip2 import crc32
from fastlz import compress


def write_header(output_file: BinaryIO, header_version: int, data_offset: int, header_data: dict) -> None:
    """
    Write the header to the output file.

    :param output_file: The file object to write the header to.
    :param header_version: The version of the header to write.
    :param data_offset: The offset to the data section.
    :param header_data: A dictionary containing the header data to write.
    :return: None
    """
    output_file.write(b"TPDC")
    output_file.write(header_version.to_bytes(2, byteorder="little"))
    output_file.write(data_offset.to_bytes(4, byteorder="little"))

    unk1_size: int = {
        0x100: 2,
        0x200: 4,
        0x300: 4,
    }[header_version]

    unk5_size: int = {
        0x100: 4,
        0x200: 4,
        0x300: 8,
    }[header_version]

    output_file.write(header_data["unk0"].to_bytes(2, byteorder="little"))
    output_file.write(header_data["unk1"].to_bytes(unk1_size, byteorder="little"))
    output_file.write(header_data["capture_save_time"].to_bytes(4, byteorder="little"))
    output_file.write(header_data["data_version"].to_bytes(2, byteorder="little"))
    output_file.write(header_data["unk3"].to_bytes(4, byteorder="little"))
    output_file.write(header_data["unk4"].to_bytes(4, byteorder="little"))
    output_file.write(header_data["unk5"].to_bytes(unk5_size, byteorder="little"))
    output_file.write(header_data["num_thing"].to_bytes(2, byteorder="little"))

    for item in header_data["thing"]:
        output_file.write(item["lower"].to_bytes(2, byteorder="little"))
        output_file.write(item["upper"].to_bytes(2, byteorder="little"))

    header_len: int = output_file.tell()
    padding_len: int = data_offset - header_len
    assert padding_len >= 0
    output_file.write(bytes([0] * padding_len))

def main() -> None:
    parser = argparse.ArgumentParser(description="Pack unpacked .tdc data back into a .tdc file.")
    parser.add_argument("input_dir", type=str, help="The directory containing unpacked .tdc data.")
    parser.add_argument("-o", "--output", type=str, default="packed.tdc", help="The output .tdc file.")
    args = parser.parse_args()

    input_dir: Path = Path(args.input_dir)
    output_file: Path = Path(args.output)

    if not input_dir.exists() or not input_dir.is_dir():
        print(f"Error: Directory '{input_dir}' does not exist or is not a directory.", file=sys.stderr)
        sys.exit(1)

    metadata_path: Path = input_dir / "metadata.json"
    if not metadata_path.exists():
        print(f"Error: Missing metadata.json in '{input_dir}'.", file=sys.stderr)
        sys.exit(1)

    with open(metadata_path, "r") as f:
        metadata: dict = json.load(f)

    header_version: int = metadata["header_version"]
    data_offset: int = metadata["data_offset"]
    header_data: dict = metadata["header"]

    with open(output_file, "wb") as f:
        write_header(f, header_version, data_offset, header_data)

        # Write each block
        block_count: int = 0
        while True:
            block_path: Path = input_dir / f"block_{block_count}.bin"
            if not block_path.exists():
                break

            with open(block_path, "rb") as block_f:
                decompressed: bytes = block_f.read()
                compressed: bytes = compress(decompressed)
                f.write((len(compressed) << 8).to_bytes(4, byteorder="little"))
                f.write(crc32(decompressed).to_bytes(4, byteorder="little"))
                f.write(compressed)

            block_count += 1

        print(f"Packed {block_count} blocks into '{output_file}'.", file=sys.stderr)


if __name__ == "__main__":
    main()