Jump to content
Official BF Editor Forums
Frankelstner

Ebx File Converter

Recommended Posts

I've updated the script, it reads all 16 types correctly. Here are some of my notes if you are interested.

ref==0 ("7d40","0dc1","3dc1","4dc1","5dc1","adc0","bdc0","ddc0","edc0","fdc0","3500"):
7d40: 4bytes; path, the value is the offset in the path section
0dc1: 4bytes; unsigned integer
3dc1: 4bytes; float, single precision
4dc1: 8bytes; float, double precision
5dc1: 16bytes; hash, really just a hash right there
adc0: 1byte; bool, usually padded to 4 if no other adc0,bdc0
bdc0: 1byte; char (signed probably) 
ddc0: 2bytes; hard to tell as it always appears in array04 as "member"
edc0: 2bytes; merges right into adc0 for padding, behaves like an integer
fdc0: 4bytes; signed integer
3500: 4bytes; GUID/hash, payload xy000080 => hashlist[xy]; 
			 payload xy000000 => go through bytes3x41 and choose the right special and repetition

ref!=0 ("4100","2900","29d0"):
4100: 4bytes; array
2900: 0bytes; complex entry, like array but data not in array section
29d0: 0bytes; <complex name="DirtTriggerColor">0.255/0.244/0.17/true</complex>     => 3/4 floats


ref sometimes 0, sometimes non 0 ("0000","8900"):
0000: 0/8bytes; either element of multiple choice (0bytes) or "$" (8bytes)
8900: 4bytes; multiple choice


4100 (array):
references to an 044100 array
if first level array, this will move the offset to the array section
if other level array, it will continue reading in the array seciont without moving

2900:
references to 012900, 042900, 082900, 102900

29d0:
references to 0429d0, 1029d0

0000:
if element of 048900:
	ref0 is 0; name is never "$"
	hexpos=mempos
	see 8900 for more details

if element of 043500, 083500 or 103500:
	name0 is "$"
	ref0 points at 103500 or 043500 
	hexpos is 0
	mempos is 0
	payload is always 8 nullbytes

8900 (multiple choice):
if ref0!=0:
	multiple choice
	pointing at 048900
	the referenced 00s are of always of type 0000
	mempos=hexpos and is looked up by the payload integer

if ref==0:
	appears only as "member", i.e. in arrays 
	ref0 is always 0, so there is nothing inside the array


e.g. multiple choice:
Slot 8900 
	04WeaponSlot 048900
	WeaponSlot_0 0000 0 0
	WeaponSlot_1 0000 1 1
	WeaponSlot_2 0000 2 2
and 01000000 in data => WeaponSlot_1

=> ref0 defines the multiple choices, payload integer chooses

A few words of advice on reading the files. The lines without any tabs on the left have a hash to the right. This hash is called upon by the assignment type 3500 in certain cases. I suppose I could've inserted the entire referenced section at this point. It's not really convenient right now. There are a few things regarding these hashes I'm not sure about though, so I left it out.

There are other hashes which I assume are 32 bytes. I've put a "-" in the middle to make reading them a bit easier. These hashes are used to call other files. E.g. one line says "ProjectileData 83e96f995daadf4095663e0b78995380-0b25cb4c7fba6740bf8eb01a36b86f9e" and there's a projectile file named "556x45mmNATO_Carbine 83e96f995daadf4095663e0b78995380-a037a226e604a94a9650291f1006b305". I'm not sure why there are two parts to the hash in the first place.

Now that I think about it, the first part seems to be the object whereas the second part is a specific object within this object.

Edited by Frankelstner

Share this post


Link to post
Share on other sites

I've finally lost all motivation to do anything bf3 related mainly because there's no way to really understand these files without extensive disassembling. I suppose it might be possible to create some kind of tweaker tool to change most values without touching the parts that I have no idea about. I would've considered writing a tool to just convert the entire file, but I'm not sure how I would go about writing a tweaker and don't feel like creating UI stuff either. Here's virtually everything I know about the file format in question put together in a hopefully coherent file format description (a fair amount of copy paste from my previous posts involved but with additions). May it help anyone interested in these things. because I will not bother with bf3 anymore and most likely won't care about future Battlefield games either.

