def getFontGlyphs(file): glyphs = {} with common.Stream(file, "rb", False) as f: # Header f.seek(36) hdwcoffset = f.readUInt() pamcoffset = f.readUInt() common.logDebug("hdwcoffset:", hdwcoffset, "pamcoffset:", pamcoffset) # HDWC f.seek(hdwcoffset - 4) hdwclen = f.readUInt() tilenum = (hdwclen - 16) // 3 firstcode = f.readUShort() lastcode = f.readUShort() f.seek(4, 1) common.logDebug("firstcode:", firstcode, "lastcode:", lastcode, "tilenum", tilenum) hdwc = [] for i in range(tilenum): hdwcstart = f.readSByte() hdwcwidth = f.readByte() hdwclength = f.readByte() hdwc.append((hdwcstart, hdwcwidth, hdwclength)) # PAMC nextoffset = pamcoffset while nextoffset != 0x00: f.seek(nextoffset) firstchar = f.readUShort() lastchar = f.readUShort() sectiontype = f.readUShort() f.seek(2, 1) nextoffset = f.readUInt() common.logDebug("firstchar:", common.toHex(firstchar), "lastchar:", common.toHex(lastchar), "sectiontype:", sectiontype, "nextoffset:", nextoffset) if sectiontype == 0: firstcode = f.readUShort() for i in range(lastchar - firstchar + 1): c = common.codeToChar(firstchar + i) glyphs[c] = common.FontGlyph(hdwc[firstcode + i][0], hdwc[firstcode + i][1], hdwc[firstcode + i][2], c, firstchar + i, firstcode + i) elif sectiontype == 1: for i in range(lastchar - firstchar + 1): charcode = f.readUShort() if charcode == 0xFFFF or charcode >= len(hdwc): continue c = common.codeToChar(firstchar + i) glyphs[c] = common.FontGlyph(hdwc[charcode][0], hdwc[charcode][1], hdwc[charcode][2], c, firstchar + i, charcode) else: common.logWarning("Unknown section type", sectiontype) return glyphs
def drawTIM(outfile, tim, transp=False, forcepal=-1, allpalettes=False, nopal=False): if tim.width == 0 or tim.height == 0: return clutwidth = clutheight = 0 if tim.bpp == 4 or tim.bpp == 8: clut = forcepal if forcepal != -1 else getUniqueCLUT(tim, transp) if not nopal: clutwidth = 40 clutheight = 5 * (len(tim.cluts[clut]) // 8) if allpalettes: clutheight *= len(tim.cluts) img = Image.new("RGBA", (tim.width + clutwidth, max(tim.height, clutheight)), (0, 0, 0, 0)) pixels = img.load() x = 0 for i in range(tim.height): for j in range(tim.width): if x >= len(tim.data): common.logWarning("Out of TIM data") break if tim.bpp == 4 or tim.bpp == 8: if len(tim.cluts[clut]) > tim.data[x]: color = tim.cluts[clut][tim.data[x]] else: common.logWarning("Index", tim.data[x], "not in CLUT") color = (0, 0, 0, 0) else: color = tim.data[x] if not transp: color = (color[0], color[1], color[2], 255) pixels[j, i] = color x += 1 if (tim.bpp == 4 or tim.bpp == 8) and not nopal: if allpalettes: for i in range(len(tim.cluts)): pixels = common.drawPalette(pixels, tim.cluts[i], tim.width, i * (clutheight // len(tim.cluts)), transp) else: pixels = common.drawPalette(pixels, tim.cluts[clut], tim.width, 0, transp) if outfile == "": return img img.save(outfile, "PNG")
def readTIMData(f, tim, pixelnum): try: for i in range(pixelnum): if tim.bpp == 4: tim.data.append(f.readHalf()) elif tim.bpp == 8: tim.data.append(f.readByte()) elif tim.bpp == 16: color = common.readRGB5A1(f.readUShort()) tim.data.append(color) elif tim.bpp == 24: tim.data.append( (f.readByte(), f.readByte(), f.readByte(), 255)) except struct.error: common.logWarning("Malformed TIM")
def readShiftJIS(f, len2=False, untilZero=False, encoding="shift_jis"): if untilZero: strlen2 = 999 else: if len2: strlen = f.readUShort() strlen2 = f.readUShort() else: strlen = f.readByte() strlen2 = f.readByte() sjis = "" i = j = 0 padding = 0 while i < strlen2: b1 = f.readByte() if b1 == 0x00: i += 1 j += 1 padding += 1 if untilZero: return sjis, i else: b2 = f.readByte() if b1 == 0x0d and b2 == 0x0a: sjis += "|" i += 2 j += 2 elif b1 == 0x81 and b2 == 0xa5: sjis += ">>" i += 2 j += 1 elif not common.checkShiftJIS(b1, b2): f.seek(-1, 1) sjis += chr(b1) i += 1 j += 1 else: f.seek(-2, 1) try: sjis += f.read(2).decode(encoding).replace("〜", "~") except UnicodeDecodeError: common.logDebug("UnicodeDecodeError at", f.tell() - 2) sjis += "UNK(" + common.toHex(b1) + common.toHex(b2) + ")" i += 2 j += 1 if not untilZero and j != strlen: common.logWarning("Wrong strlen", strlen, j) return sjis, i
def writeShiftJIS(f, str, writelen=True, maxlen=0): if str == "": if writelen: f.writeShort(1) f.writeByte(0) return 1 i = 0 strlen = 0 if writelen: lenpos = f.tell() f.writeShort(strlen) if ord(str[0]) < 256 or str[0] == "“" or str[0] == "”" or str[0] == "↓": # ASCII string while i < len(str): # Add a space if the next character is <XX>, UNK(XXXX) or CUS(XXXX) if i < len(str) - 1 and str[i+1] == "<": str = str[:i+1] + " " + str[i+1:] elif i < len(str) - 4 and (str[i+1:i+5] == "UNK(" or str[i+1:i+5] == "CUS("): str = str[:i+1] + " " + str[i+1:] char = str[i] # Code format <XX> if char == "<" and i < len(str) - 3 and str[i+3] == ">": try: if maxlen > 0 and strlen + 1 > maxlen: return -1 code = str[i+1] + str[i+2] f.write(bytes.fromhex(code)) strlen += 1 except ValueError: common.logwarning("Invalid escape code", str[i+1], str[i+2]) i += 4 # Unknown format UNK(XXXX) elif char == "U" and i < len(str) - 4 and str[i:i+4] == "UNK(": if maxlen > 0 and strlen + 2 > maxlen: return -1 code = str[i+4] + str[i+5] f.write(bytes.fromhex(code)) code = str[i+6] + str[i+7] f.write(bytes.fromhex(code)) i += 9 strlen += 2 # Custom full-size glyph CUS(XXXX) elif char == "C" and i < len(str) - 4 and str[i:i+4] == "CUS(": if maxlen > 0 and strlen + 2 > maxlen: return -1 f.write(bytes.fromhex(common.table[str[i+4:i+8]])) i += 9 strlen += 2 else: if i + 1 == len(str): bigram = char + " " else: bigram = char + str[i+1] i += 2 if maxlen > 0 and strlen + 2 > maxlen: return -1 if bigram not in common.table: try: common.logWarning("Bigram not found:", bigram, "in string", str) except UnicodeEncodeError: common.logWarning("Bigram not found in string", str) bigram = " " f.write(bytes.fromhex(common.table[bigram])) strlen += 2 else: # SJIS string str = str.replace("~", "〜") while i < len(str): char = str[i] if char == "<": code = str[i+1] + str[i+2] i += 4 f.write(bytes.fromhex(code)) strlen += 1 else: i += 1 f.write(char.encode("shift-jis")) strlen += 2 if writelen: f.writeZero(1) pos = f.tell() f.seek(lenpos) f.writeShort(strlen + 1) f.seek(pos) return strlen + 1
def drawMappedImage(width, height, mapdata, tiledata, paldata, tilesize=8, bpp=4): palnum = len(paldata) // 32 img = Image.new("RGBA", (width + 40, max(height, palnum * 10)), (0, 0, 0, 0)) pixels = img.load() # Maps maps = [] for i in range(0, len(mapdata), 2): map = struct.unpack("<h", mapdata[i:i+2])[0] pal = (map >> 12) & 0xF xflip = (map >> 10) & 1 yflip = (map >> 11) & 1 tile = map & 0x3FF maps.append((pal, xflip, yflip, tile)) common.logDebug("Loaded", len(maps), "maps") # Tiles tiles = [] for i in range(len(tiledata) // (32 if bpp == 4 else 64)): singletile = [] for j in range(tilesize * tilesize): x = i * (tilesize * tilesize) + j if bpp == 4: index = (tiledata[x // 2] >> ((x % 2) << 2)) & 0x0f else: index = tiledata[x] singletile.append(index) tiles.append(singletile) common.logDebug("Loaded", len(tiles), "tiles") # Palette palettes = readPaletteData(paldata) pals = [] for palette in palettes: pals += palette # Draw the image i = j = 0 for map in maps: try: pal = map[0] xflip = map[1] yflip = map[2] tile = tiles[map[3]] for i2 in range(tilesize): for j2 in range(tilesize): pixels[j + j2, i + i2] = pals[16 * pal + tile[i2 * tilesize + j2]] # Very inefficient way to flip pixels if xflip or yflip: sub = img.crop(box=(j, i, j + tilesize, i + tilesize)) if yflip: sub = ImageOps.flip(sub) if xflip: sub = ImageOps.mirror(sub) img.paste(sub, box=(j, i)) except (KeyError, IndexError): common.logWarning("Tile or palette", str(map), "not found") j += tilesize if j >= width: j = 0 i += tilesize # Draw palette if len(palettes) > 0: for i in range(len(palettes)): pixels = common.drawPalette(pixels, palettes[i], width, i * 10) return img
def run(): binin = "data/extract/arm9.bin" binout = "data/repack/arm9.bin" binfile = "data/bin_input.txt" tablefile = "data/table.txt" if not os.path.isfile(binfile): common.logError("Input file", binfile, "not found.") return common.copyFile(binin, binout) freeranges = [(0xEA810, 0xEEC00)] currentrange = 0 rangepos = 0 section = {} with codecs.open(binfile, "r", "utf-8") as bin: section = common.getSection(bin, "", "#", game.fixchars) chartot, transtot = common.getSectionPercentage(section) common.logMessage("Repacking BIN from", binfile, "...") common.loadTable(tablefile) rangepos = freeranges[currentrange][0] with common.Stream(binin, "rb") as fi: allbin = fi.read() strpointers = {} with common.Stream(binout, "r+b") as fo: # Skip the beginning and end of the file to avoid false-positives fi.seek(992000) while fi.tell() < 1180000: pos = fi.tell() if pos < 1010000 or pos > 1107700: check = game.detectShiftJIS(fi) if check in section and section[check][0] != "": common.logDebug("Replacing string at", pos) newstr = section[check][0] # Check how much padding space we have padding = 0 while True: if fi.readByte() == 0x00: padding += 1 else: fi.seek(-1, 1) break fo.seek(pos) endpos = fi.tell() - 1 newlen = game.writeShiftJIS(fo, newstr, False, endpos - pos) if newlen < 0: if rangepos >= freeranges[currentrange][ 1] and newstr not in strpointers: common.logWarning("No more room! Skipping ...") else: # Write the string in a new portion of the rom if newstr in strpointers: newpointer = strpointers[newstr] else: common.logDebug( "No room for the string, redirecting to", rangepos) fo.seek(rangepos) game.writeShiftJIS(fo, newstr, False) fo.writeZero(1) newpointer = 0x02000000 + rangepos rangepos = fo.tell() strpointers[newstr] = newpointer if rangepos >= freeranges[currentrange][1]: if currentrange + 1 < len(freeranges): currentrange += 1 rangepos = freeranges[ currentrange][0] # Search and replace the old pointer pointer = 0x02000000 + pos pointersearch = struct.pack("<I", pointer) index = 0 common.logDebug("Searching for pointer", pointersearch.hex().upper()) while index < len(allbin): index = allbin.find(pointersearch, index) if index < 0: break common.logDebug(" Replaced pointer at", str(index)) fo.seek(index) fo.writeUInt(newpointer) index += 4 else: fo.writeZero(endpos - fo.tell()) if check != "": pos = fi.tell() - 1 fi.seek(pos + 1) common.logMessage("Done! Translation is at {0:.2f}%".format( (100 * transtot) / chartot))
def run(firstgame, no_redirect): infolder = "data/extract/data/data/" outfolder = "data/repack/data/data/" infile = "data/dat_input.txt" redfile = "data/redirects.asm" fontfile = "data/replace/data/font/lcfont12.NFTR" if not os.path.isfile(infile): common.logError("Input file", infile, "not found") return common.makeFolder(outfolder) chartot = transtot = 0 monthsection, skipsection = game.monthsection, game.skipsection game.monthsection = game.skipsection = None encoding = "shift_jis" if firstgame else "shift_jisx0213" common.logMessage("Repacking DAT from", infile, "...") # Read the glyph size from the font if not os.path.isfile(fontfile): fontfile = fontfile.replace("replace/", "extract/") glyphs = nitro.readNFTR(fontfile).glyphs fixchars = game.getFixChars() # Copy this txt file if not firstgame and os.path.isfile(infolder + "facilityhelp.txt"): common.copyFile(infolder + "facilityhelp.txt", outfolder + "facilityhelp.txt") redirects = [] with codecs.open(infile, "r", "utf-8") as dat: files = common.getFiles(infolder, ".dat") for file in common.showProgress(files): section = common.getSection(dat, file, fixchars=fixchars) # If there are no lines, just copy the file if len(section) == 0: common.copyFile(infolder + file, outfolder + file) # Part of the AP patch if not firstgame and file == "route.dat": with common.Stream(outfolder + file, "rb+") as f: f.seek(0x5ee8) f.writeByte(0x0) continue i = 0 chartot, transtot = common.getSectionPercentage( section, chartot, transtot) common.logDebug("Processing", file, "...") size = os.path.getsize(infolder + file) with common.Stream(infolder + file, "rb") as fin: with common.Stream(outfolder + file, "wb") as f: f.write(fin.read()) fin.seek(0) # Loop the file and replace strings as needed while fin.tell() < size - 2: pos = fin.tell() check = game.detectShiftJIS(fin, encoding) if check != "": if file == "entrance_icon.dat": # For entrance_icon, just write the string and update the pointer if check in section: # For the first one, seek to the correct position in the output file if i == 0: f.seek(pos) # Write the string newsjis = check if check in section and section[check][ 0] != "": common.logDebug( "Replacing string at", pos) newsjis = section[check][0] startpos = f.tell() game.writeShiftJIS(f, newsjis, False, True, 0, encoding) endpos = f.tell() # Update the pointer f.seek(0x1c98 + 4 * i) f.writeUInt(startpos - 0x1c98) f.seek(endpos) i += 1 else: # Found a SJIS string, check if we have to replace it if check in section and section[check][0] != "": common.logDebug("Replacing string at", pos) f.seek(pos) newsjis = section[check][0] maxlen = 0 if file == "goods.dat": newsjis = common.wordwrap( newsjis, glyphs, 170) maxlen = 60 elif file == "gossip.dat": newsjis = common.wordwrap( newsjis, glyphs, 190) if newsjis.count("<<") > 0: newsjis = common.centerLines( newsjis, glyphs, 190, centercode="<<") if fin.tell() - pos < 35: maxlen = 35 else: maxlen = 160 elif file == "scenarioguide.dat": newsjis = common.wordwrap( newsjis, glyphs, 165) maxlen = 60 if newsjis.count("|") > 1: common.logError( "scenarioguide line", newsjis, "too long") newlen = game.writeShiftJIS( f, newsjis, False, True, maxlen, encoding) if newlen < 0: if file != "gossip.dat" or no_redirect or maxlen != 160: common.logError( "String {} is too long ({}/{})." .format( newsjis, len(newsjis), maxlen)) else: common.logWarning( "String {} is too long ({}/{})." .format( newsjis, len(newsjis), maxlen)) # Doesn't fit, write it shorter f.seek(pos) cutat = 155 if firstgame else 150 while ord(newsjis[cutat]) > 127: cutat -= 1 stringfit = newsjis[:cutat] stringrest = newsjis[cutat:] game.writeShiftJIS( f, stringfit, False, True, maxlen, encoding) f.seek(-1, 1) f.writeByte(0x1f) f.writeByte(len(redirects)) redirects.append(stringrest) # Pad with 0s if the line is shorter while f.tell() < fin.tell(): f.writeByte(0x00) pos = fin.tell() - 1 fin.seek(pos + 1) with codecs.open(redfile, "w", "utf-8") as f: f.write(".ascii \"NDSC\"\n\n") f.write("REDIRECT_START:\n\n") for i in range(len(redirects)): f.write(".dh REDIRECT_{} - REDIRECT_START\n".format(i)) for i in range(len(redirects)): f.write("\nREDIRECT_{}:\n".format(i)) redirect = redirects[i].replace("\"", "\\\"") redirect = redirect.replace("|", "\" :: .db 0xa :: .ascii \"") redirectascii = "" for c in redirect: if ord(c) > 127: sjisc = common.toHex( int.from_bytes(c.encode(encoding), "big")) redirectascii += "\" :: .db 0x" + sjisc[:2] + " :: .db 0x" + sjisc[ 2:] + " :: .ascii \"" else: redirectascii += c f.write(".ascii \"{}\" :: .db 0\n".format(redirectascii)) game.monthsection, game.skipsection = monthsection, skipsection common.logMessage("Done! Translation is at {0:.2f}%".format( (100 * transtot) / chartot))
def readGIMBlock(f, gim, image): offset = f.tell() id = f.readUShort() f.seek(2, 1) if id == 0xFF: # Info block common.logDebug("GIM 0xFF at", common.toHex(offset)) return 0, image elif id == 0x03: # Picture block common.logDebug("GIM 0x03 at", common.toHex(offset)) image = GIMImage() gim.images.append(image) image.picoff = offset image.picsize = f.readUInt() nextblock = f.readUInt() common.logDebug("picoff", image.picoff, "picsize", image.picsize) return image.picoff + nextblock, image elif id == 0x04: # Image block common.logDebug("GIM 0x04 at", common.toHex(offset)) image.imgoff = offset image.imgsize = f.readUInt() nextblock = f.readUInt() f.seek(4, 1) image.imgframeoff = f.readUShort() f.seek(2, 1) image.format = f.readUShort() if image.format == 0x04: image.bpp = 4 elif image.format == 0x05: image.bpp = 8 elif image.format == 0x03 or image.format == 0x07: image.bpp = 32 else: image.bpp = 16 image.tiled = f.readUShort() image.width = f.readUShort() image.height = f.readUShort() if image.tiled == 0x01: image.tilewidth = 0x80 // image.bpp image.tileheight = 8 image.blockedwidth = math.ceil( image.width / image.tilewidth) * image.tilewidth image.blockedheight = math.ceil( image.height / image.tileheight) * image.tileheight if image.format > 0x05: common.logError("Unsupported image format:", image.format) return image.imgoff + nextblock, image f.seek(image.imgoff + 32 + image.imgframeoff) for i in range(image.blockedheight if image.tiled == 0x01 else image.height): for j in range(image.blockedwidth if image.tiled == 0x01 else image.width): index = 0 if image.format == 0x04: index = f.readHalf() elif image.format == 0x05: index = f.readByte() else: index = readColor(f, image.format) image.colors.append(index) common.logDebug("imgoff", image.imgoff, "imgsize", image.imgsize, "imgframeoff", image.imgframeoff, "format", image.format, "bpp", image.bpp) common.logDebug("tiled", image.tiled, "width", image.width, "height", image.height) common.logDebug("blockedwidth", image.blockedwidth, "blockedheight", image.blockedheight, "tilewidth", image.tilewidth, "tileheight", image.tileheight) return image.imgoff + nextblock, image elif id == 0x05: # Palette common.logDebug("GIM 0x05 at", common.toHex(offset)) image.paloff = offset image.palsize = f.readUInt() nextblock = f.readUInt() f.seek(4, 1) image.palframeoff = f.readUShort() f.seek(2, 1) image.palformat = f.readUShort() f.seek(image.paloff + 32 + image.palframeoff) while f.tell() < image.paloff + nextblock: image.palette.append(readColor(f, image.palformat)) common.logDebug("paloff", image.paloff, "palsize", image.palsize, "palframeoff", image.palframeoff) common.logDebug("palformat", image.palformat, "length", len(image.palette)) return image.paloff + nextblock, image else: common.logWarning("Skipping unknown block at", offset, ":", id) f.seek(4, 1) return offset + f.readUInt(), image