├── .gitignore ├── LICENSE.md ├── README.md ├── common.py ├── convert-psx.py ├── convert-thps2x-texture-data.py ├── disassemble-ddm.py ├── disassemble-prk.py ├── disassemble-trg.py ├── extract-ddx.py ├── extract-hed-wad.py └── extract-pkr.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | **Copyright (c) 2019 Jannik Vogel** 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a collection of tools for Tony Hawk's Pro Skater 2. 2 | 3 | There's a lot of overlap with these other games, potentially based on the Neversoft Big Guns engine: 4 | 5 | - SW: Skeleton Warriors 6 | - AP: Apocalypse 7 | - MDK: MDK (PlayStation) 8 | - SM: Spiderman 9 | - THPS1!April9: Tony Hawk's Pro Skater 1 (April 9 1999 build) 10 | - THPS1!July7: Tony Hawk's Pro Skater 1 (July 7 1999 build) 11 | - THPS1!July10: Tony Hawk's Pro Skater 1 (July 10 1999 build) 12 | - THPS1: Tony Hawk's Pro Skater 1 13 | - SM2: Spider-Man 2: Enter: Electro 14 | - THPS2: Tony Hawk's Pro Skater 2 15 | - THPS2X: Tony Hawk's Pro Skater 2x 16 | - MH: Mat Hoffman's Pro BMX 17 | - THPS3: Tony Hawk's Pro Skater 3 (PlayStation / N64) 18 | - SDH: Sea-Doo Hydrocross (PlayStation) 19 | - THPS4: Tony Hawk's Pro Skater 4 (PlayStation) 20 | 21 | 22 | ## Known formats 23 | 24 | ### DDX: extract-ddx.py (THPS2X-Xbox) 25 | 26 | Extracts THPS2X DDX files (Texture collections). 27 | 28 | ### DDM: disassemble-ddm.py (THPS2X-Xbox) 29 | 30 | Make THPS2X DDM files (Material collections) human-readable. 31 | 32 | ### TRG: disassemble-trg.py (THPS1 / THPS2 / SM) 33 | 34 | Make THPS2 TRG files (Scripts / Commands and level meta-data) human-readable. 35 | 36 | A lot of this is based on SYMBOLS.TDF, which is included in THPS2 (Windows). 37 | 38 | ### PRK: disassemble-prk.py (THPS2 / THPS2X) 39 | 40 | Disassembles THPS2 PKR files (User created skatepark). 41 | 42 | ### PKR: extract-pkr.py (?) 43 | 44 | Extracts PKR file. 45 | 46 | ### PSX: psxviewer.c (THPS1 / THPS2 / SM) 47 | 48 | Viewer for PSX files. 49 | 50 | Stolen from https://gist.github.com/iamgreaser/2a67f7473d9c48a70946018b73fa1e40 51 | 52 | 53 | ## Unknown formats 54 | 55 | There's additional file formats which are commonly found in THPS2: 56 | 57 | - BON (THPS2X-Xbox) = Likely mesh and texture for characters. 58 | - REC (THPS2-Windows) = Replay files 59 | - THPS2 CRETEX.BIN = A list of items for skater editor. 60 | - THPS2 PRE = Container format for small files, assembled from S files 61 | - THPS2 PSX = Level files (psxviewer.c exists, but Blender 2.80 importer would be good) 62 | - THPS2 FNT = Font glyph map 63 | - THPS2 CD.HET/CD.HEP = List of files on CD 64 | - THPS2 CD.HED/CD.WAD = Container for files on CD 65 | - THPS2 SBL = Some debug symbols 66 | - THPS2 SFX = Some sound-effect list 67 | - THPS2 TS = Trick list 68 | 69 | 70 | 71 | ## Other formats 72 | 73 | ### STR 74 | 75 | PlayStation STR video file format. See https://github.com/m35/jpsxdec/blob/readme/jpsxdec/PlayStation1_STR_format.txt 76 | 77 | ### SFX.VAB (THPS1!-PlayStation) 78 | 79 | PlayStation VAB audio file format. 80 | 81 | ### CT.PAL (THPS1!-PlayStation) 82 | 83 | Microsoft RIFF Palette. 84 | 85 | ### title_h.zlb (THPS1!-PlayStation) 86 | 87 | `gunzip` to extract BMR image; 640x480, 16 bit per pixel little-endian grayscale. 88 | 89 | ### BMR (THPS1!-PlayStation) 90 | 91 | Raw image files, 16 bit per pixel, with 8 byte zero-footer. 92 | Typically 640x480, or 512x240. 93 | 94 | ### SYMBOLS.TDF (THPS2-Windows) 95 | 96 | This was likely assembled from a SYMBOLS.S (using a tool called stotdf.exe) 97 | 98 | ### PSH (THPS2-Windows) 99 | 100 | Header files 101 | 102 | ### S 103 | 104 | Assembler directives. The following files are named in other files, or exist. 105 | The assembler was either something custom, or common. 106 | Possibly different tools were used for different tasks. 107 | 108 | #### SCRIPT.S 109 | 110 | This uses another tool to handle S files. 111 | 112 | ### BMP (THPS2-Windows) 113 | 114 | Windows Bitmap. 115 | 116 | ### DDS (THPS2X-Xbox) 117 | 118 | DirectDraw Surface file. 119 | 120 | ### WAV (THPS2-Windows) 121 | 122 | RIFF file format for audio. 123 | Windows version uses: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 44100 Hz. 124 | 125 | ### TXT 126 | 127 | Most of these are left-overs from debugging / development. 128 | 129 | However, there's some additional files which the game potentially uses. 130 | 131 | #### CDPARKS.TXT 132 | 133 | List of user created parks that are included with the game. 134 | 135 | 136 | ## Further reading 137 | 138 | * [Hex Editing Created Parks (THPS2)](http://webcache.googleusercontent.com/search?q=cache:uUdAddR8ZGoJ:planettonyhawk.gamespy.com/Viewf0fe.php) 139 | * [Mick West Blog article: Practical Hash IDs (Used in Tony Hawk engine)](http://cowboyprogramming.com/2007/01/04/practical-hash-ids/) 140 | * [Mick West Blog article: Evolve Your Hierarchy (Used in later Tony Hawk games; also describes earlier games)](http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/) 141 | * [THPS blender importer/exporter (io_thps_scene)](https://github.com/denetii/io_thps_scene) 142 | * [PSX file format documentation](https://gist.github.com/iamgreaser/b54531e41d77b69d7d13391deb0ac6a5) 143 | * [List of all bruteforced filenames in THPS1 (PlayStation)](https://gist.github.com/iamgreaser/ee48ac4adb0a18fc8928eb8494703730) 144 | * [spidey-tools](https://github.com/krystalgamer/spidey-tools) 145 | 146 | 147 | ## License 148 | 149 | The files in this repository may be used under MIT License (see LICENSE.md) unless noted otherwise in the respective file. 150 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | def read_string(f, n=0): 4 | if n == 0: 5 | s = b'' 6 | while True: 7 | c = f.read(1) 8 | if c == b'\0': 9 | break 10 | s += c 11 | else: 12 | s = f.read(n) 13 | s = s.partition(b'\0')[0] 14 | try: 15 | s = s.decode('ascii') 16 | except: 17 | s = s.decode('latin-1') #FIXME: This is a poor fallback 18 | return s 19 | 20 | def read8(f): 21 | return struct.unpack("> 31)) & 0xFFFFFFFF 66 | if mask & 1: 67 | result ^= 0xEDB88320 68 | mask >>= 1 69 | 70 | #FIXME: Make this work somehow? 71 | if False: 72 | import zlib 73 | ref = zlib.crc32(data, 0xFFFFFFFF) 74 | ref2 = ref ^ 0xFFFFFFFF 75 | print("%08X == %08X (needs to be %08X)" % (ref, ref2, result)) 76 | 77 | return result 78 | 79 | #FIXME: Remove, this is a check for the CRC algo 80 | if False: 81 | import sys 82 | for arg in sys.argv[1:]: 83 | print("0x%08X: '%s'" % (crc32(arg.encode('ascii')), arg)) # must be B57D1831 84 | 85 | #FIXME: Remove, this is a (rather slow) bruteforce algorithm 86 | if False: 87 | 88 | 89 | alphabet = [x.encode('ascii') for x in "abcdefghijklmnopqrstuvwxyz_0123456789."] # ["%d" % i for i in range(10)] 90 | 91 | symbols = [0] 92 | results = [0xFFFFFFFF, 0] 93 | 94 | first_valid = 0 95 | 96 | checksums = [ 97 | 0xB57D1831, 98 | 0xFDA4795D, 99 | 0xFDD2C22E, 100 | 0x59BFF7DD, 101 | 0xA438A846, 102 | 0x00559DB5, 103 | 0x8E26EDC7, 104 | 0x2A4BD834, 105 | 0xF345F263, 106 | 0x5728C790, 107 | 0x7B88F05C, 108 | 0xDFE5C5AF, 109 | 0xC32151A1, 110 | 0x674C6452 111 | ] 112 | 113 | while True: 114 | 115 | # Update all invalid hashes 116 | for i in range(first_valid+1, len(symbols)+1): 117 | results[i] = crc32(alphabet[symbols[i - 1]], results[i - 1]) 118 | 119 | # Try the longest one, with all known extensions 120 | prefix_list = [b"", b"load"] # Common prefix can be removed 121 | suffix_list = [b"", b".bmp", b".pre", b".psx", b".vab", b".sfx", b".txt", b".psh"] #FIXME: Can check dot once etc. = sort and trim 122 | 123 | if False: 124 | for prefix in prefix_list: 125 | helper = crc32(prefix, results[0]) 126 | #FIXME: Need to combine prefix CRC with later CRC; see zlib crc32_combine 127 | # Also see https://stackoverflow.com/questions/23122312/crc-calculation-of-a-mostly-static-data-stream/23126768#23126768 128 | #results[-1] 129 | 130 | for suffix in suffix_list: 131 | checksum = crc32(suffix, results[-1]) 132 | if checksum in checksums: 133 | word = b"".join([alphabet[symbol] for symbol in symbols]) 134 | print("Found '%s' for 0x%08X" % (word + suffix, checksum)) 135 | 136 | if False: 137 | print() 138 | crc = crc32(word) 139 | print(word, "< ref word (0x%08X)" % crc) 140 | assert(results[-1] == crc) 141 | 142 | # Go to next letter 143 | first_valid = len(symbols) - 1 144 | symbols[first_valid] += 1 145 | 146 | # Handle overflow 147 | while symbols[first_valid] >= len(alphabet): 148 | # Check if need a new letter 149 | if first_valid == 0: 150 | # Add a new symbol and reset 151 | symbols = [0] * (len(symbols) + 1) 152 | results += [0] 153 | print("Length %d" % len(symbols)) 154 | break 155 | else: 156 | # Update the prior one 157 | symbols[first_valid] = 0 158 | first_valid -= 1 159 | symbols[first_valid] += 1 160 | 161 | if False: 162 | for i in range(0, first_valid+1): 163 | result = results[i] 164 | print(result, "< hash old [%d %d]" % (i, len(result))) 165 | 166 | for i in range(first_valid+1, len(symbols)+1): 167 | results[i] = results[i - 1] + alphabet[symbols[i - 1]] 168 | result = results[i] 169 | print(result, "< hash updated [%d %d]" % (i, len(result))) 170 | 171 | for i in range(0, len(results)): 172 | result = results[i] 173 | assert(len(result) == i) 174 | 175 | print("O" * first_valid) 176 | print(" " * first_valid + "N" * (len(symbols) - first_valid)) 177 | 178 | 179 | 180 | 181 | 182 | class _FileWriter(): 183 | def __init__(self): 184 | self._contents = [] 185 | def _write(self, data): 186 | self._contents += [data] 187 | def Save(self, path): 188 | with open(path, "wb") as fo: 189 | fo.write(''.join(self._contents).encode('utf-8')) 190 | 191 | class WavefrontMtl(_FileWriter): 192 | def __init__(self): 193 | _FileWriter.__init__(self) 194 | def NewMaterial(self, name): 195 | #FIXME: How to handle spaces? 196 | self._write("newmtl %s\n" % name) 197 | def _map(self, target, name, scale=None): 198 | line = "map_%s" % target 199 | if scale != None: 200 | line += " -s %f %f %f" % scale 201 | #FIXME: Bugs in blender prevent use of quotation marks? Debug.. 202 | # For now, replace spaces by underscore 203 | line += " %s\n" % name.replace(" ", "_") 204 | self._write(line) 205 | def IlluminationMode(self, mode): 206 | self._write("illum %d\n" % mode) 207 | def DiffuseMap(self, name, scale=None): 208 | self._map("Kd", name, scale) 209 | def DissolveMap(self, name, scale=None): 210 | self._map("d", name, scale) 211 | 212 | class WavefrontObj(_FileWriter): 213 | def __init__(self): 214 | _FileWriter.__init__(self) 215 | self._vertex_count = 0 216 | self._normal_count = 0 217 | self._texture_coordinate_count = 0 218 | def Object(self, name): 219 | self._write("o %s\n" % name) 220 | def MaterialLibrary(self, name): 221 | self._write("mtllib %s\n" % name) 222 | def UseMaterial(self, name): 223 | self._write("usemtl %s\n" % name) 224 | def Comment(self, comment): 225 | #FIXME: Split by line and ensure "# " prefix 226 | self._write("# %s\n" % comment) 227 | def Vertex(self, x, y, z): 228 | self._write("v %f %f %f\n" % (x, y, z)) 229 | self._vertex_count += 1 230 | return self._vertex_count 231 | def TextureCoordinate(self, u, v): 232 | self._write("vt %f %f\n" % (u, v)) 233 | self._texture_coordinate_count += 1 234 | return self._texture_coordinate_count 235 | def Normal(self, x, y, z): 236 | self._write("vn %f %f %f\n" % (x, y, z)) 237 | self._normal_count += 1 238 | return self._normal_count 239 | def Face(self, vertex_indices, texture_coordinate_indices, normal_indices): 240 | assert(texture_coordinate_indices == None or len(texture_coordinate_indices) == len(vertex_indices)) 241 | assert(normal_indices == None or len(normal_indices) == len(vertex_indices)) 242 | 243 | line = "f" 244 | for i, vertex_index in enumerate(vertex_indices): 245 | 246 | line += " %d" % vertex_index 247 | 248 | # Helper to keep a clean file 249 | def index(line, indices, skipped=0): 250 | if indices != None: 251 | line += "/" * skipped + "/%d" % indices[i] 252 | return line, 0 253 | return line, skipped + 1 254 | 255 | # Write additional information 256 | line, skipped = index(line, texture_coordinate_indices) 257 | line, skipped = index(line, normal_indices, skipped) 258 | 259 | line += "\n" 260 | 261 | self._write(line) 262 | -------------------------------------------------------------------------------- /convert-psx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import PIL.ImagePalette 6 | 7 | from common import * 8 | 9 | export = True 10 | 11 | for path in sys.argv[1:]: 12 | 13 | print("Converting '%s'" % path) 14 | 15 | with open(path, "rb") as f: 16 | 17 | # Header 0x0003 0x0002 - THPS1 prototype april 9 1999 18 | # Header 0x0004 0x0002 - THPS1 release version 19 | # Header 0x0006 0x0002 - THPS2X release version 20 | 21 | version = read16(f) 22 | unk = read16(f) 23 | 24 | print("Header 0x%04X 0x%04X" % (version, unk)) 25 | assert(version in [3, 4, 6]) 26 | assert(unk == 0x0002) 27 | 28 | tag_start = read32(f) 29 | print(tag_start) 30 | 31 | # Read objects 32 | object_count = read32(f) 33 | print("Objects (%d total)" % object_count) 34 | objects = [] 35 | for i in range(object_count): 36 | class Object(): 37 | def __init__(self): 38 | self.flags = read32(f) 39 | self.x = read32s(f) 40 | self.y = read32s(f) 41 | self.z = read32s(f) 42 | self.unk1 = read32(f) 43 | self.unk2 = read16(f) 44 | self.model_index = read16(f) 45 | self.unk_x = read16s(f) 46 | self.unk_y = read16s(f) 47 | self.unk3 = read32(f) 48 | self.unk_rgbx = read32(f) 49 | print("%d. flags:0x%08X, %d %d %d, 0x%08X, 0x%04X, model:%d, %d %d, 0x%08X, rgbx-pointer:0x%08X" % (i, 50 | self.flags, 51 | self.x, self.y, self.z, 52 | self.unk1, 53 | self.unk2, 54 | self.model_index, 55 | self.unk_x, self.unk_y, 56 | self.unk3, 57 | self.unk_rgbx)) 58 | assert(self.unk1 == 0x00000000) 59 | assert(self.unk2 == 0x0000) 60 | assert(self.unk3 == 0x00000000) 61 | 62 | objects += [Object()] 63 | 64 | # Read models 65 | model_offsets = [] 66 | model_count = read32(f) 67 | print("Models (%d total)" % model_count) 68 | for i in range(model_count): 69 | offset = read32(f) 70 | model_offsets += [offset] 71 | models = [] 72 | for i, offset in enumerate(model_offsets): 73 | f.seek(offset) 74 | 75 | class Model(): 76 | def __init__(self): 77 | 78 | model = self 79 | 80 | # Counts not exported, as implied by list lengths 81 | if version >= 4: 82 | self.unknown_flags = read16(f) 83 | vertex_count = read16(f) 84 | normal_count = read16(f) 85 | face_count = read16(f) 86 | print("0x%X" % self.unknown_flags) 87 | assert(self.unknown_flags in [0x8, 0xA, 0x88]) 88 | else: 89 | self.unknown_flags = read32(f) 90 | vertex_count = read32(f) 91 | normal_count = read32(f) 92 | face_count = read32(f) 93 | print("0x%X" % self.unknown_flags) 94 | assert(self.unknown_flags in [0x8, 0xA, 0x9, 0x88, 0x8A]) 95 | 96 | self.radius = read32(f) 97 | self.xmax = read16s(f) 98 | self.xmin = read16s(f) 99 | self.ymax = read16s(f) 100 | self.ymin = read16s(f) 101 | self.zmax = read16s(f) 102 | self.zmin = read16s(f) 103 | self.unknown_value = read32(f) 104 | 105 | print("%d. flags: 0x%08X radius:%d x:(%d,%d), y:(%d,%d), z:(%d,%d) 0x%08X" % (i, self.unknown_flags, 106 | self.radius, 107 | self.xmax, self.xmin, 108 | self.ymax, self.ymin, 109 | self.zmax, self.zmin, 110 | self.unknown_value)) 111 | 112 | print("Vertices (%d total)" % vertex_count) 113 | self.vertices = [] 114 | for j in range(vertex_count): 115 | class Vertex(): 116 | def __init__(self): 117 | self.x, self.y, self.z = read_struct(f, "= 4: 158 | self.vertex_indices = read_struct(f, "= 6: 204 | for _ in range(4): 205 | u += [read16(f)] 206 | for _ in range(4): 207 | v += [read16(f)] 208 | else: 209 | for _ in range(4): 210 | u += [read8(f)] 211 | v += [read8(f)] 212 | self.uvs = list(zip(u, v)) 213 | 214 | if self.base_flags & 0x8: 215 | v1 = read32(f) 216 | v2 = read32(f) 217 | print("Unknown-0x8-flag: 0x%08X 0x%08X" % (v1, v2)) 218 | #FIXME: Buggy in THPS1 proto april 9 219 | #assert(v1 == 0x00000000) 220 | #assert(v2 == 0x00000000) 221 | 222 | if model.unknown_flags & 1 == 0: #FIXME: Just a guess.. 0x9 unknown_flags fucks up logic otherwise 223 | if self.base_flags & 0x20: 224 | v = read32(f) 225 | print("Unknown-0x20-flag: 0x%08X" % v) 226 | assert(v in [0x00000000]) 227 | 228 | print(f.tell(), next_offset) 229 | 230 | # 0x3 is always dual 231 | #FIXME: according to iamgreaser docs, this should also affect 0x1000 232 | #if self.base_flags & 0x2: assert(self.base_flags & 0x1) #FIXME: Violated in THPS1 proto april 9 233 | if self.base_flags & 0x1: assert(self.base_flags & 0x2) 234 | 235 | unhandled_base_flags = self.base_flags & ~(0x4000 | 0x2000 | 0x1000 | 0x800 | 0x400 | 0x100 | 0x80 | 0x40 | 0x20 | 0x10 | 0x8 | 0x4 | 0x2 | 0x1) 236 | if unhandled_base_flags != 0: 237 | print("Unhandled flags: 0x%04X in 0x%04X" % (unhandled_base_flags, self.base_flags)) 238 | assert(False) 239 | 240 | assert(f.tell() == next_offset) 241 | f.seek(next_offset) #FIXME: Remove 242 | 243 | self.faces += [Face()] 244 | 245 | models += [Model()] 246 | 247 | 248 | 249 | 250 | #FIXME: There exist other flags which add zeros after this, but the purpose of those flags are unknown. 251 | 252 | # Read tags 253 | f.seek(tag_start) 254 | 255 | while True: 256 | tag_type = read32(f) 257 | if tag_type == 0xFFFFFFFF: 258 | break 259 | 260 | tag_length = read32(f) 261 | next_offset = f.tell() + tag_length 262 | 263 | print("Tag 0x%08X at %d" % (tag_type, f.tell())) 264 | 265 | if tag_type == 0x0000000A: 266 | print("Blockmap") 267 | elif tag_type == 0x73424752: 268 | print("RGBs") 269 | else: 270 | print("Unknown tag: 0x%08X" % tag_type) 271 | 272 | #FIXME: Assert that we ended up there? 273 | f.seek(next_offset) 274 | 275 | # Read model names 276 | print("Model names") 277 | model_names = [] 278 | for i in range(model_count): 279 | name = read32(f) 280 | print("%d. %08X" % (i, name)) 281 | model_names += [name] 282 | 283 | # Read texture names 284 | texture_name_count = read32(f) 285 | texture_names = [] 286 | print("Texture names (%d total)" % texture_name_count) 287 | for i in range(texture_name_count): 288 | name = read32(f) 289 | print("%d. %08X" % (i, name)) 290 | texture_names += [name] 291 | 292 | # Read 4bpp palettes 293 | palette4_count = read32(f) 294 | palettes4 = {} 295 | print("Palettes-4bpp (%d total)" % palette4_count) 296 | for i in range(palette4_count): 297 | name = read32(f) 298 | print("%d. %08X" % (i, name)) 299 | palette = read_struct(f, "H"*16) 300 | palettes4[name] = palette 301 | 302 | # Read 8bpp palettes 303 | palette8_count = read32(f) 304 | palettes8 = {} 305 | print("Palettes-8bpp (%d total)" % palette8_count) 306 | for i in range(palette8_count): 307 | name = read32(f) 308 | print("%d. %08X" % (i, name)) 309 | palette = read_struct(f, "H"*256) 310 | palettes8[name] = palette 311 | 312 | # Read textures 313 | texture_offsets = [] 314 | texture_count = read32(f) 315 | if version >= 6: 316 | if (texture_count == 0xFFFFFFFF): 317 | print("???") 318 | 319 | # Read texture references (?) 320 | texture_reference_count = read32(f) 321 | print("Texture references (%d total)" % texture_reference_count) 322 | for i in range(texture_reference_count): 323 | name = read_string(f, 32) 324 | unk = read32(f) 325 | print("%d. Texture reference '%s' 0x%08X" % (i, name, unk)) 326 | assert(unk == 0x00000001) 327 | 328 | # Read cubemap texture references (?) 329 | cubemap_texture_reference_count = read32(f) 330 | print("Cubemap Texture references (%d total)" % cubemap_texture_reference_count) 331 | for i in range(cubemap_texture_reference_count): 332 | name = read_string(f, 32) 333 | unk = read32(f) 334 | assert(unk == 0x00000001) 335 | print("%d. Cubemap Texture reference '%s' 0x%08X" % (i, name, unk)) 336 | #FIXME: Ensure we have read the data that follows this 337 | 338 | texture_count = read32(f) 339 | 340 | print(f.tell()) 341 | 342 | print("Textures (%d total)" % texture_count) 343 | for i in range(texture_count): 344 | offset = read32(f) 345 | print(f.tell(), offset) 346 | texture_offsets += [offset] 347 | 348 | 349 | textures = [] 350 | for i, offset in enumerate(texture_offsets): 351 | 352 | 353 | if f.tell() != offset: 354 | delta = offset - f.tell() 355 | print("BROKEN %d bytes left !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n\n" % delta) 356 | assert(False) 357 | else: 358 | assert(f.tell() == offset) 359 | f.seek(offset) 360 | 361 | class Texture(): 362 | def __init__(self): 363 | self.unk1 = read32(f) 364 | self.color_count = read32(f) 365 | self.palette_name = read32(f) 366 | self.name_index = read32(f) 367 | self.width = read16(f) 368 | self.height = read16(f) 369 | if self.color_count == 16: 370 | alignment = 3 # 4 pixels 371 | elif self.color_count == 256: 372 | alignment = 1 # 2 pixels 373 | elif self.color_count == 65536: 374 | alignment = 0 375 | else: 376 | alignment = 0 377 | assert(False) 378 | self.aligned_width = (self.width + alignment) & ~alignment 379 | self.aligned_height = (self.height + alignment) & ~alignment 380 | #FIXME: Read this texture pixel data as part of texture loading, but don't apply palette yet 381 | print("%d. %d %d 0x%08X %d (-> 0x%08X) %dx%d" % (i, self.unk1, self.color_count, self.palette_name, self.name_index, texture_names[self.name_index], self.width, self.height)) 382 | 383 | texture = Texture() 384 | textures += [texture] 385 | 386 | texture_name = texture_names[texture.name_index] 387 | 388 | # Find the right palette 389 | #FIXME: Might not be necessary? Are all palettes in one table? 390 | if texture.color_count == 16: 391 | palette15 = palettes4[texture.palette_name] 392 | elif texture.color_count == 256: 393 | palette15 = palettes8[texture.palette_name] 394 | elif texture.color_count == 65536: 395 | palette15 = list(range(256)) #palettes16[texture.palette_name] # ??? 396 | else: 397 | palette15 = [] 398 | assert(False) 399 | 400 | # Convert palette to RGB 401 | #FIXME: Just construct DDS header instead 402 | def color15_to_rgb(color15): 403 | #FIXME: Check for magic constant which marks texture as alpha? 404 | r = int(((color15 >> 0) & 0x1F) / 31.0 * 0xFF) 405 | g = int(((color15 >> 5) & 0x1F) / 31.0 * 0xFF) 406 | b = int(((color15 >> 10) & 0x1F) / 31.0 * 0xFF) 407 | #assert(color15 >> 15 == 0) #FIXME: This triggers 408 | return r, g, b 409 | palette_rgb = [channel 410 | for color15 in palette15 411 | for channel in color15_to_rgb(color15)] 412 | 413 | if version >= 6: 414 | # Path taken in SkDemo.psx from THPS2X 415 | 416 | #FIXME: Pick this automatically 417 | if texture.unk1 & 512 or texture.unk1 & 1024 or texture.unk1 & 2048: 418 | unku = f.read(8) 419 | print("version 6 unknown 1a: %s" % unku.hex()) 420 | else: 421 | unku = bytes([]) 422 | if texture.unk1 & 4096: 423 | unkv = f.read(4) # Only second half of 1a? 424 | print("version 6 unknown 1b: %s" % unkv.hex()) 425 | else: 426 | unkv = bytes([]) 427 | print(f.tell()) 428 | unk = read32(f) 429 | print("version 6 unknown 2: 0x%08X" % unk) 430 | assert(unk in [0x00000201, 0x00000301, 0x00000401, 0x00000901]) 431 | length = read32(f) 432 | print("version 6 unknown 3: %d (length?)" % length) 433 | 434 | # SkDemo 435 | # 436 | #1. 1 65536 0x00000000 0 128x128 437 | #60532 438 | #version 6 unknown 2: 0x00000401 439 | #version 6 unknown 3: 7528 (length?) 440 | # 441 | # In reality, this is 32x32 RGB565 [maybe?] 442 | 443 | 444 | # Unknown origin: 445 | # 446 | #2. 16 65536 0x00000000 61 128x64 447 | #798900 448 | #version 6 unknown 2: 0x00000901 449 | #version 6 unknown 3: 16392 (length?) 450 | #version 6 unknown data: 451 | # 452 | # Trustworthy. 128x64 R5G6B5; clearly readable text in image 453 | 454 | 455 | data = f.read(length - 8) 456 | with open(os.path.join("out", "RAW-unk-%08X__%08X-0x%08X-%d-%d-%s-%s.raw" % (unk, texture_name, texture.unk1, texture.width, texture.height, unku.hex(), unkv.hex())), "wb") as fo: 457 | fo.write(data) 458 | 459 | #unk = f.read(10) # Unknown, probably should be *after* data 460 | #print("version 6 unknown data: %s" % data.hex()) 461 | 462 | f1 = read_float(f) 463 | f2 = read_float(f) 464 | print("version 6 unknown pair: %f %f" % (f1, f2)) 465 | continue 466 | 467 | mipmap_levels = 5 #FIXME: This is just an assumption 468 | else: 469 | mipmap_levels = 1 470 | 471 | for mipmap_level in range(mipmap_levels): 472 | 473 | width = texture.aligned_width >> mipmap_level 474 | height = texture.aligned_height >> mipmap_level 475 | 476 | print("Doing mipmap %dx%d" % (width, height)) 477 | 478 | # Create a fitting array for an 8-bit palette 479 | texture_data = bytearray([0x00] * width * height) 480 | 481 | def put_pixel(x, y, palette_index): 482 | color = palette_rgb[palette_index] 483 | texture_data[y * width + x] = palette_index 484 | 485 | # Convert palette indices to 8-bit 486 | #FIXME: Could be more optimized 487 | for y in range(height): 488 | for x in range(width): 489 | if texture.color_count == 16: 490 | # We process 2 pixels at a time for 4bpp 491 | if x % 2 == 0: 492 | try: 493 | palette_indices = read8(f) 494 | except: 495 | print(x, y) 496 | assert(False) 497 | put_pixel(x + 0, y, palette_indices & 0xF) 498 | put_pixel(x + 1, y, (palette_indices >> 4) & 0xF) 499 | elif texture.color_count == 256: 500 | palette_index = read8(f) 501 | put_pixel(x, y, palette_index) 502 | elif texture.color_count == 65536: 503 | texture_data[(y * width) + x] = read16(f) & 0xFF 504 | else: 505 | assert(False) 506 | 507 | if export: 508 | 509 | # Export texture data to PNG 510 | pil_image = PIL.Image.frombytes("P", (width, height), bytes(texture_data)) 511 | pil_image.putpalette(palette_rgb, "RGB") 512 | pil_image.save(os.path.join("out", "%08X-%d.png" % (texture_name, mipmap_level))) 513 | 514 | if export: 515 | 516 | # Export texture metadata to MTL 517 | mtl = WavefrontMtl() 518 | mtl.NewMaterial("%08X" % texture_name) 519 | mtl.DiffuseMap("%08X-0.png" % texture_name, scale=(texture.aligned_width, -texture.aligned_height, 1.0)) 520 | mtl.Save(os.path.join("out", "%08X.mtl" % texture_name)) 521 | 522 | #FIXME: Make assumption where f.tell() should be 523 | # = end of file in version >= 6 ? 524 | print(f.tell()) 525 | 526 | print("Exporting Wavefront OBJ") 527 | 528 | # Create Wavefront OBJ for the world 529 | obj = WavefrontObj() 530 | 531 | # Export models 532 | for object_ in objects: 533 | 534 | model = models[object_.model_index] 535 | model_name = model_names[object_.model_index] 536 | 537 | obj.Object("%08X" % model_name) 538 | 539 | obj_vertex_indices = [] 540 | for vertex in model.vertices: 541 | #obj_vertex_indices += [obj.Vertex(vertex.x, vertex.y, vertex.z)] 542 | obj_vertex_indices += [obj.Vertex((object_.x / 4096.0 + vertex.x) / 4096.0, 543 | (object_.y / 4096.0 + vertex.y) / 4096.0, 544 | (object_.z / 4096.0 + vertex.z) / 4096.0)] 545 | 546 | obj_normal_indices = [] 547 | for normal in model.normals: 548 | obj_normal_indices += [obj.Normal(normal.x, normal.y, normal.z)] 549 | 550 | for j, face in enumerate(model.faces): 551 | 552 | # Skip invisible faces 553 | #FIXME: Just important them separately or something 554 | if face.base_flags & 0x80: 555 | continue 556 | 557 | # Helper to remove last vertex for triangles / re-order vertices for quad 558 | def pick_vertices(vertices): 559 | if face.base_flags & 0x10: 560 | return [vertices[2], vertices[1], vertices[0]] 561 | else: 562 | return [vertices[0], vertices[2], vertices[3], vertices[1]] 563 | 564 | # Get vertices 565 | obj_vertex = [] 566 | for vertex_index in pick_vertices(face.vertex_indices): 567 | obj_vertex += [obj_vertex_indices[vertex_index]] 568 | 569 | # Set material 570 | if face.base_flags & 2: 571 | if model.unknown_flags & 1 == 0: #FIXME: Just a guess.. 0x9 unknown_flags fucks up logic otherwise 572 | texture_name = texture_names[face.texture_index] 573 | else: 574 | texture_name = 0xFFFFFFFF00000000 575 | 576 | if version >= 6: 577 | #FIXME: Hack, because we don't know the texture names when exporting DDX (yet?) 578 | #FIXME: Hack doesn't work 579 | # I also tried: 580 | # - textures[face.texture_index].name_index 581 | # - face.texture_index 582 | for texture_index, texture in enumerate(textures): 583 | if texture.name_index == face.texture_index: 584 | break 585 | if texture.unk1 & 512: 586 | # Some DDS texture? 587 | obj.MaterialLibrary("texture-%d.mtl" % texture_index) 588 | obj.UseMaterial("texture-%d" % texture_index) 589 | else: 590 | # Seems to use embedded texture? 591 | name = "unknown-0x%08X-face:%d-list:%d:0x%08X" % (texture.unk1,face.texture_index,texture_index,texture_names[texture.name_index]) 592 | obj.MaterialLibrary("%s.mtl" % name ) 593 | obj.UseMaterial("%s" % name) 594 | else: 595 | obj.MaterialLibrary("%08X.mtl" % texture_name) 596 | obj.UseMaterial("%08X" % texture_name) 597 | else: 598 | obj.MaterialLibrary("None") 599 | obj.UseMaterial("None") 600 | 601 | # Get optional UVs 602 | if face.base_flags & 1: 603 | obj_texture_coordinate = [] 604 | for u, v in pick_vertices(face.uvs): 605 | 606 | # This is probably a bad idea.. just do this transform while rendering 607 | if False: 608 | # Normalize UV 609 | if face.base_flags & 2: 610 | texture_name = texture_names[face.texture_index] 611 | texture = textures[texture_name] 612 | u /= texture.width 613 | v /= texture.height 614 | else: 615 | # What to do now? 616 | assert(False) 617 | 618 | obj_texture_coordinate += [obj.TextureCoordinate(u, v)] 619 | else: 620 | obj_texture_coordinate = None 621 | 622 | # Get normal 623 | obj_normal = [obj_normal_indices[face.normal_index]] * len(obj_vertex) 624 | 625 | # Create face 626 | #print(obj_vertex) 627 | #print(obj_texture_coordinate) 628 | #print(obj_normal) 629 | obj.Face(obj_vertex, obj_texture_coordinate, obj_normal) 630 | 631 | print(object_.model_index) 632 | 633 | #obj.Save(os.path.join("out", "%d.obj" % object_.model_index)) 634 | obj.Save(os.path.join("out", "world.obj")) 635 | 636 | -------------------------------------------------------------------------------- /convert-thps2x-texture-data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | from xboxpy import nv2a 6 | 7 | from common import * 8 | 9 | 10 | path = sys.argv[1] 11 | mode = int(sys.argv[2]) 12 | width = int(sys.argv[3]) 13 | height = int(sys.argv[4]) 14 | print(mode) 15 | 16 | 17 | if mode == 201: 18 | 19 | # Working 20 | 21 | compressed = False 22 | swizzled = True 23 | mipmap = True 24 | 25 | elif mode == 301: 26 | 27 | # Working 28 | # B0135120-256-256-0x00000301.raw 29 | 30 | compressed = True 31 | swizzled = True 32 | mipmap = False 33 | elif mode == 401: 34 | 35 | # Working 36 | 37 | compressed = True 38 | swizzled = True 39 | mipmap = True 40 | elif mode == 901: 41 | 42 | # Working 43 | 44 | compressed = False 45 | swizzled = False 46 | mipmap = False 47 | else: 48 | assert(False) 49 | 50 | 51 | if mipmap: 52 | level_width = 1 53 | level_height = 1 54 | cursor = 6 if mode == 401 else 2 # ??? 55 | #FIXME: Calculate level count? 56 | else: 57 | level_width = width 58 | level_height = height 59 | cursor = 0 60 | 61 | 62 | with open(path, "rb") as f: 63 | 64 | f.seek(0, os.SEEK_END) 65 | file_size = f.tell() 66 | f.seek(0) 67 | print("file-size", file_size) 68 | 69 | if not compressed: 70 | data = f.read(file_size) 71 | else: 72 | blocks = [] 73 | for _ in range(256): 74 | block = f.read(8) 75 | if block in blocks: 76 | print("Duplicated block..") 77 | blocks += [block] 78 | 79 | # Debug print 4 colors in block 80 | tmp = [] 81 | for b in range(0, 4): 82 | tmp += [block[b*2:(b+1)*2].hex()] 83 | print(tmp) 84 | 85 | assert(len(blocks) == 256) 86 | 87 | # Assemble the data 88 | data = bytes([]) 89 | for x in range(file_size - f.tell()): 90 | data += blocks[read8(f)] 91 | 92 | #FIXME: This is stupid 93 | print(f.tell(), file_size) 94 | 95 | print("data length", len(data)) 96 | open("data.bin", "wb").write(data) 97 | 98 | 99 | # Hack for mipmap 100 | if False: 101 | skip = 0 102 | if mode == 201: 103 | print("Warning: Skipping mipmap levels") 104 | skip += len(data) - width * height * 2 105 | elif mode == 401: 106 | print("Warning: Skipping mipmap levels") 107 | skip += len(data) - width * height * 2 108 | 109 | 110 | # 401 layout for 128x128: 111 | 112 | # (going from highest to lowest address) 113 | # 0xAB00-80 = 80 bytes garbage? [10 block x 0x00 ?] 114 | # 0xAB00-80-128*128*2 = 128x128 115 | # 0xAB00-80-128*128*2-64*64*2 = 64x64 116 | # 0xAB00-80-128*128*2-64*64*2-32x32*2 = 32x32 117 | 118 | # Untested 119 | # 0xAB00-80-128*128*2-64*64*2-32x32*2-16x16*2 = 16x16 120 | # 0xAB00-80-128*128*2-64*64*2-32x32*2-16x16*2-8x8*2 = 8x8 121 | # 0xAB00-80-128*128*2-64*64*2-32x32*2-16x16*2-8x8*2-4x4*2 = 4x4 122 | # 0xAB00-80-128*128*2-64*64*2-32x32*2-16x16*2-8x8*2-4x4*2-2x2*2 = 2x2 123 | # 0xAB00-80-128*128*2-64*64*2-32x32*2-16x16*2-8x8*2-4x4*2-2x2*2-1x1*2 = 1x1 124 | 125 | # Or... from lowest to highest: 126 | # 1x1 = 6 = only last 2 bytes of first block are chosen 127 | # 2x2 = 6+(1*1)*2 = 8 128 | # 4x4 = 6+(1*1+2*2)*2 = 16 129 | # 8x8 = 6+(1*1+2*2+4*4)*2 = 48 130 | # 16x16 = 6+(1*1+2*2+4*4+8*8)*2 = 176 131 | 132 | level = 0 133 | while level_width <= width and level_height <= height: 134 | 135 | 136 | 137 | pitch = level_width * 2 138 | image_length = pitch * level_height 139 | 140 | print("level", level, level_width, level_height, "at", cursor, "length", image_length) 141 | 142 | image = data[cursor:cursor+image_length] 143 | assert(len(image) == image_length) 144 | 145 | #else: 146 | # image = bytes([]) 147 | # for _ in range(): 148 | # image += blocks[read8(f)] 149 | 150 | 151 | 152 | 153 | 154 | 155 | if swizzled: 156 | image = nv2a.Unswizzle(image, 16, (level_width, level_height), pitch) 157 | open("image-%d.bin" % level, "wb").write(image) 158 | 159 | 160 | import PIL.Image 161 | im = PIL.Image.new("RGB", (level_width, level_height)) 162 | 163 | pixels = im.load() 164 | 165 | for y in range(0, level_height): 166 | for x in range(0, level_width): 167 | index = (level_width*y+x)*2 168 | color, = struct.unpack('> 11) / 31.0 * 255.0) 170 | green = int( ((color & 0x07E0) >> 5) / 63.0 * 255.0) 171 | blue = int( ((color & 0x001F)) / 31.0 * 255.0) 172 | 173 | #print red,green,blueh 174 | pixels[x,y] = (red,green,blue) 175 | im.save("image-%d.png" % level) 176 | 177 | #FIXME: Decode image and store on disk 178 | 179 | level_width <<= 1 180 | level_height <<= 1 181 | level += 1 182 | cursor += image_length 183 | 184 | #FIXME: Align cursor to DWORD? 185 | #cursor = (cursor + 3) & ~3 186 | 187 | print(cursor, len(data)) 188 | -------------------------------------------------------------------------------- /disassemble-ddm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | from common import * 6 | 7 | with open(sys.argv[1], "rb") as f: 8 | 9 | # Read some header? 10 | unk1 = read32(f) 11 | assert(unk1 == 0x00000001) 12 | file_length = read32(f) 13 | unk3 = read32(f) 14 | print(".header 0x%08X %d 0x%08X" % (unk1, file_length, unk3)) 15 | 16 | 17 | groups = [] 18 | 19 | for i in range(unk3): 20 | offset = read32(f) 21 | size = read32(f) 22 | #print("%d: size %d # %d" % (offset, unk, size, i)) 23 | groups += [(offset, size)] 24 | 25 | #f.seek(groups[0]) 26 | for offset, size in groups: 27 | print() 28 | print() 29 | print() 30 | print() 31 | 32 | print(f.tell(), offset) 33 | assert(f.tell() == offset) 34 | f.seek(offset) 35 | 36 | print(f.tell()) 37 | 38 | for i in range(1): 39 | unk = read32(f) 40 | print("0x%08X # %d" % (unk, i)) 41 | 42 | # Might be wrong? 43 | checksum = read32(f) 44 | print("Checksum 0x%08X ???" % checksum) 45 | 46 | for i in range(5): 47 | unk = read32(f) 48 | print("0x%08X # %d" % (unk, i)) 49 | 50 | some_name = read_string(f, 64) 51 | print("material-name '%s'" % some_name) 52 | 53 | for i in range(7): 54 | unk = read32(f) # Mostly valid floats? 55 | print("0x%08X # %d" % (unk, i)) 56 | 57 | unk0_count = read32(f) 58 | 59 | unk1_count = read32(f) 60 | unk2_count = read32(f) 61 | unk3_count = read32(f) 62 | print(unk1_count, unk2_count, unk3_count) 63 | 64 | assert(unk0_count == unk3_count) 65 | 66 | for i in range(unk0_count): 67 | print("") 68 | 69 | some_name1 = read_string(f, 64) 70 | some_name2 = read_string(f, 64) 71 | print("name-in-dds: '%s'" % some_name1) 72 | print("dds-name: '%s'" % some_name2) 73 | 74 | index = read32(f) 75 | unk1 = read32(f) 76 | print("%d, 0x%08X" % (index, unk1)) # Some color? 77 | #assert(unk == 0xFFFFFFFF) 78 | 79 | unk2 = read_float(f) # Mostly 0? 80 | unk3 = read_float(f) 81 | unk4 = read_float(f) 82 | print(unk2, unk3, unk4) 83 | 84 | unk = read32(f) 85 | print("Unknown: %d" % unk) 86 | unks = [ 87 | 0x00000000, 88 | 0x00000001, 89 | 0x00000002, 90 | 0x00000003 91 | ] 92 | assert(unk in unks) 93 | 94 | print(f.tell()) 95 | 96 | print() 97 | 98 | for i in range(unk1_count): 99 | unk = [] 100 | unk += [read_float(f)] 101 | unk += [read_float(f)] 102 | unk += [read_float(f)] 103 | # Some of these don't look like floats, but there's 0x3F800000 104 | unk += [read_float(f)] 105 | unk += [read_float(f)] 106 | unk += [read_float(f)] 107 | unk += ["0x%08X" % read32(f)] 108 | unk += [read_float(f)] 109 | unk += [read_float(f)] 110 | print("%d. %s" % (i, unk)) 111 | 112 | print() 113 | 114 | for i in range(unk2_count): 115 | v = read16(f) # Probably an index into previous table 116 | print("%d. 0x%04X" % (i, v)) 117 | assert(v < unk1_count) 118 | 119 | print() 120 | 121 | for i in range(unk3_count): 122 | 123 | u1 = read32(f) 124 | u2 = read16(f) 125 | print("0x%08X" % u1, "0x%04X" % u2) 126 | #assert(u1 == 0x00000000) 127 | 128 | 129 | #if u2 in [0x46, 0xA]: 130 | # f.read(6) 131 | 132 | #data = f.read(offset + size - f.tell()) 133 | #print(unk3_count, "error", len(data), data.hex()) 134 | #assert(len(data) == 0) 135 | 136 | print(f.tell()) 137 | -------------------------------------------------------------------------------- /disassemble-prk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | from common import * 6 | 7 | for path in sys.argv[1:]: 8 | with open(path, "rb") as f: 9 | 10 | # Read some header? 11 | unk1 = read32(f) 12 | unk2 = read32(f) 13 | unk3 = read32(f) # Probably theme 14 | print(".header 0x%08X 0x%08X 0x%08X" % (unk1, unk2, unk3)) 15 | 16 | # Read objects? 17 | 18 | themes = [ 19 | "Power Plant", 20 | "Industrial", 21 | "Outdoor", 22 | "School" 23 | ] 24 | 25 | park_sizes = [ 26 | (16, 16), 27 | (24, 24), 28 | (30, 30), 29 | (30, 18), 30 | (60, 6) 31 | ] 32 | 33 | park_size = park_sizes[unk2] 34 | for i in range(park_size[0] * park_size[1]): # range(256): 35 | 36 | # These are probably parts on this location (some might overlap?) 37 | unk1 = read8(f) # Mostly 0xFF, probably part index [see PSH file] 38 | unk2 = read8(f) # Mostly 0xFF 39 | unk3 = read8(f) # Mostly 0xFF 40 | unk4 = read8(f) 41 | unk5 = read8(f) 42 | 43 | # Padding? 44 | unk6 = read8(f) 45 | 46 | # Some flags probably 47 | unk7 = read8(f) # Mostly 0x01 48 | 49 | # Some index probably 50 | unk8 = read8(f) # Mostly 0x00 51 | print(".object 0x%02X 0x%02X 0x%02X 0x%02X | 0x%02X 0x%02X 0x%02X 0x%02X # %d" % (unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8, i)) 52 | 53 | unk4s = [0x16, 0x3C, 0x4E, 0x5A, 0x92, 0xC9, 0xFF] 54 | unk5s = [0x0E, 0x10, 0x1B, 0x29, 0x2A, 0x2D, 0x34, 0x39, 0x3A, 0x3D, 0x3F, 0x4A, 0x5C, 0x6D, 0x90, 0x91, 0x92, 0xFF] 55 | unk7s = [0x01, 0x02, 0x03, 0x04, 0x05, 0x09, 0x0D, 0x11, 0x13, 0x15, 0x19, 0x1A, 0x1B, 0x1D, 0x22, 0x52, 0x62, 0x7A, 0x83, 0x84, 0x85, 0x9B, 0x9D, 0xEA] 56 | unk8s = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0E, 0x10, 0x011, 0x18, 0x19, 0x1E, 0x1F] 57 | 58 | #assert(unk4 in unk4s) - Probably part index 59 | #assert(unk5 in unk5s) - Probably part index 60 | assert(unk6 == 0x33) 61 | assert(unk8 in unk8s) 62 | # assert(unk7 in unk7s) - too many values.. wtf is this? 63 | 64 | 65 | # Read all gaps 66 | for i in range(10): 67 | 68 | # This is sometimes padding, so it's probably additional info 69 | unk = f.read(8) 70 | print(unk) 71 | 72 | # This is always valid 73 | gap_info = f.read(3) 74 | print(gap_info) 75 | 76 | name = read_string(f, 25) 77 | print(".gap '%s'" % name) 78 | 79 | # Read all highscores? 80 | for i in range(8): 81 | hs = f.read(8) 82 | print(hs) 83 | 84 | pad = f.read(76) 85 | print(pad) 86 | assert(pad == bytes([0x33] * len(pad))) 87 | -------------------------------------------------------------------------------- /disassemble-trg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | from common import * 6 | 7 | # This must be activated for THPS1 files 8 | thps1_file = False 9 | 10 | for path in sys.argv[1:]: 11 | 12 | filename = path.rpartition('/')[2] 13 | 14 | print("# Disassembly of %s" % filename) 15 | 16 | with open(path, "rb") as f: 17 | 18 | def read_array(): 19 | count = read16(f) 20 | array = [] 21 | for i in range(count): 22 | value = read16(f) 23 | array += [value] 24 | return array 25 | 26 | def read_vector(): 27 | x = read32s(f) / 4096.0 28 | y = read32s(f) / 4096.0 29 | z = read32s(f) / 4096.0 30 | return (x, y, z) 31 | 32 | # Object parser 33 | def parse_ob(): 34 | 35 | nodes = read_array() 36 | 37 | pad = align(f, 4) 38 | checksum = read32(f) 39 | 40 | return [nodes, "%4s" % pad.hex(), "0x%08X" % checksum] 41 | 42 | #FIXME: Inline again - this is from early format RE which has proven wrong 43 | def handle_script_garbage(debug=False): 44 | unk1 = [] 45 | 46 | unk2 = read_array() 47 | 48 | # These are only in baddys? 49 | if not debug: 50 | 51 | v4 = read16(f) 52 | v5 = read16(f) 53 | unk1 += ["0x%04X" % v4, "0x%04X" % v5] 54 | 55 | assert(v4 in [0x200, 0x201]) 56 | assert(v5 in [0xFF04, 0xFF05]) 57 | 58 | pad = align(f, 4) 59 | x, y, z = read_vector() 60 | unk1 += [x, y, z] 61 | 62 | v9 = read16(f) 63 | v10 = read16(f) 64 | v11 = read16(f) 65 | unk1 += ["0x%04X" % v9, "0x%04X" % v10, "0x%04X" % v11] 66 | 67 | print("Script-garbage: %4s %s # %s" % (pad.hex(), unk1, unk2)) 68 | 69 | 70 | def disassemble_script(): 71 | 72 | def disassemble_script_value(setter=False): 73 | command = read16s(f) 74 | 75 | #print("-> 0x%04X" % command) 76 | 77 | if command < 0x2000: 78 | assert(not setter) 79 | return "%d" % command 80 | 81 | #FIXME: Disallow setter for some of these 82 | 83 | elif command == 0x2120: 84 | index = read16(f) 85 | if setter: 86 | value = disassemble_script_value() 87 | return "V_REGISTER(%d, %s)" % (index, value) 88 | else: 89 | return "V_REGISTER(%d)" % index 90 | 91 | elif command == 0x2123: 92 | enable = read16(f) 93 | return "V_APPLY_GRAVITY(%d)" % enable #FIXME: ?? 94 | 95 | elif command == 0x2125: 96 | unk1 = read16(f) 97 | unk2 = read16(f) 98 | return "V_ATTRIBUTE(%d, %d)" % (unk1, unk2) 99 | elif command == 0x2127: 100 | x = disassemble_script_value() 101 | y = disassemble_script_value() 102 | z = disassemble_script_value() 103 | return "V_ANGULAR_VELOCITY(%s, %s, %s)" % (x, y, z) 104 | elif command == 0x2128: 105 | x = read16s(f) 106 | y = read16s(f) 107 | z = read16s(f) 108 | return "V_ANGULAR_ACCELERATION(%d, %d, %d)" % (x, y, z) 109 | elif command == 0x2129: 110 | unk1 = read16(f) 111 | return "V_RANDOM(%d)" % unk1 112 | elif command == 0x212A: 113 | return "V_MY_NODE()" 114 | elif command == 0x212B: 115 | node = disassemble_script_value() 116 | return "V_LINKED_NODE(%s)" % node 117 | elif command == 0x212C: 118 | if setter: 119 | value = read16(f) 120 | return "V_INPUT_SIGNAL(%d)" % value 121 | else: 122 | return "V_INPUT_SIGNAL()" 123 | elif command == 0x212F: 124 | align(f, 4) 125 | checksum = read32(f) 126 | return "V_MODEL_CHECKSUM(0x%08X)" % checksum 127 | elif command == 0x2132: 128 | return "V_BRUCE_XZ_DIST()" 129 | elif command == 0x2134: 130 | x = read16s(f) 131 | y = read16s(f) 132 | z = read16s(f) 133 | return "V_VELOCITY(%d, %d, %d)" % (x, y, z) 134 | elif command == 0x2137: 135 | x = read16s(f) 136 | y = read16s(f) 137 | z = read16s(f) 138 | return "V_ANGLES(%d, %d, %d)" % (x, y, z) 139 | 140 | #FIXME: Disallow getters in this block? 141 | 142 | elif command == 0x4100: 143 | return "C_DONE()" 144 | elif command == 0x4102: 145 | label = read16(f) 146 | return "C_GOTO_BREAK(%d)" % label 147 | elif command == 0x4104: 148 | unk1 = read16(f) 149 | return "C_LABEL(%d)" % unk1 150 | elif command == 0x4105: 151 | #FIXME: !!!! 152 | return "C_READ_LABELS()" 153 | elif command == 0x4110: 154 | a = disassemble_script_value() 155 | b = disassemble_script_value() 156 | return "C_ADD(%s, %s)" % (a, b) #FIXME: ? 157 | elif command == 0x4112: 158 | a = disassemble_script_value() 159 | b = disassemble_script_value() 160 | return "C_IF_GT(%s, %s)" % (a, b) 161 | elif command == 0x4113: 162 | a = disassemble_script_value() 163 | b = disassemble_script_value() 164 | return "C_IF_LT(%s, %s)" % (a, b) 165 | elif command == 0x4114: 166 | a = disassemble_script_value() 167 | b = disassemble_script_value() 168 | return "C_IF_EQ(%s, %s)" % (a, b) 169 | elif command == 0x4116: 170 | a = read16(f) 171 | return "C_IF_FLAG_CLEAR(%s)" % (a) 172 | elif command == 0x4120: 173 | return "C_ENDIF()" 174 | elif command == 0x4203: 175 | return "C_DISPLAY_ON()" 176 | elif command == 0x4204: 177 | return "C_DISPLAY_OFF()" 178 | elif command == 0x4205: 179 | return "C_DIE_QUIETLY()" 180 | elif command == 0x4280: 181 | duration = disassemble_script_value() 182 | return "C_WAIT(%s)" % duration 183 | elif command == 0x4281: 184 | return "C_WAIT_FOR_SIGNAL()" 185 | elif command == 0x4290: 186 | n = read16s(f) 187 | return "C_PLAY_SFX(%d)" % n 188 | elif command == 0x4291: 189 | n = read16s(f) 190 | return "C_PLAY_POSITIONAL_SFX(%d)" % n 191 | elif command == 0x4292: 192 | count = read16(f) 193 | return "C_SPARK(%d)" % count 194 | elif command == 0x4293: 195 | align(f, 4) 196 | checksum = read32(f) 197 | return "C_SET_FMV_CHECKSUM(0x%08X)" % checksum 198 | elif command == 0x4294: 199 | return "C_START_FMV()" 200 | elif command == 0x4297: 201 | frames = read16(f) 202 | return "C_MIDI_FADE_OUT(%d)" % frames 203 | elif command == 0x4298: 204 | strength = read16(f) 205 | return "C_SHAKE_CAMERA(%d)" % strength 206 | elif command == 0x4299: 207 | track = read16(f) 208 | return "C_SET_FMV_TRACK(%d)" % track 209 | elif command == 0x429B: 210 | length = read16(f) 211 | damage = read16(f) 212 | r = read16(f) 213 | g = read16(f) 214 | b = read16(f) 215 | scrollspeed = read16(f) 216 | return "C_SMOKEJET_ON(%d, %d, %d, %d, %d, %d)" % (length, damage, r, g, b, scrollspeed) 217 | elif command == 0x429C: 218 | r = read16(f) 219 | g = read16(f) 220 | b = read16(f) 221 | duration = read16(f) 222 | sort = read16(f) 223 | return "C_FLASH_SCREEN(%d, %d, %d, %d, %d)" % (r, g, b, duration, sort) 224 | elif command == 0x429E: 225 | return "C_SET_WATER_LEVEL()" 226 | elif command == 0x429F: 227 | return "C_SMOKEJET_OFF()" 228 | elif command == 0x4221: 229 | node = disassemble_script_value() 230 | return "C_MOVE_TO_NODE(%s)" % (node) 231 | elif command == 0x42B0: 232 | name = read_string(f) 233 | align(f, 2) 234 | return "C_TEXT_MESSAGE(%s)" % code_string(name) 235 | elif command == 0x42B1: 236 | node = disassemble_script_value() 237 | return "C_SEND_PULSE_TO_LINKS(%s)" % node 238 | elif command == 0x42B2: 239 | node = disassemble_script_value() 240 | return "C_SEND_PULSE_TO_LINKS(%s)" % node 241 | elif command == 0x42B3: 242 | node = disassemble_script_value() 243 | return "C_SEND_SIGNAL_TO_NODE(%s)" % node 244 | elif command == 0x42B4: 245 | node = disassemble_script_value() 246 | return "C_SEND_PULSE_TO_NODE(%s)" % node 247 | elif command == 0x42C0: 248 | index = read16(f) 249 | return "C_GOALCOUNTER(%d)" % index 250 | elif command == 0x4301: 251 | return "C_SHATTER()" 252 | elif command == 0x4306: 253 | percent = read16s(f) 254 | frames = read16(f) 255 | return "C_SCALE_X(%d, %d)" % (percent, frames) 256 | elif command == 0x4307: 257 | percent = read16s(f) 258 | frames = read16(f) 259 | return "C_SCALE_Y(%d, %d)" % (percent, frames) 260 | elif command == 0x4308: 261 | percent = read16s(f) 262 | frames = read16(f) 263 | return "C_SCALE_Z(%d, %d)" % (percent, frames) 264 | elif command == 0x4309: 265 | enable = read16(f) 266 | return "C_IS_BOUNCY(%d)" % enable 267 | elif command == 0x4503: 268 | count = read16(f) 269 | return "C_MAKE_RAIN(%d)" % count 270 | elif command == 0x4507: 271 | n = read16s(f) 272 | return "C_PLAY_LOOPING_SFX(%d)" % n 273 | elif command == 0x4508: 274 | n = read16s(f) 275 | u = read16(f) 276 | return "C_PLAY_LOOPING_POSITIONAL_SFX(%d, %d)" % (n, u) 277 | elif command == 0x4509: 278 | return "C_STOP_LOOPING_SFX()" 279 | 280 | print("Unknown script-command: 0x%04X, alignment: %d" % (command, f.tell() % 4)) 281 | 282 | return "Unknown:0x%04X" % command 283 | 284 | 285 | # There's blocks which are reached from others, so they have a indentation already 286 | indent_bias = 1 287 | 288 | indent = indent_bias 289 | while f.tell() < next_offset: 290 | s = disassemble_script_value(True) 291 | assert(s.count("(") == s.count(")")) 292 | if s[0:8] == "C_ENDIF(": 293 | indent -= 1 294 | assert(indent >= 0) #FIXME: This can't be done as the baddys seem to connect? 295 | indent = max(indent, 0) #FIXME: This hack shouldn't be needed 296 | print("%d" % indent + " " * indent + " %d " % f.tell() + s) 297 | if s[0:5] == "C_IF_": 298 | indent += 1 299 | 300 | # Reachable ENDIF when no IF is present 301 | true_bugs = [] 302 | true_bugs += [645, 646, 666, 682, 683, 694, 698, 717, 724] # SKSF_T.TRG 303 | 304 | # Unreachable ENDIF when no IF is present 305 | false_bugs = [] 306 | false_bugs += [576, 657] # SKMAR_T.TRG 307 | false_bugs += [626, 628, 629, 630, 631, 661, 662, 663, 664, 665] # SKNY_T.TRG 308 | false_bugs += [721] # SKSL2_T.TRG 309 | false_bugs += [430] # SKVEN_T.TRG 310 | 311 | assert(indent == indent_bias or node_i in [*false_bugs, *true_bugs]) 312 | 313 | # Command disassembler 314 | def disassemble_commands(): 315 | 316 | while True: 317 | 318 | command = read16(f) 319 | 320 | def disassemble_commands_command(): 321 | if command == 2: 322 | names = [] 323 | while True: 324 | #FIXME: The 2 player restart code in "Re_2P" somehow has no way to know how many strings will follow?! 325 | name = read_string(f) 326 | align(f, 2) 327 | if len(name) == 0: 328 | break 329 | names += [code_string(name)] 330 | return "SetCheatRestarts(%s)" % ", ".join(names) 331 | elif command == 3: 332 | return "SendPulse()" 333 | elif command == 4: 334 | return "SendActivate()" 335 | elif command == 5: 336 | return "SendSuspend()" 337 | elif command == 10: 338 | return "SendSignal()" 339 | elif command == 11: 340 | return "SendKill()" 341 | elif command == 12: 342 | return "SendKillLoudly()" 343 | elif command == 13: 344 | unk1 = read16(f) 345 | return "SendVisible(%d)" % unk1 346 | elif command == 104: 347 | unk1 = read16(f) # Start? 348 | unk2 = read16(f) # End? 349 | unk3 = read16(f) / 4096.0 # Unknown 350 | return "SetFoggingParams(%d, %d, %f)" % (unk1, unk2, unk3) 351 | elif command == 126: 352 | name = read_string(f) 353 | align(f, 2) 354 | return "SpoolIn(%s)" % code_string(name) 355 | elif command == 128: 356 | name = read_string(f) 357 | align(f, 2) 358 | return "SpoolIn(%s)" % code_string(name) 359 | elif command == 130: 360 | unk1 = read16(f) 361 | unk2 = read16(f) 362 | return "SetCamAngle(%d, %d)" % (unk1, unk2) 363 | elif command == 131: 364 | unk = read16(f) 365 | return "BackgroundOn(%d)" % unk 366 | elif command == 132: 367 | unk = read16(f) 368 | return "BackgroundOff(%d)" % unk 369 | elif command == 134: 370 | unk = read16s(f) 371 | return "SetInitialPulses(%d)" % unk 372 | elif command == 135: 373 | unk1 = read16(f) 374 | unk2 = read16(f) 375 | return "SetCamDistXZ(%d, %d)" % (unk1, unk2) 376 | elif command == 140: 377 | name = read_string(f) 378 | align(f, 2) 379 | return "SetRestart(%s)" % code_string(name) 380 | elif command == 141: 381 | #0: Invisible 382 | #1: Visible 383 | #2: InvisibleBoth 384 | #3: VisibleBoth 385 | 386 | #0: OutsideBox 387 | #1: InsideBox 388 | 389 | unk1 = read16(f) 390 | 391 | 392 | #FIXME: How does this work? These should be boxes with { vec3, vec3 } each 393 | if False: 394 | pad = align(f, 4) 395 | unk2 = [] 396 | while True: 397 | unkx = read16(f) 398 | if unkx == 0xFF: 399 | break 400 | if False: 401 | unky = read16(f) 402 | v = (unkx << 16) | unky 403 | unk2 += ["\n" "0x%08X" % v] 404 | for _ in range(5): 405 | v = read32(f) 406 | unk2 += ["0x%08X" % v] 407 | print(unk2) 408 | return "SetVisibilityInBox(%d, %s)" % (unk1, ", ".join(unk2)) 409 | elif command == 142: 410 | name = read_string(f) 411 | align(f, 2) 412 | return "SetObjFile(%s)" % code_string(name) 413 | elif command == 143: 414 | unk1 = read16(f) 415 | unk2 = read16(f) 416 | return "SetCamDistY(%d, %d)" % (unk1, unk2) 417 | elif command == 144: 418 | unk1 = read16(f) 419 | unk2 = read16(f) 420 | return "SetCamOffsetX(%d, %d)" % (unk1, unk2) 421 | elif command == 145: 422 | unk1 = read16(f) 423 | unk2 = read16(f) 424 | return "SetCamOffsetY(%d, %d)" % (unk1, unk2) 425 | elif command == 146: 426 | unk1 = read16(f) 427 | unk2 = read16(f) 428 | return "SetCamOffsetZ(%d, %d)" % (unk1, unk2) 429 | elif command == 147: 430 | index = read16(f) 431 | return "SetGameLevel(%d)" % index 432 | elif command == 149: 433 | return "Endif()" 434 | elif command == 151: 435 | size = read16(f) 436 | return "SetDualBufferSize(%d)" % size 437 | elif command == 152: 438 | #FIXME: Might take an argument? 439 | return "KillBruce()" 440 | elif command == 153: 441 | unk = read16(f) 442 | return "SetCamColijSide(%d)" % unk 443 | elif command == 155: 444 | unk = read16(f) 445 | return "MIDIFadeIn(%d)" % unk 446 | elif command == 157: 447 | unk = read16(f) 448 | return "SetReverbType(%d)" % unk 449 | elif command == 158: 450 | return "EndLevel()" 451 | elif command == 166: 452 | unk = read16(f) 453 | return "SetOTPushback(%d)" % unk 454 | elif command == 167: 455 | unk1 = read16(f) 456 | unk2 = read16(f) 457 | return "SetCamZoom(%d, %d)" % (unk1, unk2) 458 | elif command == 169: 459 | unk = read16(f) 460 | return "SetOTPushback2(%d)" % unk 461 | elif command == 171: 462 | align(f, 4) 463 | checksum = read32(f) 464 | unk2 = read16(f) 465 | unk3 = read16(f) 466 | unk4 = read16(f) 467 | assert(unk2 == 0) 468 | assert(unk3 == 0) 469 | assert(unk4 == 0) 470 | return "BackgroundCreate(0x%08X, %d, %d, %d)" % (checksum, unk2, unk3, unk4) 471 | elif command == 178: 472 | name = read_string(f) 473 | align(f, 2) 474 | return "SetRestart2(%s)" % code_string(name) 475 | elif command == 200: 476 | color_hi = read16(f) 477 | color_lo = read16(f) 478 | color = (color_hi << 16) | color_lo 479 | return "SetFadeColor(0x%08X)" % color 480 | elif command == 202: 481 | color_hi = read16(f) 482 | color_lo = read16(f) 483 | color = (color_hi << 16) | color_lo 484 | return "SetSkyColor(0x%08X)" % color 485 | elif command == 203: 486 | unk = read16(f) 487 | return "SetCareerFlag(%d)" % unk 488 | elif command == 204: 489 | unk = read16(f) 490 | return "IfCareerFlag(%d)" % unk 491 | elif command == 201: 492 | align(f, 4) 493 | checksum = read32(f) 494 | unk3 = read16s(f) 495 | return "GapPolyHit(0x%08X, %d)" % (checksum, unk3) 496 | elif command == 65535: 497 | return "EndCommandList()" 498 | 499 | print("Unknown commands-command: 0x%04X, alignment: %d" % (command, f.tell() % 4)) 500 | 501 | return "Unknown:0x%04X" % command 502 | 503 | s = disassemble_commands_command() 504 | assert(s.count("(") == s.count(")")) 505 | print(s) 506 | if s == "EndCommandList()": 507 | break 508 | 509 | # Read some header? 510 | magic = read32(f) 511 | version = read32(f) 512 | print(".header 0x%08X 0x%08X" % (magic, version)) 513 | 514 | assert(magic == 0x4752545F) 515 | assert(version == 2) 516 | 517 | previous_cursor = None 518 | 519 | bad_node_types = [] 520 | 521 | # Read each node 522 | node_count = read32(f) 523 | node_offsets = [] 524 | for i in range(node_count): 525 | offset = read32(f) 526 | node_offsets += [offset] 527 | 528 | for node_i, (offset, next_offset) in enumerate(zip(node_offsets, node_offsets[1:])): 529 | 530 | assumed_size = next_offset - offset 531 | 532 | f.seek(offset) 533 | 534 | node_type = read16(f) 535 | 536 | print() 537 | print("# node %d at %d (assumed size: %d): type %d" % (node_i, offset, assumed_size, node_type)) 538 | 539 | #print(node_type) 540 | if node_type == 1: # Baddy ?! 541 | 542 | 543 | unk1 = [] 544 | 545 | v1 = read16(f) 546 | v2 = read16(f) 547 | unk1 += ["0x%04X" % v1, "0x%04X" % v2] 548 | 549 | al = f.tell() % 4 550 | 551 | print("%3d: %s %d Baddy: %s %d" % (node_i, filename, al, unk1, f.tell() - offset)) 552 | 553 | assert(v1 in [203, 213, 215, 216, 217, 218, 219] + [0x192] + [0xD1]) # Game object identifier (= baddy type) 554 | assert(v2 in [0x1000, 0x1001]) 555 | 556 | if v2 == 0x1001: 557 | unk1 = read_array() 558 | unk2 = read16(f) 559 | unk3 = read16(f) 560 | align(f, 4) 561 | x, y, z = read_vector() 562 | unk4 = f.read(6).hex() 563 | 564 | assumed_gap = next_offset - f.tell() 565 | print(assumed_gap) 566 | if assumed_gap == 0: 567 | print("THPS1/THPS2") 568 | elif assumed_gap >= 2: 569 | v = read16(f) 570 | print("THPS2-COMMANDS: 0x%04X" % v) 571 | assert(v == 0x0000) 572 | 573 | # This probably doesn't include a script, but some of them still contain a C_DONE() 574 | disassemble_script() 575 | 576 | 577 | # This is the same header as for script 578 | print("unk: %s 0x%04X, 0x%04X, %4s %f %f %f %s" % (unk1, unk2, unk3, pad.hex(), x, y, z, unk4)) #FIXME: There's still a script-command sometimes.. when?! 579 | 580 | #FIXME: If there is still more bytes (only very few files of THPS1 + Bullring), then there must be 2 bytes, followed by script-commands 581 | 582 | 583 | else: 584 | handle_script_garbage(False) 585 | disassemble_script() 586 | 587 | 588 | elif node_type == 2: # Crate 589 | 590 | unk1 = read16(f) 591 | assert(unk1 == 0) 592 | 593 | pad = align(f, 4) 594 | checksum = read32(f) 595 | 596 | print("Crate 0x%04X %4s 0x%08X" % (unk1, pad.hex(), checksum)) 597 | 598 | elif node_type == 3: # Point ?! 599 | 600 | nodes = read_array() 601 | pad = align(f, 4) 602 | x, y, z = read_vector() 603 | 604 | print("Point %s %4s %f %f %f" % (nodes, pad.hex(), x, y, z)) 605 | 606 | elif node_type == 4: # AutoExec 607 | 608 | print("AutoExec:") 609 | disassemble_commands() 610 | 611 | elif node_type == 5: # PowrUp ?! 612 | 613 | # For THPS2 (PC) 614 | pickup_types = { 615 | 4: "KPickup", 616 | 5: "SPickup", 617 | 6: "APickup", 618 | 10: "EPickup", 619 | 15: "TPickup", 620 | 16: "TapePickup", 621 | 21: "BonusPickup100", # Alias: BonusPickup 622 | 22: "BonusPickup200", 623 | 23: "BonusPickup500", 624 | 24: "MoneyPickup250", # Alias: MoneyPickup20 625 | 25: "MoneyPickup50", 626 | 26: "MoneyPickup100", 627 | 33: "LevelPickup" 628 | } 629 | 630 | al = f.tell() % 8 631 | 632 | pickup_type = read16(f) # Type 633 | unk2 = read16(f) 634 | 635 | unk = [] 636 | 637 | tmp = align(f, 4) 638 | 639 | # There appears to be a bug in SKNY_T.TRG where additional info is present for LevelPickup 640 | 641 | assumed_gap = next_offset - f.tell() 642 | print(assumed_gap) 643 | if assumed_gap == 20: 644 | print("THPS1") 645 | elif assumed_gap == 18: 646 | print("THPS2") 647 | elif assumed_gap == 22: 648 | u7 = read16(f) 649 | v8 = read16(f) 650 | unk += ["0x%04X:0x%04X" % (u7, v8)] 651 | print("THPS2-NYBUG") 652 | 653 | 654 | x, y, z = read_vector() 655 | unk += ["%f %f %f" % (x, y, z)] 656 | 657 | u7 = read16(f) 658 | v8 = read16(f) 659 | unk += ["0x%04X:0x%04X" % (u7, v8)] 660 | print(unk) 661 | assert(u7 in [0x0000, 0x0001]) #FIXME: 0x0001 is never seen in THPS2-Windows 662 | assert(v8 == 0x0001) 663 | 664 | v = read16(f) 665 | unk += ["0x%04X" % v] 666 | 667 | #print(unk) 668 | assert(v == 0xFFFF) 669 | 670 | # Another 0xFFFF for THPS1 671 | if assumed_gap == 20: 672 | v = read16(f) 673 | unk += ["0x%04X" % v] 674 | assert(v == 0xFFFF) 675 | 676 | 677 | print("%s %d PowrUp: %4s %02d %s # %s" % (filename, al, tmp.hex(), pickup_type, unk, pickup_types[pickup_type])) 678 | 679 | #assert(v2 in [0x0000, 0xFFFE, 0xFFFF]) 680 | #assert(v4 in [0x0000, 0xFFFF]) 681 | #assert(v6 in [0x0000, 0x0001, 0xFFFE, 0xFFFF]) 682 | 683 | 684 | elif node_type == 6: # CommandPoint 685 | 686 | nodes = read_array() 687 | align(f, 4) 688 | checksum = read32(f) 689 | print("CommandPoint: %s, 0x%08X" % (nodes, checksum)) 690 | 691 | disassemble_commands() 692 | 693 | pass #FIXME 694 | 695 | elif node_type == 8: # Restart 696 | 697 | # Seems to be a list of CommandPoints? 698 | unk_count = read16(f) 699 | unk1 = [] 700 | for j in range(unk_count): 701 | unk1 += [read16(f)] 702 | 703 | al = f.tell() % 4 704 | 705 | #FIXME: Read this, so we can assert 0 padding? 706 | 707 | 708 | # Same stuff as in script-garbage? 709 | 710 | pad = align(f, 4) 711 | x, y, z = read_vector() 712 | 713 | unk2 = read16(f) 714 | unk3 = read16(f) 715 | unk4 = read16(f) 716 | #assert(unk3 == 0x0000) 717 | 718 | 719 | print(f.tell()) 720 | 721 | name = read_string(f) # I assumed 64 bytes, but then we read too much sometimes 722 | align(f, 2) 723 | 724 | #print(read16(f)) 725 | #print(read16(f)) 726 | 727 | if False: 728 | while True: 729 | unk = read_string(f) # I assumed 64 bytes, but then we read too much sometimes 730 | align(f, 2) 731 | print(unk) 732 | if len(unk) == 0: 733 | break 734 | 735 | print("%d %4s Restart: '%s' %s %f %f %f 0x%04X 0x%04X 0x%04X" % (al, pad.hex(), name, unk1, x, y, z, unk2, unk3, unk4)) 736 | 737 | disassemble_commands() 738 | 739 | elif node_type == 10: # RailPoint 740 | 741 | unk2 = read_array() 742 | 743 | al = f.tell() % 4 744 | 745 | pad = align(f, 4) 746 | x, y, z = read_vector() 747 | unk4 = ["%f %f %f" % (x, y, z)] 748 | 749 | unk5 = read16(f) 750 | 751 | assumed_gap = next_offset - f.tell() 752 | if assumed_gap == 6: 753 | unk6 = "THPS1 Prototype: %s" % f.read(4).hex() 754 | unk7 = read16(f) 755 | unk6 += "0x%04X" % unk7 756 | assert(unk7 == 0xFFFF) 757 | elif assumed_gap == 8: 758 | unk6 = "THPS1: %s" % f.read(8).hex() 759 | elif assumed_gap == 4: 760 | unk6 = "THPS2-SKATE_T: %s" % f.read(4).hex() 761 | else: 762 | unk6 = "UNK/THPS2" #FIXME: Also in THPS1 prototype - I assume they added some arguments at some point, but then removed them again? 763 | 764 | print("%s %d RailPoint: %s" % (filename, al, [unk2, "%4s" % pad.hex(), unk4, unk5, unk6])) 765 | 766 | elif node_type == 11: # RailDef 767 | 768 | al = f.tell() % 4 769 | 770 | unk1 = read_array() 771 | 772 | pad = align(f, 4) 773 | x, y, z = read_vector() 774 | unk2 = ["%f %f %f" % (x, y, z)] 775 | 776 | unk3 = f.read(2).hex() 777 | 778 | assumed_gap = next_offset - f.tell() 779 | print(assumed_gap) 780 | unk_extra = [] 781 | if assumed_gap == 8: 782 | print("THPS1") 783 | unk_extra += [f.read(6)] 784 | v = read16(f) 785 | unk_extra += ["0x%04X" % v] 786 | assert(v == 0xFFFF) 787 | else: 788 | print("THPS2") 789 | 790 | print("%s %d RailDef: %s" % (filename, al, [unk1, "%4s" % pad.hex(), unk2, unk3, unk_extra])) 791 | 792 | elif node_type == 12: # TrickOb 793 | 794 | # THPS1 Prototype stuff 795 | if True: 796 | 797 | assumed_gap = next_offset - f.tell() 798 | if assumed_gap == 6: 799 | pad = align(f, 4) 800 | checksum = read32(f) 801 | 802 | assumed_gap = next_offset - f.tell() 803 | if assumed_gap == 2: 804 | v = read16(f) 805 | unk = "THPS1-Prototype=Extra=%d: 0x%08X 0x%04X" % (assumed_gap, checksum, v) 806 | assert(v == 0xFFFF) 807 | else: 808 | unk = "THPS1=%d: 0x%08X" % (assumed_gap, checksum) 809 | 810 | elif assumed_gap > 6: 811 | nodes = read_array() 812 | pad = align(f, 4) 813 | checksum = read32(f) 814 | 815 | assumed_gap = next_offset - f.tell() 816 | if assumed_gap == 2: 817 | v = read16(f) 818 | unk = "THPS1-Extra=%d: %s 0x%08X 0x%04X" % (assumed_gap, nodes, checksum, v) 819 | assert(v == 0xFFFF) 820 | else: 821 | unk = "THPS2=%d: %s 0x%08X" % (assumed_gap, nodes, checksum) 822 | 823 | 824 | elif assumed_gap == 14: 825 | unk = parse_ob() 826 | 827 | unk = "THPS2 %s" % unk 828 | 829 | if thps1_file: 830 | v = read16(f) 831 | unk += ["0x%04X" % v] 832 | #assert(v == 0xFFFF) 833 | else: 834 | unk = "???=%d" % assumed_gap 835 | 836 | 837 | print("TrickOb: %4s %s" % (pad.hex(), unk)) 838 | 839 | 840 | elif node_type == 13: # CamPt 841 | 842 | #CamPtNormal = 1 843 | #CamPtFollow = 2 844 | #CamPtFollow_Close = 0 845 | #CamPtFollow_CloseLow = 1 846 | #CamPtFollow_Normal = 2 847 | #CameraModeNormal = 3 848 | #CamPtLead_Close = 4 849 | #CamPtLead_CloseLow = 5 850 | #CamPtLead_Normal = 6 851 | #CameraModeBoss = 18 852 | 853 | # Bogus names, but this kind of order is expected 854 | cam_link = read16(f) 855 | assert(cam_link == 0) 856 | pad = align(f, 4) 857 | x, y, z = read_vector() 858 | cam_radius = read16(f) 859 | cam_type = read16(f) 860 | print("CamPt: %d %4s %f %f %f, 0x%04X, 0x%04X" % (cam_link, pad.hex(), x, y, z, cam_radius, cam_type)) 861 | 862 | elif node_type == 14: # GoalOb 863 | 864 | unk = parse_ob() 865 | 866 | assumed_gap = next_offset - f.tell() 867 | print(assumed_gap) 868 | if assumed_gap == 2: 869 | v = read16(f) 870 | assert(v == 0xFFFF) 871 | print("THPS1: 0x%04X" % v) 872 | else: 873 | print("THPS2") 874 | 875 | print("GoalOb: %s" % unk) 876 | 877 | elif node_type == 15: # AutoExec2 878 | 879 | print("AutoExec2:") 880 | disassemble_commands() 881 | 882 | elif node_type == 255: # Terminator 883 | 884 | print("Terminator") 885 | 886 | elif node_type == 501: # OffLight 887 | 888 | align(f, 4) 889 | x, y, z = read_vector() 890 | print("OffLight: %f %f %f %s" % (x, y, z, f.read(20).hex())) 891 | 892 | elif node_type == 1000: # ScriptPoint 893 | 894 | print("ScriptPoint:") 895 | handle_script_garbage(True) 896 | disassemble_script() 897 | 898 | else: 899 | print("Unknown node-type %d" % node_type) 900 | 901 | cursor = f.tell() 902 | size = cursor - offset 903 | 904 | # Before we seek, check if we will go where we expect 905 | if size != assumed_size: 906 | delta = assumed_size - size 907 | data = f.read(max(0, delta)) 908 | print("# Bad read size in node (type %d); Reached %d, expected %d (%+d): %s" % (node_type, cursor, offset, delta, data.hex())) 909 | #assert(delta > 0) #FIXME: Re-enable 910 | if not node_type in bad_node_types: 911 | bad_node_types += [node_type] 912 | 913 | print("%s: Review sizes in the following types: %s" % (filename, bad_node_types)) 914 | 915 | 916 | -------------------------------------------------------------------------------- /extract-ddx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | 6 | from common import * 7 | 8 | with open(sys.argv[1], "rb") as f: 9 | 10 | out_path = os.path.join("out") 11 | 12 | try: 13 | os.mkdir(out_path) 14 | except FileExistsError: 15 | pass 16 | 17 | unk1 = read32(f) 18 | unk2 = read32(f) 19 | data_base = read32(f) 20 | count = read32(f) 21 | 22 | print("0x%08X" % unk1) 23 | print("0x%08X" % unk2) # filesize? 24 | print("%d" % data_base) 25 | print("%d files" % count) 26 | 27 | for i in range(count): 28 | offset = read32(f) 29 | size = read32(f) 30 | name = read_string(f, 256) 31 | print("%d: name: '%s' %d %d" % (i, name, offset, size)) 32 | 33 | # Extract file 34 | if True: 35 | file_export_path = os.path.join(out_path, name) 36 | cursor = f.tell() 37 | with open(file_export_path, "wb") as fo: 38 | f.seek(data_base + offset) 39 | data = f.read(size) 40 | fo.write(data) 41 | f.seek(cursor) 42 | 43 | # Export texture metadata to MTL 44 | mtl = WavefrontMtl() 45 | mtl.NewMaterial("texture-%d" % i) 46 | mtl.IlluminationMode(9) 47 | mtl.DiffuseMap("%s" % name, scale=(512.0, -512.0, 1.0)) 48 | #FIXME: Blender would use RGB channel for the alpha value 49 | #mtl.DissolveMap("%s" % name, scale=(512.0, -512.0, 1.0)) 50 | mtl.Save(os.path.join("out", "texture-%d.mtl" % i)) 51 | 52 | 53 | -------------------------------------------------------------------------------- /extract-hed-wad.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | 6 | from common import * 7 | 8 | # Open the HED file 9 | with open(sys.argv[1], "rb") as f: 10 | 11 | # Get file size to know where it ends 12 | f.seek(0, os.SEEK_END) 13 | file_size = f.tell() 14 | f.seek(0) 15 | 16 | # Also open the WAD file 17 | with open(sys.argv[2], "rb") as fw: 18 | 19 | # Loop over HED entries 20 | while f.tell() < file_size - 7: 21 | print(f.tell(), file_size) 22 | name = read_string(f) 23 | #FIXME: Check for terminator? 24 | align(f, 4) 25 | offset = read32(f) 26 | size = read32(f) 27 | 28 | print("file: '%s'" % name) 29 | 30 | fw.seek(offset) 31 | 32 | # Construct path 33 | file_export_path = os.path.join("out", name) 34 | 35 | # Extract file 36 | with open(file_export_path, "wb") as fo: 37 | data = fw.read(size) 38 | fo.write(data) 39 | 40 | terminator = read8(f) 41 | assert(terminator == 0xFF) 42 | -------------------------------------------------------------------------------- /extract-pkr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | 6 | from common import * 7 | 8 | with open(sys.argv[1], "rb") as f: 9 | magic = read32(f) 10 | version = read32(f) 11 | num_dir = read32(f) 12 | num_file = read32(f) 13 | 14 | known_files = 0 15 | 16 | for i in range(num_dir): 17 | name = read_string(f, 32) 18 | print("dir: '%s'" % name) 19 | offset = read32(f) 20 | count = read32(f) 21 | 22 | # Create path 23 | path = name.split('/') 24 | dir_export_path = os.path.join("out", *path) 25 | os.makedirs(dir_export_path, exist_ok=True) 26 | 27 | known_files += count 28 | 29 | # Dump all files 30 | cursor = f.tell() 31 | f.seek(offset) 32 | for j in range(count): 33 | 34 | name = read_string(f, 32) 35 | print("\tfile: '%s'" % name) 36 | unk1 = read32(f) 37 | assert(unk1 == 0xFFFFFFFE) 38 | data_offset = read32(f) 39 | size1 = read32(f) 40 | size2 = read32(f) 41 | assert(size1 == size2) 42 | 43 | file_cursor = f.tell() 44 | f.seek(data_offset) 45 | 46 | # Construct path 47 | file_export_path = os.path.join(dir_export_path, name) 48 | 49 | # Extract file 50 | with open(file_export_path, "wb") as fo: 51 | data = f.read(size1) 52 | fo.write(data) 53 | 54 | f.seek(file_cursor) 55 | 56 | f.seek(cursor) 57 | 58 | 59 | print("%d / %d files known" % (known_files, num_file)) 60 | 61 | --------------------------------------------------------------------------------