Skip to content

Instantly share code, notes, and snippets.

@lkolbly
Created January 3, 2017 17:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lkolbly/de99b551d55038ea50bacadc66ad92f5 to your computer and use it in GitHub Desktop.
Save lkolbly/de99b551d55038ea50bacadc66ad92f5 to your computer and use it in GitHub Desktop.
Python parser for the Star Wars: X-Wing (Collector's CD) .BRF mission briefing file format.
import json
import struct
ANIMATION_COMMANDS = {
1: ('wait_for_click', []),
10: ('clear_text', []),
11: ('show_title', [('i', 'text_id')]),
12: ('show_main', [('i', 'text_id')]),
15: ('center_map', [('f', 'x'), ('f', 'y')]),
16: ('zoom_map', [('f', 'x'), ('f', 'y')]),
21: ('clear_boxes', []),
22: ('box_1', [('i', 'ship_id')]),
23: ('box_2', [('i', 'ship_id')]),
24: ('box_3', [('i', 'ship_id')]),
25: ('box_4', [('i', 'ship_id')]),
26: ('clear_tags', []),
27: ('tag_1', [('i', 'tag_id'), ('f', 'x'), ('f', 'y')]),
28: ('tag_2', [('i', 'tag_id'), ('f', 'x'), ('f', 'y')]),
29: ('tag_3', [('i', 'tag_id'), ('f', 'x'), ('f', 'y')]),
30: ('tag_4', [('i', 'tag_id'), ('f', 'x'), ('f', 'y')])
}
class BrfParser:
def __init__(self, f):
self.f = f
self._parseHeader()
self.ships = []
for i in range(self.numShips):
self.ships.append({"coordinates": []})
# Parse all of the coordinates
#print(self.numCoordinates)
for i in range(self.numCoordinates):
for j in range(self.numShips):
c = {}
c["x"] = self._readFixed()
c["y"] = self._readFixed()
c["z"] = self._readFixed()
self.ships[j]['coordinates'].append(c)
pass
# This is where we get the value for the text jump
#print("f.tell = %s, have %d ships"%(self.f.tell(), self.numShips))
#textJumpLength = int(self.f.tell()/2)*15 + 151 - 192 - 4
#textJumpLength = int(self.numShips*6+3)*15 + 151 - 192 - 4
textJumpLength = self.numShips*90
# Parse ship data
for i in range(self.numShips):
self.ships[i]['stype'] = self._readShort()
self.ships[i]['iff'] = self._readShort()
self.ships[i]['wave_size'] = self._readShort()
self.ships[i]['num_waves'] = self._readShort()
self.ships[i]['designation'] = self._readFixedString(16)
self.ships[i]['cargo'] = self._readFixedString(16)
self.ships[i]['alt_cargo'] = self._readFixedString(16)
self._readShort() # save counter?
self._readShort() # Probably alternate cargo ship?
self._readShort() # 4
self._readShort()
#print(json.dumps(self.ships, indent=3))
"""h = self._readShort()
if h != 2:
raise Exception("Section header %X is not 2!"%h)
self.f.read(100)"""
section_size = self._readShort()
self.f.read(50*section_size)
#print(json.dumps(self.ships, indent=3))
# Parse the animation pages
numPages = self._readShort()
#print("Parsing %s pages"%numPages)
pages = []
footerType = 0
#for i in range(numPages):
while len(pages) < numPages:
#print()
#print("At 0x%X, starting page %s"%(self.f.tell(), len(pages)))
page = {}
page['clock_period'] = self._readShort()
page['size'] = self._readShort()
wordsRead = 0
if footerType == 0x29:
page['a'] = self._readShort()
page['b'] = self._readShort()
wordsRead += 2
if page['clock_period'] == 0x270f and page['size'] == 0x29:
#print("Hit footer 29")
footerType = 0x29
continue
elif page['clock_period'] == 400 and page['size'] == 1:
# Hack for t5m01wx.brf
continue
elif page['clock_period'] == 400 and page['size'] == 701:
# Hack for t4m18ym.brf
page['clock_period'] = 701
page['size'] = self._readShort()
else:
footerType = 0
#print("0x%X"%self.f.tell(), page)
#self._readShort() # Unknown
cmds = []
#gotFooter = False
while wordsRead < page['size']:
c,cnt = self._readAnimationCommand()
wordsRead += cnt
#print(c)
cmds.append(c)
#print("Got %s animation commands"%len(cmds))
page['commands'] = cmds
pages.append(page)
pass
#print("Finished at 0x%X, %d"%(self.f.tell(), footerType))
a = self._readShort()
b = self._readShort()
if a == 0x270f and b == 0x29:
self._readShort()
self._readShort()
#self._readShort()
#c = self._readShort()
#print(json.dumps(pages, indent=3))
#print(found_special_page)
#print("0x%X"%self.f.tell())
#print("Num coordinates: %s Section size: %s"%(self.numCoordinates, section_size))
# Unknown
#a = self._readShort()
a = self._readShort()
b = self._readShort()
#print("%s %s at 0x%X"%(a,b, self.f.tell()))
# Completion messages
completionMessages = []
for i in range(3):
completionMessages.append(self._readFixedString(64))
#print(json.dumps(completionMessages, indent=3))
# Now jump to the texts
#print("Jumping 0x%X bytes"%textJumpLength)
self.f.read(textJumpLength)
#print("At 0x%X"%self.f.tell())
self._checkForHeader20()
# Read the tags
tags = []
for i in range(32):
taglen = self._readShort()
if taglen > 0:
tags.append(self.f.read(taglen).decode('utf-8'))
pass
# Read the briefing texts
self._checkForHeader20()
self._readShort() # Unknown, should be zero?
texts = []
while True:
textlen = self._readShort()
if textlen == None:
break
if textlen > 0:
texts.append(self.f.read(textlen).decode('utf-8'))
self.f.read(textlen) # Extra bytes
#print(json.dumps(texts, indent=3))
#print(json.dumps(tags, indent=3))
res = {
'text': texts,
'tags': tags,
'ships': self.ships,
'pages': pages
}
#print(json.dumps(res, indent=3))
self.result = res
def _checkForHeader20(self):
p = self.f.tell()
h = self._readShort()
if h != 0x20:
self._raiseError("At 0x%X, wanted header 20, got %X"%(p,h))
def _raiseError(self, etext):
s = ""
for i in range(16):
s = "%s %02X"%(s,self._readByte())
print(s)
raise Exception(etext)
def _readAnimationCommand(self):
clock_ticks = self._readShort()
#print("Clock ticks: %s"%clock_ticks)
cmd = self._readShort()
"""if cmd == 0x29:
# This is the footer
return None, 2"""
if cmd not in ANIMATION_COMMANDS:
# Hack: These are tailered for specific mission files
if cmd == 14:
c = ('unknown', [('i', '?')])
else:
c = ('unknown', [('i', '?'), ('f', '?'), ('f', '?')])
else:
c = ANIMATION_COMMANDS[cmd]
params = {}
wordsRead = 2
for p in c[1]:
if p[0] == 'i':
params[p[1]] = self._readShort()
elif p[0] == 'f':
params[p[1]] = self._readFixed()
else:
raise Exception('Unknown parameter type %s'%p[0])
wordsRead += 1
params['type'] = c[0]
params['clock_ticks'] = clock_ticks
return params, wordsRead
def _readFixedString(self, l):
s = self.f.read(l)
return s.split(b"\x00")[0].decode('utf-8')
def _readShort(self):
v = self.f.read(2)
if len(v) == 0:
return None
return struct.unpack("h", v)[0]
def _readByte(self):
v = self.f.read(1)
return struct.unpack('B', v)[0]
def _readFixed(self):
return self._readShort()/100.0
def _parseHeader(self):
v = self.f.read(2)
if v[0] != 2 or v[1] != 0:
raise Exception("Unknown file version %X %X"%(v[0],v[1]))
self.numShips = self._readShort()
self.numCoordinates = self._readShort()
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
p = BrfParser(open(sys.argv[1], "rb"))
print(json.dumps(p.result, indent=3))
else:
import os
fails = []
successes = []
for fname in os.listdir("classic_missions"):
if fname.split(".")[-1] == 'brf':
print(fname)
try:
BrfParser(open("classic_missions/%s"%fname, "rb"))
successes.append(fname)
except:
fails.append(fname)
print("fails:")
for fname in fails:
print(fname)
print("Failed %s/%s"%(len(fails),len(successes)+len(fails)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment