hacktools 0.36.0__cp313-cp313-win32.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/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