48 bytes header, each part of it being 4 bytes long. Just check if the first four bytes are CED1B20F and make a list out of the other 4bytes so you get something like this:
       -1: 0FB2D1CE
       0: absolute offset for path beginning
       1: length from path beginning to end of file
       2: number of hash bundles; even with 0 there is one hash bundle which belongs to the file itself; 32 bytes length each
       3: 00000000
       4: number of 3x4bytes1; 12 bytes length each
       5: number of headlines; 16 bytes length each
       6: number of fields/entries; 16 bytes length each
       7: length of name section including padding
       8: length of path section including padding
       9: number of 3x4bytes2; 12 bytes length each
       10: length of normal payload section; the array payload section starts right after that. The start of the array payload section is header[0]+header[8]+header[10]




There are 9 sections after the header appearing in the following order.


1. Hash bundles:
       There are always two 16bytes hashes bundled together, the first hash belonging to a file, the second one belonging to an entry inside that file, see 3x4bytes1.
       The very first hash bundle belongs to the file itself and the second part of this bundle has another appearance as a 16byte hash in the 3x4bytes1 section. 

       I am fairly sure that this hash and its entries should actually be at the very beginning of any created text file. I think If had I handled "3500" assignments 
       differently I could have made this single main hash contain everything else in the file, similar to BC2.


2. Key word strings:
       Trailing null byte
       There is padding to a multiple of 16 at the end. Hashes of these key words appear in the fields and headlines sections. Use the script as a reference for the hashing function.



3. Fields/Entries (or "00s"):
       They are tied closely to the headlines in the next section. Fields are converted rather directly to something human-readable. The payload however is defined much later in another section.

       int hash, 2byte type, 2byte reference, int relative position in hexfile, int position in RAM?
       e.g. 1C67150C 4100 0400 0C000000 1C000000
            hash     type ref  hexpos   mempos

       "hash" is a hashed key word from the strings of the previous section. It needs to be converted back to a key word to make any sense of it obviously.
       "type" is the data type to expect in the payload section. There are 16 different types.
       "ref" is the reference to a headline which in a way can be considered the payload of this field, nah scratch that last part. 
       Most types have ref set to 0. If ref!=0 I write the referenced headline and its entries then with a tab indentation right below this field. Add all headlines to a list and just pick list[ref].
       "hexpos" is the relative position in the data payload section. It is relative to the headline it belongs to.
       "mempos" might be where the data is stored when the game runs. I am not sure about it, it was not necessary to use this part to read the files.



4. Headlines ("04/10s"): 
       They are similar to fields and are hard to distinguish at a first glance in a hex editor except for one byte which is always 0x04, 0x08 or 0x10. Headlines usually have several fields attached to them.

       int hash, int 00-position, 1byte 00-length, 3byte type, int length of 04 and its 00s in hexfile
       e.g. 7DAB92F7 52000000 04 043500 50000000
            hash     pos      ln type   hexlen

       "hash" is exactly the same as in the previous section.
       "pos" and "ln" are related to fields. Assuming that all fields of the previous section were put into a list in the order in which they appeared, 
         then the fields attached to this headline are the next "ln" elements in this list starting from "pos".
       "type" is possibly redundant. I have not used it. It is somewhat tied to the "type" of the fields section.
       "hexlen" is the length of this headline and its entries in the payload section.


5. 3x4bytes1: 
       They have 3 components of 4 bytes each. This section puts together the two sections before and is quite tricky.

       The first component is always null, the second specifies the number of repetitions of a certain headline which is specified in the third component. 
       Make a list of all headlines, then pick the element specified in the third component. This headline and its fields then appear "repetition" number of times. This headline also has
       its own 16byte hash which appears in the payload section before anything else. The actual offset defined in "hexpos" and "hexlen" ignores this hash. You start counting AFTER the hash.

       These repetitions also appear in the payload section. Assume that the headline says "hexlen" 80 bytes and 3x4bytes says 5 repetitions and now take a closer look
       at the payload section. First there are 16bytes of the hash which are not accounted for by "hexlen". Then there are 80 bytes of the first repetition of the headline. After that there is another 16bytes hash 
       and another 80 bytes data etc.

       Also the game apparently numbers all these headlines and their repetitions in the order in which they appear. This is important for the "3500" assignment type. 
       I have realized this only very late while writing the script so I made a quick and dirty hack at the end.
       There is padding at the end.


6. 3x4bytes2: 
       Again there are 3 components of 4 bytes each. This one however is about arrays only, more specifically the array payload section. It is related to the "4100" assignment type only.

       The first component is the offset in the array payload section. It is absolute if you add header[0]+header[8]+header[10] to it. The second component describes the number of repetitions in the array payload section. 
       I do not know the purpose of the third component.
       There is padding at the end.

7. Paths:
       Very similar to the key words section. There are no hashes involved however. The entries are accessed by the "7d40" assignment. There is not even numbering or anything, they are just accessed via offset.



8. Payload:
       Not much to say, all values are stored here. The values are accessed in the order in which the 3x4bytes1 appear. If the first entry of 3x4bytes1 dictates 10 repetitions of headline 67 and headline 67 has "hexlen" 20
       then in the payload section there is a 16byte hash at first (regardless of the previous numbers), and after that 20bytes data which links into the fields of headline 67. After these bytes there is another 
       hash and so on.


9. Array payload:
       Similar to payload, but at a different place because the game loves jumping between payload and array payload all the time (that is what it does anyway).






Field types. There are 16 types which I have separated by the value of their "ref". I have specified their length as well. BC2 had "21", "22", "61", "71" and different "a1", "a2", "a3" types which handled all quite similar. 
If I remember correctly 21 was for string data, 22 for path/hash data, 61 was bool and 71 meant numbers and these "a" types were the headlines which then contained other lines. 
Only there was no recursion involved and it was quite linear like a non-binary XML file. Basically 5 types, so here goes 16, enjoy:


ref==0 ("7d40","0dc1","3dc1","4dc1","5dc1","adc0","bdc0","ddc0","edc0","fdc0","3500"):
       7d40: 4bytes; path, the value is the offset in the path section
       0dc1: 4bytes; unsigned integer (might be signed too)
       3dc1: 4bytes; float, single precision
       4dc1: 8bytes; float, double precision
       5dc1: 16bytes; hash, really just a hash right there; it refers to chunk files
       adc0: 1byte; bool, usually padded to 4 if no other adc0,bdc0
       bdc0: 1byte; char (signed probably) 
       ddc0: 2bytes; hard to tell as it always appears in array04 as "member"
       edc0: 2bytes; merges right into adc0 for padding, behaves like an integer
       fdc0: 4bytes; signed integer
       3500: 4bytes; GUID/hash, payload xy000080 => hashlist[xy]; 
                                payload xy000000 => go through bytes3x41 and choose the right headline and repetition

ref!=0 ("4100","2900","29d0"):
       4100: 4bytes; array
       2900: 0bytes; complex entry, like array but data not in array section
       29d0: 0bytes; <complex name="DirtTriggerColor">0.255/0.244/0.17/true</complex>     => 3/4 floats


ref sometimes 0, sometimes non 0 ("0000","8900"):
       0000: 0/8bytes; either element of multiple choice (0bytes) or "$" (8bytes)
       8900: 4bytes; multiple choice



More details for the ref!=0 types:

4100 (array):
       references to an 044100 array
take the corresponding 3x4bytes2 and start reading at the offset specified there in the array payload section. I had issues handling the offset here. 
       The length of the headlines within arrays are essential because simply adding 4bytes for a "3500" assignment etc. does not work. In particular the "3500" assignment is supposed to use 4bytes but has 
       plenty of nullbytes after that in either payload section. I assume this length is the length of the payload for the corresponding headline, in any case this is probably memory related (or rather bad coding in 
       my opinion) and not so much of interest here. 

A big issue I have had was going to the end of "hexlen" whenever a headline is done. The problem was that I have declared arrays to have a length of 4bytes, 
       so my script would add this length at the wrong place and time destroying all array members after the first one. 

2900:
       references to 012900, 042900, 082900, 102900

29d0:
       references to 0429d0, 1029d0

0000:
       if element of 048900:
               ref0 is 0; name is never "$"
               hexpos=mempos
               see 8900 for more details

       if element of 043500, 083500 or 103500:
               name0 is "$"
               ref0 points at 103500 or 043500 
               hexpos is 0
               mempos is 0
               payload is always 8 nullbytes

8900 (multiple choice):
       if ref0!=0:
               multiple choice
               pointing at 048900
               the referenced 00s are of always of type 0000
               mempos=hexpos and is looked up by the payload integer

       if ref==0:
               appears only as "member", i.e. in arrays 
               ref0 is always 0, so there is nothing inside the array


       e.g. multiple choice:
       Slot 8900 
               04WeaponSlot 048900
               WeaponSlot_0 0000 0 0
               WeaponSlot_1 0000 1 1
               WeaponSlot_2 0000 2 2
       and 01000000 in data => WeaponSlot_1

       => ref0 defines the multiple choices, payload integer chooses

Edited by Frankelstner

Share this post


Link to post
Share on other sites

@Frankelstner

"I've finally lost all motivation to do anything bf3 related mainly because there's no way to really understand these files without extensive disassembling. I suppose it might be possible to create some kind of tweaker tool to change most values without touching the parts that I have no idea about. I would've considered writing a tool to just convert the entire file, but I'm not sure how I would go about writing a tweaker and don't feel like creating UI stuff either. Here's virtually everything I know about the file format in question put together in a hopefully coherent file format description (a fair amount of copy paste from my previous posts involved but with additions). May it help anyone interested in these things because I will not bother with bf3 anymore and most likely won't care about future Battlefield games either."

Dear Frank,

