FEZ is a puzzle indie game from 2012 featuring unique gameplay mechanics. In short, the player interacts with a 3D world through four 2D views. Because said views erase the depth dimension, the player character (Gomez) can jump between objects distant along the Z-axis.
A few days ago, I was happily going through the puzzles even when they got irredeemably hard; until something very unfortunate happened.
|A view of the exit door|
The above is an image of a puzzle room in FEZ where Gomez is standing next to the exit door. While solving the puzzle, I was expecting a secret door to be revealed since the world map suggested this. Right after stumbling upon the solution, a transitional animation started playing; I heard the sound of a stone door gliding into the ground.
|A view of the revealed secret door|
In my rush, I mistakenly went through the exit door before the animation finished playing. Going back in, the secret door was visible, yet unusable. It was as if my leaving the room prematurely caused the game to enter an invalid 'limbo' state. Like most people, I immediately restarted the game hoping it would correct itself — perhaps through built-in soundness checking of the savefile.
Alas, nothing happened. What's more, I would later learn that the game kept backups of savefiles before overwriting them. But it was too late by then, as I had triggered, though unintentionally, multiple save points. This meant the original valid savefile was lost, presumably forever.
Scouring the internet for similar incidents proved pointless. This was clearly a very rare thing to happen, thanks to my unexpected user input. Note that this happened when my game progress totaled 190%1; I had to recover my savefile because starting all over again was not an option.
First, I ran the Unix
file command on a copy of my savefile, hoping it would
be JSON or some other well-known format.
$ file SaveSlot0 SaveSlot0: data
Well, that's not very helpful. Running
hexdump on the savefile showed some
recognizable strings but no further leads. It was a custom binary format — the worst
$ hexdump -C Savefile0 00000000 cf 3a f0 47 61 28 d9 01 06 00 00 00 00 00 00 00 |.:.Ga(..........| 00000010 26 da fe 49 bd 23 d9 01 01 00 01 00 00 01 12 00 |&..I.#..........| 00000020 00 00 01 11 44 4f 54 5f 4c 4f 43 4b 45 44 5f 44 |....DOT_LOCKED_D| 00000030 4f 4f 52 5f 41 00 01 10 44 4f 54 5f 4e 55 54 5f |OOR_A...DOT_NUT_| 00000040 4e 5f 42 4f 4c 54 5f 41 01 01 0b 44 4f 54 5f 50 |N_BOLT_A...DOT_P| 00000050 49 56 4f 54 5f 41 01 01 11 44 4f 54 5f 54 49 4d |IVOT_A...DOT_TIM| 00000060 45 5f 53 57 49 54 43 48 5f 41 01 01 0f 44 4f 54 |E_SWITCH_A...DOT| 00000070 5f 54 4f 4d 42 53 54 4f 4e 45 5f 41 00 01 0c 44 |_TOMBSTONE_A...D| 00000080 4f 54 5f 54 52 45 41 53 55 52 45 00 01 0b 44 4f |OT_TREASURE...DO| 00000090 54 5f 56 41 4c 56 45 5f 41 01 01 13 44 4f 54 5f |T_VALVE_A...DOT_| ...
Decompilers are cool
To rectify the savefile, I would need to either reverse-engineer its format — which could be arbitrarily complex or decompile the source code of the game. The latter proved to be the more sensible option since I knew the game ran on .NET and was most probably written in C#.
I downloaded the notorious NSA reverse engineering tool Ghidra hoping to decompile portions of the code responsible for savefile operations.
|The Ghidra logo|
Despite having a very cool logo, I couldn't make the Ghidra decompiler work on macOS. Though it's supposed to work even without Rosetta 2 emulation. A few unsuccessful troubleshooting attempts later, I gave up on it because there were other options.
Enter dotPeek, a 'Free .NET Decompiler and Assembly Browser' by JetBrains which is exactly what I needed.
I started by searching the code for keywords such as 'Savefile' and, lo and
behold, there was a class called
SaveFileOperations with methods to read/write
objects of a
SaveData class. At this point, I needed to extract the closure of
Initially, I copied the class into a local C# console application project, then
resolved any unknown references by copying the needed classes. Some references
pointed outside the proper FEZ engine, such as Microsoft XNA's
for such cases, I sought to work with a mocked class with minimal
methods/properties, otherwise I'd end up pulling in an obscene amount of code.
Later, I found myself with a total of 15 classes — mostly relating to game objects, Cyclic Redundancy Checking of save data, and the code for reading and writing said data to disk.
I hear you asking: Can I see the source code?
First, I had to make sure I can replicate a savefile byte-to-byte:
var reader = new BinaryReader(File.OpenRead("SaveSlot0")); // When writing save data to disk, the game prepends the current time var magic = reader.ReadInt64(); var saveData = SaveFileOperations.Read(new CrcReader(reader)); var writer = new BinaryWriter(File.OpenWrite("SaveSlot0Prime")); SaveFileOperations.Write(new CrcWriter(writer), saveData);
This did not work. The game wouldn't even load
SaveSlot0Prime. The issue
seemed to stem from the fact that my generated savefile was only
20K in size
while the proper savefiles from the game were always
M-x ediff-buffers, I saw that the extra
20K was just null bytes. What the heck?
Digging a bit more into the code I found the
PCSaveDevice class responsible
for saving and loading files — note that
SaveFileOperations only interfaces
PCSaveDevice class seems to originate from the
package for XNA.
For some reason I cannot possibly fathom, the
PCSaveDevice class always writes
save data into a byte buffer of size
40960 and raises an exception if the save
data is larger. Oddly enough, the
EasyStorage version I
does not seem to do this3. The revised script now looked like this4:
var reader = new BinaryReader(File.OpenRead("SaveSlot0")); var magic = reader.ReadInt64(); var saveData = SaveFileOperations.Read(new CrcReader(reader)); var buffer = new byte; var output = new BinaryWriter(new MemoryStream(new byte)); output.Write(magic); SaveFileOperations.Write(new CrcWriter(output), saveData); var writer = new BinaryWriter(File.OpenWrite("SaveSlot0Prime")); writer.Write(buffer);
At this point,
SaveSlot0Prime were finally identical.
Restoring the level
Next, I had to address the elephant in the room: restoring my savefile to a
consistent state. An interesting starting point was the
World property of
SaveData; it's of type
Dictionary<string, LevelSaveData> and contains all of
FEZ's levels, including the one I wish to fix.
Looking around in the
LevelSaveData class, there was a
public bool FirstVisit property, which I hoped would cause the game to reset the level if I
set it to
// The level name was found using the `SaveData.Level` property, // which records the current level saveData.World["ZU_4_SIDE"].FirstVisit = true;
Sadly, this did not have the desired effect.
My next idea was to make a fresh savefile where the
ZU_4_SIDE level was
untouched and use that to replace my corrupted one. To my surprise, there was
ZU_4_SIDE level in the
World dictionary of a fresh savefile. I realized
that the game doesn't include save data for unsolved levels.
Now the solution was clear: simply remove the
ZU_4_SIDE level data from the savefile.
Et voilà, the world was saved.
|Gomez acquires the anti-cube of |
I deliberately avoid elaborating on why the progress percentage is over 100%. If your interest is peaked, I encourage you to play FEZ for yourself.
But hey, at least it wasn't encrypted!
It's important to note that the git history of the repository I linked only stretches back two commits.
The script I supplied mimics the code I found in
there is a more efficient way of setting file sizes in .NET?