Created
January 3, 2017 17:29
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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