Last active
March 16, 2018 15:20
-
-
Save ihaveamac/dfc01fa09483c275f72ad69cd7e8080f to your computer and use it in GitHub Desktop.
convert .3ds to .cia with only an exheader xorpad ~ see https://github.com/ihaveamac/3dsconv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python2 | |
import sys, os, binascii, math, subprocess, errno | |
def testcommand(cmd): | |
try: | |
proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE).wait() | |
return True | |
except OSError as e: | |
if e.errno != 2: | |
raise | |
return False | |
def runcommand(cmdargs): | |
if verbose: | |
print("$ "+" ".join(cmdargs)) | |
proc = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
proc.wait() | |
#print(proc.returncode) | |
procoutput = proc.communicate()[0] | |
if verbose: | |
print(procoutput) | |
if proc.returncode != 0: | |
print("! "+cmdargs[0]+" had an error.") | |
# prevent printing twice | |
if not verbose: | |
print("- full command: "+" ".join(cmdargs)) | |
print("- output:") | |
print(procoutput) | |
# used from http://stackoverflow.com/questions/10840533/most-pythonic-way-to-delete-a-file-which-may-not-exist | |
def silentremove(filename): | |
try: | |
os.remove(filename) | |
except OSError as e: # this would be "except OSError, e:" before Python 2.6 | |
if e.errno != errno.ENOENT: # errno.ENOENT = no such file or directory | |
raise # re-raise exception if a different error occured | |
def docleanup(): | |
silentremove("work/game-orig.cxi") | |
silentremove("work/game-conv.cxi") | |
silentremove("work/manual.cfa") | |
silentremove("work/dlpchild.cfa") | |
silentremove("work/ncchheader.bin") | |
silentremove("work/exheader.bin") | |
silentremove("work/exefs.bin") | |
silentremove("work/romfs.bin") | |
silentremove("work/logo.bcma.lz") | |
silentremove("work/plain.bin") | |
if len(sys.argv) < 2: | |
print("usage: 3dsconv.py [--force] [--nocleanup] game.3ds [game.3ds ...]") | |
print(" --force - run even if 3dstool/makerom aren't found") | |
print(" --nocleanup - don't remove temporary files once finished (only applies to last rom used)") | |
print(" --verbose - print more information") | |
print("") | |
print("- an ExHeader XORpad should exist in the working directory") | |
print(" named \"<TITLEID>.Main.exheader.xorpad\"") | |
print("- 3dstool and makerom should exist in your PATH") | |
print("") | |
print("- version 1.01") | |
sys.exit(1) | |
fail = False | |
if not testcommand("3dstool") and not "--force" in sys.argv: | |
print("! 3dstool doesn't appear to be in your PATH.") | |
print(" you can get it from here:") | |
print(" https://github.com/dnasdw/3dstool") | |
print("- if you want to force the script to run,") | |
print(" add --force as one of the arguments.") | |
fail = True | |
if not testcommand("makerom") and not "--force" in sys.argv: | |
print("! makerom doesn't appear to be in your PATH.") | |
print(" you can get it from here:") | |
print(" https://github.com/profi200/Project_CTR") | |
print("- if you want to force the script to run,") | |
print(" add --force as one of the arguments.") | |
fail = True | |
if fail: | |
sys.exit(1) | |
try: | |
os.makedirs("work") | |
except OSError: | |
if not os.path.isdir("work"): | |
raise | |
cleanup = not "--nocleanup" in sys.argv | |
verbose = "--verbose" in sys.argv | |
totalroms = 0 | |
processedroms = 0 | |
for rom in sys.argv[1:]: | |
if rom == "--force" or rom == "--nocleanup" or rom == "--verbose": | |
continue | |
totalroms += 1 | |
if not os.path.isfile(rom): | |
print("! "+rom+" doesn't exist.") | |
continue | |
romname = os.path.basename(os.path.splitext(rom)[0]) | |
print("- processing: "+romname) | |
romf = open(rom, "rb") | |
romf.seek(0x100) | |
ncsdmagic = romf.read(4) | |
romf.seek(0x190) | |
tid = binascii.hexlify(romf.read(8)[::-1]) | |
xorpad = tid.upper()+".Main.exheader.xorpad" | |
romf.close() | |
if ncsdmagic != "NCSD": | |
print("! "+rom+" is probably not a rom.") | |
print(" NCSD magic not found.") | |
continue | |
if not os.path.isfile(xorpad): | |
print("! "+xorpad+" couldn't be found.") | |
print(" use ncchinfo_gen-exh.py with this rom.") | |
continue | |
docleanup() | |
print("- extracting") | |
runcommand(["3dstool", "-xvt012f", "cci", "work/game-orig.cxi", "work/manual.cfa", "work/dlpchild.cfa", rom]) | |
runcommand(["3dstool", "-xvtf", "cxi", "work/game-orig.cxi", "--header", "work/ncchheader.bin", "--exh", "work/exheader.bin", "--exh-xor", xorpad, "--exefs", "work/exefs.bin", "--romfs", "work/romfs.bin", "--plain", "work/plain.bin", "--logo", "work/logo.bcma.lz"]) | |
print("- patching") | |
exh = open("work/exheader.bin", "r+b") | |
exh.seek(0xD) | |
x = exh.read(1) | |
y = ord(x) | |
z = y | 2 | |
if verbose: | |
print(" offset 0xD of ExHeader:") | |
print(" original: "+hex(y)) | |
print(" shifted: "+hex(z)) | |
exh.seek(0xD) | |
exh.write(chr(z)) | |
exh.seek(0x1C0) | |
savesize = exh.read(4) | |
# actually 8 bytes but the TMD only has 4 bytes | |
#print(binascii.hexlify(savesize[::-1])) | |
exh.close() | |
print("- rebuilding") | |
# CXI | |
cmds1 = ["3dstool", "-cvtf", "cxi", "work/game-conv.cxi", "--header", "work/ncchheader.bin", "--exh", "work/exheader.bin", "--exh-xor", xorpad, "--exefs", "work/exefs.bin", "--not-update-exefs-hash", "--romfs", "work/romfs.bin", "--not-update-romfs-hash", "--plain", "work/plain.bin"] | |
if os.path.isfile("work/logo.bcma.lz"): | |
cmds1.extend(["--logo", "work/logo.bcma.lz"]) | |
runcommand(cmds1) | |
# CIA | |
cmds2 = ["makerom", "-f", "cia", "-o", "work/game-conv.cia", "-content", "work/game-conv.cxi:0:0"] | |
if os.path.isfile("work/manual.cfa"): | |
cmds2.extend(["-content", "work/manual.cfa:1:1"]) | |
if os.path.isfile("work/dlpchild.cfa"): | |
cmds2.extend(["-content", "work/dlpchild.cfa:2:2"]) | |
runcommand(cmds2) | |
# makerom doesn't accept custom SaveDataSize for some reason | |
# but make_cia makes a bad CIA that doesn't support the Manual or DLP child | |
# Archive Header Size | |
cia = open("work/game-conv.cia", "r+b") | |
cia.seek(0x0) | |
cia_h_ahs = binascii.hexlify(cia.read(0x4)[::-1]) | |
cia_h_ahs_align = int(math.ceil(int(cia_h_ahs, 16) / 64.0) * 64.0) | |
# Certificate chain size | |
cia.seek(0x8) | |
cia_h_cetks = binascii.hexlify(cia.read(0x4)[::-1]) | |
cia_h_cetks_align = int(math.ceil(int(cia_h_cetks, 16) / 64.0) * 64.0) | |
# Ticket size | |
cia.seek(0xC) | |
cia_h_tiks = binascii.hexlify(cia.read(0x4)[::-1]) | |
cia_h_tiks_align = int(math.ceil(int(cia_h_tiks, 16) / 64.0) * 64.0) | |
tmdoffset = cia_h_ahs_align + cia_h_cetks_align + cia_h_tiks_align | |
cia.seek(tmdoffset + 0x140 + 0x5a) | |
cia.write(savesize) | |
cia.close() | |
os.rename("work/game-conv.cia", romname+".cia") | |
if cleanup: | |
docleanup() | |
processedroms += 1 | |
print("* done converting!") | |
print(" %i out of %i roms processed" % (processedroms, totalroms)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment