Parse and generate Rocket League replays with Haskell

by Taylor Fausak on

I am happy to announce version 1.0.0 of Rattletrap, a Rocket League replay parser and generator! It is a command-line utility for converting replays to and from JSON. Binaries are available for every platform Rocket League supports, including Windows, macOS, and Linux.

This post is split into two parts: Usage covers how to use Rattletrap and what you can do with it; Development talks about how Rattletrap works and how it was made.

Usage

Parse

> rattletrap decode input.replay replay.json

That will read the raw replay from input.replay and write the JSON to replay.json. It will parse most replays in about 2 seconds. The JSON output is minified, so if you want to read it you should probably pretty-print it. But be warned, the output is large! A 1 MB replay turns into about 100 MB of pretty JSON.

Why might you want to parse Rocket League replays? Parsing can be useful for getting stats about a match. Anything you see on the scoreboard in-game is also in the replay. You can even analyze boost usage or watch the replay in your browser! I’m sure there are other neat things to be done with the replay data. I’d love to see what you come up with!

Generate

> rattletrap encode replay.json output.replay

That reads some JSON from replay.json and writes a replay to output.replay. Generating is a lot slower than parsing. It takes about 15 seconds to generate the average replay. If the input JSON came from rattletrap decode, then the output replay will be identical to the input. In other words, replays can be converted to and from JSON without losing any information.

Modify

> rattletrap decode original.replay original.json
> your-program-here original.json modified.json
> rattletrap encode modified.json modified.replay

Those three commands parse a replay into JSON, modify that JSON, then generate a replay from it. By using Rattletrap to handle the annoying replay file format, you can work with easy-to-use JSON.

Why would you want to modify a replay? It’s not quite as useful as parsing one, but you can still have some fun. For example, you could remove your wheels or wear unusual items. You can also force every car to look the same. I would like to see something that stitches replays together to make a highlight reel, or something that makes cinematic camera paths (“smooths”) for videos.

That’s pretty much all there is to know about using Rattletrap. I’m excited to see what you can do with it!

Development

Rattletrap is not the first Rocket League replay parser. There are at least a dozen of them. I borrowed heavily from the other parsers, particularly jjbott’s C# parser and Galile0’s Python one. I literally could not have done this without them. So thanks y’all!

Rattletrap is written in Haskell. Because it’s written in Haskell, Rattletrap is fast. Compared to the Python parser, Rattletrap is about 30 times faster. And compared to the C# parser, it’s about 6 times faster.

I was able to make Rattletrap so fast thanks to the great tools available in Haskell. In particular, Stack makes profiling as easy as stack build --profile. Haskell’s runtime system gives you a window into your program’s behavior with +RTS -p. That will give you a raw profiling file that you can turn into a chart with ghc-prof-flamegraph.

CPU flame graph

This graph shows that Rattletrap spends less than 10% of its time generating JSON. Also there don’t appear to be any obvious hot spots. You can learn more about flame graphs from FP Complete.

Profiling memory usage is just as easy as profiling CPU usage. After building with profiling enabled, run your program with +RTS -hc. That will generate a heap profile that you can visualize with hp2ps.

Memory usage graph

This graph shows the memory usage over time for parsing a 1 MB replay. It reaches a peak of about 30 MB and most of that is used by CompressedWord values. That makes sense because it’s the most common value in a replay. For example, this replay has about 600,000 CompressedWord values. The Pusher blog goes into more detail about memory profiling.

Rattletrap has never crashed at runtime, and I doubt it ever will. It does have some known failure modes, like if it finds some game data it hasn’t seen before. But aside from those it should never fail at runtime. I’m confident in it because Haskell is statically typed and its test suite has very high code coverage.

Code coverage report

This coverage report was generated with stack test --coverage. It shows that 79% of expressions and 42% of top-level definitions are covered by the test suite. Those numbers are a little misleading for two reasons: One, they consider derived instances like deriving Eq to not be covered; and two, the failure modes I mentioned above aren’t exercised in the test suite. Rattletrap’s effective code coverage is much higher. You can read more about code coverage from Real World Haskell.

Because Haskell is such a high-level language, Rattletrap is a pretty small project. It weighs in at about 3,000 lines of code. For comparison, the C# parser and generator is about 4,300 lines of code. The Python parser is only about 1,000 lines of code, but it is not able to generate replays.

If you’re wondering what real-world Haskell looks like, I encourage you to read the source. I avoided using any advanced language features or weird operators, so it should be approachable even if you don’t know Haskell. To give you a taste of what you’re in for, here is the code for parsing a player’s camera settings:

getCamSettingsAttribute = do
  fov         <- getFloat32Bits
  height      <- getFloat32Bits
  angle       <- getFloat32Bits
  distance    <- getFloat32Bits
  stiffness   <- getFloat32Bits
  swivelSpeed <- getFloat32Bits
  return (CamSettingsAttribute fov height angle distance stiffness swivelSpeed)

I have really enjoyed working on this project. It has allowed me to dive deep into different areas of Haskell. And in the end I wrote a tool that I use every day to make sense of my replays. I hope you like it!