hacktools 0.37.0__tar.gz → 0.38.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hacktools-0.37.0/hacktools.egg-info → hacktools-0.38.0}/PKG-INFO +11 -3
- {hacktools-0.37.0 → hacktools-0.38.0}/README.md +1 -1
- hacktools-0.38.0/hacktools/__init__.py +1 -0
- hacktools-0.38.0/hacktools/arch.py +255 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/nitro.py +111 -94
- {hacktools-0.37.0 → hacktools-0.38.0/hacktools.egg-info}/PKG-INFO +11 -3
- hacktools-0.37.0/hacktools/__init__.py +0 -1
- hacktools-0.37.0/hacktools/arch.py +0 -137
- {hacktools-0.37.0 → hacktools-0.38.0}/LICENSE +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/MANIFEST.in +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/assets/icon.png +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/c_ext/cmp_cri.c +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/c_ext/cmp_lzss.c +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/c_ext/cmp_misc.c +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/c_ext/cmp_racjin.c +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/c_ext/inc.h +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/common.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/compression.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/cpk.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/gb.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/gui.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/nasm.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/nds.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/psp.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/psx.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/wii.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools/ws.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools.egg-info/SOURCES.txt +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools.egg-info/dependency_links.txt +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools.egg-info/requires.txt +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/hacktools.egg-info/top_level.txt +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/setup.cfg +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/setup.py +0 -0
- {hacktools-0.37.0 → hacktools-0.38.0}/tests/test_cmp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: hacktools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.38.0
|
|
4
4
|
Summary: A set of utilities and tools for rom hacking and translations.
|
|
5
5
|
Home-page: https://github.com/Illidanz/hacktools
|
|
6
6
|
Author: Illidan
|
|
@@ -47,6 +47,14 @@ Requires-Dist: Pillow; extra == "all"
|
|
|
47
47
|
Requires-Dist: click; extra == "all"
|
|
48
48
|
Requires-Dist: tqdm; extra == "all"
|
|
49
49
|
Requires-Dist: customtkinter; extra == "all"
|
|
50
|
+
Dynamic: author
|
|
51
|
+
Dynamic: classifier
|
|
52
|
+
Dynamic: description
|
|
53
|
+
Dynamic: description-content-type
|
|
54
|
+
Dynamic: home-page
|
|
55
|
+
Dynamic: provides-extra
|
|
56
|
+
Dynamic: requires-python
|
|
57
|
+
Dynamic: summary
|
|
50
58
|
|
|
51
59
|
# hacktools
|
|
52
60
|
A set of utilities and tools for rom hacking and translations.
|
|
@@ -96,7 +104,7 @@ External dependencies not included are marked as `(through *dependency*)`
|
|
|
96
104
|
- BRFNT fonts (through [brfnt2tpl](https://wiki.tockdom.com/wiki/Brfnt2tpl) and [wimgt](https://szs.wiimm.de))
|
|
97
105
|
### Other / Generic
|
|
98
106
|
- CPK archives
|
|
99
|
-
- ARCH archives
|
|
107
|
+
- ARCH archives (including compression/decompression)
|
|
100
108
|
- LZ10, LZ11, Huffman, CRILAYLA and RACJIN compression/decompression
|
|
101
109
|
- ARM/MIPS binary patching
|
|
102
110
|
- xdelta patch creation
|
|
@@ -46,7 +46,7 @@ External dependencies not included are marked as `(through *dependency*)`
|
|
|
46
46
|
- BRFNT fonts (through [brfnt2tpl](https://wiki.tockdom.com/wiki/Brfnt2tpl) and [wimgt](https://szs.wiimm.de))
|
|
47
47
|
### Other / Generic
|
|
48
48
|
- CPK archives
|
|
49
|
-
- ARCH archives
|
|
49
|
+
- ARCH archives (including compression/decompression)
|
|
50
50
|
- LZ10, LZ11, Huffman, CRILAYLA and RACJIN compression/decompression
|
|
51
51
|
- ARM/MIPS binary patching
|
|
52
52
|
- xdelta patch creation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.38.0"
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections import Counter
|
|
3
|
+
from hacktools import common
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ARCHArchive:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.filenum = 0
|
|
9
|
+
self.tableoff = 0
|
|
10
|
+
self.fatoff = 0
|
|
11
|
+
self.nameindexoff = 0
|
|
12
|
+
self.dataoff = 0
|
|
13
|
+
self.files = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ARCHFile:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.name = ""
|
|
19
|
+
self.length = 0
|
|
20
|
+
self.declength = 0
|
|
21
|
+
self.offset = 0
|
|
22
|
+
self.nameoffset = 0
|
|
23
|
+
self.encoded = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read(f):
|
|
27
|
+
f.seek(4) # Magic: ARCH
|
|
28
|
+
archive = ARCHArchive()
|
|
29
|
+
archive.filenum = f.readUInt()
|
|
30
|
+
archive.tableoff = f.readUInt()
|
|
31
|
+
archive.fatoff = f.readUInt()
|
|
32
|
+
archive.nameindexoff = f.readUInt()
|
|
33
|
+
archive.dataoff = f.readUInt()
|
|
34
|
+
common.logDebug("Archive:", vars(archive))
|
|
35
|
+
for i in range(archive.filenum):
|
|
36
|
+
f.seek(archive.fatoff + i * 16)
|
|
37
|
+
subfile = ARCHFile()
|
|
38
|
+
subfile.length = f.readUInt()
|
|
39
|
+
subfile.declength = f.readUInt()
|
|
40
|
+
subfile.offset = f.readUInt()
|
|
41
|
+
subfile.nameoffset = f.readUShort()
|
|
42
|
+
subfile.encoded = f.readUShort() == 1
|
|
43
|
+
f.seek(archive.tableoff + subfile.nameoffset)
|
|
44
|
+
subfile.name = f.readNullString()
|
|
45
|
+
common.logDebug("File", i, vars(subfile))
|
|
46
|
+
archive.files.append(subfile)
|
|
47
|
+
return archive
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def repack(fin, f, archive, infolder):
|
|
51
|
+
# Copy everything up to dataoff
|
|
52
|
+
fin.seek(0)
|
|
53
|
+
f.seek(0)
|
|
54
|
+
f.write(fin.read(archive.dataoff))
|
|
55
|
+
# Loop the files
|
|
56
|
+
dataoff = 0
|
|
57
|
+
for i in range(archive.filenum):
|
|
58
|
+
subfile = archive.files[i]
|
|
59
|
+
filepath = infolder + subfile.name
|
|
60
|
+
if not os.path.isfile(filepath):
|
|
61
|
+
# Just update the offset and copy the file
|
|
62
|
+
f.seek(archive.fatoff + i * 16)
|
|
63
|
+
f.seek(8, 1)
|
|
64
|
+
f.writeUInt(dataoff)
|
|
65
|
+
fin.seek(archive.dataoff + subfile.offset)
|
|
66
|
+
f.seek(archive.dataoff + dataoff)
|
|
67
|
+
f.write(fin.read(subfile.length))
|
|
68
|
+
else:
|
|
69
|
+
size = os.path.getsize(filepath)
|
|
70
|
+
f.seek(archive.fatoff + i * 16)
|
|
71
|
+
# If the file was not compressed, just copy it
|
|
72
|
+
if not subfile.encoded:
|
|
73
|
+
f.writeUInt(size)
|
|
74
|
+
f.writeUInt(size)
|
|
75
|
+
f.writeUInt(dataoff)
|
|
76
|
+
f.seek(2, 1)
|
|
77
|
+
f.writeUShort(0)
|
|
78
|
+
f.seek(archive.dataoff + dataoff)
|
|
79
|
+
with common.Stream(filepath, "rb") as subf:
|
|
80
|
+
f.write(subf.read())
|
|
81
|
+
else:
|
|
82
|
+
common.logDebug("Compressing", subfile.name)
|
|
83
|
+
with common.Stream(filepath, "rb") as subf:
|
|
84
|
+
filedata = subf.read()
|
|
85
|
+
compdata = compress(filedata)
|
|
86
|
+
f.writeUInt(len(compdata))
|
|
87
|
+
f.writeUInt(size)
|
|
88
|
+
f.writeUInt(dataoff)
|
|
89
|
+
f.seek(2, 1)
|
|
90
|
+
f.writeUShort(1)
|
|
91
|
+
f.seek(archive.dataoff + dataoff)
|
|
92
|
+
f.write(compdata)
|
|
93
|
+
# Align with 0s
|
|
94
|
+
if f.tell() % 16 > 0:
|
|
95
|
+
f.writeZero(16 - (f.tell() % 16))
|
|
96
|
+
dataoff = f.tell() - archive.dataoff
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def extract(f, archive, outfolder):
|
|
100
|
+
for subfile in archive.files:
|
|
101
|
+
with common.Stream(outfolder + subfile.name, "wb") as fout:
|
|
102
|
+
f.seek(archive.dataoff + subfile.offset)
|
|
103
|
+
if not subfile.encoded:
|
|
104
|
+
fout.write(f.read(subfile.length))
|
|
105
|
+
else:
|
|
106
|
+
fout.write(decompress(f.read(subfile.length), subfile.declength))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def compress(data):
|
|
110
|
+
# Find unused bytes in the data
|
|
111
|
+
dictkeys = []
|
|
112
|
+
for i in range(1, 0x100):
|
|
113
|
+
dictkeys.append(i)
|
|
114
|
+
for b in data:
|
|
115
|
+
if b in dictkeys:
|
|
116
|
+
dictkeys.remove(b)
|
|
117
|
+
dictvalues = {}
|
|
118
|
+
# Recursively find the most used pair and replace it in the copied data
|
|
119
|
+
content = bytearray(data)
|
|
120
|
+
while True:
|
|
121
|
+
if len(dictkeys) == 0:
|
|
122
|
+
break
|
|
123
|
+
# Write all the pairs in a list, for simplicity will just stick to halfwords
|
|
124
|
+
allpairs = []
|
|
125
|
+
for i in range(len(content) // 2):
|
|
126
|
+
allpairs.append((content[i], content[i+1]))
|
|
127
|
+
# Find the most common one
|
|
128
|
+
c = Counter(allpairs).most_common(1)
|
|
129
|
+
if len(c) < 1:
|
|
130
|
+
break
|
|
131
|
+
pair = c[0]
|
|
132
|
+
if pair[1] < 4:
|
|
133
|
+
break
|
|
134
|
+
dictkey = dictkeys.pop()
|
|
135
|
+
common.logDebug("setting pair", common.toHex(pair[0][0]), common.toHex(pair[0][1]), "with", pair[1], "occurrences as dict key", common.toHex(dictkey))
|
|
136
|
+
dictvalues[dictkey] = pair[0]
|
|
137
|
+
content = content.replace(bytes(pair[0]), bytes([dictkey]))
|
|
138
|
+
with common.Stream() as f:
|
|
139
|
+
# Write the dictionary values
|
|
140
|
+
currentkey = 0
|
|
141
|
+
ordkeys = list(dictvalues.keys())
|
|
142
|
+
ordkeys.sort()
|
|
143
|
+
isconsecutive = False
|
|
144
|
+
# Special case where there are no dict keys
|
|
145
|
+
if len(ordkeys) == 0:
|
|
146
|
+
f.writeByte(0x7f + 0x7f)
|
|
147
|
+
f.writeByte(0x7f)
|
|
148
|
+
f.writeByte(0x7f + 0x7f)
|
|
149
|
+
f.writeByte(0xff)
|
|
150
|
+
currentkey = 0x100
|
|
151
|
+
else:
|
|
152
|
+
for i in range(len(ordkeys)):
|
|
153
|
+
dictkey = ordkeys[i]
|
|
154
|
+
common.logDebug("Writing key", common.toHex(dictkey))
|
|
155
|
+
# If the key is not consecutive, we need to skip places
|
|
156
|
+
if dictkey > currentkey:
|
|
157
|
+
keydiff = dictkey - currentkey
|
|
158
|
+
# Since we can only skip 0x7f bytes, we need to do an additional skip if it's bigger
|
|
159
|
+
while keydiff > 0x7f:
|
|
160
|
+
f.writeByte(0x7f + 0x7f)
|
|
161
|
+
# Also write a byte equal to the index
|
|
162
|
+
f.writeByte(0x7f)
|
|
163
|
+
keydiff -= 0x80
|
|
164
|
+
f.writeByte(keydiff + 0x7f)
|
|
165
|
+
currentkey = dictkey
|
|
166
|
+
isconsecutive = False
|
|
167
|
+
elif not isconsecutive:
|
|
168
|
+
# If this is the first time we're writing a key, we need to check how many consecutive ones there are
|
|
169
|
+
consecutive = 1
|
|
170
|
+
for j in range(i+1, len(ordkeys)):
|
|
171
|
+
if ordkeys[j] == dictkey + consecutive:
|
|
172
|
+
consecutive += 1
|
|
173
|
+
f.writeByte(consecutive - 1)
|
|
174
|
+
isconsecutive = True
|
|
175
|
+
common.logDebug("Writing key pairs", common.toHex(dictvalues[dictkey][0]), common.toHex(dictvalues[dictkey][1]))
|
|
176
|
+
f.writeByte(dictvalues[dictkey][0])
|
|
177
|
+
# Don't write the 2nd byte if it's the same as the index (shouldn't happen)
|
|
178
|
+
if dictvalues[dictkey][1] != dictkey:
|
|
179
|
+
f.writeByte(dictvalues[dictkey][1])
|
|
180
|
+
currentkey += 1
|
|
181
|
+
# We're forced to write all indexes even if they aren't used
|
|
182
|
+
if currentkey < 0x100:
|
|
183
|
+
f.writeByte(0x100 - currentkey - 1)
|
|
184
|
+
while currentkey < 0x100:
|
|
185
|
+
f.writeByte(currentkey)
|
|
186
|
+
currentkey += 1
|
|
187
|
+
# Write the actual content
|
|
188
|
+
numloopspos = f.tell()
|
|
189
|
+
f.writeByte(0)
|
|
190
|
+
f.writeByte(0)
|
|
191
|
+
numloops = 0
|
|
192
|
+
for b in content:
|
|
193
|
+
f.writeByte(b)
|
|
194
|
+
numloops += 1
|
|
195
|
+
f.seek(numloopspos)
|
|
196
|
+
f.writeByte(numloops >> 8)
|
|
197
|
+
f.writeByte(numloops & 0xff)
|
|
198
|
+
f.seek(0)
|
|
199
|
+
return f.read()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def decompress(data, declen):
|
|
203
|
+
with common.Stream() as f:
|
|
204
|
+
with common.Stream() as fout:
|
|
205
|
+
f.write(data)
|
|
206
|
+
f.seek(0)
|
|
207
|
+
# Based on Tinke's ARCH implementation
|
|
208
|
+
buffer1 = []
|
|
209
|
+
buffer2 = []
|
|
210
|
+
for i in range(0x100):
|
|
211
|
+
buffer1.append(0)
|
|
212
|
+
buffer2.append(0)
|
|
213
|
+
while f.tell() < len(data):
|
|
214
|
+
# InitBuffer
|
|
215
|
+
for i in range(0x100):
|
|
216
|
+
buffer2[i] = i
|
|
217
|
+
# FillBuffer
|
|
218
|
+
index = 0
|
|
219
|
+
while index != 0x100:
|
|
220
|
+
bufid = f.readByte()
|
|
221
|
+
numloops = bufid
|
|
222
|
+
if bufid > 0x7f:
|
|
223
|
+
numloops = 0
|
|
224
|
+
index += bufid - 0x7f
|
|
225
|
+
if index == 0x100:
|
|
226
|
+
break
|
|
227
|
+
if numloops < 0:
|
|
228
|
+
continue
|
|
229
|
+
for i in range(numloops + 1):
|
|
230
|
+
byte = f.readByte()
|
|
231
|
+
buffer2[index] = byte
|
|
232
|
+
if byte != index:
|
|
233
|
+
buffer1[index] = f.readByte()
|
|
234
|
+
index += 1
|
|
235
|
+
# Process
|
|
236
|
+
numloops = (f.readByte() << 8) + f.readByte()
|
|
237
|
+
common.logDebug("Decompressing with", common.toHex(numloops), "loops starting at", common.toHex(f.tell()))
|
|
238
|
+
nextsamples = []
|
|
239
|
+
while True:
|
|
240
|
+
if len(nextsamples) == 0:
|
|
241
|
+
if numloops == 0:
|
|
242
|
+
break
|
|
243
|
+
numloops -= 1
|
|
244
|
+
index = f.readByte()
|
|
245
|
+
else:
|
|
246
|
+
index = nextsamples.pop()
|
|
247
|
+
if buffer2[index] == index:
|
|
248
|
+
fout.writeByte(index)
|
|
249
|
+
else:
|
|
250
|
+
nextsamples.append(buffer1[index])
|
|
251
|
+
nextsamples.append(buffer2[index])
|
|
252
|
+
index = len(nextsamples)
|
|
253
|
+
common.logDebug("Finished at", common.toHex(f.tell()), "with numloops", common.toHex(numloops))
|
|
254
|
+
fout.seek(0)
|
|
255
|
+
return fout.read()
|
|
@@ -1310,6 +1310,102 @@ class NSBMDPalette:
|
|
|
1310
1310
|
self.size = 0
|
|
1311
1311
|
self.data = []
|
|
1312
1312
|
|
|
1313
|
+
def readTEX0(nsbmd, f, zerotransp=False):
|
|
1314
|
+
# Read TEX0 block
|
|
1315
|
+
f.seek(nsbmd.blockoffset + 4)
|
|
1316
|
+
nsbmd.blocksize = f.readUInt()
|
|
1317
|
+
nsbmd.blocklimit = nsbmd.blocksize + nsbmd.blockoffset
|
|
1318
|
+
f.seek(4, 1)
|
|
1319
|
+
nsbmd.texdatasize = f.readUShort() * 8
|
|
1320
|
+
f.seek(6, 1)
|
|
1321
|
+
nsbmd.texdataoffset = f.readUInt() + nsbmd.blockoffset
|
|
1322
|
+
f.seek(4, 1)
|
|
1323
|
+
nsbmd.sptexsize = f.readUShort() * 8
|
|
1324
|
+
f.seek(6, 1)
|
|
1325
|
+
nsbmd.sptexoffset = f.readUInt() + nsbmd.blockoffset
|
|
1326
|
+
nsbmd.spdataoffset = f.readUInt() + nsbmd.blockoffset
|
|
1327
|
+
f.seek(4, 1)
|
|
1328
|
+
nsbmd.paldatasize = f.readUShort() * 8
|
|
1329
|
+
f.seek(2, 1)
|
|
1330
|
+
nsbmd.paldefoffset = f.readUInt() + nsbmd.blockoffset
|
|
1331
|
+
nsbmd.paldataoffset = f.readUInt() + nsbmd.blockoffset
|
|
1332
|
+
common.logDebug(vars(nsbmd))
|
|
1333
|
+
# Texture definition
|
|
1334
|
+
f.seek(1, 1)
|
|
1335
|
+
texnum = f.readByte()
|
|
1336
|
+
pos = f.tell()
|
|
1337
|
+
f.seek(nsbmd.paldefoffset + 1)
|
|
1338
|
+
palnum = f.readByte()
|
|
1339
|
+
f.seek(pos)
|
|
1340
|
+
common.logDebug("texnum:", texnum, "palnum:", palnum)
|
|
1341
|
+
f.seek(14 + (texnum * 4), 1)
|
|
1342
|
+
for i in range(texnum):
|
|
1343
|
+
offset = f.readUShort() * 8
|
|
1344
|
+
param = f.readUShort()
|
|
1345
|
+
f.seek(4, 1)
|
|
1346
|
+
tex = NSBMDTexture()
|
|
1347
|
+
tex.format = (param >> 10) & 7
|
|
1348
|
+
tex.width = 8 << ((param >> 4) & 7)
|
|
1349
|
+
tex.height = 8 << ((param >> 7) & 7)
|
|
1350
|
+
tex.size = tex.width * tex.height * NSBMDbpp[tex.format] // 8
|
|
1351
|
+
if tex.format == 5:
|
|
1352
|
+
tex.offset = offset + nsbmd.sptexoffset
|
|
1353
|
+
else:
|
|
1354
|
+
tex.offset = offset + nsbmd.texdataoffset
|
|
1355
|
+
nsbmd.textures.append(tex)
|
|
1356
|
+
# Texture name
|
|
1357
|
+
for tex in nsbmd.textures:
|
|
1358
|
+
tex.name = f.readString(16)
|
|
1359
|
+
common.logDebug(vars(tex))
|
|
1360
|
+
# Palette definition
|
|
1361
|
+
f.seek(nsbmd.paldefoffset + 2 + 14 + (palnum * 4))
|
|
1362
|
+
for i in range(palnum):
|
|
1363
|
+
pal = NSBMDPalette()
|
|
1364
|
+
pal.offset = (f.readUShort() * 8) + nsbmd.paldataoffset
|
|
1365
|
+
f.seek(2, 1)
|
|
1366
|
+
nsbmd.palettes.append(pal)
|
|
1367
|
+
# Palette size
|
|
1368
|
+
if palnum > 0:
|
|
1369
|
+
for i in range(palnum):
|
|
1370
|
+
r = i + 1
|
|
1371
|
+
while r < len(nsbmd.palettes) and nsbmd.palettes[r].offset == nsbmd.palettes[i].offset:
|
|
1372
|
+
r += 1
|
|
1373
|
+
if r != palnum:
|
|
1374
|
+
nsbmd.palettes[i].size = nsbmd.palettes[r].offset - nsbmd.palettes[i].offset
|
|
1375
|
+
else:
|
|
1376
|
+
nsbmd.palettes[i].size = nsbmd.blocklimit - nsbmd.palettes[i].offset
|
|
1377
|
+
nsbmd.palettes[i].size = nsbmd.blocklimit - nsbmd.palettes[i].offset
|
|
1378
|
+
# Palette name
|
|
1379
|
+
for pal in nsbmd.palettes:
|
|
1380
|
+
pal.name = f.readString(16)
|
|
1381
|
+
common.logDebug(vars(pal))
|
|
1382
|
+
# Traverse palettes
|
|
1383
|
+
for pal in nsbmd.palettes:
|
|
1384
|
+
f.seek(pal.offset)
|
|
1385
|
+
for i in range(pal.size // 2):
|
|
1386
|
+
palcolor = common.readPalette(f.readShort())
|
|
1387
|
+
if i == 0 and zerotransp:
|
|
1388
|
+
palcolor = (palcolor[0], palcolor[1], palcolor[2], 0)
|
|
1389
|
+
pal.data.append(palcolor)
|
|
1390
|
+
# Traverse texture
|
|
1391
|
+
spdataoffset = nsbmd.spdataoffset
|
|
1392
|
+
for texi in range(len(nsbmd.textures)):
|
|
1393
|
+
tex = nsbmd.textures[texi]
|
|
1394
|
+
if tex.format == 5:
|
|
1395
|
+
r = tex.size >> 1
|
|
1396
|
+
f.seek(spdataoffset)
|
|
1397
|
+
for i in range(r // 2):
|
|
1398
|
+
tex.spdata.append(f.readUShort())
|
|
1399
|
+
spdataoffset += r
|
|
1400
|
+
# Export texture
|
|
1401
|
+
f.seek(tex.offset)
|
|
1402
|
+
if tex.format == 5:
|
|
1403
|
+
for i in range(tex.size // 4):
|
|
1404
|
+
tex.data.append(f.readUInt())
|
|
1405
|
+
else:
|
|
1406
|
+
tex.data = f.read(tex.size)
|
|
1407
|
+
return nsbmd
|
|
1408
|
+
|
|
1313
1409
|
|
|
1314
1410
|
def readNSBMD(nsbmdfile, zerotransp=False):
|
|
1315
1411
|
nsbmd = NSBMD()
|
|
@@ -1328,100 +1424,18 @@ def readNSBMD(nsbmdfile, zerotransp=False):
|
|
|
1328
1424
|
f.seek(-4, 1)
|
|
1329
1425
|
texstart = f.readUShort()
|
|
1330
1426
|
nsbmd.blockoffset = nsbmdstart + texstart
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
f.seek(
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
nsbmd
|
|
1343
|
-
nsbmd.spdataoffset = f.readUInt() + nsbmd.blockoffset
|
|
1344
|
-
f.seek(4, 1)
|
|
1345
|
-
nsbmd.paldatasize = f.readUShort() * 8
|
|
1346
|
-
f.seek(2, 1)
|
|
1347
|
-
nsbmd.paldefoffset = f.readUInt() + nsbmd.blockoffset
|
|
1348
|
-
nsbmd.paldataoffset = f.readUInt() + nsbmd.blockoffset
|
|
1349
|
-
common.logDebug(vars(nsbmd))
|
|
1350
|
-
# Texture definition
|
|
1351
|
-
f.seek(1, 1)
|
|
1352
|
-
texnum = f.readByte()
|
|
1353
|
-
pos = f.tell()
|
|
1354
|
-
f.seek(nsbmd.paldefoffset + 1)
|
|
1355
|
-
palnum = f.readByte()
|
|
1356
|
-
f.seek(pos)
|
|
1357
|
-
common.logDebug("texnum:", texnum, "palnum:", palnum)
|
|
1358
|
-
f.seek(14 + (texnum * 4), 1)
|
|
1359
|
-
for i in range(texnum):
|
|
1360
|
-
offset = f.readUShort() * 8
|
|
1361
|
-
param = f.readUShort()
|
|
1362
|
-
f.seek(4, 1)
|
|
1363
|
-
tex = NSBMDTexture()
|
|
1364
|
-
tex.format = (param >> 10) & 7
|
|
1365
|
-
tex.width = 8 << ((param >> 4) & 7)
|
|
1366
|
-
tex.height = 8 << ((param >> 7) & 7)
|
|
1367
|
-
tex.size = tex.width * tex.height * NSBMDbpp[tex.format] // 8
|
|
1368
|
-
if tex.format == 5:
|
|
1369
|
-
tex.offset = offset + nsbmd.sptexoffset
|
|
1370
|
-
else:
|
|
1371
|
-
tex.offset = offset + nsbmd.texdataoffset
|
|
1372
|
-
nsbmd.textures.append(tex)
|
|
1373
|
-
# Texture name
|
|
1374
|
-
for tex in nsbmd.textures:
|
|
1375
|
-
tex.name = f.readString(16)
|
|
1376
|
-
common.logDebug(vars(tex))
|
|
1377
|
-
# Palette definition
|
|
1378
|
-
f.seek(nsbmd.paldefoffset + 2 + 14 + (palnum * 4))
|
|
1379
|
-
for i in range(palnum):
|
|
1380
|
-
pal = NSBMDPalette()
|
|
1381
|
-
pal.offset = (f.readUShort() * 8) + nsbmd.paldataoffset
|
|
1382
|
-
f.seek(2, 1)
|
|
1383
|
-
nsbmd.palettes.append(pal)
|
|
1384
|
-
# Palette size
|
|
1385
|
-
if palnum > 0:
|
|
1386
|
-
for i in range(palnum):
|
|
1387
|
-
r = i + 1
|
|
1388
|
-
while r < len(nsbmd.palettes) and nsbmd.palettes[r].offset == nsbmd.palettes[i].offset:
|
|
1389
|
-
r += 1
|
|
1390
|
-
if r != palnum:
|
|
1391
|
-
nsbmd.palettes[i].size = nsbmd.palettes[r].offset - nsbmd.palettes[i].offset
|
|
1392
|
-
else:
|
|
1393
|
-
nsbmd.palettes[i].size = nsbmd.blocklimit - nsbmd.palettes[i].offset
|
|
1394
|
-
nsbmd.palettes[i].size = nsbmd.blocklimit - nsbmd.palettes[i].offset
|
|
1395
|
-
# Palette name
|
|
1396
|
-
for pal in nsbmd.palettes:
|
|
1397
|
-
pal.name = f.readString(16)
|
|
1398
|
-
common.logDebug(vars(pal))
|
|
1399
|
-
# Traverse palettes
|
|
1400
|
-
for pal in nsbmd.palettes:
|
|
1401
|
-
f.seek(pal.offset)
|
|
1402
|
-
for i in range(pal.size // 2):
|
|
1403
|
-
palcolor = common.readPalette(f.readShort())
|
|
1404
|
-
if i == 0 and zerotransp:
|
|
1405
|
-
palcolor = (palcolor[0], palcolor[1], palcolor[2], 0)
|
|
1406
|
-
pal.data.append(palcolor)
|
|
1407
|
-
# Traverse texture
|
|
1408
|
-
spdataoffset = nsbmd.spdataoffset
|
|
1409
|
-
for texi in range(len(nsbmd.textures)):
|
|
1410
|
-
tex = nsbmd.textures[texi]
|
|
1411
|
-
if tex.format == 5:
|
|
1412
|
-
r = tex.size >> 1
|
|
1413
|
-
f.seek(spdataoffset)
|
|
1414
|
-
for i in range(r // 2):
|
|
1415
|
-
tex.spdata.append(f.readUShort())
|
|
1416
|
-
spdataoffset += r
|
|
1417
|
-
# Export texture
|
|
1418
|
-
f.seek(tex.offset)
|
|
1419
|
-
if tex.format == 5:
|
|
1420
|
-
for i in range(tex.size // 4):
|
|
1421
|
-
tex.data.append(f.readUInt())
|
|
1422
|
-
else:
|
|
1423
|
-
tex.data = f.read(tex.size)
|
|
1424
|
-
return nsbmd
|
|
1427
|
+
return readTEX0(nsbmd, f, zerotransp)
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def readNSBTX(nsbmdfile, zerotransp=False):
|
|
1431
|
+
nsbmd = NSBMD()
|
|
1432
|
+
with common.Stream(nsbmdfile, "rb") as f:
|
|
1433
|
+
nsbmdstart = 0
|
|
1434
|
+
# Read the TEX0 offset
|
|
1435
|
+
f.seek(nsbmdstart + 0x10)
|
|
1436
|
+
texstart = f.readUShort()
|
|
1437
|
+
nsbmd.blockoffset = nsbmdstart + texstart
|
|
1438
|
+
return readTEX0(nsbmd, f, zerotransp)
|
|
1425
1439
|
|
|
1426
1440
|
|
|
1427
1441
|
def drawNSBMD(file, nsbmd, texi):
|
|
@@ -1431,6 +1445,9 @@ def drawNSBMD(file, nsbmd, texi):
|
|
|
1431
1445
|
common.logError("PIL not found")
|
|
1432
1446
|
return
|
|
1433
1447
|
tex = nsbmd.textures[texi]
|
|
1448
|
+
if tex.format != 7 and len(nsbmd.palettes) == 0:
|
|
1449
|
+
common.logDebug("Skipping texture with no palettes")
|
|
1450
|
+
return
|
|
1434
1451
|
common.logDebug("Exporting", tex.name, "...")
|
|
1435
1452
|
palette = None
|
|
1436
1453
|
if tex.format != 7:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: hacktools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.38.0
|
|
4
4
|
Summary: A set of utilities and tools for rom hacking and translations.
|
|
5
5
|
Home-page: https://github.com/Illidanz/hacktools
|
|
6
6
|
Author: Illidan
|
|
@@ -47,6 +47,14 @@ Requires-Dist: Pillow; extra == "all"
|
|
|
47
47
|
Requires-Dist: click; extra == "all"
|
|
48
48
|
Requires-Dist: tqdm; extra == "all"
|
|
49
49
|
Requires-Dist: customtkinter; extra == "all"
|
|
50
|
+
Dynamic: author
|
|
51
|
+
Dynamic: classifier
|
|
52
|
+
Dynamic: description
|
|
53
|
+
Dynamic: description-content-type
|
|
54
|
+
Dynamic: home-page
|
|
55
|
+
Dynamic: provides-extra
|
|
56
|
+
Dynamic: requires-python
|
|
57
|
+
Dynamic: summary
|
|
50
58
|
|
|
51
59
|
# hacktools
|
|
52
60
|
A set of utilities and tools for rom hacking and translations.
|
|
@@ -96,7 +104,7 @@ External dependencies not included are marked as `(through *dependency*)`
|
|
|
96
104
|
- BRFNT fonts (through [brfnt2tpl](https://wiki.tockdom.com/wiki/Brfnt2tpl) and [wimgt](https://szs.wiimm.de))
|
|
97
105
|
### Other / Generic
|
|
98
106
|
- CPK archives
|
|
99
|
-
- ARCH archives
|
|
107
|
+
- ARCH archives (including compression/decompression)
|
|
100
108
|
- LZ10, LZ11, Huffman, CRILAYLA and RACJIN compression/decompression
|
|
101
109
|
- ARM/MIPS binary patching
|
|
102
110
|
- xdelta patch creation
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.37.0"
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from hacktools import common
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class ARCHArchive:
|
|
6
|
-
def __init__(self):
|
|
7
|
-
self.filenum = 0
|
|
8
|
-
self.tableoff = 0
|
|
9
|
-
self.fatoff = 0
|
|
10
|
-
self.nameindexoff = 0
|
|
11
|
-
self.dataoff = 0
|
|
12
|
-
self.files = []
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class ARCHFile:
|
|
16
|
-
def __init__(self):
|
|
17
|
-
self.name = ""
|
|
18
|
-
self.length = 0
|
|
19
|
-
self.declength = 0
|
|
20
|
-
self.offset = 0
|
|
21
|
-
self.nameoffset = 0
|
|
22
|
-
self.encoded = False
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def read(f):
|
|
26
|
-
f.seek(4) # Magic: ARCH
|
|
27
|
-
archive = ARCHArchive()
|
|
28
|
-
archive.filenum = f.readUInt()
|
|
29
|
-
archive.tableoff = f.readUInt()
|
|
30
|
-
archive.fatoff = f.readUInt()
|
|
31
|
-
archive.nameindexoff = f.readUInt()
|
|
32
|
-
archive.dataoff = f.readUInt()
|
|
33
|
-
common.logDebug("Archive:", vars(archive))
|
|
34
|
-
for i in range(archive.filenum):
|
|
35
|
-
f.seek(archive.fatoff + i * 16)
|
|
36
|
-
subfile = ARCHFile()
|
|
37
|
-
subfile.length = f.readUInt()
|
|
38
|
-
subfile.declength = f.readUInt()
|
|
39
|
-
subfile.offset = f.readUInt()
|
|
40
|
-
subfile.nameoffset = f.readUShort()
|
|
41
|
-
subfile.encoded = f.readUShort() == 1
|
|
42
|
-
f.seek(archive.tableoff + subfile.nameoffset)
|
|
43
|
-
subfile.name = f.readNullString()
|
|
44
|
-
common.logDebug("File", i, vars(subfile))
|
|
45
|
-
archive.files.append(subfile)
|
|
46
|
-
return archive
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def repack(fin, f, archive, infolder):
|
|
50
|
-
# Copy everything up to dataoff
|
|
51
|
-
fin.seek(0)
|
|
52
|
-
f.seek(0)
|
|
53
|
-
f.write(fin.read(archive.dataoff))
|
|
54
|
-
# Loop the files
|
|
55
|
-
dataoff = 0
|
|
56
|
-
for i in range(archive.filenum):
|
|
57
|
-
subfile = archive.files[i]
|
|
58
|
-
filepath = infolder + subfile.name
|
|
59
|
-
if not os.path.isfile(filepath):
|
|
60
|
-
# Just update the offset and copy the file
|
|
61
|
-
f.seek(archive.fatoff + i * 16)
|
|
62
|
-
f.seek(8, 1)
|
|
63
|
-
f.writeUInt(dataoff)
|
|
64
|
-
fin.seek(archive.dataoff + subfile.offset)
|
|
65
|
-
f.seek(archive.dataoff + dataoff)
|
|
66
|
-
f.write(fin.read(subfile.length))
|
|
67
|
-
else:
|
|
68
|
-
# Set the file as not encoded and copy it
|
|
69
|
-
size = os.path.getsize(filepath)
|
|
70
|
-
f.seek(archive.fatoff + i * 16)
|
|
71
|
-
f.writeUInt(size)
|
|
72
|
-
f.writeUInt(size)
|
|
73
|
-
f.writeUInt(dataoff)
|
|
74
|
-
f.seek(2, 1)
|
|
75
|
-
f.writeUShort(0)
|
|
76
|
-
f.seek(archive.dataoff + dataoff)
|
|
77
|
-
with common.Stream(filepath, "rb") as subf:
|
|
78
|
-
f.write(subf.read())
|
|
79
|
-
# Align with 0s
|
|
80
|
-
if f.tell() % 16 > 0:
|
|
81
|
-
f.writeZero(16 - (f.tell() % 16))
|
|
82
|
-
dataoff = f.tell() - archive.dataoff
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def extract(f, archive, outfolder):
|
|
86
|
-
for subfile in archive.files:
|
|
87
|
-
with common.Stream(outfolder + subfile.name, "wb") as fout:
|
|
88
|
-
f.seek(archive.dataoff + subfile.offset)
|
|
89
|
-
if not subfile.encoded:
|
|
90
|
-
fout.write(f.read(subfile.length))
|
|
91
|
-
else:
|
|
92
|
-
# Based on Tinke's ARCH implementation
|
|
93
|
-
startpos = f.tell()
|
|
94
|
-
buffer1 = []
|
|
95
|
-
buffer2 = []
|
|
96
|
-
for i in range(0x100):
|
|
97
|
-
buffer1.append(0)
|
|
98
|
-
buffer2.append(0)
|
|
99
|
-
while f.tell() - startpos < subfile.length:
|
|
100
|
-
# InitBuffer
|
|
101
|
-
for i in range(0x100):
|
|
102
|
-
buffer2[i] = i
|
|
103
|
-
# FillBuffer
|
|
104
|
-
index = 0
|
|
105
|
-
while index != 0x100:
|
|
106
|
-
bufid = f.readByte()
|
|
107
|
-
numloops = bufid
|
|
108
|
-
if bufid > 0x7f:
|
|
109
|
-
numloops = 0
|
|
110
|
-
index += bufid - 0x7f
|
|
111
|
-
if index == 0x100:
|
|
112
|
-
break
|
|
113
|
-
if numloops < 0:
|
|
114
|
-
continue
|
|
115
|
-
for i in range(numloops + 1):
|
|
116
|
-
byte = f.readByte()
|
|
117
|
-
buffer2[index] = byte
|
|
118
|
-
if byte != index:
|
|
119
|
-
buffer1[index] = f.readByte()
|
|
120
|
-
index += 1
|
|
121
|
-
# Process
|
|
122
|
-
numloops = (f.readByte() << 8) + f.readByte()
|
|
123
|
-
nextsamples = []
|
|
124
|
-
while True:
|
|
125
|
-
if len(nextsamples) == 0:
|
|
126
|
-
if numloops == 0:
|
|
127
|
-
break
|
|
128
|
-
numloops -= 1
|
|
129
|
-
index = f.readByte()
|
|
130
|
-
else:
|
|
131
|
-
index = nextsamples.pop()
|
|
132
|
-
if buffer2[index] == index:
|
|
133
|
-
fout.writeByte(index)
|
|
134
|
-
else:
|
|
135
|
-
nextsamples.append(buffer1[index])
|
|
136
|
-
nextsamples.append(buffer2[index])
|
|
137
|
-
index = len(nextsamples)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|