GTFO VR Postmortem | Rendered Obsolete<br>Post<br>Cancel<br>GTFO VR Postmortem<br>Contents GTFO VR Postmortem
Intro<br>Over half a decade, 500 commits, and the better part of a thousand hours ago, I spent a boring afternoon wondering what GTFO would look like in VR. Then I made the mistake of trying to find out.<br>I’ve kept an eye on GTFO ever since its gameplay trailer at the Game Awards in 2017. It ticked all the right boxes for me: Survival horror, co-op, shooter, extreme difficulty (allegedly.)<br>I managed to join every playtest and could not get enough of the game each time. GTFO’s setting; the ‘Complex’ is a marvel to look at and the monsters inhabiting it were unique and interesting. The game was (and still is) extremely difficult and extremely fun, just as promised.<br>First Foray Into Modding<br>I started tinkering with GTFO even before the first alpha test was finished. I quickly found out that decompiling and modifying Unity games is quite trivial. Using DnSpy/DotPeek, you can easily peer into basically-source-code of the game, because Mono (Unity’s default scripting backend) compiles to .NET bytecode, which retains almost everything needed to reconstruct the original source.<br>For example, this is what we can retrieve from a Unity project I quickly cobbled together:<br>Decompiled code
10<br>11<br>12<br>13<br>14<br>15<br>16<br>public class DeleteIfNear : MonoBehaviour<br>[SerializeField]<br>private GameObject Offender;<br>[SerializeField]<br>private float Distance = 50f;
private void Update()<br>if (!((Object) this.Offender != (Object) null) || (double) (this.transform.position - this.Offender.transform.position).sqrMagnitude >= (double) this.Distance * (double) this.Distance)<br>return;<br>Debug.Log((object) ("Deleting offender " + this.Offender.name));<br>Object.Destroy((Object) this.Offender);<br>this.Offender = (GameObject) null;
From the original source:<br>Source code
10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>///<br>/// Deletes an object if it gets close enough<br>///<br>public class DeleteIfNear : MonoBehaviour<br>[SerializeField]<br>private GameObject Offender;
[SerializeField]<br>private float Distance = 50.0f;
void Update()<br>// If distance to target is less than Distance, we delete the offender<br>if (Offender != null)<br>Vector3 toOffender = transform.position - Offender.transform.position;<br>if (toOffender.sqrMagnitude Distance * Distance)<br>Debug.Log($"Deleting offender {Offender.name}");<br>Destroy(Offender);<br>Offender = null;
Comments are stripped out, casts are explicit and the code is ‘optimized’ in general. All in all, it’s still perfectly readable. It’s not easy to navigate a large codebase like GTFO’s in the first place, but this definitely beats staring at raw disassembly.<br>At first, I only used DnSpy to modify assemblies in-place. Quite quickly, I was able to create my first (unreleased) mod for an alpha version of the game — a more ‘efficient’ solution to dealing with the game’s (at the time) most dangerous enemy, the scout, featuring custom code and ‘borrowed’ assets.<br>We blow up a scout with the BFG9000<br>The game’s full release soon followed and yet again, after getting through all the content, I still couldn’t get enough of it. With the next update nowhere in sight, that fateful, boring afternoon arrived.<br>Armed with knowledge of Unity and VR dev from my day job, beginner-level modding knowledge and some experience with SteamVR, I started cobbling together something that would allow you to admire GTFO’s bestiary up close and personal.<br>And up close and personal it was. Within VR, enemies that looked somewhat big on-screen were suddenly towering over you. Gruesome details became far more apparent and getting disoriented in the Complex was never this easy. It was everything I had hoped for.<br>Over the next few weeks I added fixes that allowed me to play even full matches in VR.<br>The community expressed interest in a public release. Meanwhile, DnSpy was giving me issues. Turns out modifying assemblies in-place repeatedly is not that stable. Keeping track of my modifications also started to become cumbersome. After one too many random crashes and the inability to legally distribute the mod as altered game assemblies, I needed a solution.<br>Harmony and BepInEx to the rescue<br>The solution was runtime patching. Harmony is a library that allows one to prepend, append (or even replace) code in existing methods at runtime. Much like the name suggests, because of prepending/appending code being the main paradigm, most mods can coexist by default. BepInEx is a mod manager for Unity games built around loading Harmony-based mods.
10<br>11<br>12<br>13<br>14<br>15<br>///<br>/// Add event calls for the player shooting weapons<br>///
[HarmonyPatch(typeof(Weapon), nameof(Weapon.ApplyRecoil))]<br>internal class InjectPlayerWeaponFireEvents<br>private static void Postfix(Weapon __instance)<br>if(__instance.Owner.IsLocallyOwned)<br>PlayerFireWeaponEvents.WeaponFired(__instance);
When the game starts, this code will be added to the end of the Weapon.ApplyRecoil method in GTFO’s code.<br>The main idea is that distributing modified game...