I recently started making ship-mods (my first one even got featured on the official ModDB twitter/X account!) for Galaxy On Fire 2, which require tampering with a proprietary file format. To mess with that file, i wrote my very own tool for it.
Weapons.bin and a short history of file formats in the GOF modding scene
Link to weapbin: https://github.com/KroeteTroete/weapbin/
I’ll be showing short snippets of code in this post. The rest of the code can be found on GitHub.
Making custom models for Galaxy On Fire 2 (GOF2) has been possible for a long time. Users on the russian-speaking 4pda forums have posted screenshots of custom models, including a model of the Millenium Falcon, seen in-game in the 2010s. However, the tool used for it, GIMS AE, could only be used if you owned a copy of Autodesk’s 3ds Max. The Kaamo Club (KC) Discord, the Galaxy On Fire Wiki’s Discord server, and it’s modding community set out to make custom 3D models possible without the use of GIMS AE.
To make models for GOF2, three proprietary file formats are used:
- .aem, Fishlabs’ 3D-model format
- .aei, Fishlabs’ 2D-texture format
- weapons.bin, the file containing hardpoint coordinates.
- (optionally) ships.bin, the file containing ship properties such as price, equipment amounts etc.
Several years ago Exilium posted his program “GOF2Modder” on the KC Discord, which made it possible to modify items.bin, stations.bin, systems.bin and ships.bin. In January of this year Ravernstal created gof2edit, an excellent program capable of modifying all the bin files, as well as patching new systems and stations.
AEI editing has also been possible for a long time thanks to the AEIEditor made by Catlabs, a modding team active on 4pda.
The .aem format has been solved in the last few months by PB-207, who made AEMConvertor, a python program capable of importing and exporting the .aem format. Manni1000 went one step further and made a blender plugin with the functionalities of AEMConvertor (The plugin is only available on the discord server at the moment).
This leaves only one file format which didn’t have a tool available, the weapons.bin format. It was already well known that weapons.bin stored coordinates for hard-points (engines, weapon muzzles, secondary muzzles). Not modifying it when importing a model causes engine and weapon effects to appear at incorrect places.
Ravernstal added weapons.bin functionality to gof2edit on October 1st, making it possible to finally edit hard-point information. On the 4th of October PB-207 posted an overview of weapons.bin’s file structure on the KC Discord. I used that overview to make my own tool (weapbin.py) to edit weapons.bin without 3ds Max and GIMS. At that moment I was unaware that Ravernstal already added weapons.bin functionality to gof2edit and looking at it, his implementation is way better than mine. So, I unknowingly fixed an issue that didn’t need any fixing anymore. However, I still think making weapbin.py was worth it, as I’ve learned a lot from making it. For example, I learned how to use python’s json and struct packages in the process.

