Advertisement
Guest User

EBXtoTEXT

a guest
Jan 7th, 2014
442
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 18.22 KB | None | 0 0
  1. #Requires Python 2.7
  2. #The floattostring.dll requires 32bit Python to write floating point numbers in a succinct manner,
  3. #but the dll is not required to run this script.
  4. import string
  5. import sys
  6. from binascii import hexlify
  7. import struct
  8. import os
  9. from cStringIO import StringIO
  10. import cProfile
  11. import cPickle
  12. import copy
  13.  
  14. #Adjust input and output folders here
  15. inputFolder=r"C:\hexing\bf4 dump\bundles\ebx"
  16. outputFolder=r"C:\hexing\bf4 ebx"
  17. guidTableName="guiTable bf4cr2patch" #Name of the guid table file; keeping separate names
  18. #for separate games is highly recommended. The table is created at the location of the script.
  19.  
  20. EXTENSION=".txt" #Use a different file extension if you like.
  21. SEP=" " #Adjust the amount of whitespace on the left of the converted file.
  22.  
  23. #Show offsets to the left
  24. printOffsets=False #True/False
  25.  
  26. #Ignore all instances and fields with these names when converting to text:
  27. IGNOREINSTANCES=["RawFileDataAsset"] #used in WebBrowser\Fonts, crashes the script otherwise
  28. IGNOREFIELDS=[]
  29. ##IGNOREINSTANCES=["ShaderAdjustmentData","SocketData","WeaponSkinnedSocketObjectData","WeaponRegularSocketObjectData"]
  30. ##IGNOREFIELDS=["Mesh3pTransforms","Mesh3pRigidMeshSocketObjectTransforms"]
  31.  
  32. #I recommend ignoring a few fields/instances which are related to meshes,
  33. #take up lots of space, and contain no useful information as the mesh format is not even known.
  34. #As an example, Mesh3pTransforms contains nothing but xyz vectors and is found in most weapon
  35. #files. This field takes up 715 lines in the 870 shotgun (the entire file is 3829 lines).
  36. #If you enjoy having to scroll past these 700 lines all the time, then ignore nothing.
  37. #Note however that the lists above applied to bf3. In bf4 I can only find Mesh3pTransforms in the files but not the other strings.
  38. #Nevertheless, use this as a guide to ignore fields/instances on your own.
  39.  
  40.  
  41.  
  42. #First run through all files to create a guid table to resolve external file references.
  43. #Then run through all files once more, but this time convert them using the guid table.
  44. def main():
  45. createGuidTable()
  46. dumpText()
  47.  
  48.  
  49. ##############################################################
  50. ##############################################################
  51. unpackLE = struct.unpack
  52. def unpackBE(typ,data): return struct.unpack(">"+typ,data)
  53.  
  54. def createGuidTable():
  55. for dir0, dirs, ff in os.walk(inputFolder):
  56. for fname in ff:
  57. if fname[-4:]!=".ebx": continue
  58. f=open(lp(dir0+"\\"+fname),"rb")
  59. relPath=(dir0+"\\"+fname)[len(inputFolder):-4]
  60. if relPath[0]=="\\": relPath=relPath[1:]
  61. try:
  62. dbx=Dbx(f,relPath)
  63. f.close()
  64. except ValueError as msg:
  65. f.close()
  66. if str(msg).startswith("The file is not ebx: "):
  67. continue
  68. else: asdf
  69. guidTable[dbx.fileGUID]=dbx.trueFilename
  70. f5=open(guidTableName,"wb") #write the table
  71. cPickle.dump(guidTable,f5)
  72. f5.close()
  73.  
  74. def dumpText():
  75. for dir0, dirs, ff in os.walk(inputFolder):
  76. for fname in ff:
  77. if fname[-4:]!=".ebx": continue
  78. print fname
  79. f=open(lp(dir0+"\\"+fname),"rb")
  80. relPath=(dir0+"\\"+fname)[len(inputFolder):-4]
  81. if relPath[0]=="\\": relPath=relPath[1:]
  82. try:
  83. dbx=Dbx(f,relPath)
  84. f.close()
  85. except ValueError as msg:
  86. f.close()
  87. if str(msg).startswith("The file is not ebx: "):
  88. continue
  89. else: asdf
  90. dbx.dump(outputFolder)
  91.  
  92. def open2(path,mode="rb"):
  93. if mode=="wb":
  94. #create folders if necessary and return the file handle
  95.  
  96. #first of all, create one folder level manully because makedirs might fail
  97. pathParts=path.split("\\")
  98. manualPart="\\".join(pathParts[:2])
  99. if not os.path.isdir(manualPart):
  100. os.makedirs(manualPart)
  101.  
  102. #now handle the rest, including extra long path names
  103. folderPath=lp(os.path.dirname(path))
  104. if not os.path.isdir(folderPath): os.makedirs(folderPath)
  105. return open(lp(path),mode)
  106.  
  107. def lp(path): #long, normalized pathnames
  108. if len(path)<=247 or path=="" or path[:4]=='\\\\?\\': return os.path.normpath(path)
  109. return unicode('\\\\?\\' + os.path.normpath(path))
  110.  
  111.  
  112. try:
  113. from ctypes import *
  114. floatlib = cdll.LoadLibrary("floattostring")
  115. def formatfloat(num):
  116. bufType = c_char * 100
  117. buf = bufType()
  118. bufpointer = pointer(buf)
  119. floatlib.convertNum(c_double(num), bufpointer, 100)
  120. rawstring=(buf.raw)[:buf.raw.find("\x00")]
  121. if rawstring[:2]=="-.": return "-0."+rawstring[2:]
  122. elif rawstring[0]==".": return "0."+rawstring[1:]
  123. elif "e" not in rawstring and "." not in rawstring: return rawstring+".0"
  124. return rawstring
  125. except:
  126. def formatfloat(num):
  127. return str(num)
  128. def hasher(keyword): #32bit FNV-1 hash with FNV_offset_basis = 5381 and FNV_prime = 33
  129. hash = 5381
  130. for byte in keyword:
  131. hash = (hash*33) ^ ord(byte)
  132. return hash & 0xffffffff # use & because Python promotes the num instead of intended overflow
  133. class Header:
  134. def __init__(self,varList):
  135. self.absStringOffset = varList[0] ## absolute offset for string section start
  136. self.lenStringToEOF = varList[1] ## length from string section start to EOF
  137. self.numGUID = varList[2] ## number of external GUIDs
  138. self.numInstanceRepeater = varList[3] ## total number of instance repeaters
  139. self.numGUIDRepeater = varList[4] ## instance repeaters with GUID
  140. self.unknown = varList[5]
  141. self.numComplex = varList[6] ## number of complex entries
  142. self.numField = varList[7] ## number of field entries
  143. self.lenName = varList[8] ## length of name section including padding
  144. self.lenString = varList[9] ## length of string section including padding
  145. self.numArrayRepeater = varList[10]
  146. self.lenPayload = varList[11] ## length of normal payload section; the start of the array payload section is absStringOffset+lenString+lenPayload
  147. class FieldDescriptor:
  148. def __init__(self,varList,keywordDict):
  149. self.name = keywordDict[varList[0]]
  150. self.type = varList[1]
  151. self.ref = varList[2] #the field may contain another complex
  152. self.offset = varList[3] #offset in payload section; relative to the complex containing it
  153. self.secondaryOffset = varList[4]
  154. if self.name=="$": self.offset-=8
  155. class ComplexDescriptor:
  156. def __init__(self,varList,keywordDict):
  157. self.name = keywordDict[varList[0]]
  158. self.fieldStartIndex = varList[1] #the index of the first field belonging to the complex
  159. self.numField = varList[2] #the total number of fields belonging to the complex
  160. self.alignment = varList[3]
  161. self.type = varList[4]
  162. self.size = varList[5] #total length of the complex in the payload section
  163. self.secondarySize = varList[6] #seems deprecated
  164. class InstanceRepeater:
  165. def __init__(self,varList):
  166. self.complexIndex = varList[0] #index of complex used as the instance
  167. self.repetitions = varList[1] #number of instance repetitions
  168. class arrayRepeater:
  169. def __init__(self,varList):
  170. self.offset = varList[0] #offset in array payload section
  171. self.repetitions = varList[1] #number of array repetitions
  172. self.complexIndex = varList[2] #not necessary for extraction
  173. class Complex:
  174. def __init__(self,desc):
  175. self.desc=desc
  176. class Field:
  177. def __init__(self,desc,offset):
  178. self.desc=desc
  179. self.offset=offset #track absolute offset of each field in the ebx
  180.  
  181. numDict={0xC12D:("Q",8),0xc0cd:("B",1) ,0x0035:("I",4),0xc10d:("I",4),0xc14d:("d",8),0xc0ad:("?",1),0xc0fd:("i",4),0xc0bd:("b",1),0xc0ed:("h",2), 0xc0dd:("H",2), 0xc13d:("f",4)}
  182.  
  183. class Dbx:
  184. def __init__(self, f, relPath):
  185. #metadata
  186. magic=f.read(4)
  187. if magic=="\xCE\xD1\xB2\x0F": self.unpack=unpackLE
  188. elif magic=="\x0F\xB2\xD1\xCE": self.unpack=unpackBE
  189. else: raise ValueError("The file is not ebx: "+relPath)
  190.  
  191. self.relPath=relPath #to give more feedback for unknown field types
  192. self.trueFilename=""
  193. self.header=Header(self.unpack("3I6H3I",f.read(36)))
  194. self.arraySectionstart=self.header.absStringOffset+self.header.lenString+self.header.lenPayload
  195. self.fileGUID=f.read(16)
  196. while f.tell()%16!=0: f.seek(1,1) #padding
  197. self.externalGUIDs=[(f.read(16),f.read(16)) for i in xrange(self.header.numGUID)]
  198. self.keywords=str.split(f.read(self.header.lenName),"\x00")
  199. self.keywordDict=dict((hasher(keyword),keyword) for keyword in self.keywords)
  200. self.fieldDescriptors=[FieldDescriptor(self.unpack("IHHii",f.read(16)), self.keywordDict) for i in xrange(self.header.numField)]
  201. self.complexDescriptors=[ComplexDescriptor(self.unpack("IIBBHHH",f.read(16)), self.keywordDict) for i in xrange(self.header.numComplex)]
  202. self.instanceRepeaters=[InstanceRepeater(self.unpack("2H",f.read(4))) for i in xrange(self.header.numInstanceRepeater)]
  203. while f.tell()%16!=0: f.seek(1,1) #padding
  204. self.arrayRepeaters=[arrayRepeater(self.unpack("3I",f.read(12))) for i in xrange(self.header.numArrayRepeater)]
  205.  
  206. #payload
  207. f.seek(self.header.absStringOffset+self.header.lenString)
  208. self.internalGUIDs=[]
  209. self.instances=[] # (guid, complex)
  210. nonGUIDindex=0
  211. self.isPrimaryInstance=True #first instance is primary
  212. for i, instanceRepeater in enumerate(self.instanceRepeaters):
  213. for repetition in xrange(instanceRepeater.repetitions):
  214. #obey alignment of the instance; peek into the complex for that
  215. while f.tell()%self.complexDescriptors[instanceRepeater.complexIndex].alignment!=0: f.seek(1,1)
  216.  
  217. #all instances after numGUIDRepeater have no guid
  218. if i<self.header.numGUIDRepeater:
  219. instanceGUID=f.read(16)
  220. else:
  221. #just numerate those instances without guid and assign a big endian int to them.
  222. instanceGUID=struct.pack(">I",nonGUIDindex)
  223. nonGUIDindex+=1
  224. self.internalGUIDs.append(instanceGUID)
  225.  
  226. self.instances.append( (instanceGUID,self.readComplex(instanceRepeater.complexIndex,f,True)) )
  227. self.isPrimaryInstance=False #the readComplex function has used isPrimaryInstance by now
  228. f.close()
  229.  
  230. #if no filename found, use the relative input path instead
  231. #it's just as good though without capitalization
  232. if self.trueFilename=="":
  233. self.trueFilename=relPath
  234.  
  235.  
  236.  
  237.  
  238. def readComplex(self, complexIndex, f, isInstance=False):
  239. complexDesc=self.complexDescriptors[complexIndex]
  240. cmplx=Complex(complexDesc)
  241. cmplx.offset=f.tell()
  242.  
  243. cmplx.fields=[]
  244. #alignment 4 instances require subtracting 8 for all field offsets and the complex size
  245. obfuscationShift=8 if (isInstance and cmplx.desc.alignment==4) else 0
  246.  
  247. for fieldIndex in xrange(complexDesc.fieldStartIndex,complexDesc.fieldStartIndex+complexDesc.numField):
  248. f.seek(cmplx.offset+self.fieldDescriptors[fieldIndex].offset-obfuscationShift)
  249. cmplx.fields.append(self.readField(fieldIndex,f))
  250.  
  251. f.seek(cmplx.offset+complexDesc.size-obfuscationShift)
  252. return cmplx
  253.  
  254. def readField(self,fieldIndex,f):
  255. fieldDesc = self.fieldDescriptors[fieldIndex]
  256. field=Field(fieldDesc,f.tell())
  257.  
  258. if fieldDesc.type in (0x0029, 0xd029,0x0000,0x8029):
  259. field.value=self.readComplex(fieldDesc.ref,f)
  260. elif fieldDesc.type==0x0041:
  261. arrayRepeater=self.arrayRepeaters[self.unpack("I",f.read(4))[0]]
  262. arrayComplexDesc=self.complexDescriptors[fieldDesc.ref]
  263.  
  264. f.seek(self.arraySectionstart+arrayRepeater.offset)
  265. arrayComplex=Complex(arrayComplexDesc)
  266. arrayComplex.fields=[self.readField(arrayComplexDesc.fieldStartIndex,f) for repetition in xrange(arrayRepeater.repetitions)]
  267. field.value=arrayComplex
  268.  
  269. elif fieldDesc.type in (0x407d, 0x409d):
  270. startPos=f.tell()
  271. stringOffset=self.unpack("i",f.read(4))[0]
  272. if stringOffset==-1:
  273. field.value="*nullString*"
  274. else:
  275. f.seek(self.header.absStringOffset+stringOffset)
  276. field.value=""
  277. while 1:
  278. a=f.read(1)
  279. if a=="\x00": break
  280. else: field.value+=a
  281. f.seek(startPos+4)
  282.  
  283. if self.isPrimaryInstance and fieldDesc.name=="Name" and self.trueFilename=="": self.trueFilename=field.value
  284.  
  285.  
  286. elif fieldDesc.type in (0x0089,0xc089): #incomplete implementation, only gives back the selected string
  287. compareValue=self.unpack("i",f.read(4))[0]
  288. enumComplex=self.complexDescriptors[fieldDesc.ref]
  289.  
  290. if enumComplex.numField==0:
  291. field.value="*nullEnum*"
  292. for fieldIndex in xrange(enumComplex.fieldStartIndex,enumComplex.fieldStartIndex+enumComplex.numField):
  293. if self.fieldDescriptors[fieldIndex].offset==compareValue:
  294. field.value=self.fieldDescriptors[fieldIndex].name
  295. break
  296.  
  297. elif fieldDesc.type==0xc15d:
  298. field.value=f.read(16)
  299. elif fieldDesc.type==0x417d:
  300. field.value=f.read(8)
  301. else:
  302. try:
  303. (typ,length)=numDict[fieldDesc.type]
  304. num=self.unpack(typ,f.read(length))[0]
  305. field.value=num
  306. except:
  307. print "Unknown field type: "+str(fieldDesc.type)+" File name: "+self.relPath
  308. field.value="*unknown field type*"
  309.  
  310. return field
  311.  
  312.  
  313. def dump(self,outputFolder):
  314. ## if not self.trueFilename: self.trueFilename=hexlify(self.fileGUID)
  315.  
  316. outName=outputFolder+self.trueFilename+EXTENSION
  317.  
  318. ## dirName=os.path.dirname(outputFolder+self.trueFilename)
  319. ## if not os.path.isdir(dirName): os.makedirs(dirName)
  320. ## if not self.trueFilename: self.trueFilename=hexlify(self.fileGUID)
  321. ## f2=open(outputFolder+self.trueFilename+EXTENSION,"wb")
  322. f2=open2(outName,"wb")
  323.  
  324. for (guid,instance) in self.instances:
  325. if instance.desc.name not in IGNOREINSTANCES: #############
  326. #print
  327. writeInstance(f2,instance,hexlify(guid))
  328. self.recurse(instance.fields,f2,0)
  329. f2.close()
  330.  
  331. def recurse(self, fields, f2, lvl): #over fields
  332. lvl+=1
  333. for field in fields:
  334. if field.desc.type in (0x0029,0xd029,0x0000,0x8029):
  335. if field.desc.name not in IGNOREFIELDS: #############
  336. writeField(f2,field,lvl,"::"+field.value.desc.name)
  337. self.recurse(field.value.fields,f2,lvl)
  338. elif field.desc.type == 0xc13d:
  339. writeField(f2,field,lvl," "+formatfloat(field.value))
  340. elif field.desc.type == 0xc15d:
  341. writeField(f2,field,lvl," "+hexlify(field.value).upper()) #upper case=> chunk guid
  342. elif field.desc.type==0x417d:
  343. val=hexlify(field.value)
  344. ## val=val[:16]+"/"+val[16:]
  345. writeField(f2,field,lvl," "+val)
  346. elif field.desc.type == 0x0035:
  347. towrite=""
  348. if field.value>>31:
  349. extguid=self.externalGUIDs[field.value&0x7fffffff]
  350. try: towrite=guidTable[extguid[0]]+"/"+hexlify(extguid[1])
  351. except: towrite=hexlify(extguid[0])+"/"+hexlify(extguid[1])
  352. elif field.value==0: towrite="*nullGuid*"
  353. else:
  354. intGuid=self.internalGUIDs[field.value-1]
  355. towrite=hexlify(intGuid)
  356. writeField(f2,field,lvl," "+towrite)
  357. elif field.desc.type==0x0041:
  358. if len(field.value.fields)==0:
  359. writeField(f2,field,lvl," *nullArray*")
  360. else:
  361. writeField(f2,field,lvl,"::"+field.value.desc.name)
  362.  
  363. #quick hack so I can add indices to array members while using the same recurse function
  364. for index in xrange(len(field.value.fields)):
  365. member=field.value.fields[index]
  366. if member.desc.name=="member":
  367. desc=copy.deepcopy(member.desc)
  368. desc.name="member("+str(index)+")"
  369. member.desc=desc
  370. self.recurse(field.value.fields,f2,lvl)
  371. else:
  372. writeField(f2,field,lvl," "+str(field.value))
  373.  
  374. def hex2(num):
  375. #take int, return 8byte string
  376. a=hex(num)
  377. if a[:2]=="0x": a=a[2:]
  378. if a[-1]=="L": a=a[:-1]
  379. while len(a)<8:
  380. a="0"+a
  381. return a
  382.  
  383. if printOffsets:
  384. def writeField(f,field,lvl,text):
  385. f.write(hex2(field.offset)+SEP+lvl*SEP+field.desc.name+text+"\r\n")
  386. def writeInstance(f,cmplx,text):
  387. f.write(hex2(cmplx.offset)+SEP+cmplx.desc.name+" "+text+"\r\n")
  388. else:
  389. def writeField(f,field,lvl,text):
  390. f.write(lvl*SEP+field.desc.name+text+"\r\n")
  391. def writeInstance(f,cmplx,text):
  392. f.write(cmplx.desc.name+" "+text+"\r\n")
  393.  
  394.  
  395. if outputFolder[-1] not in ("/","\\"): outputFolder+="\\"
  396. if inputFolder[-1] not in ("/","\\"): inputFolder+="\\"
  397.  
  398.  
  399. #if there's a guid table already, use it
  400. try:
  401. f5=open(guidTableName,"rb")
  402. guidTable=cPickle.load(f5)
  403. f5.close()
  404. except:
  405. guidTable=dict()
  406.  
  407. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement