def write_savegame(self, savefile): """Restore savegame from file to sdcard This function (re)stores a given savegame file to the corresponding location on sdcard. Since the savegame backup has the product-code inside this function doesn't need any further arguments. It first opens the savegame backup, validates the header and retrieves the product-code to use find_game(product_code) for looking up the slot of the corresponding game. The savegame file itself stores the information wether it's Card1 or Card2, but i found it easier to just read the ncsd-header of the game. For Card1 savegames the file gets written to the corresponding slot in the region of Card1-savegames. For Card2 savegames it gets written to the writable_address offset of the game.""" self.fail_on_non_sky3ds() savegamefp = open(savefile, "rb") # CTR_SAVE ctr_save = savegamefp.read(0x8) if ctr_save != b'CTR_SAVE': raise Exception("Not a valid savegame") # Product Code product_code = savegamefp.read(0xa).decode('ascii') slot, ncsd_header = self.find_game(product_code) if slot == None: raise Exception("Game not on disk") # Zero Padding (ignored) savegamefp.read(0x1) # Save Type (ignored, read directly from ncsd_header) savegamefp.read(0x1) # NAND save offset (ignored, read directly from ncsd_header) savegamefp.read(0x4) # Unique ID (+ recalculate crc) self.diskfp.seek(self.rom_list[slot][1] + 0x1440) self.diskfp.write(savegamefp.read(0x40)) self.diskfp.seek(self.rom_list[slot][1] + 0x1400) card_data = self.diskfp.read(0x200) crc16 = titles.crc16(bytearray(card_data[:-2])) self.diskfp.seek(self.rom_list[slot][1] + 0x1400 + 0x200 - 0x2) self.diskfp.write(bytearray([(crc16 & 0xFF00) >> 8, crc16 & 0x00FF])) # Savegame data if ncsd_header['card_type'] == 'Card1': self.diskfp.seek(0x100000 * (slot + 1)) self.diskfp.write(savegamefp.read(0x100000)) os.fsync(self.diskfp) elif ncsd_header['card_type'] == 'Card2': self.diskfp.seek(self.rom_list[slot][1] + ncsd_header['writable_address']) for i in range(0, 10): self.diskfp.write(savegamefp.read(0x100000)) os.fsync(self.diskfp) savegamefp.close()
def write_savegame(self, savefile): """Restore savegame from file to sdcard This function (re)stores a given savegame file to the corresponding location on sdcard. Since the savegame backup has the product-code inside this function doesn't need any further arguments. It first opens the savegame backup, validates the header and retrieves the product-code to use find_game(product_code) for looking up the slot of the corresponding game. The savegame file itself stores the information wether it's Card1 or Card2, but i found it easier to just read the ncsd-header of the game. For Card1 savegames the file gets written to the corresponding slot in the region of Card1-savegames. For Card2 savegames it gets written to the writable_address offset of the game.""" self.fail_on_non_sky3ds() savegamefp = open(savefile, "rb") # CTR_SAVE ctr_save = savegamefp.read(0x8) if ctr_save != b'CTR_SAVE': raise Exception("Not a valid savegame") # Product Code product_code = savegamefp.read(0xa).decode('ascii') slot,ncsd_header = self.find_game(product_code) if slot == None: raise Exception("Game not on disk") # Zero Padding (ignored) savegamefp.read(0x1) # Save Type (ignored, read directly from ncsd_header) savegamefp.read(0x1) # NAND save offset (ignored, read directly from ncsd_header) savegamefp.read(0x4) # Unique ID (+ recalculate crc) self.diskfp.seek(self.rom_list[slot][1] + 0x1440) self.diskfp.write(savegamefp.read(0x40)) self.diskfp.seek(self.rom_list[slot][1] + 0x1400) card_data = self.diskfp.read(0x200) crc16 = titles.crc16(bytearray(card_data[:-2])) self.diskfp.seek(self.rom_list[slot][1] + 0x1400 + 0x200 - 0x2) self.diskfp.write(bytearray([(crc16 & 0xFF00) >> 8, crc16 & 0x00FF])) # Savegame data if ncsd_header['card_type'] == 'Card1': self.diskfp.seek(0x100000 * (slot + 1)) self.diskfp.write(savegamefp.read(0x100000)) os.fsync(self.diskfp) elif ncsd_header['card_type'] == 'Card2': self.diskfp.seek(self.rom_list[slot][1] + ncsd_header['writable_address']) for i in range(0, 10): self.diskfp.write(savegamefp.read(0x100000)) os.fsync(self.diskfp) savegamefp.close()
def write_rom(self, rom, silent=False, progress=None, use_header_bin=False, verbose=False): """Write rom to sdcard. Roms are stored at the position marked in the position headers (starting at 0x2000000). This code first looks for a free block with enough space to hold the specified rom, then continues to write the data to that location. After successful writing the savegame slot for this game is filled with zero. The last thing to do is to find the game in template.txt and write the data from that file to offset 0x1400 inside the rom on sdcard. Keyword Arguments: rom -- path to rom file""" self.fail_on_non_sky3ds() # follow symlink rom = os.path.realpath(rom) # get rom size and calculate block count rom_size = os.path.getsize(rom) rom_blocks = int(rom_size / 0x200) # get free blocks on sd-card and search for a block big enough for the rom start_block = 0 for free_block in self.free_blocks[::-1]: if free_block[1] >= rom_blocks: start_block = free_block[0] break if start_block == 0: raise Exception("Not enough free continous blocks") self.diskfp.seek(0) position_header_length = 0x100 # find free slot for game (card format is limited to 31 games) free_slot = -1 for i in range(0, int(position_header_length / 0x8) - 1): position = struct.unpack("ii", self.diskfp.read(0x8)) if position == (-1, -1): free_slot = i break if free_slot == -1: raise Exception( "No free slot found. There can be a maximum of %d games on one card." % int(position_header_length / 0x8)) # seek to start of rom on sd-card self.diskfp.seek(start_block * 0x200) # open rom file romfp = open(rom, "rb") # get card specific data from template.txt serial = gamecard.ncsd_serial(romfp) sha1 = gamecard.ncch_sha1sum(romfp) template_data = titles.get_template(serial, sha1) if template_data: generated_template = False card_data = bytearray.fromhex(template_data['card_data']) else: generated_template = True logging.warning( "Automagically creating sky3ds header (this will fail, lol)") card_data = bytearray() # card crypto + card id + eeprom id(?) romfp.seek(0x1244) card_data += romfp.read(0x4) romfp.seek(0x1240) card_data += romfp.read(0x4) romfp.seek(0x1248) card_data += romfp.read(0x4) # CRC16 of NCCH header? romfp.seek(0x1000) crc16 = titles.crc16(bytearray(romfp.read(0x200))) card_data += bytearray(struct.pack("H", crc16)[::-1]) card_data += bytearray( struct.pack("H", (crc16 << 16 | crc16 ^ 0xffff) & 0xFFFF)[::-1]) # CTRIMAGE + zero-padding card_data += bytearray("CTRIMAGE", "ascii") card_data += bytearray([0x00] * 0x8) # ?!?!?!? card_data += bytearray([0x00] * 0x10) # zero-padding card_data += bytearray([0x00] * 0x10) # unique id romfp.seek(0x1200) card_data += romfp.read(0x40) # name romfp.seek(0x1150) card_data += romfp.read(0x10) card_data += bytearray([0x00] * 0xf0) # zero-padding card_data += bytearray([0x00] * 0x80) if use_header_bin: header_bin = os.path.join(data_dir, 'header.bin') if os.path.exists(header_bin): logging.info( "Injecting headers from header.bin instead of template.txt!" ) try: header_bin_fp = open(header_bin, "rb") rom_header = bytearray(header_bin_fp.read(0x44)) header_bin_fp.close() for byte in range(0x40): card_data[0x40 + byte] = rom_header[byte] except: raise Exception( "Error: Can't inject headers from header.bin") elif rom[-4:] == ".3dz": romfp.seek(0x1200) rom_header = romfp.read(0x44) if rom_header[0x00:0x10] != bytearray([0xff] * 0x10): logging.info( "Injecting headers from 3dz file instead of template.txt!") for byte in range(0x40): card_data[0x40 + byte] = rom_header[byte] for byte in range(0x4): card_data[0x4 + byte] = rom_header[0x40 + byte] # recalculate checksum for sky3ds header (important after injection from 3dz or header.bin) crc16 = titles.crc16(card_data[:-2]) card_data[-2] = (crc16 & 0xFF00) >> 8 card_data[-1] = (crc16 & 0x00FF) if len(card_data) != 0x200: raise Exception("Invalid template data") if verbose: template = "\nUsed template:\n" template += "** : %s\n" % card_data[0x80:0x90].decode("ascii") template += "SHA1: %s\n" % gamecard.ncch_sha1sum(romfp).upper() for i in range(0, 0x20): line = "" for j in range(0, 0x10): line += ("%.2x " % card_data[i * 0x10 + j]).upper() template += line + "\n" template += "\n" logging.info(template) # write rom (with fancy progressbar!) romfp.seek(0) try: if not silent and not progress: progress = ProgressBar( widgets=[Percentage(), Bar(), FileTransferSpeed()], maxval=rom_size).start() except: pass written = 0 while written < rom_size: chunk = romfp.read(1024 * 1024 * 8) self.diskfp.write(chunk) os.fsync(self.diskfp) written = written + len(chunk) try: if not silent: progress.update(written) except: pass try: if not silent: progress.finish() except: pass # seek to slot header and write position + block-count of rom self.diskfp.seek(free_slot * 0x8) self.diskfp.write(struct.pack("ii", start_block, rom_blocks)) # add savegame slot self.diskfp.seek(0x100000 * (1 + len(self.rom_list))) self.diskfp.write(bytearray([0xff] * 0x100000)) self.diskfp.seek(start_block * 0x200 + 0x1400) self.diskfp.write(card_data) # cleanup romfp.close() os.fsync(self.diskfp) self.update_rom_list()
def write_rom(self, rom, silent=False, progress=None, use_header_bin=False, verbose=False): """Write rom to sdcard. Roms are stored at the position marked in the position headers (starting at 0x2000000). This code first looks for a free block with enough space to hold the specified rom, then continues to write the data to that location. After successful writing the savegame slot for this game is filled with zero. The last thing to do is to find the game in template.txt and write the data from that file to offset 0x1400 inside the rom on sdcard. Keyword Arguments: rom -- path to rom file""" self.fail_on_non_sky3ds() # follow symlink rom = os.path.realpath(rom) # get rom size and calculate block count rom_size = os.path.getsize(rom) rom_blocks = int(rom_size / 0x200) # get free blocks on sd-card and search for a block big enough for the rom start_block = 0 for free_block in self.free_blocks[::-1]: if free_block[1] >= rom_blocks: start_block = free_block[0] break if start_block == 0: raise Exception("Not enough free continous blocks") self.diskfp.seek(0) position_header_length = 0x100 # find free slot for game (card format is limited to 31 games) free_slot = -1 for i in range(0, int(position_header_length / 0x8) - 1): position = struct.unpack("ii", self.diskfp.read(0x8)) if position == (-1, -1): free_slot = i break if free_slot == -1: raise Exception("No free slot found. There can be a maximum of %d games on one card." % int(position_header_length / 0x8)) # seek to start of rom on sd-card self.diskfp.seek(start_block * 0x200) # open rom file romfp = open(rom, "rb") # get card specific data from template.txt serial = gamecard.ncsd_serial(romfp) sha1 = gamecard.ncch_sha1sum(romfp) template_data = titles.get_template(serial, sha1) if template_data: generated_template = False card_data = bytearray.fromhex(template_data['card_data']) else: generated_template = True logging.warning("Automagically creating sky3ds header (this will fail, lol)") card_data = bytearray() # card crypto + card id + eeprom id(?) romfp.seek(0x1244) card_data += romfp.read(0x4) romfp.seek(0x1240) card_data += romfp.read(0x4) romfp.seek(0x1248) card_data += romfp.read(0x4) # CRC16 of NCCH header? romfp.seek(0x1000) crc16 = titles.crc16(bytearray(romfp.read(0x200))) card_data += bytearray(struct.pack("H", crc16)[::-1]) card_data += bytearray(struct.pack("H", (crc16 << 16 | crc16 ^ 0xffff) & 0xFFFF)[::-1]) # CTRIMAGE + zero-padding card_data += bytearray("CTRIMAGE", "ascii") card_data += bytearray([0x00]*0x8) # ?!?!?!? card_data += bytearray([0x00] * 0x10) # zero-padding card_data += bytearray([0x00] * 0x10) # unique id romfp.seek(0x1200) card_data += romfp.read(0x40) # name romfp.seek(0x1150) card_data += romfp.read(0x10) card_data += bytearray([0x00] * 0xf0) # zero-padding card_data += bytearray([0x00] * 0x80) if use_header_bin: header_bin = os.path.join(data_dir,'header.bin') if os.path.exists(header_bin): logging.info("Injecting headers from header.bin instead of template.txt!") try: header_bin_fp = open(header_bin, "rb") rom_header = bytearray(header_bin_fp.read(0x44)) header_bin_fp.close() for byte in range(0x40): card_data[0x40+byte] = rom_header[byte] except: raise Exception("Error: Can't inject headers from header.bin") elif rom[-4:] == ".3dz": romfp.seek(0x1200) rom_header = romfp.read(0x44) if rom_header[0x00:0x10] != bytearray([0xff]*0x10): logging.info("Injecting headers from 3dz file instead of template.txt!") for byte in range(0x40): card_data[0x40+byte] = rom_header[byte] for byte in range(0x4): card_data[0x4+byte] = rom_header[0x40+byte] # recalculate checksum for sky3ds header (important after injection from 3dz or header.bin) crc16 = titles.crc16(card_data[:-2]) card_data[-2] = (crc16 & 0xFF00) >> 8 card_data[-1] = (crc16 & 0x00FF) if len(card_data) != 0x200: raise Exception("Invalid template data") if verbose: template = "\nUsed template:\n" template += "** : %s\n" % card_data[0x80:0x90].decode("ascii") template += "SHA1: %s\n" % gamecard.ncch_sha1sum(romfp).upper() for i in range(0, 0x20): line = "" for j in range(0, 0x10): line += ("%.2x " % card_data[i*0x10+j]).upper() template += line + "\n" template += "\n" logging.info(template) # write rom (with fancy progressbar!) romfp.seek(0) try: if not silent and not progress: progress = ProgressBar(widgets=[Percentage(), Bar(), FileTransferSpeed()], maxval=rom_size).start() except: pass written = 0 while written < rom_size: chunk = romfp.read(1024*1024*8) self.diskfp.write(chunk) os.fsync(self.diskfp) written = written + len(chunk) try: if not silent: progress.update(written) except: pass try: if not silent: progress.finish() except: pass # seek to slot header and write position + block-count of rom self.diskfp.seek(free_slot * 0x8) self.diskfp.write(struct.pack("ii", start_block, rom_blocks)) # add savegame slot self.diskfp.seek(0x100000 * (1 + len(self.rom_list))) self.diskfp.write(bytearray([0xff]*0x100000)) self.diskfp.seek(start_block * 0x200 + 0x1400) self.diskfp.write(card_data) # cleanup romfp.close() os.fsync(self.diskfp) self.update_rom_list()