How weapbin.py works
Although I didn’t know Ravernstal‘s tool could unpack weapons.bin, I know it dumped the information of the other .bin files into JSON. I thought that was a great method, so I also used JSON.
The bytes in the weapons.bin file were stored as little endian bytes, meaning the most significant part of a value was always stored in the last byte, like reading from right to left instead of left to right. To account for this I made two functions converting little-endian bytes to integers and floats respectively.
def leBytesInt(file):
#converts the next 2 bytes to an integer
next2 = file.read(2)
print(next2)
converted2Bytes = int.from_bytes(next2, "little",signed = True)
return converted2Bytes
def leBytesFloat(file):
#converts the next 4 bytes to a float
converted4Bytes = struct.unpack("<f", file.read(4))
print(converted4Bytes)
return converted4Bytes
The extract() function, the purpose of which is to put the information from weapons.bin into JSON, opens the weapons.bin file in binary read mode and enters a loop. The loop starts with a check of the first/next 2 bytes. If they are empty, the program stops (for example if the end of the file has been reached). If not, then the first 2 bytes are interpreted as the ship-ID. The ship-ID can be used to identify which ship you are modifying by comparing it to the ship‘s 3d-model-id. For example, the Inflict‘s 3d model is called ship_05_terran.aem, so it’s id in weapons.bin is 5. The ship ID bytes are followed by 2 bytes defining the total amount of hardpoints the ship possesses.
while not end:
nextID = f.read(2)
#check if next 2 bytes are empty
if nextID == b'':
break
#if they are not empty -> store it as the ship ID
shipID = int.from_bytes(nextID, byteorder="little")
#total amount of hardpoints the ship possesses
totalAmount = leBytesInt(f)
Weapbin then creates a bunch of lists, used to store the hardpoints and their coordinates. Here are the primary hardpoint lists as an example:
primary = []
primaryX = []
primaryY = []
primaryZ = []
primaryCoords = [primaryX, primaryY, primaryZ]
The engine hardpoints have a few additional lists because they have both coordinate and size values.
engineSizeX = []
engineSizeY = []
engineSizeZ = []
engineSizes = [engineSizeX, engineSizeY, engineSizeZ]
engineCoords = [engineX, engineY, engineZ]
Now, a nested loop starts. The program compares the next 2 bytes with the hardpoint identifiers. The hardpoint identifiers specify whether the following coordinates belong to a primary, secondary, turret or engine hardpoint.
The Identifiers are as follows:
- 0 = primary
- 1 = secondary
- 2 = turret
- 3 = engine
As already mentioned, the actual bytes are little endian, so the indicators viewed in a hex editor would look like this:
- 00 00
- 00 10
- 00 20
- 00 30
If a hardpoint identifiers is found, it adds the next 6 bytes (as 3 integers) to the coordinate lists of the ship. It also makes a list entry for the general hardpoint lists, to keep track of which coordinates belong to which hardpoint.
Here is the code for the secondary identifier as an example.
#check for secondary hardpoint indicator (01 00)
elif nextBytes == 1:
print("bytes = 1")
secAmount += 1
secondary.append(f"secondary_{secAmount}")
#add coordinates to lists
for i in secondaryCoords:
i.append(leBytesInt(f))
When it comes to engines, the coordinates are followed by 3 floats (4 bytes each) corresponding to the X, Y and Z engine plume sizes. So the engine check also has this little snippet of code:
#add engine plume sizes to lists
for i in engineSizes:
i.append(leBytesFloat(f))
Once the loop looped through all the hardpoints included in the total value extracted in the beginning, the programs writes the JSON file.
for i in range(0, totalAmount):
if end:
break
pIteration = 0
for i in primary:
shipDict[i + "_key"] = i
shipDict[i + "_coordX"] = primaryX[pIteration]
shipDict[i + "_coordY"] = primaryY[pIteration]
shipDict[i + "_coordZ"] = primaryZ[pIteration]
pIteration += 1
sIteration = 0
for i in secondary:
shipDict[i + "_key"] = ..
#put dictionary into JSON file
with open(f"ship_{shipID}.json", 'w') as g:
json.dump(shipDict, g, indent=4)
g.close()
print("End reached")
To avoid messing up the order the ship IDs found in the weapons.bin file (because they’re not sorted at all in the file itself), the program creates a separate loadorder.txt file. The program repeats this entire process until it created a JSON file for each ship and reaches the end of the weapons.bin file.
Reassembling the file
The program has now created around forty JSON files (one for each ship). The actual amount varies between the game versions. If the weapons.bin file is from a game version with both the Valkyrie and Supernova DLCs installed, the number of ships, and thus the amount of JSON files, would be larger. The program has also created a loadorder.txt file, which usually starts with ship_22, as it’s the first ship mentioned in weapons.bin.
The build() function, which creates a new weapons.bin from the JSON files after modifications have been made, loads this loadorder.txt and proceeds the values from each JSON file. It starts by writing the ship ID and total amount of hardpoints into the file first.
if j == 'ID' or j == 'total':
print("writing " + j)
inBytes = struct.pack('<h', loadedJSON[j])
w.write(inBytes)
At first this part of the code didn’t work, because I forgot to account for signed integers/shorts. Instead of using the ‘<h’ data type in the struct.pack() method (‘<h’ is a signed little-endian short) I just used ‘<H’, which made the coordinates go into the tenths of thousands.
After writing the ID and total, the program checks whether the next JSON property is either a coordinate/size or a hardpoint-indicator.
If any of the words “primary”, “secondary”, “turret” or “engine” as well as the word “coord” exist in the JSON property, it writes it’s value (the coordinate) as a signed short. If the property includes “size” instead of “coord”, it writes it’s value (the engine size) as a float.
If neither of these two cases are the true, the JSON property is probably a hardpoint-indicator, which I’ve chosen the very, very bad keyword “key” for. So if the property has the word “key” in it, it writes the according short for each hardpoint type (0 for primary, 1 for secondary, …).
And that’s it. That’s how the script extracts and reconstructs the weapons.bin file format from Galaxy On Fire 2.
The full code can be found here:


