Advertisement
Guest User

Untitled

a guest
Nov 18th, 2017
465
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 15.16 KB | None | 0 0
  1. #This script runs through all toc files it can find and uses that information to extract the files to a target directory.
  2. #Often the assets are actually stored in cascat archives (the sbtoc knows where to search in the cascat), which is taken care of too.
  3. #The script does not overwrite existing files (mainly because 10 sbtocs pointing at the same asset in the cascat would make the extraction time unbearable).
  4.  
  5. #Adjust paths here.
  6.  
  7. bf4Directory=r"x:\swbf2"
  8. targetDirectory = r"d:\GameRip\SWBF2_Full"
  9.  
  10. ###The following paths do not require adjustments (unless the devs decided to rename their folders). Note that they are relative to bf4Directory.
  11.  
  12. #As files are not overwritten, the patched files need to be extracted first.
  13. #The script will dump all tocs it can find in these two folders+subfolders:
  14. #tocRoot  = r"Patch\Win32" #patched and xpack files FIRST
  15. tocRoot = r"data\win32"   #unpatched vanilla files SECOND
  16.  
  17. #Note: The "Update" tocRoot contains both patch (for vanilla AND xpack) and unpatched xpack files. The reason it still
  18. #      works correctly is because it goes through the folders alphabetically, so the patch comes first.
  19.  
  20.  
  21. #Feel free to comment out one or both cats if they don't exist (some Frostbite 2 games shipped without cats).
  22. #Although in that case you could just as well use an invalid path, i.e. not change anything.
  23.  
  24. catPath   = r"Data\Win32\installation\frontend\cas.cat"
  25. catPath2   = r"Data\Win32\installation\initialexperience\cas.cat"
  26. catPath3   = r"Data\Win32\installation\mp\cas.cat"
  27. catPath4   = r"Data\Win32\installation\sp\cas.cat"
  28. catPath5   = r"Data\Win32\installation\sp_a1\cas.cat"
  29. catPath6   = r"Data\Win32\installation\sp_a2\cas.cat"
  30. catPath7   = r"Data\Win32\installation\sp_a3\cas.cat"
  31. catPath8   = r"Data\Win32\installation\space\cas.cat"
  32.  
  33. #catPath       = r"\Data\Win32\gameconfigurations\initialinstallpackage\cas.cat"
  34. #updateCatPath = r"Patch\Win32\installation\frontend\cas.cat"
  35.  
  36.  
  37. #About the names of the res files:
  38. #   The first part after the actual name (but before the file extension) is the RID. I think it's used by ebx to import res files.
  39. #   When it is not just nulls, the resMeta is added after the RID. Its purpose depends on the file type.
  40. #   Finally, the res file extensions are just an invention of mine. See the resTypes dict right below.
  41.  
  42.  
  43.  
  44. #####################################
  45. #####################################
  46. import cas
  47. import noncas
  48. import os
  49. import subprocess
  50. from binascii import hexlify,unhexlify
  51. from struct import pack,unpack
  52. from cStringIO import StringIO
  53. from ctypes import *
  54. #LZ77 = cdll.LoadLibrary("LZ77")
  55.  
  56. resTypes={ #not really updated for bf4 though
  57.     0xC6DBEE07:".AnimatedPointCloud",
  58.     0xD070EED1:".AnimTrackData",
  59.     0x51A3C853:".AssetBank",
  60.     0x957C32B1:".AtlasTexture",
  61.     0xafecb022:".CompiledLuaResource",
  62.     0xf04f0c81:".Dx11ShaderProgramDatabase",
  63.     0x10F0E5A1:".DxShaderProgramDatabase",
  64.     0x5C4954A6:".DxTexture",
  65.     0x70C5CB3E:".EnlightenDatabase",
  66.     0x59CEEB57:".EnlightenShaderDatabase",
  67.     0xC6CD3286:".EnlightenStaticDatabase",
  68.     0x5BDFDEFE:".EnlightenSystem",
  69.     0xE156AF73:".EnlightenProbeSet",
  70.     0xE36F0D59:".HavokClothPhysicsData",
  71.     0x91043F65:".HavokPhysicsData",
  72.     0x4864737B:".HavokDestructionphysicsData",
  73.     0x36F3F2C0:".IShaderDatabase",
  74.     0xC611F34A:".MeshEmitterResource",
  75.     0x49B156D4:".MeshSet",
  76.     0x30B4A553:".OccluderMesh",
  77.     0x319D8CD0:".RagdollResource",
  78.     0x7AEFC446:".StaticEnlightenDatabase",
  79.     0x2D47A5FF:".SwfMovie",
  80.     0x6BB6D7D2:".Terrain",
  81.     0x15E1F32E:".TerrainDecals",
  82.     0xA23E75DB:".TerrainLayerCombinations",
  83.     0x22FE8AC8:".TerrainStreamingTree",
  84.     0x6BDE20BA:".Texture",
  85.     0x9D00966A:".UITtfFontFile",
  86.     0x1CA38E06:".VisualTerrain",
  87. }
  88. def hex2(num): return hexlify(pack(">I",num)) #e.g. 10 => '0000000a'
  89. class Stub(): pass #generic struct for the cat entry
  90.  
  91. def readCat(catDict, catPath):
  92.     """Take a dict and fill it using a cat file: sha1 vs (offset, size, cas path)"""
  93.     cat=cas.unXor(catPath)
  94.     cat.seek(0,2) #get eof
  95.     catSize=cat.tell()
  96.     cat.seek(40) #skip nyan
  97.     casDirectory=os.path.dirname(catPath)+"\\" #get the full path so every entry knows whether it's from the patched or unpatched cat.
  98.     while cat.tell()<catSize:
  99.         entry=Stub()
  100.         sha1=cat.read(20)
  101.         entry.offset, entry.size, dummy, casNum = unpack("<IIII",cat.read(16))
  102.         entry.path=casDirectory+"cas_"+("0"+str(casNum) if casNum<10 else str(casNum))+".cas"
  103.         #if (dummy==0 and entry.size>4000000):
  104.         catDict[sha1]=entry
  105.  
  106.  
  107. def dump(tocPath, targetFolder):
  108.     """Take the filename of a toc and dump all files to the targetFolder."""
  109.  
  110.     #Depending on how you look at it, there can be up to 2*(3*3+1)=20 different cases:
  111.     #    The toc has a cas flag which means all assets are stored in the cas archives. => 2 options
  112.     #        Each bundle has either a delta or base flag, or no flag at all. => 3 options
  113.     #            Each file in the bundle is one of three types: ebx/res/chunks => 3 options
  114.     #        The toc itself contains chunks. => 1 option
  115.     #
  116.     #Simplify things by ignoring base bundles (they just state that the unpatched bundle is used),
  117.     #which is alright, as the user needs to dump the unpatched files anyway.
  118.     #
  119.     #Additionally, add some common fields to the ebx/res/chunks entries so they can be treated the same.
  120.     #=> 6 cases.
  121.  
  122.     toc=cas.readToc(tocPath)
  123.     if not (toc.get("bundles") or toc.get("chunks")): return #there's nothing to extract (the sb might not even exist)
  124.     sbPath=tocPath[:-3]+"sb"
  125.     sb=open(sbPath,"rb")
  126.  
  127.     for tocEntry in toc.bundles:
  128.         if tocEntry.get("base"): continue
  129.         sb.seek(tocEntry.offset)
  130.  
  131.         ###read the bundle depending on the four types (+cas+delta, +cas-delta, -cas+delta, -cas-delta) and choose the right function to write the payload
  132.         if toc.get("cas"):
  133.             bundle=cas.Entry(sb)
  134.             #make empty lists for every type to make it behave the same way as noncas
  135.             for listType in ("ebx","res","chunks"):
  136.                 if listType not in vars(bundle):
  137.                     vars(bundle)[listType]=[]
  138.                    
  139.             #The noncas chunks already have originalSize calculated in Bundle.py (it was necessary to seek through the entries).
  140.             #Calculate it for the cas chunks too. From here on, both cas and noncas ebx/res/chunks (within bundles) have size and originalSize.
  141.             for chunk in bundle.chunks:
  142.                 chunk.originalSize=chunk.logicalOffset+chunk.logicalSize
  143.                    
  144.             #pick the right function
  145.             if tocEntry.get("delta"):
  146.                 writePayload=casPatchedPayload
  147.                 sourcePath=None #the noncas writing function requires a third argument, while the cas one does not. Hence make a dummy variable.
  148.             else:
  149.                 writePayload=casPayload
  150.                 sourcePath=None
  151.         else:
  152.             if tocEntry.get("delta"):
  153.                 #The sb currently points at the delta file.
  154.                 #Read the unpatched toc of the same name to get the base bundle.
  155.                 #First of all though, get the correct path.
  156.  
  157.                 #Does it work like this?
  158.                 #   Update\Patch\Data\Win32\XP1\Levels\XP1_003\XP1_003.toc
  159.                 #=> Update\Xpack1\Data\Win32\XP1\Levels\XP1_003\XP1_003.toc
  160.                 xpNum=os.path.basename(tocPath)[2] #"XP1_003.toc" => "1"
  161.                 split=tocPath.lower().rfind("patch")
  162.                 baseTocPath=tocPath[:split]+"xpack"+xpNum+tocPath[split+5:]
  163.                 if not os.path.exists(baseTocPath): #Nope? Then it must work like this:
  164.                     #   Update\Patch\Data\Win32\XP1Weapons.toc
  165.                     #=> Data\Win32\XP1Weapons.toc
  166.                     baseTocPath=tocPath[:split-7]+tocPath[split+6:] #just cut out Update\Patch
  167.                 #now open the file and get the correct bundle (with the same name as the delta bundle)  
  168.                 baseToc=cas.readToc(baseTocPath)
  169.                 for baseTocEntry in baseToc.bundles:
  170.                     if baseTocEntry.id.lower() == tocEntry.id.lower():
  171.                         break
  172.                 else: #if no base bundle has with this name has been found:
  173.                     pass #use the last base bundle. This is okay because it is actually not used at all (the delta has uses instructionType 3 only).
  174.                    
  175.                 basePath=baseTocPath[:-3]+"sb"
  176.                 base=open(basePath,"rb")
  177.                 base.seek(baseTocEntry.offset)
  178.                 bundle = noncas.patchedBundle(base, sb) #create a patched bundle using base and delta
  179.                 base.close()
  180.                 writePayload=noncasPatchedPayload
  181.                 sourcePath=[basePath,sbPath] #base, delta
  182.             else:
  183.                 bundle=noncas.unpatchedBundle(sb)
  184.                 writePayload=noncasPayload
  185.                 sourcePath=sbPath
  186.  
  187.         ###pick a good filename, make sure the file does not exist yet, create folders, call the right function to write the payload  
  188.         for entry in bundle.ebx:
  189.             targetPath=targetFolder+"/bundles/ebx/"+entry.name+".ebx"
  190.             #if "sound/" in entry.name: continue
  191.             if prepareDir(targetPath): continue
  192.             writePayload(entry, targetPath, sourcePath)
  193.  
  194.         for entry in bundle.res: #always add resRid to the filename. Add resMeta if it's not just nulls. resType becomes file extension.
  195.             targetPath=targetFolder+"/bundles/res/"+entry.name+" "+hexlify(pack(">Q",entry.resRid))
  196.             #if not "d_assault_newera_skb_01" in entry.name: continue
  197.             if entry.resMeta!="\0"*16: targetPath+=" "+hexlify(entry.resMeta)
  198.             if entry.resType not in resTypes: targetPath+=".unknownres "+hex2(entry.resType)
  199.             else: targetPath+=resTypes[entry.resType]
  200.             if prepareDir(targetPath): continue
  201.             writePayload(entry, targetPath, sourcePath)
  202.  
  203.         for i in xrange(len(bundle.chunks)): #id becomes the filename. If meta is not empty, add it to filename.
  204.             entry=bundle.chunks[i]
  205.             targetPath=targetFolder+"/chunks/"+hexlify(entry.id) +".chunk" #keep the .chunk extension for legacy reasons
  206.             #if bundle.chunkMeta[i].meta!="\x00": targetPath+=" firstMip"+str(unpack("B",bundle.chunkMeta[i].meta[10])[0])
  207.             #chunkMeta is useless. The same payload may have several values for firstMips so chunkMeta contains info specific to bundles, not the file itself.
  208.             if prepareDir(targetPath): continue
  209.             #if hexlify(entry.id)[:8]=="882CD8F1":
  210.             writePayload(entry, targetPath, sourcePath)
  211.  
  212.     #Deal with the chunks which are defined directly in the toc.
  213.     #These chunks do NOT know their originalSize.
  214.     #Available fields: id, offset, size
  215.     for entry in toc.chunks:
  216.         targetPath=targetFolder+"/chunks/"+hexlify(entry.id)+".chunk"
  217.         if prepareDir(targetPath): continue
  218.         if toc.get("cas"):
  219.             try:
  220.                 catEntry=cat[entry.sha1]
  221.                 #if not checkchunk(catEntry.path,catEntry.offset):
  222.                 #if hexlify(entry.id)[:8]=="1f175f8e":
  223.                 try:
  224.                     process = subprocess.Popen(["fb_zstd.exe",catEntry.path,str(catEntry.offset),str(catEntry.size),targetPath],stderr=subprocess.PIPE,startupinfo=startupinfo)
  225.                     process.communicate() #this should set the returncode
  226.                     if process.returncode:
  227.                         print process.stderr.readlines()
  228.                 except:
  229.                     print "Error executing fb_zstd."
  230.                     print catEntry.path,str(catEntry.offset),str(catEntry.size),targetPath
  231.             except:
  232.                 continue
  233.         else:
  234.             if not checkchunk(sbPath,entry.offset):
  235.                 LZ77.decompressUnknownOriginalSize(sbPath,entry.offset,entry.size,targetPath)
  236.  
  237.     sb.close()
  238.  
  239. def checkchunk(filename, offset):
  240.     try:
  241.         f=open(filename,"rb")
  242.     except:
  243.         return False
  244.     f.seek(offset+8)
  245.     magic=f.read(4)
  246.     f.close()
  247.     if magic=="\x48\x00\x00\x0c" or magic=="\x01\x10\x06\xC0" or magic=="\x01\x10\x00\x00":
  248.         return True
  249.     return False
  250.  
  251. def prepareDir(targetPath):
  252.     if os.path.exists(targetPath): return True
  253.     dirName=os.path.dirname(targetPath)
  254.     if not os.path.exists(dirName): os.makedirs(dirName) #make the directory for the dll
  255.     #print targetPath
  256.  
  257.  
  258. #for each bundle, the dump script selects one of these four functions
  259. def casPayload(bundleEntry, targetPath, sourcePath):
  260.     try:
  261.     #if checkchunk(catEntry.path,catEntry.offset):
  262.         catEntry=cat[bundleEntry.sha1]
  263.         try:
  264.             process = subprocess.Popen(["fb_zstd.exe",catEntry.path,str(catEntry.offset),str(catEntry.size),targetPath],stderr=subprocess.PIPE,startupinfo=startupinfo)
  265.             process.communicate() #this should set the returncode
  266.             if process.returncode:
  267.                 print process.stderr.readlines()
  268.         except:
  269.             print "Error executing fb_zstd."
  270.             print catEntry.path,str(catEntry.offset),str(catEntry.size),targetPath
  271.     except: print "Chunk not found."
  272.  
  273.     #LZ77.decompress(catEntry.path,catEntry.offset,catEntry.size, bundleEntry.originalSize,targetPath)
  274. def noncasPayload(entry, targetPath, sourcePath):
  275.     #if checkchunk(sourcePath,entry.offset):
  276.     LZ77.decompress(sourcePath,entry.offset,entry.size, entry.originalSize,targetPath)
  277. def casPatchedPayload(bundleEntry, targetPath, sourcePath):
  278.     if bundleEntry.get("casPatchType")==2:
  279.         catDelta=cat[bundleEntry.deltaSha1]
  280.         catBase=cat[bundleEntry.baseSha1]
  281.         LZ77.patchCas(catBase.path,catBase.offset,
  282.                       catDelta.path,catDelta.offset,catDelta.size,
  283.                       bundleEntry.originalSize,targetPath)
  284.     else:
  285.         casPayload(bundleEntry, targetPath, sourcePath) #if casPatchType is not 2, use the unpatched function.
  286. def noncasPatchedPayload(entry, targetPath, sourcePath):
  287.     LZ77.patchNoncas(sourcePath[0],entry.baseOffset,#entry.baseSize,
  288.                      sourcePath[1], entry.deltaOffset, entry.deltaSize,
  289.                      entry.originalSize,targetPath,
  290.                      entry.midInstructionType, entry.midInstructionSize)
  291.  
  292.  
  293. #make the paths absolute and normalize the slashes
  294. for path in "catPath", "catPath2", "tocRoot", "tocRoot2":
  295.     if path in locals():
  296.         locals()[path]= os.path.normpath(bf4Directory+"\\"+locals()[path])
  297.  
  298. targetDirectory=os.path.normpath(targetDirectory) #it's an absolute path already
  299.  
  300. startupinfo = subprocess.STARTUPINFO()
  301. startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  302. startupinfo.wShowWindow = subprocess.SW_HIDE
  303.  
  304. def dumpRoot(root):
  305.     for dir0, dirs, ff in os.walk(root):
  306.         for fname in ff:
  307.             if fname[-4:]==".toc":
  308.                 print fname
  309.                 fname=dir0+"\\"+fname
  310.                 dump(fname,targetDirectory)
  311.  
  312. cat=dict()
  313. try: readCat(cat, catPath)
  314. except: print "Unpatched cat not found."
  315. #try: readCat(cat, catPath2)
  316. #except: print "Patched cat not found."
  317.  
  318. if "tocRoot" in locals():  dumpRoot(tocRoot)
  319. #if "tocRoot2" in locals(): dumpRoot(tocRoot2)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement