Tristan Hume

Github Resume + Project List Blog

Writing a Beat Saber Patcher for the Oculus Quest

26 July 2019

After trying out VR and Beat Saber at Ctrl-V and really enoying it, I decided to pre-order an Oculus Quest, the first standalone VR headset with 6 DOF head and hand tracking. As expected I really enjoyed playing Beat Saber and practicing to play more difficult songs, but I also ended up getting wrapped up in the Beat Saber modding community and developing a patcher for adding custom songs which has been downloaded 80,000 times. I figured out how to read and modify the Unity asset file format used by Beat Saber, learned C#, and wrote a patcher that could read in the game’s assets, modify them to add custom songs, and modify the APK in-place with the replaced asset files.

Early Discoveries

I started off by joining the Beat Saber Modding Group Discord chat while my Quest was still shipping, and chatting with the other modders who were eager to figure out how to add custom songs to Beat Saber on the Quest like they have with the PC version. I didn’t have anything to poke at myself yet but I could still chime in with ideas, and I created a Google Doc where I collated and documented other people’s discoveries, which I encouraged other people to edit and write things in as they experimented.

Over a couple days we figured out that Beat Saber was compiled with IL2CPP so modding the C# code would be tricky, but while the levels were stored in a different format than the PC game, they were stored in the Unity asset bundles present in the APK. Some people who had Unity modding tools installed that could read and modify Unity asset files looked at the assets and found the levels but the beat maps looked like indecipherable compressed or encrypted data, then through some digging in disassembly and deduction emulamer figured out that it was the same data types the PC version used for levels, just encoded with C#’s BinaryFormatter and then run through a DeflateStream.

With this information emulamer could convert PC beatmaps (maps of the patterns of blocks to slash along with the song) to the format that went inside the Unity asset. This could then be patched into an APK using a Unity modding tool like DevX. However, we had noticed that levels contained a signature field so we suspected this wouldn’t work on its own and it didn’t, but it turned out the demo version didn’t check the signature and this lead to the first successful test. We still needed to patch the signature check in the full version though, so I tried poking around in Binary Ninja but couldn’t get anywhere because the library with all the code didn’t have symbols for the function names in it. However people on Windows were able to use tools like Il2CppDumper to get symbols, and emulamer found the signature check and figured out an ARM machine code patch to replace the call to the verify signature function with a constant true. Elliot Tate used DevX and this knowledge to perform the first successful patch of the full game.

Figuring Out the Format

So we knew how to patch in custom songs, the problem was we only knew how to do it with closed-source Windows GUI applications like DevX, which wasn’t going to help us deliver custom songs to lots of people, and as a macOS user it wasn’t going to help me. We needed to figure out how to patch Unity assets ourselves. So I did some Googling and while I didn’t find any open source code that could modify Unity assets, I did find code that could read them. Now I just needed to extract an understanding of the asset file format from that code so I could write my own code that could read and modify.

I had heard about Kaita Struct which lets you write descriptions of a binary format and it will parse it into a tree in a nice IDE for you, but when I tried it I found the IDE really slow, cumbersome and kinda broken. The format was also very declarative and verbose and I found it hard to use. So I considered buying Synalyze It, which is a native macOS hex editor with similar capabilities, but its specification system seemed just as limited. Then (for some reason I forget) I realized that my version of Hex Fiend was many years old, and went looking for a newer version, which I found on their Github. In the changelog I saw that they had very recently added support for Binary Templates, which used Tcl (a fully featured programming language!), this was exactly what I was looking for!

So I gradually put together a Hex Fiend template for Unity assets by figuring out open source asset loading code and adding more fields, debugging by reloading the template in Hex Fiend and checking the parse in the tree view to see that the values made sense. Eventually I figured out all the parts of the file necessary to mod in custom levels. The Hex Fiend template was invaluable for making it really easy to write a quick parser and debug my understanding against the real files. It was also valuable later on when I could look at the output of my patcher in a pretty tree view.

I also needed to figure out how the audio files referenced by the audio assets were included. I was originally worried it would be complex because the built-in songs were packed into concatenated resources in the proprietary FSB5 format that I thought I might need to reverse-engineer. However upon further testing it turned out we could just drop .ogg files in the APK and reference them as offset 0 in a resource pack, and Unity could load them.

Writing a Patcher

Now I needed to write a patcher program that could take a Beat Saber APK and some custom songs in the PC JSON format, and produce an APK with the custom songs. I decided to use C# even though I had never used it before, because then I wouldn’t need to reverse-engineer the BinaryFormatter format used for the beatmap conversion, and it’s a nice enough language that works cross-platform. I also decided to structure my patcher as a library so it could be theoretically used in multiple different front ends, possibly including a C#-based GUI, as well as a command line tool and unit tests.

I started by writing a parser for the asset file format that parsed it into C# classes, but classes with a structure carefully designed so that they preserved all the information necessary to recreate the file exactly, while being straightforward to modify. This involved combining the separate directory and contents of the file format into a unified list of assets with no offsets, and ensuring that I saved amounts of padding in fields in relevant objects. Then I wrote the functions to write those classes back out to an assets file. I used a unit test to check that I could parse and then write out an assets file to a byte-identical one with no errors, starting with a small file and fixing bugs until I could round-trip the main assets file with all the levels.

After that worked I implemented loading the JSON level files and modifying my assets file data structure to insert the new levels into the existing “Extras” level pack. I also needed to copy the audio files into the APK and patch the binary to disable the level signature check. I needed to modify the APK file, but I didn’t want to use the normal method of unzipping it (APKs are just zip files) into a temporary location and then zipping it back up, so I used the standard library ZipArchive functionality which allowed me to modify the Zip file in-place.

