mehmedbasic.dk

Changing Gears of War 2 graphical settings, part 1

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.

The 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:

Our first pattern.

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:

The first string decoded.

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:

Some more strings.

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:

INI values decoded.

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[];
};
IniSection struct.

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:

IniSection struct.

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:

Final decode.

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

© 2025 Jesenko Mehmedbasic - this is a footer.