Skip to content

Commit

Permalink
Patching Code: Mark old info, Change basic patch example & Add warnin…
Browse files Browse the repository at this point in the history
…g about cheats (#126)

* Correction in explaning instance argument

* Change simple patch example to not be a cheat

* Add warnings and notes to monomod examples
  • Loading branch information
Hamunii authored Aug 11, 2024
1 parent bcfb0ce commit 1628b1f
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 9 deletions.
27 changes: 20 additions & 7 deletions docs/dev/fundamentals/patching-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ private static void MyPatch(On.GameNetcodeStuff.PlayerControllerB.orig_Update or
```
What we just did is call the original method `orig` with the arguments it takes, which is just `self`. If it had more arguments, we would also pass them in the `orig` call, e.g. `orig(self, arg2, arg3);`. The reason why `self` is an argument is because the method we are patching is not static meaning it has an instance, which is what `self` is.

With `self`, we can access and manipulate the variables of the GameObject, which in this case would be an instance of `PlayerControllerB`. Now we have gone through the basics, and we will move on to examples.
With `self`, we can access and manipulate the variables of the class (script), which in this case would be an instance of `PlayerControllerB`. Now we have gone through the basics, and can move on to examples.

### Example Patch With MonoMod {#example-patch-monomod}
One of the easiest patches you can do is an infinite sprint patch by setting sprint meter to full every frame. Here we have hooked `PlayerControllerB`'s `Update` method which runs every frame. In the Hook, we run the original method, and then set `sprintMeter` to `1`. In this case it doesn't really matter if our code runs before or after the original method, because this is such a simple patch.
A simple patch we can do is killing the player if they get exhausted (run out of stamina). Here we have hooked `PlayerControllerB`'s `Update` method which runs every frame. In the Hook, we run the original method, and then get the value of `isExhausted` and check if it's true in an if statement. If it's true, we call `KillPlayer` on the `PlayerControllerB` instance.
```cs
// Somewhere in our code we subscribe to the event once:
On.GameNetcodeStuff.PlayerControllerB.Update += PlayerControllerB_Update;
Expand All @@ -99,10 +99,21 @@ private static void PlayerControllerB_Update(On.GameNetcodeStuff.PlayerControlle
orig(self); // Call the original method with its arguments
// Code here runs after the original method
// Set the sprintMeter variable of this instance of PlayerControllerB to 1
self.sprintMeter = 1;
// isExhausted is a boolean field of PlayerControllerB which
// is set to true after running out of stamina.
if (self.isExhausted)
{
// KillPlayer is a method of PlayerControllerB which
// kills the player instance.
// This method takes in multiple arguments, but the only
// required argument is the velocity as a Vector3 for the
// spawned dead body object, which we'll specify as zero.
self.KillPlayer(Vector3.zero);
}
}
```
Notice that patches will always apply for every instance of a class, so this code runs for every other player instance in your game. However, this patch will not kill other players running out of stamina even if you have this patch applied, since `KillPlayer` makes sure that it only runs if a client calls it on themselves.

::: tip
See [Patching Code With MonoMod — Examples](./patching-code/monomod-examples.md) for more examples and information about MonoMod usage, and see our unofficial [MonoMod Documentation](./patching-code/monomod-documentation.md) for more in-depth details on how things work!
:::
Expand All @@ -117,7 +128,7 @@ With Harmony, you need to follow certain rules to write your patches. You must s
For more information about Harmony, see the [Harmony](https://harmony.pardeike.net/articles/intro.html) or [HarmonyX](https://github.com/BepInEx/HarmonyX/wiki/Basic-usage) documentation.

### Example Patch With Harmony {#example-patch-harmony}
We will now do the same patch for infinite sprint as we did with MonoMod:
We will now do the same patch for dying from exhaustion as we did with MonoMod:

```cs
using HarmonyLib;
Expand All @@ -132,8 +143,10 @@ class MyPatches
[HarmonyPostfix]
private static void PlayerControllerB_Update(PlayerControllerB __instance)
{
// Set the sprintMeter variable of this instance of PlayerControllerB to 1
__instance.sprintMeter = 1;
if (__instance.isExhausted)
{
__instance.KillPlayer(Vector3.zero);
}
}
}
```
Expand Down
19 changes: 17 additions & 2 deletions docs/dev/fundamentals/patching-code/monomod-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ See our unofficial [Legacy MonoMod Documentation](./monomod-documentation.md) fo
::: info
This is the same example patch as shown in [Patching Code — Example Patch With MonoMod](../patching-code.md#example-patch-monomod).
:::
One of the easiest patches you can do is an infinite sprint patch by setting sprint meter to full every frame. Here we have hooked `PlayerControllerB`'s `Update` method which runs every frame. In the Hook, we run the original method, and then set `sprintMeter` to `1`. In this case it doesn't really matter if our code runs before or after the original method, because this is such a simple patch.
A simple patch we can do is killing the player if they get exhausted (run out of stamina). Here we have hooked `PlayerControllerB`'s `Update` method which runs every frame. In the Hook, we run the original method, and then get the value of `isExhausted` and check if it's true in an if statement. If it's true, we call `KillPlayer` on the `PlayerControllerB` instance.
```cs
// Somewhere in our code we subscribe to the event once:
On.GameNetcodeStuff.PlayerControllerB.Update += PlayerControllerB_Update;
// ...
private static void PlayerControllerB_Update(On.GameNetcodeStuff.PlayerControllerB.orig_Update orig, GameNetcodeStuff.PlayerControllerB self)
{
orig(self);
self.sprintMeter = 1;

if (self.isExhausted)
self.KillPlayer(Vector3.zero);
}
```

Expand Down Expand Up @@ -121,6 +123,13 @@ See our unofficial [MonoMod Documentation](./monomod-documentation.md) for more
Let's say we want to make the following modification to the jumping behavior in the game:
*When running, the character should perform much bigger jumps.*

::: danger WARNING
On Thunderstore's Lethal Company Community, even a new game mechanic like this **will be considered as cheating and is not allowed on the site** if it provides an unfair advantage on other players *and* your mod can run for non-host clients without the host being able to opt-out of this feature.

In a case like this, you could register a new dummy network object which will automatically prevent players without that network object from joining your lobby, by showing an error message instead.

:::

To do this, we must know that the variable `jumpForce` of `PlayerControllerB` affects how strong jumps are, so let's try making a normal Hook to change it depending on whether or not we are sprinting:

```cs
Expand All @@ -142,6 +151,9 @@ This is because `jumpForce` is used when calculating fall speed, and changing it
There are many ways to work around this, and one way to make sure it is only changed when jumping is to use an ILHook. We will move the above logic to only run when the `Jump_performed` method is run and the player is allowed to jump.

The decompiled `Jump_performed` method will look something like this:
::: info
The latest versions have new method calls in this method that we could hook to, and *even an event that can be subscribed to when the player jumps,* but for the sake of this example we'll ignore those and show the older decompiled version of the method which doesn't have those.
:::
```cs
private void Jump_performed(InputAction.CallbackContext context)
{
Expand Down Expand Up @@ -244,6 +256,9 @@ The hard part about ILHooking is that it is very easy to emit invalid IL code, w

### Replacing a Method Call

::: warning
This patch example is for an older version of the game, and isn't valid for the latest versions.
:::
::: info
The following ILHook example is taken from the [JetpackFallFix](https://thunderstore.io/c/lethal-company/p/Hamunii/JetpackFallFix/) mod.
:::
Expand Down

0 comments on commit 1628b1f

Please sign in to comment.