I am sad to hear that you got fed up with the Frostbite encryption, I have been following all the threads on this all over the internet and it seems no one has got this far figuring out Dice`s latest engine code cipher. I am not gonna pretend that I understand what goes under the hood of the scripts you have made so I cannot help you with the technical stuff(I wish I could, believe me) , but I urge you to keep up you courage and continue you brilliant work on this. Who knows what treasures your mighty effort will bring.... ;]

Please keep up your much appreciated good work and good luck.

Share this post


Link to post
Share on other sites

Here's a more technical, objectified version. The script can resolve guids with actual filenames to make navigating much easier. If the files were extracted with the sbtoc dumper, then there are already filenames shown in the explorer which can be used by the script to quickly resolve the guids. Alternatively the script can run through the files and parse them just to grab the filename vs fileguid and store them in a file in the same folder as the script (this is slower but necessary when using the cascat extractor; this will also give proper capitalization to the file references whereas the sbtoc dumper will use lower case only). When dealing with the files from the cascat extractor, the ideal order of extraction is to run createGuidTable and dumpText for the unpatched files, then do the same for the patched files. This way the patched files can make use of the unpatched guids. Make sure to adjust input and output folder too. For the sbtoc extractor you should have put both patched and unpatched files in the same folder and should be able to convert everything in a single run. Don't forget this dll for better float representation: http://www.gamefront.com/files/22080360/floattostring_rar

Note that imported chunks (mostly audio) are now written in uppercase, whereas imported ebx files are still written in lowercase.

I have just noticed that the sbtoc dumper uses lower case only (it's not the fault of the script but rather the sbtoc archives). So if you want proper capitalization you should set useExplorerNames to False even when you have used the sbtoc dumper.

#Requires Python 2.7
import string
import sys
from binascii import hexlify
import struct
import os
from cStringIO import StringIO
import cProfile
import cPickle
import copy

#adjust input and output folders here
inputFolder=r"D:\hexing\bf3 dump\bundles\ebx"
outputFolder=r"D:\hexing\bf3 ebx"
guidTableName="guidTable bf3" #name of the guid table file; keeping separate names for separate games is highly recommended

EXTENSION=".txt"
SEP="    "

#the script can use the actual filenames in the explorer for the guid table (faster)
#or it can parse almost the entire file to retrieve the filename (slow, but necessary when the explorer names are just hashes)
#in case #2, create a separate guidTable file, in case#1 do not create that file.
#It's still rather slow either way. Also note that the explorer names are all lower case.
#Thus use False for proper capitalization and True if you want faster progress.
useExplorerNames=False #True/False

#Note: This is not about the filenames themselves, which are always capitalized. It's about fields in one file making
#references to another file; compare these two lines in LevelListReport.txt:
#True:  levels/coop_002/coop_002/description_win32/4c89939b7a8f046a1658504d64b5b4da
#False: Levels/COOP_002/COOP_002/Description_Win32/4c89939b7a8f046a1658504d64b5b4da


#show field offsets to the left
printOffsets=True #True/False

#ignore all instances and fields with these names when converting to text:
IGNOREINSTANCES=[]
IGNOREFIELDS=[]
##IGNOREINSTANCES=["ShaderAdjustmentData","SocketData","WeaponSkinnedSocketObjectData","WeaponRegularSocketObjectData"]
##IGNOREFIELDS=["Mesh3pTransforms","Mesh3pRigidMeshSocketObjectTransforms"]


#run createGuidTable or dumpText, or both (preferably in the right order)
#When using explorer names, do not change anything below.
#When not using explorer names you might want to make the guid table first, then restart the script to dump text only,
#though it should work fine without change too.
def main():
   createGuidTable()
   dumpText()





##############################################################
##############################################################
unpackLE = struct.unpack
def unpackBE(typ,data): return struct.unpack(">"+typ,data) 

if useExplorerNames:
   def createGuidTable(): #guid vs filename
       for dir0, dirs, ff in os.walk(inputFolder):
           for fname in ff:
               path=os.path.join(dir0,fname)
               f=open(lp(path),"rb") #
               if f.read(4) not in ("\xCE\xD1\xB2\x0F","\x0F\xB2\xD1\xCE"):
                   f.close()
                   continue
               #grab the file guid directly, absolute offset 48 bytes
               f.seek(48)
               fileguid=f.read(16)
               f.close()
               filename=path[len(inputFolder):-4].replace("\\","/")
               guidTable[fileguid]=filename
else:
   def createGuidTable():
       for dir0, dirs, ff in os.walk(inputFolder):
           for fname in ff:
               f=open(lp(dir0+"\\"+fname),"rb") #
               magic=f.read(4)
               if magic=="\xCE\xD1\xB2\x0F":
                   dbx=Dbx(f,unpackLE)
               elif magic=="\x0F\xB2\xD1\xCE":
                   dbx=Dbx(f,unpackBE)
               else:
                   f.close()
                   continue

               guidTable[dbx.fileGUID]=dbx.trueFilename
       f5=open(guidTableName,"wb") #write the table
       cPickle.dump(guidTable,f5)
       f5.close()

def dumpText():
   for dir0, dirs, ff in os.walk(inputFolder):
       for fname in ff:
           f=open(lp(dir0+"\\"+fname),"rb") #
           magic=f.read(4)
           if magic=="\xCE\xD1\xB2\x0F":
               dbx=Dbx(f,unpackLE)
           elif magic=="\x0F\xB2\xD1\xCE":
               dbx=Dbx(f,unpackBE)
           else:
               f.close()
               continue
           dbx.dump(outputFolder)

def open2(path,mode="rb"):
   if mode=="wb":    
       #create folders if necessary and return the file handle

       #first of all, create one folder level manully because makedirs might fail
       pathParts=path.split("\\")
       manualPart="\\".join(pathParts[:2])
       if not os.path.isdir(manualPart):
           os.makedirs(manualPart)

       #now handle the rest, including extra long path names
       folderPath=lp(os.path.dirname(path))
       if not os.path.isdir(folderPath): os.makedirs(folderPath)
   return open(lp(path),mode)

def lp(path): #long pathnames
   if len(path)<=247 or path=="" or path[:4]=='\\\\?\\': return path
   return unicode('\\\\?\\' + os.path.normpath(path))


try:
   from ctypes import *
   floatlib = cdll.LoadLibrary("floattostring")
   def formatfloat(num):
       bufType = c_char * 100
       buf = bufType()
       bufpointer = pointer(buf)
       floatlib.convertNum(c_double(num), bufpointer, 100)
       rawstring=(buf.raw)[:buf.raw.find("\x00")]
       if rawstring[:2]=="-.": return "-0."+rawstring[2:]
       elif rawstring[0]==".": return "0."+rawstring[1:]
       elif "e" not in rawstring and "." not in rawstring: return rawstring+".0"
       return rawstring
except:
   def formatfloat(num):
       return str(num)
def hasher(keyword): #32bit FNV-1 hash with FNV_offset_basis = 5381 and FNV_prime = 33
   hash = 5381
   for byte in keyword:
       hash = (hash*33) ^ ord(byte)
   return hash & 0xffffffff # use & because Python promotes the num instead of intended overflow
class Header:
   def __init__(self,varList): ##all 4byte unsigned integers
       self.absStringOffset     = varList[0]  ## absolute offset for string section start
       self.lenStringToEOF      = varList[1]  ## length from string section start to EOF
       self.numGUID             = varList[2]  ## number of external GUIDs
       self.null                = varList[3]  ## 00000000
       self.numInstanceRepeater = varList[4]
       self.numComplex          = varList[5]  ## number of complex entries
       self.numField            = varList[6]  ## number of field entries
       self.lenName             = varList[7]  ## length of name section including padding
       self.lenString           = varList[8]  ## length of string section including padding
       self.numArrayRepeater    = varList[9]
       self.lenPayload          = varList[10] ## length of normal payload section; the start of the array payload section is absStringOffset+lenString+lenPayload
class FieldDescriptor:
   def __init__(self,varList,keywordDict):
       self.name            = keywordDict[varList[0]]
       self.type            = varList[1]
       self.ref             = varList[2] #the field may contain another complex
       self.offset          = varList[3] #offset in payload section; relative to the complex containing it
       self.secondaryOffset = varList[4]
class ComplexDescriptor:
   def __init__(self,varList,keywordDict):
       self.name            = keywordDict[varList[0]]
       self.fieldStartIndex = varList[1] #the index of the first field belonging to the complex
       self.numField        = varList[2] #the total number of fields belonging to the complex
       self.alignment       = varList[3]
       self.type            = varList[4]
       self.size            = varList[5] #total length of the complex in the payload section
       self.secondarySize   = varList[6] #seems deprecated
class InstanceRepeater:
   def __init__(self,varList):
       self.null            = varList[0] #called "internalCount", seems to be always null
       self.repetitions     = varList[1] #number of instance repetitions
       self.complexIndex    = varList[2] #index of complex used as the instance
class arrayRepeater:
   def __init__(self,varList):
       self.offset          = varList[0] #offset in array payload section
       self.repetitions     = varList[1] #number of array repetitions
       self.complexIndex    = varList[2] #not necessary for extraction
class Complex:
   def __init__(self,desc):
       self.desc=desc
class Field:
   def __init__(self,desc,offset):
       self.desc=desc
       self.offset=offset #track absolute offset of each field in the ebx

numDict={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)}


class Dbx:
   def __init__(self, f, unpacker):
       #metadata
       self.unpack=unpacker
       self.trueFilename=""
       self.header=Header(self.unpack("11I",f.read(44)))

       self.arraySectionstart=self.header.absStringOffset+self.header.lenString+self.header.lenPayload
       self.fileGUID, self.primaryInstanceGUID = f.read(16), f.read(16)    
       self.externalGUIDs=[(f.read(16),f.read(16)) for i in xrange(self.header.numGUID)]
       self.keywords=str.split(f.read(self.header.lenName),"\x00")
       self.keywordDict=dict((hasher(keyword),keyword) for keyword in self.keywords)
       self.fieldDescriptors=[FieldDescriptor(self.unpack("IHHII",f.read(16)), self.keywordDict) for i in xrange(self.header.numField)]
       self.complexDescriptors=[ComplexDescriptor(self.unpack("IIBBHHH",f.read(16)), self.keywordDict) for i in xrange(self.header.numComplex)]
       self.instanceRepeaters=[instanceRepeater(self.unpack("3I",f.read(12))) for i in xrange(self.header.numInstanceRepeater)] 
       while f.tell()%16!=0: f.seek(1,1) #padding
       self.arrayRepeaters=[arrayRepeater(self.unpack("3I",f.read(12))) for i in xrange(self.header.numArrayRepeater)]

##        for key in vars(self.header):
##            print key,vars(self.header)[key]

       #payload
       f.seek(self.header.absStringOffset+self.header.lenString)
       self.internalGUIDs=[]
       self.instances=[] # (guid, complex)
       for instanceRepeater in self.instanceRepeaters:
           for repetition in xrange(instanceRepeater.repetitions):
               instanceGUID=f.read(16)
               self.internalGUIDs.append(instanceGUID)
               if instanceGUID==self.primaryInstanceGUID: self.isPrimaryInstance=True
               else: self.isPrimaryInstance=False

               self.instances.append( (instanceGUID,self.readComplex(instanceRepeater.complexIndex,f)) )
       f.close()



   def readComplex(self, complexIndex,f):
       complexDesc=self.complexDescriptors[complexIndex]
       cmplx=Complex(complexDesc)
       cmplx.offset=f.tell()-16 ###

       startPos=f.tell()                 
       cmplx.fields=[]
       for fieldIndex in xrange(complexDesc.fieldStartIndex,complexDesc.fieldStartIndex+complexDesc.numField):
           f.seek(startPos+self.fieldDescriptors[fieldIndex].offset)
           cmplx.fields.append(self.readField(fieldIndex,f))

       f.seek(startPos+complexDesc.size)
       return cmplx


   def readField(self,fieldIndex,f):
       fieldDesc = self.fieldDescriptors[fieldIndex]
       field=Field(fieldDesc,f.tell())

       if fieldDesc.type in (0x0029, 0xd029,0x0000,0x8029):
           field.value=self.readComplex(fieldDesc.ref,f)
       elif fieldDesc.type==0x0041:
           arrayRepeater=self.arrayRepeaters[self.unpack("I",f.read(4))[0]]
           arrayComplexDesc=self.complexDescriptors[fieldDesc.ref]

##            if arrayRepeater.repetitions==0: field.value = "*nullArray*"
           f.seek(self.arraySectionstart+arrayRepeater.offset)
           arrayComplex=Complex(arrayComplexDesc)
           arrayComplex.fields=[self.readField(arrayComplexDesc.fieldStartIndex,f) for repetition in xrange(arrayRepeater.repetitions)]
           field.value=arrayComplex

       elif fieldDesc.type in (0x407d, 0x409d):
           startPos=f.tell()
           f.seek(self.header.absStringOffset+self.unpack("I",f.read(4))[0])
           string=""
           while 1:
               a=f.read(1)
               if a=="\x00": break
               else: string+=a
           f.seek(startPos+4)

           if len(string)==0: field.value="*nullString*" #actually the string is ""
           else: field.value=string

           if self.isPrimaryInstance and self.trueFilename=="" and fieldDesc.name=="Name": self.trueFilename=string


       elif fieldDesc.type in (0x0089,0xc089): #incomplete implementation, only gives back the selected string
           compareValue=self.unpack("I",f.read(4))[0] 
           enumComplex=self.complexDescriptors[fieldDesc.ref]

           if enumComplex.numField==0:
               field.value="*nullEnum*"
           for fieldIndex in xrange(enumComplex.fieldStartIndex,enumComplex.fieldStartIndex+enumComplex.numField):
               if self.fieldDescriptors[fieldIndex].offset==compareValue:
                   field.value=self.fieldDescriptors[fieldIndex].name
                   break
       elif fieldDesc.type==0xc15d:
           field.value=f.read(16)
       elif fieldDesc.type==0x417d:
           field.value=f.read(24)
       else:
           (typ,length)=numDict[fieldDesc.type]
           num=self.unpack(typ,f.read(length))[0]
           field.value=num

       return field


   def dump(self,outputFolder):
       if not self.trueFilename: self.trueFilename=hexlify(self.fileGUID)

       outName=outputFolder+self.trueFilename+EXTENSION
##        dirName=os.path.dirname(outputFolder+self.trueFilename)
##        if not os.path.isdir(dirName): os.makedirs(dirName)
##        if not self.trueFilename: self.trueFilename=hexlify(self.fileGUID)
##        f2=open(outputFolder+self.trueFilename+EXTENSION,"wb")
       f2=open2(outName,"wb")
       print self.trueFilename

       for (guid,instance) in self.instances:
           if instance.desc.name not in IGNOREINSTANCES: #############
               if guid==self.primaryInstanceGUID: writeInstance(f2,instance,hexlify(guid)+ " #primary instance")
               else: writeInstance(f2,instance,hexlify(guid))
               self.recurse(instance.fields,f2,0)
       f2.close()

   def recurse(self, fields, f2, lvl): #over fields
       lvl+=1
       for field in fields:
           if field.desc.type in (0xc14d, 0xc0fd, 0xc10d, 0xc0ed, 0xc0dd, 0xc0bd, 0xc0ad, 0x407d, 0x409d, 0x0089, 0xc0cd, 0xc089):
               writeField(f2,field,lvl," "+str(field.value))
           elif field.desc.type == 0xc13d:
               writeField(f2,field,lvl," "+formatfloat(field.value))
           elif field.desc.type == 0xc15d:
               writeField(f2,field,lvl," "+hexlify(field.value).upper()) #upper case=> chunk guid
           elif field.desc.type==0x417d:
               val=hexlify(field.value)
               val=val[:16]+"/"+val[16:]
               writeField(f2,field,lvl," "+val)
           elif field.desc.type == 0x0035:
               towrite=""
               if field.value>>31:
                   extguid=self.externalGUIDs[field.value&0x7fffffff]
                   try: towrite=guidTable[extguid[0]]+"/"+hexlify(extguid[1])
                   except: towrite=hexlify(extguid[0])+"/"+hexlify(extguid[1])
               elif field.value==0: towrite="*nullGuid*"
               else: towrite=hexlify(self.internalGUIDs[field.value-1])
               writeField(f2,field,lvl," "+towrite) 
           elif field.desc.type==0x0041:
               if len(field.value.fields)==0:
                   writeField(f2,field,lvl," *nullArray*")
               else:
                   writeField(f2,field,lvl,"::"+field.value.desc.name)

                   #quick hack so I can add indices to array members while using the same recurse function
                   for index in xrange(len(field.value.fields)):
                       member=field.value.fields[index]
                       if member.desc.name=="member":
                           desc=copy.deepcopy(member.desc)
                           desc.name="member("+str(index)+")"
                           member.desc=desc
                   self.recurse(field.value.fields,f2,lvl)

           else:
               if field.desc.name not in IGNOREFIELDS: #############
                   writeField(f2,field,lvl,"::"+field.value.desc.name)
                   self.recurse(field.value.fields,f2,lvl)

def hex2(num):
   #take int, return 8byte string
   a=hex(num)
   if a[:2]=="0x": a=a[2:]
   if a[-1]=="L": a=a[:-1]
   while len(a)<8:
       a="0"+a
   return a

if printOffsets:
   def writeField(f,field,lvl,text):
       f.write(hex2(field.offset)+SEP+lvl*SEP+field.desc.name+text+"\r\n")
   def writeInstance(f,cmplx,text):
       f.write(hex2(cmplx.offset)+SEP+cmplx.desc.name+" "+text+"\r\n")     
else:
   def writeField(f,field,lvl,text):
       f.write(lvl*SEP+field.desc.name+" "+text+"\r\n")
   def writeInstance(f,cmplx,text):
       f.write(cmplx.desc.name+" "+text+"\r\n")


if outputFolder[-1] not in ("/","\\"): outputFolder+="\\"
if inputFolder[-1] not in ("/","\\"): inputFolder+="\\"


#if there's a guid table already, use it
try:
   f5=open(guidTableName,"rb")
   guidTable=cPickle.load(f5)
   f5.close()
except:
   guidTable=dict()

main()

Edited by Frankelstner

Share this post


Link to post
Share on other sites

Traceback (most recent call last):
 File "C:\Python27\ebx.py", line 387, in <module>
   main()
 File "C:\Python27\ebx.py", line 49, in main
   dumpText()
 File "C:\Python27\ebx.py", line 106, in dumpText
   dbx.dump(outputFolder)
 File "C:\Python27\ebx.py", line 305, in dump
   f2=open2(outName,"wb")
 File "C:\Python27\ebx.py", line 119, in open2
   return open(lp(path),mode)
IOError: [Errno 13] Permission denied: 'C:\\ao2 ebx/AO4_Assets/Sound/Music/FE/AO4_FE_Music_Loop_A.txt'
>>> 

Im getting this.

Share this post


Link to post
Share on other sites

oops NVM something was screwed with my C root directory. Did it in a different path and worked. Can you maybe update the bfs3decoder's EBX handling as i have another XAS game with fb2 i would like to test out.

Share this post


Link to post
Share on other sites

oops NVM something was screwed with my C root directory. Did it in a different path and worked. Can you maybe update the bfs3decoder's EBX handling as i have another XAS game with fb2 i would like to test out.

Will do when I manage to get it handle mohw audio. However, I thought console games used a different audio encoding.

Share this post


Link to post
Share on other sites

I've made a few minor changes, I can't be bothered to fully test this version right here, so just tell me when something goes wrong.

Edited by Frankelstner

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×