Skip to content

Instantly share code, notes, and snippets.

@ihaveamac
Last active March 16, 2018 15:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ihaveamac/dfc01fa09483c275f72ad69cd7e8080f to your computer and use it in GitHub Desktop.
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
#!/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