hacktools 0.36.0__cp313-cp313-win_amd64.whl
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/__init__.py +1 -0
- hacktools/arch.py +137 -0
- hacktools/assets/icon.png +0 -0
- hacktools/cmp_cri.cp313-win_amd64.pyd +0 -0
- hacktools/cmp_lzss.cp313-win_amd64.pyd +0 -0
- hacktools/cmp_misc.cp313-win_amd64.pyd +0 -0
- hacktools/cmp_racjin.cp313-win_amd64.pyd +0 -0
- hacktools/common.py +1471 -0
- hacktools/compression.py +216 -0
- hacktools/cpk.py +643 -0
- hacktools/gb.py +92 -0
- hacktools/gui.py +304 -0
- hacktools/nasm.py +84 -0
- hacktools/nds.py +367 -0
- hacktools/nitro.py +1751 -0
- hacktools/psp.py +1257 -0
- hacktools/psx.py +295 -0
- hacktools/wii.py +285 -0
- hacktools/ws.py +564 -0
- hacktools-0.36.0.dist-info/LICENSE +21 -0
- hacktools-0.36.0.dist-info/METADATA +102 -0
- hacktools-0.36.0.dist-info/RECORD +24 -0
- hacktools-0.36.0.dist-info/WHEEL +5 -0
- hacktools-0.36.0.dist-info/top_level.txt +1 -0
hacktools/common.py
ADDED
|
@@ -0,0 +1,1471 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
from io import BytesIO, StringIO
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
import logging
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
import struct
|
|
12
|
+
import subprocess
|
|
13
|
+
import typing
|
|
14
|
+
import zlib
|
|
15
|
+
|
|
16
|
+
hasClick = False
|
|
17
|
+
hasTqdm = False
|
|
18
|
+
hasGUI = False
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import click
|
|
22
|
+
hasClick = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from tqdm import tqdm
|
|
28
|
+
hasTqdm = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
table = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# File reading
|
|
36
|
+
class Stream(object):
|
|
37
|
+
def __init__(self, fpath="", mode="m", little=True):
|
|
38
|
+
self.f = fpath
|
|
39
|
+
self.mode = mode
|
|
40
|
+
self.endian = "<" if little else ">"
|
|
41
|
+
self.half = None
|
|
42
|
+
|
|
43
|
+
def __enter__(self):
|
|
44
|
+
if self.mode == "m":
|
|
45
|
+
self.f = BytesIO()
|
|
46
|
+
else:
|
|
47
|
+
self.f = open(self.f, self.mode)
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
51
|
+
self.f.close()
|
|
52
|
+
|
|
53
|
+
def close(self):
|
|
54
|
+
self.f.close()
|
|
55
|
+
|
|
56
|
+
def tell(self):
|
|
57
|
+
return self.f.tell()
|
|
58
|
+
|
|
59
|
+
def seek(self, pos, whence=0):
|
|
60
|
+
self.f.seek(pos, whence)
|
|
61
|
+
|
|
62
|
+
def read(self, n=-1):
|
|
63
|
+
return self.f.read(n)
|
|
64
|
+
|
|
65
|
+
def readAt(self, pos, n=-1):
|
|
66
|
+
current = self.tell()
|
|
67
|
+
self.seek(pos)
|
|
68
|
+
ret = self.read(n)
|
|
69
|
+
self.seek(current)
|
|
70
|
+
return ret
|
|
71
|
+
|
|
72
|
+
def write(self, data):
|
|
73
|
+
self.f.write(data)
|
|
74
|
+
|
|
75
|
+
def writeAt(self, pos, data):
|
|
76
|
+
current = self.tell()
|
|
77
|
+
self.seek(pos)
|
|
78
|
+
self.write(data)
|
|
79
|
+
self.seek(current)
|
|
80
|
+
|
|
81
|
+
def peek(self, n):
|
|
82
|
+
pos = self.tell()
|
|
83
|
+
ret = self.read(n)
|
|
84
|
+
self.seek(pos)
|
|
85
|
+
return ret
|
|
86
|
+
|
|
87
|
+
def writeLine(self, data):
|
|
88
|
+
self.f.write(data + "\n")
|
|
89
|
+
|
|
90
|
+
def setEndian(self, little):
|
|
91
|
+
self.endian = "<" if little else ">"
|
|
92
|
+
|
|
93
|
+
def swapEndian(self):
|
|
94
|
+
self.endian = "<" if self.endian == ">" else ">"
|
|
95
|
+
|
|
96
|
+
def readLong(self):
|
|
97
|
+
return struct.unpack(self.endian + "q", self.read(8))[0]
|
|
98
|
+
|
|
99
|
+
def readLongAt(self, pos):
|
|
100
|
+
current = self.tell()
|
|
101
|
+
self.seek(pos)
|
|
102
|
+
ret = self.readLong()
|
|
103
|
+
self.seek(current)
|
|
104
|
+
return ret
|
|
105
|
+
|
|
106
|
+
def readULong(self):
|
|
107
|
+
return struct.unpack(self.endian + "Q", self.read(8))[0]
|
|
108
|
+
|
|
109
|
+
def readULongAt(self, pos):
|
|
110
|
+
current = self.tell()
|
|
111
|
+
self.seek(pos)
|
|
112
|
+
ret = self.readULong()
|
|
113
|
+
self.seek(current)
|
|
114
|
+
return ret
|
|
115
|
+
|
|
116
|
+
def readInt(self):
|
|
117
|
+
return struct.unpack(self.endian + "i", self.read(4))[0]
|
|
118
|
+
|
|
119
|
+
def readIntAt(self, pos):
|
|
120
|
+
current = self.tell()
|
|
121
|
+
self.seek(pos)
|
|
122
|
+
ret = self.readInt()
|
|
123
|
+
self.seek(current)
|
|
124
|
+
return ret
|
|
125
|
+
|
|
126
|
+
def readUInt(self):
|
|
127
|
+
return struct.unpack(self.endian + "I", self.read(4))[0]
|
|
128
|
+
|
|
129
|
+
def readUIntAt(self, pos):
|
|
130
|
+
current = self.tell()
|
|
131
|
+
self.seek(pos)
|
|
132
|
+
ret = self.readUInt()
|
|
133
|
+
self.seek(current)
|
|
134
|
+
return ret
|
|
135
|
+
|
|
136
|
+
def readFloat(self):
|
|
137
|
+
return struct.unpack(self.endian + "f", self.read(4))[0]
|
|
138
|
+
|
|
139
|
+
def readFloatAt(self, pos):
|
|
140
|
+
current = self.tell()
|
|
141
|
+
self.seek(pos)
|
|
142
|
+
ret = self.readFloat()
|
|
143
|
+
self.seek(current)
|
|
144
|
+
return ret
|
|
145
|
+
|
|
146
|
+
def readDouble(self):
|
|
147
|
+
return struct.unpack(self.endian + "d", self.read(8))[0]
|
|
148
|
+
|
|
149
|
+
def readDoubleAt(self, pos):
|
|
150
|
+
current = self.tell()
|
|
151
|
+
self.seek(pos)
|
|
152
|
+
ret = self.readDouble()
|
|
153
|
+
self.seek(current)
|
|
154
|
+
return ret
|
|
155
|
+
|
|
156
|
+
def readShort(self):
|
|
157
|
+
return struct.unpack(self.endian + "h", self.read(2))[0]
|
|
158
|
+
|
|
159
|
+
def readShortAt(self, pos):
|
|
160
|
+
current = self.tell()
|
|
161
|
+
self.seek(pos)
|
|
162
|
+
ret = self.readShort()
|
|
163
|
+
self.seek(current)
|
|
164
|
+
return ret
|
|
165
|
+
|
|
166
|
+
def readUShort(self):
|
|
167
|
+
return struct.unpack(self.endian + "H", self.read(2))[0]
|
|
168
|
+
|
|
169
|
+
def readUShortAt(self, pos):
|
|
170
|
+
current = self.tell()
|
|
171
|
+
self.seek(pos)
|
|
172
|
+
ret = self.readUShort()
|
|
173
|
+
self.seek(current)
|
|
174
|
+
return ret
|
|
175
|
+
|
|
176
|
+
def readByte(self):
|
|
177
|
+
return struct.unpack("B", self.read(1))[0]
|
|
178
|
+
|
|
179
|
+
def readByteAt(self, pos):
|
|
180
|
+
current = self.tell()
|
|
181
|
+
self.seek(pos)
|
|
182
|
+
ret = self.readByte()
|
|
183
|
+
self.seek(current)
|
|
184
|
+
return ret
|
|
185
|
+
|
|
186
|
+
def readSByte(self):
|
|
187
|
+
return struct.unpack("b", self.read(1))[0]
|
|
188
|
+
|
|
189
|
+
def readSByteAt(self, pos):
|
|
190
|
+
current = self.tell()
|
|
191
|
+
self.seek(pos)
|
|
192
|
+
ret = self.readSByte()
|
|
193
|
+
self.seek(current)
|
|
194
|
+
return ret
|
|
195
|
+
|
|
196
|
+
def readHalf(self, big=False):
|
|
197
|
+
if self.half is None:
|
|
198
|
+
self.half = self.readByte()
|
|
199
|
+
if big:
|
|
200
|
+
return self.half >> 4
|
|
201
|
+
return self.half & 0x0f
|
|
202
|
+
else:
|
|
203
|
+
ret = self.half >> 4 if not big else self.half & 0x0f
|
|
204
|
+
self.half = None
|
|
205
|
+
return ret
|
|
206
|
+
|
|
207
|
+
def readZeros(self, size):
|
|
208
|
+
while self.tell() < size:
|
|
209
|
+
byte = self.readByte()
|
|
210
|
+
if byte != 0x00:
|
|
211
|
+
self.seek(-1, 1)
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
def readBytes(self, n, upper=False):
|
|
215
|
+
ret = ""
|
|
216
|
+
for i in range(n):
|
|
217
|
+
ret += toHex(self.readByte(), upper) + " "
|
|
218
|
+
return ret
|
|
219
|
+
|
|
220
|
+
def readString(self, length):
|
|
221
|
+
ret = ""
|
|
222
|
+
for _ in range(length):
|
|
223
|
+
byte = self.readByte()
|
|
224
|
+
# These control characters can be found in texture names, replace them with a space
|
|
225
|
+
if byte == 0x82 or byte == 0x86:
|
|
226
|
+
byte = 0x20
|
|
227
|
+
if byte != 0:
|
|
228
|
+
ret += chr(byte)
|
|
229
|
+
return ret
|
|
230
|
+
|
|
231
|
+
def readStringAt(self, pos, length):
|
|
232
|
+
current = self.tell()
|
|
233
|
+
self.seek(pos)
|
|
234
|
+
ret = self.readString(length)
|
|
235
|
+
self.seek(current)
|
|
236
|
+
return ret
|
|
237
|
+
|
|
238
|
+
def readNullString(self):
|
|
239
|
+
ret = ""
|
|
240
|
+
while True:
|
|
241
|
+
byte = self.readByte()
|
|
242
|
+
if byte == 0:
|
|
243
|
+
break
|
|
244
|
+
else:
|
|
245
|
+
ret += chr(byte)
|
|
246
|
+
return ret
|
|
247
|
+
|
|
248
|
+
def readNullStringAt(self, pos):
|
|
249
|
+
current = self.tell()
|
|
250
|
+
self.seek(pos)
|
|
251
|
+
ret = self.readNullString()
|
|
252
|
+
self.seek(current)
|
|
253
|
+
return ret
|
|
254
|
+
|
|
255
|
+
def readEncodedString(self, encoding="utf-8"):
|
|
256
|
+
num = 0
|
|
257
|
+
pos = self.tell()
|
|
258
|
+
while True:
|
|
259
|
+
byte = self.readByte()
|
|
260
|
+
if byte == 0:
|
|
261
|
+
break
|
|
262
|
+
else:
|
|
263
|
+
num += 1
|
|
264
|
+
self.seek(pos)
|
|
265
|
+
ret = self.read(num).decode(encoding)
|
|
266
|
+
self.readByte()
|
|
267
|
+
return ret
|
|
268
|
+
|
|
269
|
+
def readEncodedStringAt(self, pos, encoding="utf-8"):
|
|
270
|
+
current = self.tell()
|
|
271
|
+
self.seek(pos)
|
|
272
|
+
ret = self.readEncodedString(encoding)
|
|
273
|
+
self.seek(current)
|
|
274
|
+
return ret
|
|
275
|
+
|
|
276
|
+
def writeLong(self, num):
|
|
277
|
+
self.f.write(struct.pack(self.endian + "q", num))
|
|
278
|
+
|
|
279
|
+
def writeLongAt(self, pos, num):
|
|
280
|
+
current = self.tell()
|
|
281
|
+
self.seek(pos)
|
|
282
|
+
self.writeLong(num)
|
|
283
|
+
self.seek(current)
|
|
284
|
+
|
|
285
|
+
def writeULong(self, num):
|
|
286
|
+
self.f.write(struct.pack(self.endian + "Q", num))
|
|
287
|
+
|
|
288
|
+
def writeULongAt(self, pos, num):
|
|
289
|
+
current = self.tell()
|
|
290
|
+
self.seek(pos)
|
|
291
|
+
self.writeULong(num)
|
|
292
|
+
self.seek(current)
|
|
293
|
+
|
|
294
|
+
def writeInt(self, num):
|
|
295
|
+
self.f.write(struct.pack(self.endian + "i", num))
|
|
296
|
+
|
|
297
|
+
def writeIntAt(self, pos, num):
|
|
298
|
+
current = self.tell()
|
|
299
|
+
self.seek(pos)
|
|
300
|
+
self.writeInt(num)
|
|
301
|
+
self.seek(current)
|
|
302
|
+
|
|
303
|
+
def writeUInt(self, num):
|
|
304
|
+
self.f.write(struct.pack(self.endian + "I", num))
|
|
305
|
+
|
|
306
|
+
def writeUIntAt(self, pos, num):
|
|
307
|
+
current = self.tell()
|
|
308
|
+
self.seek(pos)
|
|
309
|
+
self.writeUInt(num)
|
|
310
|
+
self.seek(current)
|
|
311
|
+
|
|
312
|
+
def writeFloat(self, num):
|
|
313
|
+
self.f.write(struct.pack(self.endian + "f", num))
|
|
314
|
+
|
|
315
|
+
def writeFloatAt(self, pos, num):
|
|
316
|
+
current = self.tell()
|
|
317
|
+
self.seek(pos)
|
|
318
|
+
self.writeFloat(num)
|
|
319
|
+
self.seek(current)
|
|
320
|
+
|
|
321
|
+
def writeDouble(self, num):
|
|
322
|
+
self.f.write(struct.pack(self.endian + "d", num))
|
|
323
|
+
|
|
324
|
+
def writeDoubleAt(self, pos, num):
|
|
325
|
+
current = self.tell()
|
|
326
|
+
self.seek(pos)
|
|
327
|
+
self.writeDouble(num)
|
|
328
|
+
self.seek(current)
|
|
329
|
+
|
|
330
|
+
def writeShort(self, num):
|
|
331
|
+
self.f.write(struct.pack(self.endian + "h", num))
|
|
332
|
+
|
|
333
|
+
def writeShortAt(self, pos, num):
|
|
334
|
+
current = self.tell()
|
|
335
|
+
self.seek(pos)
|
|
336
|
+
self.writeShort(num)
|
|
337
|
+
self.seek(current)
|
|
338
|
+
|
|
339
|
+
def writeUShort(self, num):
|
|
340
|
+
self.f.write(struct.pack(self.endian + "H", num))
|
|
341
|
+
|
|
342
|
+
def writeUShortAt(self, pos, num):
|
|
343
|
+
current = self.tell()
|
|
344
|
+
self.seek(pos)
|
|
345
|
+
self.writeUShort(num)
|
|
346
|
+
self.seek(current)
|
|
347
|
+
|
|
348
|
+
def writeByte(self, num):
|
|
349
|
+
self.f.write(struct.pack("B", num))
|
|
350
|
+
|
|
351
|
+
def writeByteAt(self, pos, num):
|
|
352
|
+
current = self.tell()
|
|
353
|
+
self.seek(pos)
|
|
354
|
+
self.writeByte(num)
|
|
355
|
+
self.seek(current)
|
|
356
|
+
|
|
357
|
+
def writeSByte(self, num):
|
|
358
|
+
self.f.write(struct.pack("b", num))
|
|
359
|
+
|
|
360
|
+
def writeSByteAt(self, pos, num):
|
|
361
|
+
current = self.tell()
|
|
362
|
+
self.seek(pos)
|
|
363
|
+
self.writeSByte(num)
|
|
364
|
+
self.seek(current)
|
|
365
|
+
|
|
366
|
+
def writeHalf(self, num, little=True):
|
|
367
|
+
if self.half is None:
|
|
368
|
+
self.half = num
|
|
369
|
+
else:
|
|
370
|
+
if little:
|
|
371
|
+
self.writeByte((num << 4) | self.half)
|
|
372
|
+
else:
|
|
373
|
+
self.writeByte((self.half << 4) | num)
|
|
374
|
+
self.half = None
|
|
375
|
+
|
|
376
|
+
def writeString(self, str):
|
|
377
|
+
self.f.write(str.encode("ascii"))
|
|
378
|
+
|
|
379
|
+
def writeZero(self, num):
|
|
380
|
+
self.writeBytes(0x0, num)
|
|
381
|
+
|
|
382
|
+
def writeBytes(self, byte, num):
|
|
383
|
+
if num > 0:
|
|
384
|
+
for i in range(num):
|
|
385
|
+
self.writeByte(byte)
|
|
386
|
+
|
|
387
|
+
def truncate(self):
|
|
388
|
+
self.f.truncate()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# CLI/GUI
|
|
392
|
+
if hasClick:
|
|
393
|
+
appname = ""
|
|
394
|
+
appversion = ""
|
|
395
|
+
datafolder = ""
|
|
396
|
+
filecheck = ""
|
|
397
|
+
crc = -1
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def setupTool(_appname="", _appversion="", _datafolder="", _filecheck="", _crc=-1):
|
|
401
|
+
global appname, appversion, datafolder, filecheck, crc
|
|
402
|
+
appname = _appname
|
|
403
|
+
appversion = _appversion
|
|
404
|
+
datafolder = _datafolder
|
|
405
|
+
filecheck = _filecheck
|
|
406
|
+
crc = _crc
|
|
407
|
+
cli()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@click.group(invoke_without_command=True)
|
|
411
|
+
@click.option("--log", is_flag=True, default=False)
|
|
412
|
+
@click.option("--gui", is_flag=True, default=False)
|
|
413
|
+
@click.pass_context
|
|
414
|
+
def cli(ctx, log, gui):
|
|
415
|
+
setupFileLogging(log)
|
|
416
|
+
if ctx.invoked_subcommand is None:
|
|
417
|
+
multi = typing.cast(click.MultiCommand, ctx.command)
|
|
418
|
+
ctx.invoke(multi.get_command(ctx, "main"), gui=gui)
|
|
419
|
+
else:
|
|
420
|
+
if not runStartup():
|
|
421
|
+
quit()
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@cli.command(hidden=True)
|
|
425
|
+
@click.option("--gui", is_flag=True, default=False)
|
|
426
|
+
def main(gui):
|
|
427
|
+
if not gui:
|
|
428
|
+
runCLI()
|
|
429
|
+
else:
|
|
430
|
+
runGUI()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def runStartup():
|
|
434
|
+
if datafolder != "" and not os.path.isdir(datafolder):
|
|
435
|
+
makeFolder(datafolder)
|
|
436
|
+
if appname != "":
|
|
437
|
+
logMessage(appname + " version " + appversion)
|
|
438
|
+
logMessage("Python", sys.version)
|
|
439
|
+
from . import __version__
|
|
440
|
+
logMessage("hacktools version", __version__)
|
|
441
|
+
if filecheck != "" and not os.path.isfile(filecheck):
|
|
442
|
+
logError(filecheck, "file not found.")
|
|
443
|
+
return False
|
|
444
|
+
if crc >= 0 and not os.path.isfile(".skipcrc"):
|
|
445
|
+
checkcrc = crcFile(filecheck)
|
|
446
|
+
if crc != checkcrc:
|
|
447
|
+
logMessage("Checksum mismatch for", filecheck, "(" + toHex(checkcrc) + ", expected", toHex(crc) + ")")
|
|
448
|
+
logMessage("The tool might still work but you should run it on a good dump of the game.")
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def runCLI():
|
|
453
|
+
if not runStartup():
|
|
454
|
+
quit()
|
|
455
|
+
if len(sys.argv) > 1:
|
|
456
|
+
cli()
|
|
457
|
+
return
|
|
458
|
+
with click.Context(cli) as ctx:
|
|
459
|
+
click.echo(cli.get_help(ctx))
|
|
460
|
+
click.echo("")
|
|
461
|
+
while True:
|
|
462
|
+
cmd = click.prompt("Type a command").strip()
|
|
463
|
+
cmdlow = cmd.lower()
|
|
464
|
+
if cmdlow == "exit" or cmdlow == "quit" or cmdlow == "q":
|
|
465
|
+
break
|
|
466
|
+
sys.argv = shlex.split(sys.argv[0] + " " + cmd)
|
|
467
|
+
cli(standalone_mode=False)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def runGUI():
|
|
471
|
+
global hasGUI
|
|
472
|
+
hasGUI = True
|
|
473
|
+
from .gui import GUIApp
|
|
474
|
+
guiapp = GUIApp()
|
|
475
|
+
guiapp.initialize(cli, appname, appversion, datafolder)
|
|
476
|
+
guiapp.mainloop()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# Logging
|
|
480
|
+
class FileFilter(logging.Filter):
|
|
481
|
+
def filter(self, record):
|
|
482
|
+
return not record.getMessage().startswith("prg-")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def setupFileLogging(log):
|
|
486
|
+
logging.getLogger("PIL.PngImagePlugin").setLevel(logging.CRITICAL + 1)
|
|
487
|
+
filehandler = logging.FileHandler(filename="tool.log", encoding="utf-8", mode="w")
|
|
488
|
+
filehandler.addFilter(FileFilter())
|
|
489
|
+
logging.basicConfig(handlers=[filehandler], format="[%(levelname)s] %(message)s", level=logging.DEBUG if log else logging.INFO)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def logMessage(*messages):
|
|
493
|
+
message = " ".join(str(x) for x in messages)
|
|
494
|
+
logging.info(message)
|
|
495
|
+
if hasTqdm and sys.stdout is not None:
|
|
496
|
+
tqdm.write(message)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def logDebug(*messages):
|
|
500
|
+
message = " ".join(str(x) for x in messages)
|
|
501
|
+
logging.debug(message)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def logWarning(*messages):
|
|
505
|
+
message = " ".join(str(x) for x in messages)
|
|
506
|
+
logging.debug("[WARNING]" + message)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def logError(*messages):
|
|
510
|
+
message = " ".join(str(x) for x in messages)
|
|
511
|
+
logging.error(message)
|
|
512
|
+
if hasTqdm and sys.stdout is not None:
|
|
513
|
+
tqdm.write("[ERROR] " + message)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def varsHex(o):
|
|
517
|
+
ret = []
|
|
518
|
+
for k in o.__dict__.keys():
|
|
519
|
+
v = o.__dict__.__getitem__(k)
|
|
520
|
+
if type(v) is int:
|
|
521
|
+
ret.append("'" + k + "': " + toHex(v))
|
|
522
|
+
elif type(v) is str:
|
|
523
|
+
ret.append("'" + k + "': '" + v + "'")
|
|
524
|
+
elif type(v) is list:
|
|
525
|
+
ret.append("'" + k + "': " + str(v) + "")
|
|
526
|
+
return ", ".join(ret)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def showProgress(iterable):
|
|
530
|
+
if hasTqdm:
|
|
531
|
+
if hasGUI:
|
|
532
|
+
from .gui import tqdm_gui
|
|
533
|
+
return tqdm_gui(iterable=iterable)
|
|
534
|
+
else:
|
|
535
|
+
return tqdm(iterable=iterable)
|
|
536
|
+
return iterable
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# Strings
|
|
540
|
+
def toHex(byte, upper=False):
|
|
541
|
+
hexstr = hex(byte)[2:]
|
|
542
|
+
if not upper:
|
|
543
|
+
hexstr = hexstr.lower()
|
|
544
|
+
else:
|
|
545
|
+
hexstr = hexstr.upper()
|
|
546
|
+
if len(hexstr) == 1:
|
|
547
|
+
return "0" + hexstr
|
|
548
|
+
if hexstr[0] == "x":
|
|
549
|
+
return "-" + hexstr[1:]
|
|
550
|
+
return hexstr
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def isAscii(s):
|
|
554
|
+
for i in range(len(s)):
|
|
555
|
+
if ord(s[i]) >= 128 or ord(s[i]) < 0x20:
|
|
556
|
+
return False
|
|
557
|
+
return True
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def codeToChar(code, encoding="shift_jis", little=True):
|
|
561
|
+
try:
|
|
562
|
+
if code < 128:
|
|
563
|
+
return struct.pack("B", code).decode("ascii")
|
|
564
|
+
return struct.pack(("<" if little else ">") + "H", code).decode(encoding)
|
|
565
|
+
except UnicodeDecodeError:
|
|
566
|
+
return ""
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def loadTable(tablefile):
|
|
570
|
+
if os.path.isfile(tablefile):
|
|
571
|
+
with codecs.open(tablefile, "r", "utf-8") as ft:
|
|
572
|
+
for line in ft:
|
|
573
|
+
line = line.strip("\r\n")
|
|
574
|
+
if line.find("=") > 0:
|
|
575
|
+
linesplit = line.split("=", 1)
|
|
576
|
+
table[linesplit[0]] = linesplit[1]
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def shiftPointer(pointer, pointerdiff):
|
|
580
|
+
newpointer = pointer
|
|
581
|
+
for k, v in pointerdiff.items():
|
|
582
|
+
if k < pointer:
|
|
583
|
+
newpointer += v
|
|
584
|
+
# if newpointer != pointer:
|
|
585
|
+
# logDebug("Shifted pointer", toHex(pointer), "to", toHex(newpointer))
|
|
586
|
+
return newpointer
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def checkShiftJIS(first, second):
|
|
590
|
+
# Based on https://www.lemoda.net/c/detect-shift-jis/
|
|
591
|
+
status = False
|
|
592
|
+
if (first >= 0x81 and first <= 0x84) or (first >= 0x87 and first <= 0x9f):
|
|
593
|
+
if second >= 0x40 and second <= 0xfc:
|
|
594
|
+
status = True
|
|
595
|
+
elif first >= 0xe0 and first <= 0xef:
|
|
596
|
+
if second >= 0x40 and second <= 0xfc:
|
|
597
|
+
status = True
|
|
598
|
+
return status
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def openSection(file, filestart=1, fileend=10):
|
|
602
|
+
if not os.path.isfile(file.format("")):
|
|
603
|
+
if os.path.isfile(file.format(str(filestart))):
|
|
604
|
+
section = StringIO()
|
|
605
|
+
for i in range(filestart, fileend + 1):
|
|
606
|
+
sectionfile = file.format(str(i))
|
|
607
|
+
if os.path.isfile(sectionfile):
|
|
608
|
+
with codecs.open(sectionfile, "r", "utf-8") as f:
|
|
609
|
+
section.write(f.read())
|
|
610
|
+
else:
|
|
611
|
+
break
|
|
612
|
+
return section
|
|
613
|
+
else:
|
|
614
|
+
return None
|
|
615
|
+
else:
|
|
616
|
+
return codecs.open(file.format(""), "r", "utf-8")
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def getSectionNames(f):
|
|
620
|
+
ret = []
|
|
621
|
+
try:
|
|
622
|
+
f.seek(0)
|
|
623
|
+
for line in f:
|
|
624
|
+
line = line.rstrip("\r\n").replace("\ufeff", "")
|
|
625
|
+
if line.startswith("!FILE:"):
|
|
626
|
+
ret.append(line[6:].split("#")[0])
|
|
627
|
+
except UnicodeDecodeError:
|
|
628
|
+
return ret
|
|
629
|
+
return ret
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def getSection(f, title, comment="#", fixchars=[], justone=True, inorder=False):
|
|
633
|
+
ret = {} if not inorder else []
|
|
634
|
+
found = title == ""
|
|
635
|
+
try:
|
|
636
|
+
f.seek(0)
|
|
637
|
+
for line in f:
|
|
638
|
+
line = line.rstrip("\r\n").replace("\ufeff", "")
|
|
639
|
+
if found and line.startswith("!FILE:"):
|
|
640
|
+
if not justone:
|
|
641
|
+
found = False
|
|
642
|
+
else:
|
|
643
|
+
break
|
|
644
|
+
if not found and line.startswith("!FILE:" + title):
|
|
645
|
+
found = True
|
|
646
|
+
elif found and line.startswith(comment) and inorder:
|
|
647
|
+
ret.append({"name": line, "value": ""})
|
|
648
|
+
elif found and line.find("=") > 0:
|
|
649
|
+
split = line.split("=", 1)
|
|
650
|
+
split[1] = split[1].split(comment)[0]
|
|
651
|
+
if split[0] not in ret and not inorder:
|
|
652
|
+
ret[split[0]] = []
|
|
653
|
+
for fixchar in fixchars:
|
|
654
|
+
split[1] = split[1].replace(fixchar[0], fixchar[1])
|
|
655
|
+
if inorder:
|
|
656
|
+
ret.append({"name": split[0], "value": split[1]})
|
|
657
|
+
else:
|
|
658
|
+
ret[split[0]].append(split[1])
|
|
659
|
+
except UnicodeDecodeError:
|
|
660
|
+
return ret
|
|
661
|
+
return ret
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def getSections(file, comment="#", fixchars=[], inorder=False):
|
|
665
|
+
sections = {}
|
|
666
|
+
with codecs.open(file, "r", "utf-8") as wsb:
|
|
667
|
+
files = getSectionNames(wsb)
|
|
668
|
+
for file in files:
|
|
669
|
+
sections[file] = getSection(wsb, file, comment, fixchars, inorder=inorder)
|
|
670
|
+
return sections
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def getSectionPercentage(section, chartot=0, transtot=0):
|
|
674
|
+
for s in section.keys():
|
|
675
|
+
strlen = len(s)
|
|
676
|
+
for s2 in section[s]:
|
|
677
|
+
chartot += strlen
|
|
678
|
+
if s2 != "":
|
|
679
|
+
transtot += strlen
|
|
680
|
+
return chartot, transtot
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def mergeSections(file1, file2, output, comment="#", fixchars=[]):
|
|
684
|
+
sections1 = getSections(file1, comment, fixchars, inorder=True)
|
|
685
|
+
sections2 = getSections(file2, comment, fixchars)
|
|
686
|
+
with codecs.open(output, "w", "utf-8") as out:
|
|
687
|
+
for section in sections1.keys():
|
|
688
|
+
out.write("!FILE:" + section + "\n")
|
|
689
|
+
for v in sections1[section]:
|
|
690
|
+
s = v["name"]
|
|
691
|
+
if s.startswith(comment):
|
|
692
|
+
out.write(s + "\n")
|
|
693
|
+
continue
|
|
694
|
+
sectionstr = v["value"]
|
|
695
|
+
if sectionstr == "":
|
|
696
|
+
for section2 in sections2.keys():
|
|
697
|
+
if s in sections2[section2] and sections2[section2][s][0] != "":
|
|
698
|
+
sectionstr = sections2[section2][s][0]
|
|
699
|
+
break
|
|
700
|
+
out.write(s + "=" + sectionstr + "\n")
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class TranslationFile:
|
|
704
|
+
def __init__(self, path=""):
|
|
705
|
+
self.files = {}
|
|
706
|
+
self.lookup = {}
|
|
707
|
+
self.chartot = 0
|
|
708
|
+
self.transtot = 0
|
|
709
|
+
if path == "":
|
|
710
|
+
self.root = ET.Element("xliff")
|
|
711
|
+
self.root.set("version", "1.2")
|
|
712
|
+
self.root.set("xmlns", "urn:oasis:names:tc:xliff:document:1.2")
|
|
713
|
+
else:
|
|
714
|
+
ET.register_namespace("", "urn:oasis:names:tc:xliff:document:1.2")
|
|
715
|
+
tree = ET.parse(path)
|
|
716
|
+
self.root = tree.getroot()
|
|
717
|
+
for file in self.root:
|
|
718
|
+
self.files[file.attrib["original"]] = file
|
|
719
|
+
|
|
720
|
+
def mergeSection(self, path, filename="", section="", comments="#", fixchars=[]):
|
|
721
|
+
with codecs.open(path, "r", "utf-8") as bin:
|
|
722
|
+
mergesection = getSection(bin, section, comments, fixchars=fixchars, justone=False)
|
|
723
|
+
# Check the merge section
|
|
724
|
+
for file in self.root:
|
|
725
|
+
if filename != "" and file.attrib["original"] != filename:
|
|
726
|
+
continue
|
|
727
|
+
for unit in file[0]:
|
|
728
|
+
check = unit[0].text
|
|
729
|
+
if check in mergesection and mergesection[check][0] != "":
|
|
730
|
+
newcheck = mergesection[check][0]
|
|
731
|
+
if len(mergesection[check]) > 1:
|
|
732
|
+
mergesection[check].pop(0)
|
|
733
|
+
unit[1].text = newcheck
|
|
734
|
+
unit[1].set("state", "translated")
|
|
735
|
+
unit.set("approved", "no")
|
|
736
|
+
|
|
737
|
+
def addEntry(self, text, filename, offset, translation="", comment=""):
|
|
738
|
+
# Check if we need to add a new file
|
|
739
|
+
if filename not in self.files:
|
|
740
|
+
file = ET.SubElement(self.root, "file", {"xml:space": "preserve"})
|
|
741
|
+
file.set("original", filename)
|
|
742
|
+
file.set("source-language", "ja")
|
|
743
|
+
file.set("target-language", "en")
|
|
744
|
+
ET.SubElement(file, "body")
|
|
745
|
+
self.files[filename] = file
|
|
746
|
+
else:
|
|
747
|
+
file = self.files[filename]
|
|
748
|
+
# Add the new entry
|
|
749
|
+
unit = ET.SubElement(file[0], "trans-unit", {"id": str(offset), "xml:space": "preserve"})
|
|
750
|
+
source = ET.SubElement(unit, "source")
|
|
751
|
+
source.text = text
|
|
752
|
+
target = ET.SubElement(unit, "target")
|
|
753
|
+
if translation != "":
|
|
754
|
+
target.text = translation
|
|
755
|
+
if comment != "":
|
|
756
|
+
note = ET.SubElement(unit, "note")
|
|
757
|
+
note.text = comment
|
|
758
|
+
|
|
759
|
+
def preloadLookup(self, comments="#"):
|
|
760
|
+
self.lookup = {}
|
|
761
|
+
self.chartot = 0
|
|
762
|
+
self.transtot = 0
|
|
763
|
+
for file in self.files:
|
|
764
|
+
for unit in self.files[file][0]:
|
|
765
|
+
self.chartot += len(unit[0].text)
|
|
766
|
+
if unit[1].text is not None and unit[1].text != "":
|
|
767
|
+
if comments in unit[1].text:
|
|
768
|
+
unit[1].text = unit[1].text.split(comments)[0]
|
|
769
|
+
self.lookup[unit[0].text] = unit[1].text
|
|
770
|
+
self.transtot += len(unit[0].text)
|
|
771
|
+
|
|
772
|
+
def getEntry(self, text, filename, offset):
|
|
773
|
+
stroffset = str(offset)
|
|
774
|
+
if filename in self.files:
|
|
775
|
+
# Try to match offset
|
|
776
|
+
for unit in self.files[filename][0]:
|
|
777
|
+
if unit.attrib["id"] == stroffset and unit[1].text is not None and unit[1].text != "":
|
|
778
|
+
return unit[1].text
|
|
779
|
+
# Try to match string
|
|
780
|
+
for unit in self.files[filename][0]:
|
|
781
|
+
if unit[0].text == text and unit[1].text is not None and unit[1].text != "":
|
|
782
|
+
return unit[1].text
|
|
783
|
+
# If nothing was found, run a search on the whole file
|
|
784
|
+
if text in self.lookup:
|
|
785
|
+
return self.lookup[text]
|
|
786
|
+
return ""
|
|
787
|
+
|
|
788
|
+
def hasFile(self, filename):
|
|
789
|
+
return filename in self.files
|
|
790
|
+
|
|
791
|
+
def getProgress(self):
|
|
792
|
+
if self.chartot == 0:
|
|
793
|
+
return 0
|
|
794
|
+
return (100 * self.transtot) / self.chartot
|
|
795
|
+
|
|
796
|
+
def save(self, filename, dummy=False):
|
|
797
|
+
if dummy:
|
|
798
|
+
self.addEntry("dummy line", "dummy", 0, "dummy translation", "Ignore this")
|
|
799
|
+
makeFolders(os.path.dirname(filename))
|
|
800
|
+
self._pretty_print(self.root)
|
|
801
|
+
xmlstr = ET.tostring(self.root, encoding="unicode", xml_declaration=True)
|
|
802
|
+
# Change this to match what Weblate does
|
|
803
|
+
xmlstr = xmlstr.replace("<target />", "<target/>") + "\n"
|
|
804
|
+
with codecs.open(filename, "w", "utf-8") as f:
|
|
805
|
+
f.write(xmlstr)
|
|
806
|
+
|
|
807
|
+
def _pretty_print(self, current, parent=None, index=-1, depth=0):
|
|
808
|
+
for i, node in enumerate(current):
|
|
809
|
+
self._pretty_print(node, current, i, depth + 1)
|
|
810
|
+
if parent is not None:
|
|
811
|
+
if index == 0:
|
|
812
|
+
parent.text = "\n" + (" " * depth)
|
|
813
|
+
else:
|
|
814
|
+
parent[index - 1].tail = "\n" + (" " * depth)
|
|
815
|
+
if index == len(parent) - 1:
|
|
816
|
+
current.tail = "\n" + (" " * (depth - 1))
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
class FontGlyph:
|
|
820
|
+
def __init__(self, start, width, length, char="", code=0, index=0):
|
|
821
|
+
self.start = start
|
|
822
|
+
self.width = width
|
|
823
|
+
self.length = length
|
|
824
|
+
self.char = char
|
|
825
|
+
self.code = code
|
|
826
|
+
self.index = index
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def wordwrap(text, glyphs, width, codefunc=None, default=6, linebreak="|", sectionsep=">>", strip=True):
|
|
830
|
+
# Based on http://code.activestate.com/recipes/577946-word-wrap-for-proportional-fonts/
|
|
831
|
+
lines = []
|
|
832
|
+
if sectionsep != "" and text.count(sectionsep) > 0:
|
|
833
|
+
lines = text.split(sectionsep)
|
|
834
|
+
for i in range(len(lines)):
|
|
835
|
+
lines[i] = wordwrap(lines[i], glyphs, width, codefunc, default, linebreak, sectionsep)
|
|
836
|
+
return sectionsep.join(lines)
|
|
837
|
+
if linebreak != "\n":
|
|
838
|
+
text = text.replace(linebreak, "\n")
|
|
839
|
+
pattern = re.compile(r"(\s+)")
|
|
840
|
+
lookup = dict((c, glyphs[c].length if c in glyphs else default) for c in set(text))
|
|
841
|
+
for line in text.splitlines():
|
|
842
|
+
tokens = pattern.split(line)
|
|
843
|
+
tokens.append("")
|
|
844
|
+
widths = []
|
|
845
|
+
for token in tokens:
|
|
846
|
+
tokenwidth = 0
|
|
847
|
+
i = 0
|
|
848
|
+
while i < len(token):
|
|
849
|
+
if codefunc is not None:
|
|
850
|
+
skip = codefunc(token, i)
|
|
851
|
+
if skip > 0:
|
|
852
|
+
i += skip
|
|
853
|
+
continue
|
|
854
|
+
tokenwidth += lookup[token[i]]
|
|
855
|
+
i += 1
|
|
856
|
+
widths.append(tokenwidth)
|
|
857
|
+
start, total = 0, 0
|
|
858
|
+
for index in range(0, len(tokens), 2):
|
|
859
|
+
if total + widths[index] > width:
|
|
860
|
+
end = index + 2 if index == start else index
|
|
861
|
+
lines.append("".join(tokens[start:end]))
|
|
862
|
+
start, total = end, 0
|
|
863
|
+
if end == index + 2:
|
|
864
|
+
continue
|
|
865
|
+
total += widths[index] + widths[index + 1]
|
|
866
|
+
if start < len(tokens):
|
|
867
|
+
lines.append("".join(tokens[start:]))
|
|
868
|
+
if strip:
|
|
869
|
+
lines = [line.strip() for line in lines]
|
|
870
|
+
else:
|
|
871
|
+
for i in range(len(lines)):
|
|
872
|
+
if lines[i].startswith(" "):
|
|
873
|
+
lines[i] = lines[i][1:]
|
|
874
|
+
if lines[i].endswith(" "):
|
|
875
|
+
lines[i] = lines[i][:-1]
|
|
876
|
+
return linebreak.join(lines)
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def centerLines(text, glyphs, width, codefunc=None, default=6, linebreak="|", centercode="<<"):
|
|
880
|
+
lines = text.split(linebreak)
|
|
881
|
+
for i in range(len(lines)):
|
|
882
|
+
if not lines[i].startswith(centercode):
|
|
883
|
+
continue
|
|
884
|
+
lines[i] = lines[i][len(centercode):]
|
|
885
|
+
length = 0
|
|
886
|
+
j = 0
|
|
887
|
+
while j < len(lines[i]):
|
|
888
|
+
if codefunc is not None:
|
|
889
|
+
skip = codefunc(lines[i], j)
|
|
890
|
+
if skip > 0:
|
|
891
|
+
j += skip
|
|
892
|
+
continue
|
|
893
|
+
length += glyphs[lines[i][j]].length if lines[i][j] in glyphs else default
|
|
894
|
+
j += 1
|
|
895
|
+
spacelen = glyphs[" "].length
|
|
896
|
+
spacing = int(((width - length) / 2) / spacelen)
|
|
897
|
+
lines[i] = (" " * spacing) + lines[i]
|
|
898
|
+
return linebreak.join(lines)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def readEncodedString(f, encoding="shift_jis", upper=False):
|
|
902
|
+
sjis = ""
|
|
903
|
+
while True:
|
|
904
|
+
b1 = f.readByte()
|
|
905
|
+
if b1 == 0x0a:
|
|
906
|
+
sjis += "|"
|
|
907
|
+
elif b1 == 0x00:
|
|
908
|
+
break
|
|
909
|
+
else:
|
|
910
|
+
b2 = f.readByte()
|
|
911
|
+
if not checkShiftJIS(b1, b2):
|
|
912
|
+
if b2 == 0x01:
|
|
913
|
+
sjis += "UNK(" + toHex(b1, upper) + toHex(b2, upper) + ")"
|
|
914
|
+
else:
|
|
915
|
+
f.seek(-1, 1)
|
|
916
|
+
sjis += chr(b1)
|
|
917
|
+
else:
|
|
918
|
+
f.seek(-2, 1)
|
|
919
|
+
try:
|
|
920
|
+
sjis += f.read(2).decode(encoding).replace("〜", "~")
|
|
921
|
+
except UnicodeDecodeError:
|
|
922
|
+
logError("[ERROR] UnicodeDecodeError")
|
|
923
|
+
sjis += "[ERROR" + str(f.tell() - 2) + "]"
|
|
924
|
+
return sjis
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def detectEncodedString(f, encoding="shift_jis", startascii=[0x25], startenc=[], upper=False):
|
|
928
|
+
ret = ""
|
|
929
|
+
sjis = 0
|
|
930
|
+
while True:
|
|
931
|
+
b1 = f.readByte()
|
|
932
|
+
if b1 == 0x0a:
|
|
933
|
+
ret += "|"
|
|
934
|
+
elif b1 == 0x00:
|
|
935
|
+
break
|
|
936
|
+
elif b1 >= 28 and b1 <= 126 and (len(ret) > 0 or b1 in startascii):
|
|
937
|
+
ret += chr(b1)
|
|
938
|
+
else:
|
|
939
|
+
b2 = f.readByte()
|
|
940
|
+
if checkShiftJIS(b1, b2):
|
|
941
|
+
f.seek(-2, 1)
|
|
942
|
+
try:
|
|
943
|
+
ret += f.read(2).decode(encoding).replace("〜", "~")
|
|
944
|
+
sjis += 1
|
|
945
|
+
except UnicodeDecodeError:
|
|
946
|
+
if ret.count("UNK(") >= 5:
|
|
947
|
+
return ""
|
|
948
|
+
ret += "UNK(" + toHex(b1, upper) + toHex(b2, upper) + ")"
|
|
949
|
+
elif (b1, b2) in startenc or (len(ret) > 1 and ret.count("UNK(") < 5):
|
|
950
|
+
ret += "UNK(" + toHex(b1, upper) + toHex(b2, upper) + ")"
|
|
951
|
+
else:
|
|
952
|
+
return ""
|
|
953
|
+
return ret
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def detectASCIIString(f, encoding="ascii", startascii=[]):
|
|
957
|
+
ret = ""
|
|
958
|
+
while True:
|
|
959
|
+
b1 = f.readByte()
|
|
960
|
+
if b1 == 0x0a:
|
|
961
|
+
ret += "|"
|
|
962
|
+
elif b1 == 0x00:
|
|
963
|
+
break
|
|
964
|
+
elif b1 >= 28 and b1 <= 126:
|
|
965
|
+
ret += chr(b1)
|
|
966
|
+
else:
|
|
967
|
+
return ""
|
|
968
|
+
return ret
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def writeEncodedString(f, s, maxlen=0, encoding="shift_jis"):
|
|
972
|
+
i = 0
|
|
973
|
+
x = 0
|
|
974
|
+
s = s.replace("~", "〜")
|
|
975
|
+
while x < len(s):
|
|
976
|
+
c = s[x]
|
|
977
|
+
if c == "U" and x < len(s) - 4 and s[x:x+4] == "UNK(":
|
|
978
|
+
if maxlen > 0 and i+2 > maxlen:
|
|
979
|
+
return -1
|
|
980
|
+
code = s[x+4] + s[x+5]
|
|
981
|
+
f.write(bytes.fromhex(code))
|
|
982
|
+
code = s[x+6] + s[x+7]
|
|
983
|
+
f.write(bytes.fromhex(code))
|
|
984
|
+
x += 8
|
|
985
|
+
i += 2
|
|
986
|
+
elif c == "|":
|
|
987
|
+
if maxlen > 0 and i+1 > maxlen:
|
|
988
|
+
return -1
|
|
989
|
+
f.writeByte(0x0a)
|
|
990
|
+
i += 1
|
|
991
|
+
elif ord(c) < 128:
|
|
992
|
+
if maxlen > 0 and i+1 > maxlen:
|
|
993
|
+
return -1
|
|
994
|
+
f.writeByte(ord(c))
|
|
995
|
+
i += 1
|
|
996
|
+
else:
|
|
997
|
+
if maxlen > 0 and i+2 > maxlen:
|
|
998
|
+
return -1
|
|
999
|
+
f.write(c.encode(encoding))
|
|
1000
|
+
i += 2
|
|
1001
|
+
x += 1
|
|
1002
|
+
f.writeByte(0x00)
|
|
1003
|
+
return i
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def extractBinaryStrings(infile, binranges, func=detectEncodedString, encoding="shift_jis"):
|
|
1007
|
+
strings = []
|
|
1008
|
+
positions = []
|
|
1009
|
+
insize = os.path.getsize(infile)
|
|
1010
|
+
with Stream(infile, "rb") as f:
|
|
1011
|
+
for binrange in binranges:
|
|
1012
|
+
f.seek(binrange[0])
|
|
1013
|
+
while f.tell() < binrange[1] and f.tell() < insize - 2:
|
|
1014
|
+
pos = f.tell()
|
|
1015
|
+
check = func(f, encoding)
|
|
1016
|
+
if check != "":
|
|
1017
|
+
if check not in strings:
|
|
1018
|
+
logDebug("Found string at", pos)
|
|
1019
|
+
strings.append(check)
|
|
1020
|
+
positions.append([pos])
|
|
1021
|
+
else:
|
|
1022
|
+
positions[strings.index(check)].append(pos)
|
|
1023
|
+
pos = f.tell() - 1
|
|
1024
|
+
f.seek(pos + 1)
|
|
1025
|
+
return strings, positions
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
class BinaryPointer:
|
|
1029
|
+
def __init__(self, old, new, str):
|
|
1030
|
+
self.old = old
|
|
1031
|
+
self.new = new
|
|
1032
|
+
self.str = str
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def repackBinaryStrings(section, infile, outfile, binranges, freeranges=None, readfunc=detectEncodedString, writefunc=writeEncodedString, encoding="shift_jis", pointerstart=0, injectstart=0, fallbackf=None, injectfallback=0, sectionname="bin", preformat=None, postformat=None):
|
|
1036
|
+
insize = os.path.getsize(infile)
|
|
1037
|
+
notfound = []
|
|
1038
|
+
with Stream(infile, "rb") as fi:
|
|
1039
|
+
if freeranges is not None:
|
|
1040
|
+
allbin = fi.read()
|
|
1041
|
+
strpointers = {}
|
|
1042
|
+
freeranges = [list(x) for x in freeranges]
|
|
1043
|
+
with Stream(outfile, "r+b") as fo:
|
|
1044
|
+
for binrange in binranges:
|
|
1045
|
+
fi.seek(binrange[0])
|
|
1046
|
+
while fi.tell() < binrange[1] and fi.tell() < insize - 2:
|
|
1047
|
+
pos = fi.tell()
|
|
1048
|
+
check = readfunc(fi, encoding)
|
|
1049
|
+
if check != "":
|
|
1050
|
+
pre = post = ""
|
|
1051
|
+
if preformat != None:
|
|
1052
|
+
check, pre, post = preformat(check)
|
|
1053
|
+
if isinstance(section, TranslationFile):
|
|
1054
|
+
newsjis = section.getEntry(check, sectionname, pos)
|
|
1055
|
+
else:
|
|
1056
|
+
newsjis = section[check][0] if check in section else ""
|
|
1057
|
+
if newsjis != "":
|
|
1058
|
+
if len(section[check]) > 1:
|
|
1059
|
+
section[check].pop(0)
|
|
1060
|
+
if newsjis != "":
|
|
1061
|
+
if newsjis == "!":
|
|
1062
|
+
newsjis = ""
|
|
1063
|
+
if postformat != None:
|
|
1064
|
+
newsjis = postformat(newsjis, pre, post)
|
|
1065
|
+
newsjislog = newsjis.encode("ascii", "ignore")
|
|
1066
|
+
logDebug("Replacing string at", toHex(pos), "with", newsjislog)
|
|
1067
|
+
fo.seek(pos)
|
|
1068
|
+
endpos = fi.tell() - 1
|
|
1069
|
+
newlen = writefunc(fo, newsjis, endpos - pos + 1, encoding)
|
|
1070
|
+
fo.seek(-1, 1)
|
|
1071
|
+
if fo.readByte() != 0:
|
|
1072
|
+
fo.writeZero(1)
|
|
1073
|
+
if newlen < 0:
|
|
1074
|
+
if (freeranges is None and injectfallback == 0) or pointerstart == 0:
|
|
1075
|
+
logError("String", newsjislog, "is too long.")
|
|
1076
|
+
else:
|
|
1077
|
+
# Add this to the freeranges
|
|
1078
|
+
freeranges.append([pos, endpos])
|
|
1079
|
+
logDebug("Adding new freerage", toHex(pos), toHex(endpos))
|
|
1080
|
+
range = None
|
|
1081
|
+
rangelen = 0
|
|
1082
|
+
for c in newsjis:
|
|
1083
|
+
rangelen += 1 if ord(c) < 256 else 2
|
|
1084
|
+
for freerange in freeranges:
|
|
1085
|
+
if freerange[1] - freerange[0] > rangelen:
|
|
1086
|
+
range = freerange
|
|
1087
|
+
break
|
|
1088
|
+
if range is None and newsjis not in strpointers and injectfallback == 0:
|
|
1089
|
+
logError("No more room! Skipping", newsjislog, "...")
|
|
1090
|
+
freeranges.pop()
|
|
1091
|
+
else:
|
|
1092
|
+
# Write the string in a new portion of the rom
|
|
1093
|
+
if newsjis in strpointers:
|
|
1094
|
+
newpointer = strpointers[newsjis]
|
|
1095
|
+
elif range is None:
|
|
1096
|
+
logDebug("No room for the string", newsjislog, ", redirecting to fallback")
|
|
1097
|
+
fallbackpos = fallbackf.tell()
|
|
1098
|
+
writefunc(fallbackf, newsjis, 0, encoding)
|
|
1099
|
+
fallbackf.seek(-1, 1)
|
|
1100
|
+
if fallbackf.readByte() != 0:
|
|
1101
|
+
fallbackf.writeZero(1)
|
|
1102
|
+
newpointer = injectfallback + fallbackpos
|
|
1103
|
+
strpointers[newsjis] = newpointer
|
|
1104
|
+
else:
|
|
1105
|
+
logDebug("No room for the string", newsjislog, ", redirecting to", toHex(range[0]))
|
|
1106
|
+
fo.seek(range[0])
|
|
1107
|
+
writefunc(fo, newsjis, 0, encoding)
|
|
1108
|
+
fo.seek(-1, 1)
|
|
1109
|
+
if fo.readByte() != 0:
|
|
1110
|
+
fo.writeZero(1)
|
|
1111
|
+
newpointer = range[0]
|
|
1112
|
+
# For the injected range, add injectstart, otherwise add pointerstart
|
|
1113
|
+
if (len(range) == 3):
|
|
1114
|
+
if isinstance(range[2], bool):
|
|
1115
|
+
newpointer += injectstart
|
|
1116
|
+
else:
|
|
1117
|
+
newpointer += int(range[2])
|
|
1118
|
+
else:
|
|
1119
|
+
newpointer += pointerstart
|
|
1120
|
+
range[0] = fo.tell()
|
|
1121
|
+
strpointers[newsjis] = newpointer
|
|
1122
|
+
# Search and replace the old pointer
|
|
1123
|
+
pointer = pointerstart + pos
|
|
1124
|
+
pointersearch = struct.pack("<I", pointer)
|
|
1125
|
+
index = 0
|
|
1126
|
+
logDebug("Searching for pointer", toHex(pointer))
|
|
1127
|
+
foundone = False
|
|
1128
|
+
while index < len(allbin):
|
|
1129
|
+
index = allbin.find(pointersearch, index)
|
|
1130
|
+
if index < 0:
|
|
1131
|
+
break
|
|
1132
|
+
foundone = True
|
|
1133
|
+
logDebug("Replaced pointer at", toHex(pointerstart + index), "with", toHex(newpointer))
|
|
1134
|
+
fo.seek(index)
|
|
1135
|
+
fo.writeUInt(newpointer)
|
|
1136
|
+
index += 4
|
|
1137
|
+
if not foundone:
|
|
1138
|
+
logWarning("Pointer", toHex(pointer), "->", toHex(newpointer), "not found for string", newsjislog)
|
|
1139
|
+
# freeranges.pop()
|
|
1140
|
+
notfound.append(BinaryPointer(pointer, newpointer, newsjislog))
|
|
1141
|
+
else:
|
|
1142
|
+
fo.writeZero(endpos - fo.tell())
|
|
1143
|
+
pos = fi.tell() - 1
|
|
1144
|
+
fi.seek(pos + 1)
|
|
1145
|
+
return notfound
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
# Folders
|
|
1149
|
+
def makeFolder(folder, clear=True):
|
|
1150
|
+
if clear:
|
|
1151
|
+
clearFolder(folder)
|
|
1152
|
+
os.mkdir(folder)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def clearFolder(folder):
|
|
1156
|
+
if os.path.isdir(folder):
|
|
1157
|
+
shutil.rmtree(folder)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def copyFolder(f1, f2):
|
|
1161
|
+
clearFolder(f2)
|
|
1162
|
+
shutil.copytree(f1, f2)
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def mergeFolder(f1, f2):
|
|
1166
|
+
shutil.copytree(f1, f2, dirs_exist_ok=True)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def copyFile(f1, f2):
|
|
1170
|
+
if os.path.isfile(f2):
|
|
1171
|
+
os.remove(f2)
|
|
1172
|
+
shutil.copyfile(f1, f2)
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def makeFolders(path):
|
|
1176
|
+
try:
|
|
1177
|
+
os.makedirs(path)
|
|
1178
|
+
except FileExistsError:
|
|
1179
|
+
pass
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def getFiles(path, extensions=[]):
|
|
1183
|
+
if isinstance(extensions, str) and extensions != "":
|
|
1184
|
+
extensions = [extensions]
|
|
1185
|
+
ret = []
|
|
1186
|
+
for (root, dirs, files) in os.walk(path):
|
|
1187
|
+
for file in sorted(files):
|
|
1188
|
+
file = os.path.join(root, file).replace(path, "").replace("\\", "/")
|
|
1189
|
+
if len(extensions) > 0 and os.path.splitext(file)[1] not in extensions:
|
|
1190
|
+
continue
|
|
1191
|
+
ret.append(file)
|
|
1192
|
+
return ret
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def getFolders(path):
|
|
1196
|
+
ret = []
|
|
1197
|
+
for (root, dirs, files) in os.walk(path):
|
|
1198
|
+
for dir in sorted(dirs):
|
|
1199
|
+
dir = os.path.join(root, dir).replace(path, "").replace("\\", "/")
|
|
1200
|
+
ret.append(dir)
|
|
1201
|
+
return ret
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def bundledFile(name):
|
|
1205
|
+
if os.path.isfile(name):
|
|
1206
|
+
return name
|
|
1207
|
+
try:
|
|
1208
|
+
return os.path.join(sys._MEIPASS, name)
|
|
1209
|
+
except AttributeError:
|
|
1210
|
+
return name
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def bundledExecutable(name):
|
|
1214
|
+
if os.name != "nt":
|
|
1215
|
+
name = name.replace(".exe", "")
|
|
1216
|
+
if os.path.isfile(name):
|
|
1217
|
+
if os.name != "nt":
|
|
1218
|
+
name = "./" + name
|
|
1219
|
+
return name
|
|
1220
|
+
try:
|
|
1221
|
+
return os.path.join(sys._MEIPASS, name)
|
|
1222
|
+
except AttributeError:
|
|
1223
|
+
return name
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def execute(cmd, show=True):
|
|
1227
|
+
result = ""
|
|
1228
|
+
try:
|
|
1229
|
+
if os.name != "nt":
|
|
1230
|
+
result = str(subprocess.check_output(shlex.split(cmd)))
|
|
1231
|
+
else:
|
|
1232
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
1233
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
1234
|
+
result = str(subprocess.check_output(cmd, startupinfo=startupinfo))
|
|
1235
|
+
except FileNotFoundError:
|
|
1236
|
+
logError("Command too long:", len(cmd), cmd)
|
|
1237
|
+
if result != "":
|
|
1238
|
+
logError(result)
|
|
1239
|
+
return
|
|
1240
|
+
except subprocess.CalledProcessError:
|
|
1241
|
+
logError("Command error", cmd)
|
|
1242
|
+
if result != "":
|
|
1243
|
+
logError(result)
|
|
1244
|
+
return
|
|
1245
|
+
if result != "":
|
|
1246
|
+
if show:
|
|
1247
|
+
logMessage(result)
|
|
1248
|
+
else:
|
|
1249
|
+
logDebug(result)
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def crcFile(f):
|
|
1253
|
+
buffersize = 0x10000
|
|
1254
|
+
crc = 0
|
|
1255
|
+
with open(f, "rb") as crcf:
|
|
1256
|
+
buffer = crcf.read(buffersize)
|
|
1257
|
+
while len(buffer) > 0:
|
|
1258
|
+
crc = zlib.crc32(buffer, crc)
|
|
1259
|
+
buffer = crcf.read(buffersize)
|
|
1260
|
+
return crc & 0xffffffff
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def crc16(data):
|
|
1264
|
+
crc = 0xffff
|
|
1265
|
+
for i in range(len(data)):
|
|
1266
|
+
crc ^= data[i]
|
|
1267
|
+
for j in range(8):
|
|
1268
|
+
carry = crc & 1
|
|
1269
|
+
crc >>= 1
|
|
1270
|
+
if carry:
|
|
1271
|
+
crc ^= 0xa001
|
|
1272
|
+
return crc & 0xffff
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def xdeltaPatch(patchfile, infile, outfile):
|
|
1276
|
+
logMessage("Creating xdelta patch", patchfile, "...")
|
|
1277
|
+
try:
|
|
1278
|
+
import pyxdelta
|
|
1279
|
+
pyxdelta.run(infile, outfile, patchfile)
|
|
1280
|
+
except ImportError:
|
|
1281
|
+
xdelta = bundledExecutable("xdelta.exe")
|
|
1282
|
+
if not os.path.isfile(xdelta):
|
|
1283
|
+
logError("xdelta not found")
|
|
1284
|
+
return
|
|
1285
|
+
execute(xdelta + " -f -e -s \"{rom}\" \"{rompatch}\" \"{patch}\"".format(rom=infile, rompatch=outfile, patch=patchfile), False)
|
|
1286
|
+
logMessage("Done!")
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def ipsPatch(patchfile, infile, outfile):
|
|
1290
|
+
try:
|
|
1291
|
+
from ips_util import Patch
|
|
1292
|
+
except ImportError:
|
|
1293
|
+
logError("ips_util not found")
|
|
1294
|
+
return
|
|
1295
|
+
logMessage("Creating ips patch", patchfile, "...")
|
|
1296
|
+
with Stream(infile, "rb") as f:
|
|
1297
|
+
indata = f.read()
|
|
1298
|
+
with Stream(outfile, "rb") as f:
|
|
1299
|
+
outdata = f.read()
|
|
1300
|
+
result = Patch.create(indata, outdata)
|
|
1301
|
+
with Stream(patchfile, "wb") as f:
|
|
1302
|
+
f.write(result.encode())
|
|
1303
|
+
logMessage("Done!")
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def armipsPatch(file, defines={}, labels={}):
|
|
1307
|
+
logMessage("Applying armips patch ...")
|
|
1308
|
+
try:
|
|
1309
|
+
import pyarmips
|
|
1310
|
+
pyarmips.run(file)
|
|
1311
|
+
except ImportError:
|
|
1312
|
+
armips = bundledExecutable("armips.exe")
|
|
1313
|
+
if not os.path.isfile(armips):
|
|
1314
|
+
logError("armips not found")
|
|
1315
|
+
return
|
|
1316
|
+
params = ""
|
|
1317
|
+
for define in defines:
|
|
1318
|
+
params += " -equ " + define + " " + str(defines[define])
|
|
1319
|
+
for label in labels:
|
|
1320
|
+
params += " -definelabel " + label + " " + str(labels[label])
|
|
1321
|
+
execute(armips + " {binpatch}{params}".format(binpatch=file, params=params), False)
|
|
1322
|
+
logMessage("Done!")
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def deltaToFrame(delta, fps=30):
|
|
1326
|
+
return int((delta.seconds * fps) + math.ceil(delta.microseconds / (1000 * 1000 / fps)))
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
# Generic texture
|
|
1330
|
+
def readPalette(p):
|
|
1331
|
+
return (((p >> 0) & 0x1f) << 3, ((p >> 5) & 0x1f) << 3, ((p >> 10) & 0x1f) << 3, 0xff)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def getColorDistance(c1, c2, checkalpha=False):
|
|
1335
|
+
if len(c1) == 4:
|
|
1336
|
+
(r1, g1, b1, a1) = c1
|
|
1337
|
+
else:
|
|
1338
|
+
(r1, g1, b1) = c1
|
|
1339
|
+
a1 = 255
|
|
1340
|
+
if len(c2) == 4:
|
|
1341
|
+
(r2, g2, b2, a2) = c2
|
|
1342
|
+
else:
|
|
1343
|
+
(r2, g2, b2) = c2
|
|
1344
|
+
a2 = 255
|
|
1345
|
+
sum = (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2
|
|
1346
|
+
if checkalpha:
|
|
1347
|
+
sum += (a1 - a2) ** 2
|
|
1348
|
+
return math.sqrt(sum)
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def sumColors(c1, c2, a=1, b=1, c=2):
|
|
1352
|
+
(r1, g1, b1, a1) = c1
|
|
1353
|
+
(r2, g2, b2, a2) = c2
|
|
1354
|
+
return ((r1 * a + r2 * b) // c, (g1 * a + g2 * b) // c, (b1 * a + b2 * b) // c, a1)
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
# https://github.com/marco-calautti/Rainbow/blob/master/Rainbow.ImgLib/ImgLib/Common/ImageUtils.cs
|
|
1358
|
+
cc38 = [
|
|
1359
|
+
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff
|
|
1360
|
+
]
|
|
1361
|
+
cc48 = [
|
|
1362
|
+
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
|
|
1363
|
+
]
|
|
1364
|
+
cc58 = [
|
|
1365
|
+
0x00, 0x08, 0x10, 0x19, 0x21, 0x29, 0x31, 0x3a, 0x42, 0x4a, 0x52, 0x5a, 0x63, 0x6b, 0x73, 0x7b,
|
|
1366
|
+
0x84, 0x8c, 0x94, 0x9c, 0xa5, 0xad, 0xb5, 0xbd, 0xc5, 0xce, 0xd6, 0xde, 0xe6, 0xef, 0xf7, 0xff
|
|
1367
|
+
]
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
# https://github.com/marco-calautti/Rainbow/blob/master/Rainbow.ImgLib/ImgLib/Encoding/Implementation/ColorCodecRGB5A3.cs
|
|
1371
|
+
def readRGB5A3(color):
|
|
1372
|
+
r, g, b, a = (0, 0, 0, 0)
|
|
1373
|
+
if color & 0x8000 != 0:
|
|
1374
|
+
a = 255
|
|
1375
|
+
r = cc58[(color >> 10) & 0x1f]
|
|
1376
|
+
g = cc58[(color >> 5) & 0x1f]
|
|
1377
|
+
b = cc58[(color) & 0x1f]
|
|
1378
|
+
else:
|
|
1379
|
+
a = cc38[(color >> 12) & 0x7]
|
|
1380
|
+
r = cc48[(color >> 8) & 0xf]
|
|
1381
|
+
g = cc48[(color >> 4) & 0xf]
|
|
1382
|
+
b = cc48[(color) & 0xf]
|
|
1383
|
+
return (r, g, b, a)
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def readRGB5A1(color):
|
|
1387
|
+
r = cc58[color & 0x1f]
|
|
1388
|
+
g = cc58[color >> 5 & 0x1f]
|
|
1389
|
+
b = cc58[color >> 10 & 0x1f]
|
|
1390
|
+
a = 0 if (color >> 15 & 0x1) == 0 else 255
|
|
1391
|
+
return (r, g, b, a)
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def getPaletteIndex(palette, color, fixtransp=False, starti=0, palsize=-1, checkalpha=False, zerotransp=True, backwards=False, logcolor=False):
|
|
1395
|
+
if zerotransp and color[3] == 0:
|
|
1396
|
+
return 0
|
|
1397
|
+
if palsize == -1:
|
|
1398
|
+
palsize = len(palette)
|
|
1399
|
+
zeroalpha = -1
|
|
1400
|
+
palrange = range(starti, starti + palsize)
|
|
1401
|
+
if backwards:
|
|
1402
|
+
palrange = reversed(palrange)
|
|
1403
|
+
for i in palrange:
|
|
1404
|
+
if fixtransp and i == starti:
|
|
1405
|
+
continue
|
|
1406
|
+
if palette[i][0] == color[0] and palette[i][1] == color[1] and palette[i][2] == color[2] and (not checkalpha or palette[i][3] == color[3]):
|
|
1407
|
+
return i - starti
|
|
1408
|
+
if checkalpha and palette[i][3] == 0:
|
|
1409
|
+
zeroalpha = i - starti
|
|
1410
|
+
if palette[starti][0] == color[0] and palette[starti][1] == color[1] and palette[starti][2] == color[2] and (not checkalpha or palette[starti][3] == color[3]):
|
|
1411
|
+
return 0
|
|
1412
|
+
if checkalpha and color[3] == 0 and zeroalpha != -1:
|
|
1413
|
+
return zeroalpha
|
|
1414
|
+
mindist = 0xffffffff
|
|
1415
|
+
disti = 0
|
|
1416
|
+
palrange = range(starti + 1, starti + palsize)
|
|
1417
|
+
if backwards:
|
|
1418
|
+
palrange = reversed(palrange)
|
|
1419
|
+
for i in palrange:
|
|
1420
|
+
distance = getColorDistance(color, palette[i], checkalpha)
|
|
1421
|
+
if distance < mindist:
|
|
1422
|
+
mindist = distance
|
|
1423
|
+
disti = i - starti
|
|
1424
|
+
if logcolor:
|
|
1425
|
+
logDebug("Color", color, "not found, closest color:", palette[disti])
|
|
1426
|
+
return disti
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def findBestPalette(palettes, colors):
|
|
1430
|
+
if len(palettes) == 1:
|
|
1431
|
+
return 0
|
|
1432
|
+
mindist = 0xffffffff
|
|
1433
|
+
disti = 0
|
|
1434
|
+
for i in range(len(palettes)):
|
|
1435
|
+
distance = 0
|
|
1436
|
+
for color in colors:
|
|
1437
|
+
singledist = 0xffffffff
|
|
1438
|
+
for palcolor in palettes[i]:
|
|
1439
|
+
singledist = min(singledist, getColorDistance(color, palcolor))
|
|
1440
|
+
distance += singledist
|
|
1441
|
+
if distance < mindist:
|
|
1442
|
+
mindist = distance
|
|
1443
|
+
disti = i
|
|
1444
|
+
if mindist == 0:
|
|
1445
|
+
break
|
|
1446
|
+
return disti
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def drawPalette(pixels, palette, width, ystart=0, transp=True):
|
|
1450
|
+
for x in range(len(palette)):
|
|
1451
|
+
j = width + ((x % 8) * 5)
|
|
1452
|
+
i = ystart + ((x // 8) * 5)
|
|
1453
|
+
for j2 in range(5):
|
|
1454
|
+
for i2 in range(5):
|
|
1455
|
+
color = palette[x]
|
|
1456
|
+
if not transp:
|
|
1457
|
+
color = (color[0], color[1], color[2], 255)
|
|
1458
|
+
pixels[j + j2, i + i2] = color
|
|
1459
|
+
return pixels
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def flipTile(tile, hflip, vflip, tilewidth=8, tileheight=8):
|
|
1463
|
+
newtile = [0] * len(tile)
|
|
1464
|
+
xrange = range(0, tilewidth) if not hflip else range(tilewidth - 1, -1, -1)
|
|
1465
|
+
yrange = range(0, tileheight) if not vflip else range(tileheight - 1, -1, -1)
|
|
1466
|
+
i = 0
|
|
1467
|
+
for y in yrange:
|
|
1468
|
+
for x in xrange:
|
|
1469
|
+
newtile[i] = tile[y * tileheight + x]
|
|
1470
|
+
i += 1
|
|
1471
|
+
return newtile
|