diff --git a/docs/documentation/_category_.json b/docs/documentation/_category_.json index 5062e4e..751363c 100644 --- a/docs/documentation/_category_.json +++ b/docs/documentation/_category_.json @@ -1,4 +1,4 @@ { "label": "Documentation", - "position": 7 + "position": 6 } diff --git a/docs/tutorials/_category_.json b/docs/tutorials/_category_.json new file mode 100644 index 0000000..ae26fa5 --- /dev/null +++ b/docs/tutorials/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Tutorials", + "position": 5 +} diff --git a/docs/tutorials/patching-a-method.md b/docs/tutorials/patching-a-method.md new file mode 100644 index 0000000..6cccad1 --- /dev/null +++ b/docs/tutorials/patching-a-method.md @@ -0,0 +1,128 @@ +--- +sidebar_position: 1 +sidebar_label: Patching a method +title: Patching a method +--- + +### Objectives + - Learn how to find and cancel a base game method + - Learn the different patching possibilities + +### Prerequisites + - [dnSpyEx](/docs/getting-started/debugging) or any .NET assembly editor + +# Getting Started + +First of all you want to know what code you're willing to change. For that, you want to look into the game code thanks to any assembly editor like dnSpy. + +1. In my case, I found it interesting to patch the `OnTriggerEnter` method from the `SignalPing` class. +:::note +My final goal will be to send an event to the server when this method happens. But this part is omitted from this guide. +::: +2. To do so, I'll create a new class in the NitroxPatcher project, under `Patches/Dynamic`, which I'll call `SignalPing_OnTriggerEnter_Patch` + > NB 1: The naming convention is `__Patch`
+ > NB 2: Dynamic patches are applied once the player has loaded into the world. Persistent patches are applied on game launch. For most of the cases, you will want to make a dynamic patch. +3. You need to make your class implement `NitroxPatch` and `IDynamicPatch` and add the override for the Patch method + ```cs + using HarmonyLib; + + namespace NitroxPatcher.Patches.Dynamic; + + public sealed partial class SignalPing_OnTriggerEnter_Patch : NitroxPatch, IDynamicPatch + { + } + ``` +4. Now, you need to tell the class to actually patch the method. To do so, first create a reference to the method itself from the base game. + ```csharp + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SignalPing t) => t.OnTriggerEnter(default)); + ``` + > NB 1: It should always be private (or internal), static and be named TARGET_METHOD (by convention)
+ > NB 2: Import from System.Reflection and NitroxModel.Helper +5. Then, depending on your goal, you want to choose the [type of patch](https://harmony.pardeike.net/articles/patching.html) you want to apply. In my case, I just want to patch the Prefix of this method. + :::tip + You can find a list of them in the [Harmony documentation](https://harmony.pardeike.net/articles/patching.html), for basic understanding this is what happens when you call a function `foo()` + ```csharp + ... foo() + { + foo_Prefix(); + foo_actual(); + foo_Postfix(); + } + + ``` + :::note + Where `foo_Prefix()` is the prefix you (or someone else) may have set for this method and `foo_Postfix()` is the postfix that may have been set for this method. `foo_actual()` is evidently the method itself. + ::: +6. We create the `Prefix()` method just like this + ```csharp + public static bool Prefix(SignalPing __instance, Collider collider) + { + return true; + } + ``` + :::tip + Please look [at the list of injections you can take from the actual method in the Harmony documentation](https://harmony.pardeike.net/articles/patching-injections.html). + In my case, I'll be using the `__instance` one which refers to the instance of the `SignalPing` class in which this method was called. I'll also use the base method's parameter `Collider other` (which is optional) because I need it. + ::: +7. As I want to be able to cancel the method's execution (it depends on the situation), I need to make `Prefix()` return a bool. If I return true, it means the actual method will happen, but if I return false, the actual method won't happen. + + This is what the actual method looks like btw + ```csharp + public void OnTriggerEnter(Collider other) + { + if (this.disableOnEnter && other.gameObject.Equals(Player.main.gameObject)) + { + this.pingInstance.SetVisible(false); + } + } + ``` + My goal is to add some code inside the if condition to make it look like this + ```csharp + public void OnTriggerEnter(Collider other) + { + if (this.disableOnEnter && other.gameObject.Equals(Player.main.gameObject)) + { + // In the case the ping instance was still visible, we want to acknowledge it's "removal" + if (__instance.pingInstance.visible) + { + // DO SOMETHING + } + this.pingInstance.SetVisible(false); + } + } + ``` + :::note + Usually we would use a [Transpiler](https://harmony.pardeike.net/articles/patching-transpiler.html) for that, which lets you insert code at a precise place in the method. But it also is a pain to make, so I'll go with the Prefix to keep readability and because this method is kinda small :) + ::: +8. Because I need the exact same conditions as in the original method, I'll just return false at the end of the executed code (inside the if statement) so that it doesn't execute twice. + +### Final code +```csharp +using System.Reflection; +using HarmonyLib; +using NitroxModel.Helper; +using UnityEngine; + +namespace NitroxPatcher.Patches.Dynamic; + +public sealed partial class SignalPing_OnTriggerEnter_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SignalPing t) => t.OnTriggerEnter(default)); + + public static bool Prefix(SignalPing __instance, Collider other) + { + if (__instance.disableOnEnter && other.gameObject == Player.main.gameObject) + { + // In the case the ping instance was still visible, we want to acknowledge its "removal" + if (__instance.pingInstance.visible) + { + Log.Debug("A Signal Ping was removed, now do something"); + } + + // Original code: + __instance.pingInstance.SetVisible(false); + } + return false; + } +} +``` \ No newline at end of file diff --git a/docusaurus.config.js b/docusaurus.config.js index b5b24df..bd3de54 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -156,6 +156,7 @@ const config = { prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, + additionalLanguages: ['csharp'] }, algolia: { // The application ID provided by Algolia diff --git a/package.json b/package.json index 9acacd0..571ab0f 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,13 @@ "@docusaurus/preset-classic": "3.5.2", "@docusaurus/theme-mermaid": "^3.5.2", "@mdx-js/react": "^3.0.0", + "prismjs": "^1.29.0", + "@types/prismjs": "^1.26.5", "clsx": "^1.2.1", "prism-react-renderer": "^2.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "utility-types": "^3.11.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6688bfc..e0e9457 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,18 +20,27 @@ importers: '@mdx-js/react': specifier: ^3.0.0 version: 3.1.0(@types/react@18.3.12)(react@18.3.1) + '@types/prismjs': + specifier: ^1.26.5 + version: 1.26.5 clsx: specifier: ^1.2.1 version: 1.2.1 prism-react-renderer: specifier: ^2.1.0 version: 2.4.0(react@18.3.1) + prismjs: + specifier: ^1.29.0 + version: 1.29.0 react: specifier: ^18.2.0 version: 18.3.1 react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + utility-types: + specifier: ^3.11.0 + version: 3.11.0 devDependencies: '@docusaurus/module-type-aliases': specifier: ^3.5.2 @@ -4615,8 +4624,8 @@ packages: utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} - utility-types@3.10.0: - resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} engines: {node: '>= 4'} utils-merge@1.0.1: @@ -5894,7 +5903,7 @@ snapshots: srcset: 4.0.0 tslib: 2.6.2 unist-util-visit: 5.0.0 - utility-types: 3.10.0 + utility-types: 3.11.0 webpack: 5.88.2 transitivePeerDependencies: - '@mdx-js/react' @@ -5934,7 +5943,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 - utility-types: 3.10.0 + utility-types: 3.11.0 webpack: 5.88.2 transitivePeerDependencies: - '@mdx-js/react' @@ -6201,7 +6210,7 @@ snapshots: react-router-dom: 5.3.4(react@18.3.1) rtlcss: 4.3.0 tslib: 2.6.2 - utility-types: 3.10.0 + utility-types: 3.11.0 transitivePeerDependencies: - '@parcel/css' - '@swc/core' @@ -6237,7 +6246,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 - utility-types: 3.10.0 + utility-types: 3.11.0 transitivePeerDependencies: - '@docusaurus/types' - '@swc/core' @@ -6298,7 +6307,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 - utility-types: 3.10.0 + utility-types: 3.11.0 transitivePeerDependencies: - '@algolia/client-search' - '@docusaurus/types' @@ -6337,7 +6346,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - utility-types: 3.10.0 + utility-types: 3.11.0 webpack: 5.88.2 webpack-merge: 5.9.0 transitivePeerDependencies: @@ -6393,7 +6402,7 @@ snapshots: shelljs: 0.8.5 tslib: 2.6.2 url-loader: 4.1.1(file-loader@6.2.0(webpack@5.88.2))(webpack@5.88.2) - utility-types: 3.10.0 + utility-types: 3.11.0 webpack: 5.88.2 optionalDependencies: '@docusaurus/types': 3.5.2(acorn@8.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10797,7 +10806,7 @@ snapshots: utila@0.4.0: {} - utility-types@3.10.0: {} + utility-types@3.11.0: {} utils-merge@1.0.1: {} diff --git a/src/theme/prism-include-languages.ts b/src/theme/prism-include-languages.ts new file mode 100644 index 0000000..7863ff4 --- /dev/null +++ b/src/theme/prism-include-languages.ts @@ -0,0 +1,31 @@ +import siteConfig from '@generated/docusaurus.config'; +import type * as PrismNamespace from 'prismjs'; +import type {Optional} from 'utility-types'; + +export default function prismIncludeLanguages( + PrismObject: typeof PrismNamespace, +): void { + const { + themeConfig: {prism}, + } = siteConfig; + const {additionalLanguages} = prism as {additionalLanguages: string[]}; + + // Prism components work on the Prism instance on the window, while prism- + // react-renderer uses its own Prism instance. We temporarily mount the + // instance onto window, import components to enhance it, then remove it to + // avoid polluting global namespace. + // You can mutate PrismObject: registering plugins, deleting languages... As + // long as you don't re-assign it + globalThis.Prism = PrismObject; + + additionalLanguages.forEach((lang) => { + if (lang === 'php') { + // eslint-disable-next-line global-require + require('prismjs/components/prism-markup-templating'); + } + // eslint-disable-next-line global-require, import/no-dynamic-require + require(`prismjs/components/prism-${lang}`); + }); + + delete (globalThis as Optional).Prism; +}