def save(self, writer=None): writer = BinaryIO() # FIXME blocks = {} num = 0 for name in self.files: match = re.match( '^(?P<block>[0-9]+c?)_' '(?P<idx>[0-9]{1,5})' '(?P<flags>[A-O]*c)?' '(?:\\[(?P<key>[0-9A-F]{1,4})\\])?$', name) if not match: raise ValueError( '{0} is not a valid identifier+options'.format(name)) block_name = match.group('block') if block_name not in blocks: blocks[block_name] = {} idx = int(match.group('idx')) num = max(idx, num) flags = match.group('flags') try: key = int(match.group('key'), 16) except: key = 0 blocks[block_name][idx] = (flags, key, self.files[name]) if self.version in game.GEN_IV: if set(blocks.keys()) | {'0c', '0'} != {'0c', '0'}: raise ValueError('Gen IV cannot have any blocks other than' ' 0 (and 0c). Got: {0}'.format(blocks.keys())) blocks.pop('0c', None) # TODO: handle comment blocks self.numblocks = 1 else: self.numblocks = len(blocks) # base_offset = self.size() # if self.version > game.GEN_IV: # base_offset += 4*self.numblocks # base_offset += TableEntry.instance(self.version).size()*self.numblocks self.num = num + 1 start = writer.tell() writer = AtomicStruct.save(self, writer) text_writer = BinaryIO() text_offs = writer.tell() + 8 * self.num prev_text_pos = 0 if self.version in game.GEN_IV: for i, block_name in enumerate(blocks): # if self.version > game.GEN_IV: # text_offs += 4*self.numblocks+text_writer.tell()-prev_text_pos # prev_text_pos = text_writer.tell() for j in xrange(self.num): try: flags, key, text = blocks[block_name][j] except KeyError: flags = key = None text = '' string = [] cidx = 0 while cidx < len(text): char = text[cidx] cidx += 1 if char == '\\': char = text[cidx] cidx += 1 if char == 'x': # n = int(text[cidx:cidx+2], 16) n = rtable['\\x' + text[cidx:cidx + 2]] cidx += 2 elif char == 'n': n = 0xE000 elif char == 'r': n = 0x25BC elif char == 'f': n = 0x25BD elif char == 'u': n = rtable['\\u' + text[cidx:cidx + 4]] cidx += 4 elif char == '?': n = int(text[cidx:cidx + 4], 16) cidx += 4 else: n = 1 string.append(n) elif char == '\n': string.append(0xE000) elif char == '\r': string.append(0x25BC) elif char == '\f': string.append(0x25BD) elif char == 'V' and text[cidx:cidx + 3] == 'AR(': eov = text.find(')', cidx + 3) if eov == -1: raise RuntimeError( 'Could not find end of VAR()') args = [] for arg in text[cidx + 3:eov].split(','): args.append(int(arg.strip(), 0)) cidx = eov + 1 string.append(0xFFFE) string.append(args.pop(0)) string.append(len(args)) string.extend(args) else: string.append(rtable[char]) if flags and 'c' in flags: string = compress(string, 15) string.append(0xFFFF) size = len(string) text_writer.writeAlign(4) state = (((self.seed * 0x2FD) & 0xFFFF) * (j + 1)) & 0xFFFF key = state | state << 16 writer.writeUInt32(key ^ (text_offs + text_writer.tell())) writer.writeUInt32(key ^ size) key = (TEXT_KEY4_INIT * (j + 1)) & 0xFFFF for char in string: text_writer.writeUInt16(char ^ key) key = (key + TEXT_KEY4_STEP) & 0xFFFF # TODO: comments writer.write(text_writer.getvalue()) else: block = Editable() block.uint32('size') block.array('entries', TableEntry(self.version).base_struct, length=self.num) block.freeze() block_offset_pos = writer.tell() for i in xrange(self.numblocks): writer.writeUInt32(0) for i, block_name in enumerate(blocks): text_writer = BinaryIO() block.save(text_writer) for j, entry in enumerate(block.entries): entry.offset = text_writer.tell() try: flags, key, text = blocks[block_name][j] except KeyError: flags = key = None text = '' string = [] cidx = 0 while cidx < len(text): char = text[cidx] cidx += 1 if char == '\\': char = text[cidx] cidx += 1 if char == 'x': n = int(text[cidx:cidx + 2], 16) cidx += 2 elif char == 'u' or char == '?': n = int(text[cidx:cidx + 4], 16) cidx += 4 elif char == 'n': n = 0xFFFE elif char == 'r': string.append(0xF000) string.append(0xBE01) string.append(0) continue elif char == 'f': string.append(0xF000) string.append(0xBE00) string.append(0) continue else: n = 1 string.append(n) elif char == '\n': string.append(0xFFFE) elif char == '\r': string.append(0xF000) string.append(0xBE01) string.append(0) elif char == '\f': string.append(0xF000) string.append(0xBE00) string.append(0) elif char == 'V' and text[cidx:cidx + 3] == 'AR(': eov = text.find(')', cidx + 3) if eov == -1: raise RuntimeError( 'Could not find end of VAR()') args = [] for arg in text[cidx + 3:eov].split(','): args.append(int(arg.strip(), 0)) cidx = eov + 1 string.append(0xF000) string.append(args.pop(0)) string.append(len(args)) string.extend(args) else: string.append(ord(char)) flag = 0 if flags: for shift in range(16): if chr(65 + shift) in flags: flag |= 1 << shift if 'c' in flags: string = compress(string, 16) if not key: key = 0 string.append(0xFFFF) encchars = [] while string: char = string.pop() ^ key key = ((key >> 3) | (key << 13)) & 0xFFFF encchars.insert(0, char) entry.charcount = len(encchars) entry.flags = flag for char in encchars: text_writer.writeUInt16(char) text_writer.writeAlign(4) block.size = text_writer.tell() with text_writer.seek(0): block.save(text_writer) block_offset = writer.tell() - start with writer.seek(block_offset_pos + 4 * i): writer.writeUInt32(block_offset) writer.write(text_writer.getvalue()) self.filesize = writer.tell() - start with writer.seek(start): AtomicStruct.save(self, writer) return writer
def load(self, reader): reader = BinaryIO.reader(reader) AtomicStruct.load(self, reader) self.files = {} self.ids = {} offsets = [] sizes = [] if self.version in game.GEN_IV: commented = False # (self.seed & 0x1FF) == 0x1FF for i in xrange(1, self.num + 1): state = (((self.seed * 0x2FD) & 0xFFFF) * i) & 0xFFFF key = state | state << 16 offsets.append(reader.readUInt32() ^ key) sizes.append(reader.readUInt32() ^ key) if commented: state = (((self.seed * 0x2FD) & 0xFFFF) * i) & 0xFFFF key = state | state << 16 comment_ofs = reader.readUInt32() ^ key term = reader.readUInt32() ^ key if term != 0xFFFF: raise ValueError('Expected 0xFFFF comment ofs terminator.' ' Got {0:#x}'.format(term)) for i in xrange(self.num): compressed = False reader.seek(offsets[i]) size = sizes[i] string = [] key = (TEXT_KEY4_INIT * (i + 1)) & 0xFFFF for j in range(size): string.append(reader.readUInt16() ^ key) key = (key + TEXT_KEY4_STEP) & 0xFFFF if string[0] == 0xF100: compressed = True string = decompress(string) text = '' while string: char = string.pop(0) if char == 0xFFFF: break elif char == 0xFFFE: text += 'VAR(' args = [string.pop(0)] count = string.pop(0) args += string[:count] string = string[count:] text += ', '.join(map(str, args)) text += ')' elif char == 0xE000: text += '\\n' elif char == 0x25bc: text += '\\r' elif char == 0x25bd: text += '\\f' else: try: text += table[char] except KeyError: text += '\\?{0:04x}'.format(char) name = '0_{0:05}'.format(i) if compressed: name += 'c' self.files[name] = text self.ids[i] = name else: raise RuntimeError('Did not have a terminating character') else: commented = False for i in xrange(self.numblocks): offsets.append(reader.readUInt32()) block = Editable() block.uint32('size') block.array('entries', TableEntry(self.version).base_struct, length=self.num) block.freeze() for i, block_offset in enumerate(offsets): reader.seek(block_offset) block.load(reader) for j, entry in enumerate(block.entries): compressed = False text = '' reader.seek(block_offset + entry.offset) encchars = [ reader.readUInt16() for k in xrange(entry.charcount) ] seed = key = encchars[-1] ^ 0xFFFF string = [] # decrypted chars while encchars: char = encchars.pop() ^ key key = ((key >> 3) | (key << 13)) & 0xFFFF string.insert(0, char) if string[0] == 0xF100: compressed = True string = decompress(string, 16) while string: char = string.pop(0) if char == 0xFFFF: break elif char == 0xFFFE: text += '\\n' elif char < 20 or char > 0xF000: text += '\\?{0:04X}'.format(char) elif char == 0xF000: kind = string.pop(0) count = string.pop(0) if kind == 0xbe00 and not count: text += '\\f' elif kind == 0xbe01 and not count: text += '\\r' else: text += 'VAR(' args = [kind] args += string[:count] string = string[count:] text += ', '.join(map(str, args)) text += ')' else: text += unichr(char) name = '{0}_{1:05}'.format(i, j) c = 65 for k in xrange(16): if (entry.flags >> k) & 0x1: name += ord(c + k) if compressed: name += 'c' name += '[{0:04X}]'.format(seed) self.files[name] = text self.ids[j] = name if commented: reader.seek(comment_ofs) num = reader.readUInt16() for i in xrange(num): commentid = reader.readUInt16() text = '' while True: char = reader.readUInt16() if char == 0xFFFF: break text += unichr(char) name = '0c_{0:05}'.format(commentid) self.files[name] = text return self