After I got all this working and tested I just needed to write a small command line tool using the library I had written. This allowed me to patch my own Beat Saber for the first time and play my first custom level on my own Oculus Quest!

The Competition

All this time, emulamer had also been working on his own patcher with a similar approach to mine. He was making faster progress than me, and patched in his first songs somewhat before I did, and by the time I patched in my first songs his patcher had already been packaged into a Windows GUI by someone else and people were using it. It also supported things mine didn’t yet like cover art and a separate “Custom Levels” pack instead of just putting things in the existing “Extras” pack.

I persisted though because I was having fun and my patcher had some differentiating factors that I imagined could make it competitive with more work:

So I set out to add more functionality to my patcher to compete!

Catching Up

The first thing I did is add support for song cover art. I knew that emulamer’s cover art gave the game frame rate issues, which he assumed was because he didn’t resize textures and use texture compression. But when I looked at how the covers from the base game were stored, I noticed they didn’t use any compression but they did use mipmaps, and lack of mipmaps definitely seemed like it could explain the lag. Looking at the cover data in my hex editor I noticed a repeating pattern that got higher-frequency further into the cover, so I guessed that it was probably raw RGB data for all the mip levels concatenated together. I checked what my guess would predict the file size would be against the actual file size and it matched exactly. So I added support for covers with concatenated mipmaps of the size from the base game using ImageSharp. I let emulamer know about the mipmapping so he could reference the code and fix his frame rate issues.

Then I noticed a developer of SideQuest (a popular Electron GUI for side-loading apps onto the Oculus Quest) mentioning in the Discord that he was working on adding Beat Saber custom song support to SideQuest. I used the power of my patcher being a library to throw together a separate command line binary that used a JSON interface over stdin/stdout to provide an easy programmatic interface with more control. Then I included it in the cross-platform CI builds that raftario contributed, which used DotNet core’s ability to build a self-contained folder that includes the C# runtime and compiled program IL. I chatted with the SideQuest developer and pitched him on using my patcher because of the convenient cross-platform binaries with a uniform easy interface that could patch in-place.

The last major remaining obstacle to an easy cross-platform patcher was that after patching the APK needed to be signed using a Java-based JAR signer, requiring users to have 64-bit Java installed. Emulamer and I chatted about this and decided it seemed feasible to write a signer in C#, which he managed to do fairly quickly and let me use his code, and in turn I figured out how to speed up the signing by a lot and let him know how to improve the speed in his own patcher.

Soon my patcher was incorporated in a SideQuest release that people could use to (somewhat) easily patch custom songs into their Beat Saber!

Finishing Touches

At this point sc2ad had started working on my codebase and adding support for custom saber colors, removing songs, and custom packs. His code was still experimental, but I worked with him and did a lot of refactoring myself to integrate his code with how I wanted my patcher to work and eventually merged all of his work into master. As part of this process I wrote a new JSON-based command line with a new interface that allowed creating unlimited custom packs and organizing and ordering songs within them. The new code would then take an APK and synchronize the state with the songs you requested: adding, removing and rearranging the minimal amount necessary to update the APK quickly.

I let the SideQuest developer I talked to know these capabilities were coming and the SideQuest team developed an awesome interface for organizing your songs into custom playlists and synchronizing them. Soon the new version of my patcher was integrated into SideQuest and released to the world.

I had been scaling down the amount of time I spent working on Beat Saber patching, but as Beat Saber released updated versions I continued to update my patcher to be compatible with the new versions and improve its reliability. One of the Beat Saber updates removed the BinaryFormatter based beatmaps and switched to just JSON strings in the same format as the PC version with no signatures, which means eventually there was no reason my patcher needed to be in C# instead of my preferred Rust but I had already written thousands of lines of code so there was no point in switching.

Eventually things were working smoothly enough and I announced my intent to retire from Beat Saber patching and work on other things. SideQuest continued to be used by tons of people, with my patcher (downloaded automatically when people tried to use the SideQuest Beat Saber functionality) racking up 80,000 downloads.

The Next Chapter

Emulamer hadn’t stopped working on Beat Saber patching though, after I retired he continued plugging away and eventually released BeatOn. BeatOn is an on-device patcher than uses a C hook injection system by jakibaki to redirect asset loading to mutable Android /sdcard/ storage. This means that you can load new songs on your Quest and it doesn’t have to re-sign and re-install the APK so its faster. It also supports installing hook and asset mods for things like custom sabers and better swing score feedback. SideQuest also recently added support for installing BeatOn, accessing its UI from your computer, and copying your SideQuest song library to BeatOn. It’s basically completely replaced my patcher and is better in many ways, I’m glad for the progress and now use it myself.

Conclusion

My Beat Saber patching journey is now over but it was a fun one. I learned a bunch from figuring out how to mod a game in practice, as well as some C# programming. I also had fun collaborating with everyone on the BSMG Discord and figuring out how Beat Saber worked with them. Competing with emulamer was also a fun experience since I think we both benefitted from trying to implement cool things the other hadn’t and then letting each other take the ideas or code so that both of our patchers could improve, it was a very fun friendly casual competition. I think it was a good use of some of my summer, I had fun doing it, I’ve been playing Beat Saber nearly every day enjoying my custom songs and now can comfortably play at expert+ level, and many people have presumably also had fun with their custom levels through SideQuest and my patcher.

Vote on HN