]> Witch of Git - web/blog/blob - posts/2019/modding-games-and-freezing-fish.md
Add eleventyImport annotations to fix build order
[web/blog] / posts / 2019 / modding-games-and-freezing-fish.md
1 ---
2 title: "Modding Games and Freezing Fish"
3 date: 2019-11-06
4 tags:
5 - csharp
6 - reverse engineering
7 - games
8 summary: "I learn how to write my own mod for a C# game so that I can skip a part I was stuck on. You can do it to! (This post contains minor spoilers for the Dark Bramble in Outer Wilds)"
9 ---
10
11 {% aside "warning" %}
12 This article will contain minor spoilers for the Dark Bramble in *Outer Wilds*.
13 {% endaside %}
14
15 In October I played [*Outer Wilds*][outer wilds], a game about being an astronaut and a space archaeologist in a weird solar system.
16 Their website describes:
17
18 > Outer wilds is an exploration game about curiosity, roasting marshmallows, and unraveling the mysteries of the cosmos.
19
20 It very quickly became one of my favorite games that I've ever played.
21 But unfortunately, even though it's almost entirely a pure joy to play, there's one *single* part that I hate.
22
23 [outer wilds]: http://outerwilds.com/
24
25 It's the anglerfish.
26 These guys.
27
28 {% figure src="/img/2019-anglerfish.png", alt="An anglerfish floating in fog" %}
29 Image from [the Outer Wilds Wiki][], licensed as [CC BY-SA 3.0][].
30
31 [the outer wilds wiki]: https://outerwilds.gamepedia.com/File:Angler_crop.png
32 [cc by-sa 3.0]: https://creativecommons.org/licenses/by-sa/3.0/
33 {% endfigure %}
34
35 In the Dark Bramble, these anglerfish are constantly looming out in the fog, making everything terrifying.
36 They're *great* for the spooky atmosphere, but I found them absolutely miserable mechanically.
37 At one point you need to sneak past them in an enclosed space, not making any noise with your engines.
38 I was never able to manage it.
39 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.
40
41 But finally, I came back with a solution.
42
43 ### Hacking the Game
44
45 I decided that I could probably mod the game.
46 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.
47 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.
48 Nonetheless, I used it to look around in some Unity games to make sure I could understand them.
49 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.
50
51 [ilspy]: https://github.com/icsharpcode/ILSpy
52 [dnspy]: https://github.com/0xd4d/dnSpy
53
54 Armed with this tool, I set out to hack apart the game.
55
56 {% figure src="/img/2019-outer-wilds-mod-01.png", alt="A Windows file explorer dialogue." %}
57 To disassemble a game, you have to find where the code is.
58 For Unity games, it's the `Assembly-CSharp.dll`, generally stored in a folder next to the game executable.
59 {% endfigure %}
60
61 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.
62 You'll also want to make a backup of your save files, just in case something goes save-corruptingly wrong.
63 Once you're all set up with backup files, you can open the assembly in dnSpy.
64
65 {% figure src="/img/2019-outer-wilds-mod-02.png", alt="Using the file > open dialogue in dnSpy." %}
66 {% endfigure %}
67
68 When we open up the assembly, it gets added to the assembly listing.
69 Inside it is a single DLL, and expanding that shows a list of namespaces.
70 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.
71 If we look inside this namespace, near the top is the `AnglerfishController` class, which seems like it's probably exactly what we want.
72
73 {% figure src="/img/2019-outer-wilds-mod-03.png", alt="dnSpy showing the assembly listing, with the AnglerfishController selected." %}
74 {% endfigure %}
75
76 Once we've located it, we should look around inside the class.
77 Because I know some Unity, I expect that the `Awake` and `Start` methods are likely to have interesting initialization in them.
78 And indeed, if we look at `Awake()`, we see can see a `_noiseSensor` field.
79 Since the anglerfish chase you when you make noise, this seems like a useful thing to disable.
80
81 ```csharp
82 protected override void Awake() {
83 base.Awake();
84 this._anglerBody = this.GetRequiredComponent<OWRigidbody>();
85 this._impactSensor = this.GetRequiredComponent<ImpactSensor>();
86 this._noiseSensor = this.GetRequiredComponentInChildren<NoiseSensor>();
87 this._anglerfishFluidVolume = this.GetRequiredComponentInChildren<AnglerfishFluidVolume>();
88 this._currentState = AnglerfishController.AnglerState.Lurking;
89 this._turningInPlace = false;
90 this._stunTimer = 0f;
91 this._consumeStartTime = -1f;
92 this._consumeComplete = false;
93 }
94 ```
95
96 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`.
97 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.
98
99 ```csharp
100 private void OnEnable() {
101 this._impactSensor.OnImpact += this.OnImpact;
102 this._noiseSensor.OnClosestAudibleNoise += this.OnClosestAudibleNoise;
103 this._anglerfishFluidVolume.OnCaughtObject += this.OnCaughtObject;
104 }
105
106 private void OnDisable() {
107 this._impactSensor.OnImpact -= this.OnImpact;
108 this._noiseSensor.OnClosestAudibleNoise -= this.OnClosestAudibleNoise;
109 this._anglerfishFluidVolume.OnCaughtObject -= this.OnCaughtObject;
110 }
111 ```
112
113 So let's remove that registration entirely.
114 We can right click on a method and select "Edit Method" to modify the code.
115
116 {% figure src="/img/2019-outer-wilds-mod-04.png", alt="Right clicking on a method to demonstrate the 'Edit Method' option in the context menu." %}
117 {% endfigure %}
118
119 In here, let's remove the registration and deregistration statements.
120 I didn't touch the `_impactSensor` registration, so potentially if you run into an anglerfish it'll still kill you.
121 Just that alone was plenty tension to leave them terrifying monsters sitting in the fog.
122 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.
123
124 ```diff
125 private void OnEnable() {
126 this._impactSensor.OnImpact += this.OnImpact;
127 - this._noiseSensor.OnClosestAudibleNoise += this.OnClosestAudibleNoise;
128 this._anglerfishFluidVolume.OnCaughtObject += this.OnCaughtObject;
129 }
130
131 private void OnDisable() {
132 this._impactSensor.OnImpact -= this.OnImpact;
133 - this._noiseSensor.OnClosestAudibleNoise -= this.OnClosestAudibleNoise;
134 this._anglerfishFluidVolume.OnCaughtObject -= this.OnCaughtObject;
135 }
136 ```
137
138 When I tried to click the compile button after making my modifications, it gave me some confusing error messages.
139 Apparently some of the attributes that disassembly put on `event` declarations aren't valid to go there, or something?
140
141 {% figure src="/img/2019-outer-wilds-mod-05.png", alt="A list of errors and warnings from the compile dialogue, saying that events can't be DebuggerBrowsable." %}
142 {% endfigure %}
143
144 Double-clicking on one of the errors leads to this attribute declaration:
145
146 {% figure src="/img/2019-outer-wilds-mod-06.png", alt="A System.Diagnostics.DebuggerBrowsable attribute on an event declaration." %}
147 {% endfigure %}
148
149 I just deleted every attribute it was complaining about, and then it compiled cleanly.
150 After you compile, you can save the assembly... and then try out the game!
151
152 ### The Test
153
154 It worked perfectly for me.
155 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.
156 With this little mod, I was able to beat the game (the end of *Outer Wilds* is absolutely fantastic).
157
158 I found it super empowering to be able to change a game so that it accommodated the way I wanted to play it.
159 I hope it's useful to some of you too.