Parts
Part 1: Decoding the Gears of War 2 graphical settings.
Part 2: Writing the code to read and write the settings.
Introduction
The Xbox 360 holds a special place in my heart. It came out the year I turned 17 and it was the cornerstone of social gaming for a couple of years of my early adulthood. One of the games that defined my hungover sofa time with friends was Gears of War 2. The Horde mode to be exact. It was without doubt the game we played the most and actually managed to complete all 50 levels on one of the tougher difficulties, which at the time felt like quite the feat. Did it run like a dog’s turd once you had 4 players and a screen full of Locusts? Yes. Was it still awesome? Hell yea. Much of this is, of course, nostalgia for a time where I actually had days to practice a game before we attempted a level 50 run, but my logical brain can filter some of that out and conclude that Gears of War 2 was, in fact, a pretty good game.
Wanting to revisit it I did what any nerd would and I dusted off my trusty Xbox 360 and plugged it in to my 55" TV. The green lights on the front lit up in a clockwise fashion and I was greeted by the Aurora dashboard. I scrolled to Gears of War 2 and after a second or so the game was booted and I could start a solo campaign to experience the story once again.
As powerful as nostalgia can be, it will never be able to conceal the gigantic gap between a 360 title from 2008 and just about any more shooter I can run on my PC. I was greeted with a fairly detailed game capped at 30 FPS and what feels like a very, very narrow field of view (FOV). Some of the late-in-lifecycle 360 and PS3 games had some Gandalf The Grey level code wizards working on them considering how they looked and ran with just 512 MB RAM. Gears of War 2 does run at 30 FPS and does have some visual quirks like texture pop-in, but it held the 30 FPS fairly consistently throughout my initial testing.
The game was enjoyable but that FOV is really narrow compared to what I am used to on PC. Then it hit me: Gears of War 2 is written in the Unreal 3 (UE3) engine and I know from the PC that UE3 games have a bunch of INI files in various places that store a lot of settings that are not always changeable from the in-game menus. Since the game’s predecessor did come out on PC and had INI files, one can only assume that Gears 2 does too.
Down the rabbit hole I went and looked for existing mods and tools did something with the game. I fell over a couple of mods that changed how weapons behaved.
A couple of guides on how to edit the settings was based on hex editing a file called Coalesced_int.bin
. There’s also a tool called Gears of War 2 Editor.
This tool has an easy-to-use GUI that can edit the binary encoded INI files in-place and update some other file called Xbox360TOC.txt
, which stores a length in bytes
and the MD5 hash of game resource files, one of which is Coalesced_int.bin
. The GUI editor is a decent piece of software but it lacks a search feature and I have no way of comparing any two modded files besides a binary diff. My idea then hit me: create a tool that can unpack all the binary encoded INI files as actual .ini
files on disk and later coalesce them into the same binary format. I mean how hard can it be?
Enter ImHex
My hex editor of choice is ImHex because it has a lot of nice features and the GUI is very 90s hacker-like, which makes you 200% more capable at reverse engineering. Opening the settings file you are greeted with the following HEX view:
0000:0000 | 00 00 00 3F FF FF FF E2 2E 00 2E 00 5C 00 47 00 | ...?ÿÿÿâ....\.G.
0000:0010 | 65 00 61 00 72 00 47 00 61 00 6D 00 65 00 5C 00 | e.a.r.G.a.m.e.\.
0000:0020 | 43 00 6F 00 6E 00 66 00 69 00 67 00 5C 00 47 00 | C.o.n.f.i.g.\.G.
0000:0030 | 65 00 61 00 72 00 41 00 49 00 2E 00 69 00 6E 00 | e.a.r.A.I...i.n.
0000:0040 | 69 00 00 00 00 00 00 20 FF FF FF F0 47 00 65 00 | i...... ÿÿÿðG.e.
0000:0050 | 61 00 72 00 47 00 61 00 6D 00 65 00 2E 00 47 00 | a.r.G.a.m.e...G.
0000:0060 | 65 00 61 00 72 00 41 00 49 00 00 00 00 00 00 35 | e.a.r.A.I......5
0000:0070 | FF FF FF EA 4D 00 69 00 6E 00 44 00 69 00 73 00 | ÿÿÿêM.i.n.D.i.s.
0000:0080 | 74 00 42 00 65 00 74 00 77 00 65 00 65 00 6E 00 | t.B.e.t.w.e.e.n.
0000:0090 | 4D 00 61 00 6E 00 74 00 6C 00 65 00 73 00 00 00 | M.a.n.t.l.e.s...
0000:00A0 | FF FF FF FB 31 00 30 00 32 00 34 00 00 00 FF FF | ÿÿÿû1.0.2.4...ÿÿ
0000:00B0 | FF EC 45 00 6E 00 65 00 6D 00 79 00 44 00 69 00 | ÿìE.n.e.m.y.D.i.
0000:00C0 | 73 00 74 00 61 00 6E 00 63 00 65 00 5F 00 4D 00 | s.t.a.n.c.e._.M.
0000:00D0 | 65 00 6C 00 65 00 65 00 00 00 FF FF FF FA 32 00 | e.l.e.e...ÿÿÿú2.
0000:00E0 | 35 00 36 00 2E 00 66 00 00 | 5.6...f..
Jumping into the first couple of hundred bytes gives us a pretty good idea of the structure of the file.
The first four bytes 00 00 00 3F
look a lot like a 32-bit number. We will write this as 0x0000003F
.
The Xbox 360 is a big endian architecture, which means the most significant byte is at the left, much like
the normal base 10 numbers you see every day. This means we can ignore the first 3 00
bytes and calculate 0x3F
which is 63
.
The next 4 bytes FF FF FF E2
look like some type of separator since the pattern is repeated after the string ..\GearGame\Config\GearAI.ini
.
The string itself is obviously a Windows UTF-16 string stored in little endian (because why not?) with two bytes termination 00 00
. The FF FF FF xx
pattern is repeated for every string,
which indicates that this has some correlation to the text.
At first I thought this was some kind of checksum, but after some bit fiddling it occurred to me that this is just the length of the string in big endian, bitwise inverted. The inversion may seem silly, but becomes obvious later.
The next 4 bytes 00 00 00 20
or 0x00000020
is 32 as big endian. This looks like a count of some sort. Then we have another string GearGame.GearAI
followed by a number and some pairs of strings that look suspiciously like an INI file’s key/value pairs. The number is 0x00000035
which is big endian 53. InThe first two strings are: MinDistanceBetweenMantles
followed by 1024
. The next one is EnemyDistance_Melee
followed by 5.6f
, which indeed sounds like fun times are to be had.
Patterns in ImHex
Now that we have some idea of the structure, let us use the pattern editor DSL built into ImHex to parse and visualize the file.
We know that our file contains n INI files encoded in a binary format. We assume the first 4 bytes are a number followed by an array of a yet-to-be-decoded something. So our pattern looks like this:
struct CoalescedIniFolder {
be u32 some_number;
};
CoalescedIniFolder folder @ 0x00;
The above tells ImHex to interpret the file as a CoalescedIniFolder
struct starting from the address 0x00
.
It shows up in the pattern data as:
Here we can see that the number is decoded as 63 and then the pattern stops. The next part is a string as described in the section above, so let’s encode that in the pattern. AFAIK there’s no built-in way to encode u32 of inverted bits so we have to write a function here:
fn invert(u32 input) {
return u32(~input);
};
struct IniString {
be u32 len [[transform("invert")]];
char16 val[];
};
struct CoalescedIniFolder {
be u32 some_number;
IniString string;
};
CoalescedIniFolder folder @ 0x00;
This immediately marks shows up as something useful in the pattern data view:
ImHex really helps us here and assumes the array of char16
is nul-terminated. The decoded string is ..\GearGame\Config\GearAI.ini
, which is exactly what we see in the hex view. So what about the next part, the 32-bit int there followed by the two strings? Let’s add them!
fn invert(u32 input) {
return u32(~input);
};
struct IniString {
be u32 len [[transform("invert")]];
char16 val[];
};
struct CoalescedIniFolder {
be u32 some_number;
IniString string;
be u32 next_number;
IniString string2;
be u32 another_number;
IniString string3;
IniString string4;
};
CoalescedIniFolder folder @ 0x00;
The names could be better, but the results are clear, we are definitely on the right track:
The another_number
value is 53, which is right before a pair of strings. This is most likely the number of key/value pairs following. We can add a count to arrays in the ImHex pattern. Let’s rename string3
and string4
to key
and value
and put them in a struct called IniValue
. Then we rename another_number
to num_values
and tell ImHex that the array has the length num_values
:
[...]
struct IniValue {
IniString key;
IniString value;
};
struct CoalescedIniFolder {
be u32 some_number;
IniString string;
be u32 next_number;
IniString string2;
be u32 num_values;
IniValue values[num_values];
};
Now we have decoded all 53 INI key/value pairs:
The string before the values must be the INI section name so let’s rename string2
to section_name
. The number before section_name
is counting something, we assume it is the number of sections in the current file. We create a IniSection
struct which has a name and num_values
values. Let’s have a look at what ImHex decodes:
[...]
struct IniSection {
IniString name;
be u32 num_values;
IniValue values[num_values];
};
struct CoalescedIniFolder {
be u32 some_number;
IniString string;
be u32 next_number;
IniSection sections[];
};
We are getting close! The next_number
value could look a lot like the number of sections in the file, let’s call it num_sections
:
[...]
struct CoalescedIniFolder {
be u32 some_number;
IniString string;
be u32 num_sections;
IniSection sections[num_sections];
};
And we have decoded a bunch of sections:
Now we are really only left with the file itself. The string we called string
looks like the name of the INI file, so let’s name it file_name
and the number before it is most likely the number of binary encoded INI files in the entire coalesced structure. Let’s rename some_number
to num_files
and then create a new struct IniFile
which contains a name and a count of IniSection
:
[...]
struct IniFile {
IniString file_name;
be u32 num_sections;
IniSection sections[num_sections];
};
struct CoalescedIniFolder {
be u32 num_files;
IniFile files[num_files];
};
This fails with an error, which is not good! Somewhere a section has declared more than 65536
values, which seems quite unlikely. To debug it we add some print statements by importing a couple of stuff:
#include "std/io.pat"
#include <std/string.pat>
[...]
struct IniSection {
IniString name;
be u32 num_values;
std::print("{}", name.val);
std::print("num {}", num_values);
IniValue values[num_values];
};
This runs for a while and fails again, but this time with some more information in the console:
I: Engine.AccessControl
I: num 1
I: Engine.GameReplicationInfo
I: num 3
I: ￿ﯿName
I: num 4294967289
Aha! Somewhere there’s a section called Engine.GameReplicationInfo
that has 3
values after which the parsing fails with a garbled string that should have been Name
. Let’s use the hex view to search for the section string as UTF-16. In my file the string is found at offset 0x005fc10
:
0005FC10 FF FF FF E5 45 00 6E 00 67 00 69 00 6E 00 65 00 ....E.n.g.i.n.e.
0005FC20 2E 00 47 00 61 00 6D 00 65 00 52 00 65 00 70 00 ..G.a.m.e.R.e.p.
0005FC30 6C 00 69 00 63 00 61 00 74 00 69 00 6F 00 6E 00 l.i.c.a.t.i.o.n.
0005FC40 49 00 6E 00 66 00 6F 00 00 00 00 00 00 03 FF FF I.n.f.o.........
0005FC50 FF F5 53 00 65 00 72 00 76 00 65 00 72 00 4E 00 ..S.e.r.v.e.r.N.
0005FC60 61 00 6D 00 65 00 00 00 FF FF FF F1 41 00 6E 00 a.m.e.......A.n.
0005FC70 6F 00 74 00 68 00 65 00 72 00 20 00 53 00 65 00 o.t.h.e.r. .S.e.
0005FC80 72 00 76 00 65 00 72 00 00 00 FF FF FF F6 53 00 r.v.e.r.......S.
0005FC90 68 00 6F 00 72 00 74 00 4E 00 61 00 6D 00 65 00 h.o.r.t.N.a.m.e.
0005FCA0 00 00 FF FF FF F9 53 00 65 00 72 00 76 00 65 00 ......S.e.r.v.e.
0005FCB0 72 00 00 00 FF FF FF F0 4D 00 65 00 73 00 73 00 r.......M.e.s.s.
0005FCC0 61 00 67 00 65 00 4F 00 66 00 54 00 68 00 65 00 a.g.e.O.f.T.h.e.
0005FCD0 44 00 61 00 79 00 00 00 00 00 00 00 FF FF FF F2 D.a.y...........
0005FCE0 44 00 65 00 66 00 61 00 75 00 6C 00 74 00 50 00 D.e.f.a.u.l.t.P.
0005FCF0 6C 00 61 00 79 00 65 00 72 00 00 00 00 00 00 02 l.a.y.e.r.......
Let’s decode the above by hand. The section title Engine.GameReplicationInfo
looks fine followed by the number 3. Then we have the INI value pair ServerName = Another Server
, then ShortName = Server
and MessageOfTheDay
followed by 4 zeroes. The struct is expecting a string length that is encoded as a bitwise inverted u32 and 0x00000000
is 0xFFFFFFFF
when inverted, which means it attempts to allocate 4 billion entries for the struct. This is totally out of range for an INI file which makes me conclude, that 0x00000000
denotes a missing value. This is probably why the developers chose to invert the bits of the length.
We fix this minor issue by adding an if statement to our IniString
struct:
[...]
struct IniString {
be u32 len [[transform("invert")]];
if (len != 0xFFFFFFFF) {
char16 val[];
}
};
And heureka! The entire thing decodes in the pattern data view:
If we take all the snippets from above and print it out, we get the following pattern editor script:
#include "std/io.pat"
#include <std/string.pat>
fn invert(u32 input) {
return u32(~input);
};
struct IniString {
be u32 len [[transform("invert")]];
if (len != 0xFFFFFFFF) {
char16 val[];
}
};
struct IniValue {
IniString key;
IniString value;
};
struct IniSection {
IniString name;
be u32 num_values;
IniValue values[num_values];
};
struct IniFile {
IniString file_name;
be u32 num_sections;
IniSection sections[num_sections];
};
struct CoalescedIniFolder {
be u32 num_files;
IniFile files[num_files];
};
CoalescedIniFolder folder @ 0x00;
The 39 lines above decode the entire file and really show how powerful a well-written DSL can be. It is much, much quicker to prototype the structure of a binary file using something like this than raw-dogging it with C structs like a savage. The struct definitions, while not entirely portable to “real” languages, are still a very good foundation for the future implementation of a tool that can read and write these files.
Conclusion
The format itself seems weird though, why would you coalesce text files into a binary blob?
Remember that Gears of War 2 came out on Xbox 360 where some of the early models had no harddrive, which means it had to be runnable entirely from DVD. The 63 files are very small and are barely compressed when you do the coalescing, so it is likely a load time/DVD data layout optimization. On a DVD the laser has to physically move to change tracks and if the 63 files were laid out badly you would have a worst case seek time of 65ms per file, which would make the reading of the settings take around 4 seconds. That’s without considering scratches.
This was a very fun reverse engineering exercise and working with ImHex patterns is really awesome.
Thanks for reading and tune in for more Gears of War 2 hacking.
/J