Modding Games and Freezing Fish
In October I played Outer Wilds, a game about being an astronaut and a space archaeologist in a weird solar system. Their website describes:
Outer wilds is an exploration game about curiosity, roasting marshmallows, and unraveling the mysteries of the cosmos.
It very quickly became one of my favorite games that I’ve ever played. But unfortunately, even though it’s almost entirely a pure joy to play, there’s one single part that I hate.
It’s the anglerfish. These guys.
In the Dark Bramble, these anglerfish are constantly looming out in the fog, making everything terrifying. They’re great for the spooky atmosphere, but I found them absolutely miserable mechanically. At one point you need to sneak past them in an enclosed space, not making any noise with your engines. I was never able to manage it. This was almost the last thing I needed to do in the entire game, and I was so stuck I just put the game down for about two weeks in frustration.
But finally, I came back with a solution.
¶ Hacking the Game
I decided that I could probably mod the game. I knew it was made in Unity, which means it had a .NET core, so I set out to figure out how you mod a Unity game or a .NET executable. My initial searching found ILSpy, which is an absolutely excellent tool for disassembling and viewing .NET code, but it didn’t let you modify it. Nonetheless, I used it to look around in some Unity games to make sure I could understand them. What I ended up actually using was dnSpy, which in addition to letting you disassemble and view code, also acts as a debugger and an editor.
Armed with this tool, I set out to hack apart the game.
To do this, make sure you make a copy of the original Assembly-CSharp.dll
so you can revert to that if you want to. You’ll also want to make a backup of your save files, just in case something goes save-corruptingly wrong. Once you’re all set up with backup files, you can open the assembly in dnSpy.
When we open up the assembly, it gets added to the assembly listing. Inside it is a single DLL, and expanding that shows a list of namespaces. There are some various editor related namespaces that have some classes, but the vast majority are in the top-level namespace, labeled with empty braces and no name. If we look inside this namespace, near the top is the AnglerfishController
class, which seems like it’s probably exactly what we want.
Once we’ve located it, we should look around inside the class. Because I know some Unity, I expect that the Awake
and Start
methods are likely to have interesting initialization in them. And indeed, if we look at Awake()
, we see can see a _noiseSensor
field. Since the anglerfish chase you when you make noise, this seems like a useful thing to disable.
protected override void Awake() {
base.Awake();
this._anglerBody = this.GetRequiredComponent<OWRigidbody>();
this._impactSensor = this.GetRequiredComponent<ImpactSensor>();
this._noiseSensor = this.GetRequiredComponentInChildren<NoiseSensor>();
this._anglerfishFluidVolume = this.GetRequiredComponentInChildren<AnglerfishFluidVolume>();
this._currentState = AnglerfishController.AnglerState.Lurking;
this._turningInPlace = false;
this._stunTimer = 0f;
this._consumeStartTime = -1f;
this._consumeComplete = false;
}
If we go look for the other places that this field is used, we find that the enable and disable methods register a noise listener OnClosestAudibleNoise
. If we look at that method, it looks like it starts chasing the source of the noise, which is exactly what we don’t want.
private void OnEnable() {
this._impactSensor.OnImpact += this.OnImpact;
this._noiseSensor.OnClosestAudibleNoise += this.OnClosestAudibleNoise;
this._anglerfishFluidVolume.OnCaughtObject += this.OnCaughtObject;
}
private void OnDisable() {
this._impactSensor.OnImpact -= this.OnImpact;
this._noiseSensor.OnClosestAudibleNoise -= this.OnClosestAudibleNoise;
this._anglerfishFluidVolume.OnCaughtObject -= this.OnCaughtObject;
}
So let’s remove that registration entirely. We can right click on a method and select “Edit Method” to modify the code.
In here, let’s remove the registration and deregistration statements. I didn’t touch the _impactSensor
registration, so potentially if you run into an anglerfish it’ll still kill you. Just that alone was plenty tension to leave them terrifying monsters sitting in the fog. If you want to get rid of that difficulty as well, you could remove that registration, or even modify one of the update methods to delete the anglerfish itself.
private void OnEnable() {
this._impactSensor.OnImpact += this.OnImpact;
- this._noiseSensor.OnClosestAudibleNoise += this.OnClosestAudibleNoise;
this._anglerfishFluidVolume.OnCaughtObject += this.OnCaughtObject;
}
private void OnDisable() {
this._impactSensor.OnImpact -= this.OnImpact;
- this._noiseSensor.OnClosestAudibleNoise -= this.OnClosestAudibleNoise;
this._anglerfishFluidVolume.OnCaughtObject -= this.OnCaughtObject;
}
When I tried to click the compile button after making my modifications, it gave me some confusing error messages. Apparently some of the attributes that disassembly put on event
declarations aren’t valid to go there, or something?
Double-clicking on one of the errors leads to this attribute declaration:
I just deleted every attribute it was complaining about, and then it compiled cleanly. After you compile, you can save the assembly… and then try out the game!
¶ The Test
It worked perfectly for me. I could sneak right past anglerfish at full engine blast if I wanted to and they would never hear… and I was still terrified of accidentally bumping into them, so the atmosphere of the scene was preserved pretty well. With this little mod, I was able to beat the game (the end of Outer Wilds is absolutely fantastic).
I found it super empowering to be able to change a game so that it accommodated the way I wanted to play it. I hope it’s useful to some of you too.