diff --git a/AGENTS.md b/AGENTS.md
index 8cefb69..b6bc1ef 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,12 +3,14 @@
## Repo
- Modding workflow and references live in `docs/pc-modding.md` (Linux + Cursor + `dotnet` CLI; no VS/Rider BSMT extension).
+- `docs/pc-modding.md` §References (wiki paths, BSMT templates, BSIPA, SongCore, etc.) match local git checkouts under `~/src/…` on this machine (same layout as the guide’s `../../../src/…` links from here). Read those directories first; only fetch upstream (raw GitHub, bsmg.wiki) if a checkout is missing.
+- BSMG wiki (Modding section): Available on disk at `~/src/bsmg/wiki` (also opened via `bs-modding-tools.code-workspace`). Prefer that tree over web mirrors for static wiki content.
## Game install (BSManager)
-- **Path:** `/home/pleb/.local/share/BSManager/BSInstances/1.40.8`
-- **Version pin:** `1.40.8` (managed copy; launch modded build from BSManager, not Steam’s live folder).
-- **BSIPA:** Present (`IPA/`, `IPA.exe`, `winhttp.dll`, `Plugins/`).
+- Path: `/home/pleb/.local/share/BSManager/BSInstances/1.40.8`
+- Version pin: `1.40.8` (managed copy; launch modded build from BSManager, not Steam’s live folder).
+- BSIPA: Present (`IPA/`, `IPA.exe`, `winhttp.dll`, `Plugins/`).
## Plugins currently in `Plugins/`
@@ -16,11 +18,11 @@ BeatSaverDownloader, BeatSaverUpdater, BSML, BS_Utils, PlaylistManager, SiraUtil
## Host toolchain
-- **dotnet:** `9.0.312` (SDK 6+ is fine for `net472` plugin builds per guide).
-- **ilspycmd:** `9.1.0.0` (decompile/reference game or plugin assemblies from CLI).
-- **NuGet:** Installed (per user setup).
+- dotnet: `9.0.312` (SDK 6+ is fine for `net472` plugin builds per guide).
+- ilspycmd: `9.1.0.0` (decompile/reference game or plugin assemblies from CLI).
+- NuGet: Installed (per user setup).
## Conventions agents should respect
-- Plugin projects are **.NET Framework 4.7.2** class libraries loaded by BSIPA; builds are **CIL** — Linux `dotnet build` output is valid for the Proton game instance.
+- Plugin projects are .NET Framework 4.7.2 class libraries loaded by BSIPA; builds are CIL — Linux `dotnet build` output is valid for the Proton game instance.
- Point `BeatSaberDir` / game references at the BSManager instance path above when editing project user files or HintPaths.
diff --git a/Setlist/Directory.Build.props b/Setlist/Directory.Build.props
new file mode 100644
index 0000000..e53d942
--- /dev/null
+++ b/Setlist/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+ True
+ BSIPA
+
+
diff --git a/Setlist/Plugin.cs b/Setlist/Plugin.cs
new file mode 100644
index 0000000..2879303
--- /dev/null
+++ b/Setlist/Plugin.cs
@@ -0,0 +1,34 @@
+using IPA;
+using IPALogger = IPA.Logging.Logger;
+
+namespace Setlist
+{
+ [Plugin(RuntimeOptions.SingleStartInit)]
+ public class Plugin
+ {
+ internal static Plugin Instance { get; private set; }
+
+ ///
+ /// BSIPA logger (shows in BSIPA console / game logs when verbose).
+ ///
+ internal static IPALogger Log { get; private set; }
+
+ [Init]
+ public Plugin(IPALogger logger)
+ {
+ Instance = this;
+ Log = logger;
+ }
+
+ [OnStart]
+ public void OnApplicationStart()
+ {
+ Log.Info("Hello World");
+ }
+
+ [OnExit]
+ public void OnApplicationQuit()
+ {
+ }
+ }
+}
diff --git a/Setlist/Properties/AssemblyInfo.cs b/Setlist/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..e60cfb0
--- /dev/null
+++ b/Setlist/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("Setlist")]
+[assembly: AssemblyDescription("Playlist sync plugin for Beat Saber")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Setlist")]
+[assembly: AssemblyCopyright("")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: ComVisible(false)]
+[assembly: Guid("50F53E6E-21D5-4780-8E67-273877DAA28C")]
+[assembly: AssemblyVersion("0.0.1.0")]
+[assembly: AssemblyFileVersion("0.0.1.0")]
diff --git a/Setlist/Setlist.csproj b/Setlist/Setlist.csproj
new file mode 100644
index 0000000..d1bb5e8
--- /dev/null
+++ b/Setlist/Setlist.csproj
@@ -0,0 +1,123 @@
+
+
+
+ Debug
+ AnyCPU
+ 8.0.30703
+ 2.0
+ {18417954-9A66-445B-A3E1-F1E4C216E79D}
+ Library
+ Properties
+ Setlist
+ Setlist
+ v4.7.2
+ 512
+ true
+ portable
+ ..\Refs
+ $(LocalRefsDir)
+ $(MSBuildProjectDirectory)\
+ prompt
+ 4
+
+
+ false
+ bin\Debug\
+ DEBUG;TRACE
+
+
+ true
+ bin\Release\
+ prompt
+ 4
+
+
+ True
+
+
+ True
+ True
+
+
+
+
+
+
+
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll
+ False
+
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2.0.0-beta7
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ 1.0.3
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/Setlist/manifest.json b/Setlist/manifest.json
new file mode 100644
index 0000000..131ad9d
--- /dev/null
+++ b/Setlist/manifest.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json",
+ "id": "Setlist",
+ "name": "Setlist",
+ "author": "",
+ "version": "0.0.1",
+ "description": "Syncs playlists with external sources.",
+ "gameVersion": "1.40.8",
+ "dependsOn": {
+ "BSIPA": "^4.3.0"
+ }
+}
diff --git a/docs/bootstrap.md b/docs/bootstrap.md
new file mode 100644
index 0000000..1aedc58
--- /dev/null
+++ b/docs/bootstrap.md
@@ -0,0 +1,95 @@
+# Bootstrapping the Setlist BSIPA plugin
+
+This documents how the `Setlist/` project was created (aligned with `docs/pc-modding.md` §5). Official references: [BSMT templates repo](https://github.com/Zingabopp/UnityModdingTools.Templates.BeatSaber), [BeatSaberModdingTools.Tasks on NuGet](https://www.nuget.org/packages/BeatSaberModdingTools.Tasks), [BSIPA user install](https://nike4613.github.io/BeatSaber-IPA-Reloaded/articles/start-user.html).
+
+## 1. Choose template
+
+We used the **BSIPA Plugin (Bare)** template (smallest layout: `Plugin.cs`, `manifest.json`, `AssemblyInfo`, MSBuild wiring). The upstream folder is `BSIPA Plugin (Bare)/` in the templates repository.
+
+The guide’s relative paths (e.g. `~/src/Zingabopp/UnityModdingTools.Templates.BeatSaber`) assume a local clone; this machine had no clone, so files were taken from raw GitHub with `curl` (same content as a copy from a clone).
+
+## 2. Project layout
+
+Created under repo root:
+
+| File | Role |
+|------|------|
+| `Setlist.csproj` | `net472` library, game assembly references via `$(BeatSaberDir)`, `BeatSaberModdingTools.Tasks` package |
+| `Directory.Build.props` | `ImportBSMTTargets` + `BSMTProjectType` = BSIPA (from `Directory.Build.props.template`) |
+| `Setlist.csproj.user` | **Local only** (gitignored via `*.user`): `BeatSaberDir` → BSManager managed instance |
+| `manifest.json` | BSIPA metadata; embedded at build |
+| `Plugin.cs` | `[Plugin]` entry; `Log.Info("Hello World")` in `[OnStart]` |
+| `Properties/AssemblyInfo.cs` | Assembly identity / version |
+
+## 3. Placeholders
+
+Bare template uses `$safeprojectname$`, `$guid1$`, `$targetframeworkversion$`, etc. These were expanded by hand to:
+
+- `Setlist` (assembly / namespace / plugin id)
+- A new `ProjectGuid` and `[assembly: Guid(...)]`
+- `TargetFrameworkVersion` → `4.7.2`
+
+## 4. `manifest.json`
+
+- **id / name:** `Setlist`
+- **description:** Short line describing playlist sync intent
+- **gameVersion:** `1.40.8` — taken from the install’s `BeatSaberVersion.txt` (use the `major.minor.patch` prefix; the file may include a `_build` suffix)
+- **dependsOn.BSIPA:** `^4.3.0` (per guide; adjust if your BSIPA major differs)
+
+## 5. NuGet packages (`dotnet restore`)
+
+No separate `nuget install` was required; `dotnet restore` / `dotnet build` resolved everything from nuget.org.
+
+| Package | Purpose |
+|---------|---------|
+| `BeatSaberModdingTools.Tasks` | BSMT MSBuild targets: manifest embedding, copy to game, release zip, etc. Template used `2.0.0-beta1`; we pinned **`2.0.0-beta7`** (latest beta on NuGet at bootstrap time). |
+| `Microsoft.NETFramework.ReferenceAssemblies.net472` **`1.0.3`** | **Required on this Linux host** (Nix-provided .NET SDK): first build failed with **MSB3644** (missing .NET Framework 4.7.2 reference assemblies). Adding this package fixes that without a Windows targeting pack. The guide notes BSMT may pull reference assemblies transitively; that did not satisfy MSBuild here, so the package was added **explicitly** to `Setlist.csproj`. |
+
+## 6. `BeatSaberDir` / `GameDirectory`
+
+`Setlist.csproj.user` sets:
+
+```xml
+/home/pleb/.local/share/BSManager/BSInstances/1.40.8
+```
+
+BSMT resolves the game root as `GameDirectory` and expects `Beat Saber.exe` (or `Beat Saber`) there. Point at the **BSManager managed copy**, not the Steam tree, if that is what you mod.
+
+## 7. Build
+
+```bash
+cd Setlist
+dotnet restore
+dotnet build -c Debug
+```
+
+Expect:
+
+- `bin/Debug/Setlist.dll` (+ `.pdb`)
+- BSMT artifact layout under `bin/Debug/Artifact/Plugins/`
+
+## 8. Where the DLL lands (Linux caveat)
+
+`BeatSaberModdingTools.Tasks` runs an `IsProcessRunning` check before copying. **On Unix the task is unsupported**; with `Fallback="True"` BSMT behaves as if the game were running and copies to:
+
+`$GameDirectory/IPA/Pending/Plugins/`
+
+So you may see **`IPA/Pending/Plugins/Setlist.dll`** even when Beat Saber is not running.
+
+This repo adds a small **`CopyToPluginsOnUnixHost`** target at the end of `Setlist.csproj` that, on `$(OS) == 'Unix'`, also copies `$(OutputPath)Setlist.dll` (and `.pdb`) into **`$(BeatSaberDir)/Plugins/`**, so a normal Debug build matches the layout the game loads from **`Plugins/`**.
+
+After a successful `dotnet build -c Debug`, verify:
+
+```bash
+test -f "$BEAT_SABER_DIR/Plugins/Setlist.dll" && echo OK
+```
+
+(Replace `$BEAT_SABER_DIR` with your `BeatSaberDir`.)
+
+## 9. Seeing “Hello World”
+
+BSIPA logging uses `IPA.Logging.Logger` (exposed as `Log` in `Plugin.cs`). With **`--verbose`** (and optionally **`--debug`** for Unity logs), start the game from BSManager and check the BSIPA console or logs under the game folder (see guide §6).
+
+## 10. CI note
+
+Pass **`-p:ContinuousIntegrationBuild=true`** (or define `CIBuild` per template) to disable copying into the game directory on build agents.
diff --git a/docs/modding/index.md b/docs/modding/index.md
new file mode 100644
index 0000000..40e0713
--- /dev/null
+++ b/docs/modding/index.md
@@ -0,0 +1,30 @@
+---
+prev: false
+next: false
+description: Learn how to create your own mods!
+---
+
+# Making Mods
+
+Beat Saber _**does not**_ have built in mod support.
+
+Development for [PC](#pc-mod-development) and [Quest standalone](#quest-mod-development) are two vastly different workflows.
+
+## PC Mod Development
+
+If you want to make mods for the PC version of the game, the following guide will cover a multitude of different
+processes involved in making mods from scratch, as well as some of the different APIs you have access to.
+
+Visit the [PC Mod Development](./pc/index.md) page to begin.
+
+## Quest Mod Development
+
+The following guide covers most of the concepts you will need for creating mods for the Quest. This includes but is not
+limited to:
+
+- Hooking
+- Configuration using `config-utils`
+- User Interfaces using `bsml`
+- Custom types
+
+Visit the [Quest Mod Development Intro](./quest/intro.md) page for more information on getting started!
diff --git a/docs/modding/pc/bsml.md b/docs/modding/pc/bsml.md
new file mode 100644
index 0000000..e2c5ffd
--- /dev/null
+++ b/docs/modding/pc/bsml.md
@@ -0,0 +1,362 @@
+---
+prev: Harmony Patching
+next: Zenject and SiraUtil
+---
+
+# Creating Beat Saber UI
+
+[BeatSaberMarkupLanguage (BSML)](https://github.com/monkeymanboy/BeatSaberMarkupLanguage) is the most common way to
+create customized UI in Beat Saber. BSML is effectively a tag-based language that mimics the GameObject hierarchy
+of Unity. It parses tags into GameObjects, and attaches the relevant Unity and Beat Saber UI elements to them.
+
+The documentation for all BSML components can be found [here](https://monkeymanboy.github.io/BSML-Docs/).
+
+## Getting Set Up
+
+Of course, if you want to add BSML in your mod, make sure that you have it installed in your game, and your project
+is referencing BSML.
+
+### Creating the BSML file
+
+You can name the file anything you want, just make sure that its file extension is `.bsml`.
+
+
+
+BSML will require that the bsml file be embedded in the assembly. You can do this by right-clicking the file in the
+explorer, going to properties, and then changing the build action to `EmbeddedResource`.
+
+
+
+### Writing in BSML
+
+If you're using Rider, you may have to add a file association for `.bsml` files to get basic syntax highlighting. To
+do this, go to `File | Settings | Editor | File Types` and search for `XML`. Add a new file name pattern as `*.bsml`.
+This will make Rider accept `.bsml` files as XML files and do highlighting accordingly.
+
+To get autocompletion in a BSML file, you will need to provide a schema. A way to do this is to use the
+[background tag](https://monkeymanboy.github.io/BSML-Docs/Tags/BackgroundTag/) and add the schema to it:
+
+```xml
+
+
+
+```
+
+Rider may prompt you that the resource is not found. Simply right click on the URL, or press `Alt+Enter`, and select
+fetch external resource.
+
+Once set up, you should have basic autocompletion for tags if you start typing inside the `` tag.
+
+## Running Code In The Menu
+
+There are a couple different ways you can display your BSML in game, however, it is first important to note that
+you should not call any of the methods mentioned below outside of the main menu. You should make sure the game has finished
+loading the main menu before doing anything.
+
+- The recommended method, if you don't already use SiraUtil, is BSML's own `MainMenuAwaiter` class that has an
+ [event](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/) called `MainMenuInitializing`
+ that is invoked when the main menu loads
+- If you are using SiraUtil, it is recommended to bind a type with a `Location.Menu`, or on the `MainSettingsMenuViewControllersInstaller`
+- BS Utils also provides events in `BSEvents` and they are called `earlyMenuSceneLoadedFresh` and `lateMenuSceneLoadedFresh`
+- You can use the game's `GameScenesManager` and the `transitionDidFinishEvent`, then check if the output `ScenesTransitionSetupDataSO`
+ is a `MenuScenesTransitionSetupDataSO` using a
+ [type test expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/type-testing-and-cast)
+- If you want to get an event when the main menu loads yourself, you can use Unity's
+ [SceneManager](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/SceneManagement.SceneManager.html)
+ and check the name of the loaded scene manually, however this is a lot more effort than all of the other methods
+- You could also use a Harmony patch into a method that will run every time the menu reinitializes, but this is also unnecessarily
+ complicated
+
+## Adding Menus
+
+Once you have code running in the main menu, it's time to decide where you want to display your UI.
+
+### Mod Settings
+
+The mod settings menu is added by BSML and can be accessed from a custom button in the main menu settings. To register
+your own tab, check the `BSMLSettings` class. `TutorialMenu` is just a normal class.
+
+```c#
+private readonly TutorialMenu tutorialMenu = new TutorialMenu();
+
+public void AddSettingsMenu()
+{
+ BSMLSettings.Instance.AddSettingsMenu(
+ name: "Tutorial Mod",
+ resource: "TutorialMod.tutorial.bsml",
+ host: tutorialMenu);
+}
+```
+
+
+
+### Gameplay Setup
+
+The mods tab is added by BSML in the Gameplay Setup menu, which is found to the left of the song list, where
+you can normally find player settings and gameplay modifiers. To register a new tab, check the `GameplaySetup` class.
+`TutorialMenu` is just a normal class.
+
+```c#
+private readonly TutorialMenu tutorialMenu = new TutorialMenu();
+
+public void AddTab()
+{
+ GameplaySetup.Instance.AddTab(
+ name: "Tutorial Mod",
+ resource: "TutorialMod.tutorial.bsml",
+ host: tutorialMenu);
+}
+```
+
+
+
+### Custom Flow Coordinator
+
+BSML gives you a way to create a button in the left screen of the main menu. This button can do anything you want it
+to do, but most modders make it present their mod's UI. This is done by using a `FlowCoordinator`, and by adding one
+or more `ViewController` objects to it.
+
+BSML provides methods to create both flow coordinators and view controllers, which makes this process a lot cleaner.
+
+BSML has a few choices of view controller types you can inherit; we are going to use the `BSMLAutomaticViewController`
+because it has the option of hot reloading the menu when you make changes to the bsml file.
+
+```c#
+[ViewDefinition("TutorialMod.tutorial.bsml")]
+public class TutorialViewController : BSMLAutomaticViewController { }
+```
+
+The flow coordinator is responsible for managing view controllers. `FlowCoordinator` has many members that you can use
+or override, so it's worth checking out the code for it.
+
+```c#
+public class TutorialFlowCoordinator : FlowCoordinator
+{
+ private readonly TutorialViewController tutorialViewController = BeatSaberUI.CreateViewController();
+
+ // Called immediately when the flow coordinator is activated
+ protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling)
+ {
+ if (firstActivation)
+ {
+ // Sets the title text in the top bar
+ SetTitle("Tutorial Mod");
+ showBackButton = true;
+ }
+
+ if (addedToHierarchy)
+ {
+ ProvideInitialViewControllers(tutorialViewController);
+ }
+ }
+
+ protected override void BackButtonWasPressed(ViewController topViewController)
+ {
+ BeatSaberUI.MainFlowCoordinator.DismissFlowCoordinator(this);
+ }
+}
+```
+
+The code below is related to managing the menu button. We tell the `MainFlowCoordinator` to present our own
+flow coordinator. You can also have your own way to dismiss your flow coordinator but, in the example above,
+we are relying on the back button to do this.
+
+```c#
+private readonly TutorialFlowCoordinator tutorialFlowCoordinator;
+private readonly MenuButton menuButton;
+
+public MenuManager()
+{
+ tutorialFlowCoordinator = BeatSaberUI.CreateFlowCoordinator();
+ menuButton = new MenuButton("Tutorial Mod", ShowFlowCoordinator);
+}
+
+public void AddMenuButton()
+{
+ MenuButtons.Instance.RegisterButton(menuButton);
+}
+
+private void ShowFlowCoordinator()
+{
+ BeatSaberUI.MainFlowCoordinator.PresentFlowCoordinator(tutorialFlowCoordinator);
+}
+```
+
+
+
+### Floating Screen
+
+If you want to place your UI components anywhere, you can create a floating screen. This will allow you to have a view controller
+anywhere in the world. You can also create a handle for the floating screen which will allow the player to move the screen
+around.
+
+The example below creates just creates a small screen near the ground in front of the player's place.
+
+```c#
+private readonly TutorialViewController tutorialViewController = BeatSaberUI.CreateViewController();
+
+public void CreateFloatingScreen()
+{
+ var floatingScreen = FloatingScreen.CreateFloatingScreen(
+ screenSize: new Vector2(25f, 10f),
+ createHandle: false,
+ position: new Vector3(0f, 0.5f, 2f),
+ rotation: Quaternion.Euler(45f, 0f, 0f));
+
+ floatingScreen.SetRootViewController(tutorialViewController, ViewController.AnimationType.None);
+}
+```
+
+Since floating screens aren't part of the screen system, and because the menu persists during gameplay, you can have
+the floating screen active in the game scene. The below screenshot is of the floating screen from
+[SliceDetails](https://github.com/qqrz997/SliceDetails), which is activated when the game is paused.
+
+
+
+## Interacting With The Menu
+
+Now let's take a look at some of the ways you can make use of your UI. Again, to find out more about the components
+that we will talk about in the following sections, check the [BSML documentation](https://monkeymanboy.github.io/BSML-Docs/).
+
+### Buttons And Actions
+
+We are going to add a [button](https://monkeymanboy.github.io/BSML-Docs/Tags/ButtonTag/) to the menu:
+
+```xml
+
+```
+
+And add the corresponding method in the object host or view controller:
+
+```c#
+public void ButtonClicked() => Plugin.Log.Info("Button Clicked");
+```
+
+Now, `ButtonClicked()` will get called whenever our button is clicked.
+
+If you want to run a different method or a method with a different name to the one specified, you can use the
+[UIAction](https://monkeymanboy.github.io/BSML-Docs/Attributes/UIAction/) annotation and specify the name:
+
+```c#
+[UIAction("ButtonClicked")]
+public void SomeMethodName() { }
+```
+
+### UI Components
+
+BSML components must be part of and accessed from the provided host object or view controller. To access the instance of
+a BSML component, you must give one an `id`:
+
+```xml
+
+```
+
+And then add it in the object host by adding a [UIComponent](https://monkeymanboy.github.io/BSML-Docs/Attributes/UIComponent/)
+annotation:
+
+```c#
+[UIComponent("textComponent")]
+private readonly TextMeshProUGUI textComponent = null!; // assigned by BSML
+```
+
+If you want to have initialization logic for components in your UI, do not use Unity's `Awake()` or `Start()` or a constructor,
+instead use the post-parse event provided by BSML. This will be called after all of the UI has been created and all components
+on the object host have been assigned a value.
+
+```c#
+[UIAction("#post-parse")]
+public void PostParse()
+{
+ textComponent.text = "The text has changed.";
+}
+```
+
+### Settings And Values
+
+There are many different ways to get input values from BSML. Let's take a look at the
+[toggle](https://monkeymanboy.github.io/BSML-Docs/Tags/ToggleSettingTag/) and
+[slider](https://monkeymanboy.github.io/BSML-Docs/Tags/SliderSettingTag/) settings:
+
+```xml
+
+
+
+
+```
+
+We use `apply-on-change` to make the property get set when the input value changes, otherwise you would need to use
+[INotifyPropertyChanged](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged)
+when you want to apply the values, which can still be useful if you want to manually do it.
+
+```c#
+private bool toggleValue;
+private float sliderValue;
+
+public bool ToggleValue
+{
+ get => toggleValue;
+ set
+ {
+ toggleValue = value;
+ Plugin.Log.Info($"Toggle set to {value}");
+ }
+}
+
+public float SliderValue
+{
+ get => sliderValue;
+ set
+ {
+ sliderValue = value;
+ Plugin.Log.Info($"Slider set to {value}");
+ }
+}
+```
+
+If you want a property with a different name to the one specified, you can use the [UIValue](https://monkeymanboy.github.io/BSML-Docs/Attributes/UIValue/)
+annotation and specify the name:
+
+```c#
+[UIValue("ToggleValue")]
+public bool SomePropertyName { get; set; }
+```
+
+### Displaying Data
+
+As well as taking input in your UI, it's very common to need to display data. Let's add a [list](https://monkeymanboy.github.io/BSML-Docs/Tags/ListTag/):
+
+```xml
+
+```
+
+And set the data through a property:
+
+```c#
+private IList ListData =>
+[
+ new("A list cell", "and"),
+ new("Another list cell", "and"),
+ new("Another list cell", "that is all.")
+];
+```
+
+Or alternatively, you can grab the
+[CustomListTableData](https://monkeymanboy.github.io/BSML-Docs/TypeHandlers/CustomListTableData/)
+component from the list by adding an `id` and use that:
+
+```c#
+[UIComponent("List")]
+private readonly CustomListTableData list = null!; // assigned by BSML
+
+[UIAction("#post-parse")]
+public void PostParse()
+{
+ list.Data = [
+ new("A list cell", "and"),
+ new("Another list cell", "and"),
+ new("Another list cell", "that is all.")
+ ];
+ list.TableView.ReloadData();
+}
+```
diff --git a/docs/modding/pc/decompiling.md b/docs/modding/pc/decompiling.md
new file mode 100644
index 0000000..28bccd3
--- /dev/null
+++ b/docs/modding/pc/decompiling.md
@@ -0,0 +1,72 @@
+---
+prev: Runtime Unity Editor
+next: Harmony Patching
+---
+
+# Decompiling
+
+When modding Beat Saber and patching the game to change certain behaviour, it's important to be able to read
+the game's code itself. There are some tools to help with this.
+
+## Tools
+
+Rider and Visual Studio do have built-in decompilers to let you see under the hood of types.
+
+
+
+This will only have limited usage and won't help you browse the types the game has to offer, or see how different
+parts of the game's code interact.
+
+### ILSpy
+
+[ILSpy](https://github.com/icsharpcode/ILSpy) is a lightweight decompiler for C# dlls which will allow you to freely
+browse the different types, variables, and methods that are contained within the game's own dlls. Grab the installer
+from the [releases](https://github.com/icsharpcode/ILSpy/releases) and install ILSpy.
+
+Once you have ILSpy opened, find the `Manage Assembly Lists` icon in the top bar and create a new list. You can name it
+after the Beat Saber version you are working on. Once created, double click it to open the list.
+
+
+
+To add binaries, click the `Open` icon in the top bar and navigate to your game folder. You are looking for
+`/Beat Saber_Data/Managed`, select everything in this folder and open them into ILSpy. This will also include the
+.NET framework and Unity assemblies, so that when you are looking at types from Beat Saber, all of the references will
+be resolved.
+
+### dnSpy
+
+[dnSpy](https://github.com/dnSpyEx/dnSpy) is a much more in-depth tool for developing .NET programs; it has a
+debugger, assembly editor, and more. It also has a decompiler built in to it for browsing decompiled C#, just like
+ILSpy.
+
+You can get dnSpy from the [releases](https://github.com/dnSpyEx/dnSpy/releases) on GitHub. Extract the zip archive and
+run the .exe to get started. Similarly to ILSpy, you create a new list by going to `File`, then `Open List...`, and
+adding a new list. You can name it after the Beat Saber version you are working on. Once created, double click it to
+open the list.
+
+Click the `Open` icon in the top bar or press `Ctrl+O` and navigate to `Beat Saber/Beat Saber_Data/Managed`,
+select everything in this folder and open them into your list. To start searching, click the `Search Assemblies` in the
+top bar.
+
+## Browsing the Code
+
+Beat Saber is a complex game with a lot of different assemblies, but it is pretty well organized and you can expect to
+find what you are looking for where it should be. Something that may help is to find an object in game using RUE,
+and by checking the MonoBehaviours attached to them, you can search for them in ILSpy.
+
+
+
+If you double click a type in the search window, or in the assembly list, you will see the decompiler's interpretation
+of that type and the corresponding C# code.
+
+
+
+An important trick to know is analyzing members of a type. By pressing `Ctrl+R` or right-clicking and `Analyze` on,
+for example, a public method, you will see the usages of that member. In the example below, the method
+`FlyingScoreEffect.InitAndPresent` is called by `FlyingScoreSpawner.SpawnFlyingScore`.
+
+
+
+This tool will be very important when writing [Harmony patches](./harmony-patching.md), which will be covered in the next
+section of this wiki. You will want to be able to know how different parts of the code interact so that you can work out
+where you should implement custom behaviour in your mod.
diff --git a/docs/modding/pc/full-mod-guide.md b/docs/modding/pc/full-mod-guide.md
new file mode 100644
index 0000000..072c568
--- /dev/null
+++ b/docs/modding/pc/full-mod-guide.md
@@ -0,0 +1,627 @@
+---
+prev: Zenject and SiraUtil
+next: false
+---
+
+# Full Mod Guide
+
+This part of the wiki will be dedicated to showing the full process of making a Beat Saber mod.
+
+## The Mod
+
+The first step of creating a mod is understanding exactly what you want to achieve.
+
+In this tutorial, we will be creating a mod capable of changing the "MISS" effect and replacing it with text. The mod
+will have an in-game interface to allow you to change the text through a text input. The mod will be designed in a
+decoupled way, which will make it easier to add new features to the mod later if we wish.
+
+We can use [BSML](./bsml.md) for the UI, and we can use [SiraUtil](./zenject.md) to create our custom text effects
+while remaining loosely coupled to in-game functions.
+
+### Creating The Project
+
+The first thing we are going to do is set up the plugin template. Refer to the [setup guide](./setup.md) for more information.
+We will name the plugin `MissTextChanger` and add dependencies to `BSML` and `SiraUtil` in the metadata.
+
+This will start from a bare-bones BSIPA template, going step by step through the testing process of making a simple
+plugin to help people understand everything. If you're following along, you can also just use the full template, which
+has a basic SiraUtil and BSML setup already done.
+
+### Figuring Out The Game
+
+Before going any further, we need to get an understanding of how the game handles miss text normally. First, let's go in
+to [ILSpy](./decompiling.md), and search for "ScoreController". This class is responsible for basically everything related
+to giving the player score, so we can figure out how misses are handled from here.
+
+In the `Start()` method of the `ScoreController`, we can see the `noteWasMissedEvent` being assigned to which is a part
+of the `BeatmapObjectManager`. Let's analyze this event and see what the `add` method of the event is used by. We can now
+see the `MissedNoteEffectSpawner` which, as we can assume by its name, is exactly what we're looking for.
+
+
+
+Looking into the `MissedNoteEffectSpawner` we can see all it is doing is taking data from the missed note's `NoteController`
+and passing it to a `FlyingSpriteSpawner` to spawn the effect. The sprite spawner manages a
+[Zenject Pool](https://github.com/Mathijs-Bakker/Extenject/blob/master/Documentation/MemoryPools.md)
+of sprite effects.
+
+If we analyze the `FlyingSpriteEffect.Pool` we can figure out where it is bound by checking where it is used.
+
+
+
+Now, looking at the `EffectPoolsManualInstaller.ManualInstallBindings()` method we see a couple different memory pools here.
+One that is particularly interesting is the `FlyingTextEffect`, which if we analyze we can see the `FlyingTextSpawner`.
+
+This is surely something we can use to achieve customizable miss text, however, looking at and comparing the spawn
+methods for the sprite and text spawners, they are not exactly the same. The `x` of the `targetPos` vector is anchored
+in the sprite spawner by its sign, which is why we see miss effects only fly to two locations to the left and right of
+the track; there are only two possible values for sign.
+
+Because of this difference, if we wanted to maintain the same visuals, we cannot use the `FlyingTextSpawner` for our needs.
+We could use a harmony patch to change how the `SpawnFlyingSprite()` method works, but this may affect other mods that may
+want to use this.
+
+### The Solution
+
+Instead of using the game's methods for our needs, let's make a custom effect spawner, and a custom flying object
+effect. This should ensure that our mod doesn't conflict with other mods' features, but we're going to have to patch in to
+the `MissedNoteEffectSpawner` to replace the base-game's miss effect with our custom one.
+
+Let's start with the `MissTextEffect`, which will inherit `FlyingObjectEffect` like the other effects. For the text, we
+will want a `TextMeshPro`.
+
+```c#
+internal class MissTextEffect : FlyingObjectEffect
+{
+ // This is the pool from Zenject
+ public class Pool : MonoMemoryPool;
+
+ // We don't have something to use here yet, we will get one later
+ private AnimationCurve fadeAnimationCurve;
+
+ // This field is serialized so that it will be included on instantiation
+ [SerializeField]
+ public TextMeshPro? textMesh;
+
+ private Color color;
+
+ public void InitAndPresent(string text, float duration, Vector3 targetPos,
+ Quaternion rotation, Color color, float fontSize, bool shake)
+ {
+ if (textMesh == null) return;
+ this.color = color;
+ textMesh.text = text;
+ textMesh.fontSize = fontSize;
+ InitAndPresent(duration, targetPos, rotation, shake);
+ }
+
+ public override void ManualUpdate(float t)
+ {
+ if (textMesh != null)
+ textMesh.color = color with { a = fadeAnimationCurve.Evaluate(t) };
+ }
+}
+```
+
+We have some things to fill in on this object, but we will figure that out a bit later.
+
+Next let's look at the spawner. We will be making sure to match the logic of the sprite spawner so
+that the behaviour is the same.
+
+```c#
+internal class MissTextEffectSpawner : MonoBehaviour,
+ IFlyingObjectEffectDidFinishEvent
+{
+ // There is a lot of data here that needs filling
+ private float duration;
+ private float xSpread;
+ private float targetYPos;
+ private float targetZPos;
+ private Color color;
+ private float fontSize;
+ private MissTextEffect.Pool missTextEffectPool;
+
+ public void SpawnText(
+ Vector3 pos, Quaternion rotation, Quaternion inverseRotation)
+ {
+ var text = "CUSTOM MISS";
+ var targetPos = rotation * new Vector3(
+ Mathf.Sign((inverseRotation * pos).x) * xSpread,
+ targetYPos,
+ targetZPos);
+
+ var missTextEffect = missTextEffectPool.Spawn();
+ missTextEffect.didFinishEvent.Add(this);
+ missTextEffect.transform.localPosition = pos;
+ missTextEffect.InitAndPresent(
+ text, duration, targetPos, rotation, color, fontSize, false);
+ }
+
+ public void HandleFlyingObjectEffectDidFinish(
+ FlyingObjectEffect flyingObjectEffect)
+ {
+ flyingObjectEffect.didFinishEvent.Remove(this);
+ missTextEffectPool.Despawn((MissTextEffect)flyingObjectEffect);
+ }
+}
+```
+
+All we need to do is register these components in an installer. Let's create a `PlayerInstaller` and
+add our bindings.
+
+- Bind the `MissTextEffectSpawner` as a component on a single new game object
+- Bind the memory pool for the `MissTextEffect` similar to the other score effects in the
+ `EffectPoolsManualInstaller`
+
+```c#
+internal class PlayerInstaller : Installer
+{
+ public override void InstallBindings()
+ {
+ Container.Bind()
+ .FromNewComponentOnNewGameObject()
+ .AsSingle();
+ Container.BindMemoryPool()
+ .WithInitialSize(20)
+ .FromComponentInNewPrefab(GetMissTextEffectPrefab());
+ }
+
+ private static MissTextEffect GetMissTextEffectPrefab()
+ {
+ var prefabObject = new GameObject("MissTextEffect");
+ var textEffect = prefabObject.AddComponent();
+
+ var textObject = new GameObject("Text") { layer = 5 };
+ textObject.transform.SetParent(prefabObject.transform, false);
+
+ textEffect.textMesh = textObject.AddComponent();
+ textEffect.textMesh.alignment = TextAlignmentOptions.Capline;
+ textEffect.textMesh.fontStyle = FontStyles.Bold | FontStyles.Italic;
+
+ return textEffect;
+ }
+}
+```
+
+Creating the `MissTextEffect` prefab here doesn't make much sense and should realistically move to its
+own class but for now this is fine to demonstrate what we're doing.
+
+Remember to add the zenjector to the `Plugin` init too.
+
+```c#
+[Plugin(RuntimeOptions.SingleStartInit), NoEnableDisable]
+internal class Plugin
+{
+ [Init]
+ public Plugin(Logger log, Config config,
+ PluginMetadata metadata, Zenjector zenjector)
+ {
+ log.Info($"{metadata.Name} {metadata.HVersion} initialized.");
+
+ zenjector.UseLogger(log);
+ zenjector.Install(Location.Player);
+ }
+}
+```
+
+Now that we have the main components of the mod outlined, we need to set their fields. There are two ways
+we can do this. We can do it manually by loading up a map in-game, opening [Runtime Unity Editor](rue.md),
+and looking for the miss effect spawner to see the values. This may work, but we should figure out how to automate
+it in case the values aren't constant.
+
+As seen before, we found the prefab for the `FlyingSpriteEffect` in the `EffectPoolsManualInstaller`. This isn't
+actually an installer, instead it's a part of the _much_ larger `GameplayCoreInstaller`.
+
+If we were to patch in to the `GameplayCoreInstaller`, we can access the prefabs for the `FlyingTextEffect` and
+the instance of the `FlyingSpriteSpawner` to get the fields we need for our custom components.
+
+Since we're using SiraUtil for this mod, let's make an [affinity patch](./zenject.md#affinity-patching)
+into the `InstallBindings()` method. We can take the fields from the prefabs and bind their values with an ID,
+so that we can inject them into our own components.
+
+```c#
+internal class GameCoreInstallerHook : IAffinity
+{
+ [AffinityPrefix]
+ [AffinityPatch(typeof(GameplayCoreInstaller), "InstallBindings")]
+ private void InstallBindingsPostfix(GameplayCoreInstaller __instance)
+ {
+ var container = __instance.Container;
+ var flyingSpriteSpawner = __instance._missedNoteEffectSpawnerPrefab._missedNoteFlyingSpriteSpawner;
+ var flyingTextEffect = __instance._effectPoolsManualInstaller._flyingTextEffectPrefab;
+
+ float duration = flyingSpriteSpawner._duration;
+ float spread = flyingSpriteSpawner._xSpread;
+ float targetYPos = flyingSpriteSpawner._targetYPos;
+ float targetZPos = flyingSpriteSpawner._targetZPos;
+ var color = Color.white;
+ const float fontSize = 4.5f; // Miss text is a sprite; estimate the font size
+ var fadeAnimationCurve = flyingTextEffect._fadeAnimationCurve;
+ var moveAnimationCurve = flyingTextEffect._moveAnimationCurve;
+
+ container.BindInstance(duration).WithId("missEffectDuration").AsCached();
+ container.BindInstance(spread).WithId("missEffectSpread").AsCached();
+ container.BindInstance(targetYPos).WithId("missEffectTargetYPos").AsCached();
+ container.BindInstance(targetZPos).WithId("missEffectTargetZPos").AsCached();
+ container.BindInstance(color).WithId("missEffectColor").AsCached();
+ container.BindInstance(fontSize).WithId("missEffectFontSize").AsCached();
+ container.BindInstance(fadeAnimationCurve).WithId("textEffectFadeAnimationCurve").AsCached();
+ container.BindInstance(moveAnimationCurve).WithId("textEffectMoveAnimationCurve").AsCached();
+ }
+}
+```
+
+Make sure not to forget to bind this patch. Since we're patching the installer itself, binding it alongside
+the installer we are patching won't work because the `InstallBindings` will be called before our patch is applied.
+Instead let's make an `AppInstaller`, because that will be applied when the game initializes.
+
+```c#
+internal class AppInstaller : Installer
+{
+ public override void InstallBindings()
+ {
+ Container.BindInterfacesTo().AsSingle();
+ }
+}
+```
+
+Remember to add this to the `Plugin` init too.
+
+```c#
+zenjector.Install(Location.App);
+```
+
+And now we add [inject methods](./zenject.md#methods) to our components, starting with the `MissTextEffect`. Note that
+the `_moveAnimationCurve` is part of the base class. We need this so that the movement animation matches the base game's
+movement.
+
+```c#
+[Inject]
+public void Init(
+ [Inject(Id = "textEffectFadeAnimationCurve")] AnimationCurve fadeAnimationCurve,
+ [Inject(Id = "textEffectMoveAnimationCurve")] AnimationCurve moveAnimationCurve)
+{
+ this.fadeAnimationCurve = fadeAnimationCurve;
+ _moveAnimationCurve = moveAnimationCurve;
+}
+```
+
+And for `MissTextEffectSpawner` there are quite a few properties. Also, remember to inject the `Pool`.
+
+```c#
+[Inject]
+public void Init(
+ [Inject(Id = "missEffectDuration")] float duration,
+ [Inject(Id = "missEffectSpread")] float xSpread,
+ [Inject(Id = "missEffectTargetYPos")] float targetYPos,
+ [Inject(Id = "missEffectTargetZPos")] float targetZPos,
+ [Inject(Id = "missEffectColor")] Color color,
+ [Inject(Id = "missEffectFontSize")] float fontSize,
+ MissTextEffect.Pool missTextEffectPool)
+{
+ this.duration = duration;
+ this.xSpread = xSpread;
+ this.targetYPos = targetYPos;
+ this.targetZPos = targetZPos;
+ this.color = color;
+ this.fontSize = fontSize;
+ this.missTextEffectPool = missTextEffectPool;
+}
+```
+
+Now we're all set up to implement our custom text effect. We just need to figure out how to spawn them. Ultimately,
+the goal is to replace the game's "MISS" sprite effect with our own, so let's go back to the `MissedNoteEffectSpawner`
+and patch it to replace the `FlyingSpriteSpawner` with our spawner by using a patch.
+
+By using an affinity patch we can inject the `MissTextEffectSpawner` and use it within the patch with ease.
+
+```c#
+internal class OnMissEffectPatch : IAffinity
+{
+ private readonly MissTextEffectSpawner missTextEffectSpawner;
+
+ public OnMissEffectPatch(MissTextEffectSpawner missTextEffectSpawner)
+ {
+ this.missTextEffectSpawner = missTextEffectSpawner;
+ }
+
+ [AffinityPrefix]
+ [AffinityPatch(typeof(MissedNoteEffectSpawner), nameof(MissedNoteEffectSpawner.HandleNoteWasMissed))]
+ private bool HandleNoteWasMissedPrefix(MissedNoteEffectSpawner __instance, NoteController noteController)
+ {
+ if (noteController.hidden
+ || noteController.noteData.time + 0.5f < __instance._audioTimeSyncController.songTime
+ || noteController.noteData.colorType == ColorType.None)
+ {
+ // Do nothing
+ return false;
+ }
+
+ var position = noteController.inverseWorldRotation * noteController.noteTransform.position;
+ position.z = __instance._spawnPosZ;
+
+ // Spawn our miss text effect
+ missTextEffectSpawner.SpawnText(
+ noteController.worldRotation * position,
+ noteController.worldRotation,
+ noteController.inverseWorldRotation);
+
+ // Cancel the original implementation
+ return false;
+ }
+}
+```
+
+Apart from being syntactically different to the original method we can see from the decompiler, the logic is the same.
+We can bind this in the `PlayerInstaller` because this method runs during gameplay, and that's where our effect spawner
+is bound too.
+
+```c#
+Container.BindInterfacesTo().AsSingle();
+```
+
+### Testing
+
+At this point we should be able to see this in action. Open the game with [FPFC](./index.md#launch-args) and open any map.
+Using No Fail will help.
+
+
+
+## Adding Settings
+
+There are many ways to add interactive menus in to the game, which you can see in the [UI section of this wiki](./bsml.md#adding-menus).
+
+For this guide we will be using a [custom flow coordinator](./bsml.md#custom-flow-coordinator) which will provide plenty
+of space to add more features to the UI in the future if we need to.
+
+Before creating the UI, let's decide what features we need it to have.
+
+- We want a setting to toggle the mod off and on - this is for the player's convenience and most mods should have one
+- We need a way to input text to change the miss text, we can use the [ModalKeyboard](https://monkeymanboy.github.io/BSML-Docs/Tags/ModalKeyboardTag/)
+ for this
+- As well as the input, we should also have some [Text](https://monkeymanboy.github.io/BSML-Docs/Tags/TextTag/) to show
+ the current miss text
+- And finally, we need a way to open the modal keyboard. A simple [Button](https://monkeymanboy.github.io/BSML-Docs/Tags/ButtonTag/)
+ can do this
+
+### Creating A Config
+
+To make settings that will save between sessions, we can utilize BSIPA's config. Let's create a config class, and
+add it to the plugin init. Instead of making a static config, we should pass it as a param of the `AppInstaller`, then bind
+it there so we can inject it anywhere.
+
+```c#
+[assembly: InternalsVisibleTo(GeneratedStore.AssemblyVisibilityTarget)]
+namespace MissTextChanger;
+
+internal class PluginConfig
+{
+ public virtual bool Enabled { get; set; } = true;
+ public virtual string MissText { get; set; } = "MISS";
+}
+```
+
+Then add it to the `Plugin` init:
+
+```c#
+var pluginConfig = config.Generated();
+
+zenjector.Install(Location.App, pluginConfig);
+```
+
+And in the installer:
+
+```c#
+internal class AppInstaller : Installer
+{
+ public AppInstaller(PluginConfig pluginConfig)
+ {
+ this.pluginConfig = pluginConfig;
+ }
+
+ public override void InstallBindings()
+ {
+ Container.BindInstance(pluginConfig).AsSingle();
+ /* ... */
+```
+
+### Implementing The Settings
+
+Before we mess around with the UI, let's make sure we can make these new features work. First, inject the config
+into the `PlayerInstaller` so we can use it to stop our bindings from being made:
+
+```c#
+internal class PlayerInstaller : Installer
+{
+ private readonly PluginConfig pluginConfig;
+
+ public PlayerInstaller(PluginConfig pluginConfig)
+ {
+ this.pluginConfig = pluginConfig;
+ }
+
+ public override void InstallBindings()
+ {
+ if (!pluginConfig.Enabled) return;
+ /* ... */
+```
+
+You can go to the config `.json` file in the `UserData` folder and tweak the settings manually to test that this
+is working. Next, let's add the config to the `MissTextEffectSpawner` and use the text property in the
+`SpawnText()` method.
+
+```c#
+public void SpawnText(
+ Vector3 pos, Quaternion rotation, Quaternion inverseRotation)
+{
+ /* ... */
+ var text = pluginConfig.MissText;
+ missTextEffect.InitAndPresent(text, duration, targetPos, rotation,
+ color, fontSize, false);
+ /* ... */
+}
+```
+
+That was simple thanks to zenject. Now let's move on to setting up the UI.
+
+### Adding The UI
+
+Now we will set up the flow coordinator so that we can start playing around with the BSML immediately.
+Let's start at the end of the dependency tree with the view controller.
+
+```xml
+
+
+
+
+
+
+```
+
+And the host for our view:
+
+```c#
+[HotReload(RelativePathToLayout = @".\settingsView.bsml")]
+[ViewDefinition("MissTextChanger.Menu.settingsView.bsml")]
+internal class SettingsViewController : BSMLAutomaticViewController
+{
+ [Inject] private readonly PluginConfig pluginConfig = null!;
+
+ [UIComponent("MissText")] private readonly TextMeshProUGUI missText = null!;
+
+ [UIAction("#post-parse")]
+ private void PostParse()
+ {
+ SetMissTextPreview(pluginConfig.MissText);
+ }
+
+ private bool Enabled
+ {
+ get => pluginConfig.Enabled;
+ set => pluginConfig.Enabled = value;
+ }
+
+ private string KeyboardInput
+ {
+ get => pluginConfig.MissText;
+ set
+ {
+ pluginConfig.MissText = value;
+ SetMissTextPreview(value);
+ }
+ }
+
+ private void SetMissTextPreview(string v) =>
+ missText.text = string.IsNullOrEmpty(v) ? "No miss text" : v;
+}
+```
+
+By using the `HotReload` attribute we can get live updates to the view controller when we change the bsml file,
+without having to re-build the mod.
+
+Now for the `FlowCoordinator`, which is responsible for managing our view controller.
+
+```c#
+internal class MissTextChangerFlowCoordinator : FlowCoordinator
+{
+ [Inject] private readonly SettingsViewController settingsViewController = null!;
+
+ public event Action? DidFinish;
+
+ protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling)
+ {
+ if (firstActivation)
+ {
+ showBackButton = true;
+ SetTitle("MissTextChanger");
+ }
+
+ if (addedToHierarchy)
+ {
+ ProvideInitialViewControllers(settingsViewController);
+ }
+ }
+
+ protected override void BackButtonWasPressed(ViewController topViewController)
+ {
+ DidFinish?.Invoke();
+ }
+}
+```
+
+We're using an action here to signal when we are done so that we don't need an extra dependency to handle returning to the
+main flow coordinator. This event will be used by our button manager.
+
+```c#
+internal class MenuButtonManager : IInitializable, IDisposable
+{
+ private readonly MainFlowCoordinator mainFlowCoordinator;
+ private readonly MissTextChangerFlowCoordinator missTextChangerFlowCoordinator;
+ private readonly MenuButtons menuButtons;
+ private readonly MenuButton menuButton;
+
+ public MenuButtonManager(
+ MainFlowCoordinator mainFlowCoordinator,
+ MissTextChangerFlowCoordinator missTextChangerFlowCoordinator,
+ MenuButtons menuButtons)
+ {
+ this.mainFlowCoordinator = mainFlowCoordinator;
+ this.missTextChangerFlowCoordinator = missTextChangerFlowCoordinator;
+ this.menuButtons = menuButtons;
+ menuButton = new("MissTextChanger", PresentFlowCoordinator);
+ }
+
+ public void Initialize()
+ {
+ menuButtons.RegisterButton(menuButton);
+ missTextChangerFlowCoordinator.DidFinish += DismissFlowCoordinator;
+ }
+
+ public void Dispose()
+ {
+ missTextChangerFlowCoordinator.DidFinish -= DismissFlowCoordinator;
+ }
+
+ private void PresentFlowCoordinator() =>
+ mainFlowCoordinator.PresentFlowCoordinator(missTextChangerFlowCoordinator);
+
+ private void DismissFlowCoordinator() =>
+ mainFlowCoordinator.DismissFlowCoordinator(missTextChangerFlowCoordinator);
+}
+```
+
+That's everything we need to create our UI. Now we just need a `MenuInstaller` to create the bindings.
+
+```c#
+internal class MenuInstaller : Installer
+{
+ public override void InstallBindings()
+ {
+ Container.BindInterfacesTo().AsSingle();
+ Container.Bind().FromNewComponentOnNewGameObject().AsSingle();
+ Container.Bind().FromNewComponentAsViewController().AsSingle();
+ }
+}
+```
+
+And of course, remember to add this to the `Plugin` init.
+
+```c#
+zenjector.Install(Location.Menu);
+```
+
+
+
+## Closing Remarks
+
+We have now covered every step of creating a new Beat Saber mod.
+
+This example mod has been designed in a way which allows easy changes and extension to its features. When designing a mod,
+it's important to figure out what you want to do so that development doesn't reach a halt.
+
+If you want to learn more we highly recommend checking the source code for other mods to learn more about different APIs
+and how Beat Saber works. You can find that most mods are open source, and you can find that source by visiting
+[BeatMods](https://beatmods.com/) and going to the more info section for any given mod.
+
+You can view all of the source code used in this guide [here](https://github.com/qqrz997/TutorialPCMod).
diff --git a/docs/modding/pc/harmony-patching.md b/docs/modding/pc/harmony-patching.md
new file mode 100644
index 0000000..1dbed65
--- /dev/null
+++ b/docs/modding/pc/harmony-patching.md
@@ -0,0 +1,234 @@
+---
+prev: Decompiling
+next: Creating UI
+---
+
+# Harmony Patching
+
+A common method of altering the behavior of the game is through the Harmony API, and every modder should know how
+to use it.
+
+## Summary
+
+Harmony patching is a way of hooking the games methods and pointing them to different implementations.
+By writing harmony patches, you are essentially adding code to methods, changing parts of them, or entirely rewriting
+them.
+
+Harmony patches are quite powerful and are used in a great amount of different mods. There is a lot of detail about
+patching and you should read the [documentation](https://harmony.pardeike.net/articles/patching.html) if you ever need
+to do something specific.
+
+## Harmony Setup
+
+There are different methods of setting up your patches as stated
+[here](https://harmony.pardeike.net/articles/basics.html#patching-using-annotations)
+in the documentation. We are simply going to patch all methods marked with the `HarmonyPatch` attributes using
+`PatchAll()`:
+
+```c#
+internal class Plugin
+{
+ private Harmony harmony;
+ private Assembly executingAssembly = Assembly.GetExecutingAssembly();
+
+ [Init]
+ public Plugin(PluginMetadata pluginMetadata)
+ {
+ harmony = new Harmony(pluginMetadata.Id);
+ }
+
+ [OnStart]
+ public void OnApplicationStart() => harmony.PatchAll(executingAssembly);
+
+ [OnExit]
+ public void OnApplicationQuit() => harmony.UnpatchSelf();
+}
+```
+
+Now, any classes and methods with the `HarmonyPatch` attribute will be registered as a patch. Once again, if you want
+more control, there are different methods to do this as stated in the
+[documentation](https://harmony.pardeike.net/articles/basics.html#manual-patching).
+
+## Examples
+
+To make understanding harmony patches easier, we will provide some simple examples of the different things you can do.
+
+### Postfix
+
+The simplest method of patching involves adding code at the end of a method - a postfix.
+
+- This is very commonly seen and can be used as events to execute code when the game does certain events
+- It can be used to reliably get references to objects without having to use expensive methods like
+ [`Resources.FindObjectsOfTypeAll`](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Resources.FindObjectsOfTypeAll.html).
+- They can also change the return result of methods as mentioned in the
+ [documentation](https://harmony.pardeike.net/articles/patching-postfix.html).
+
+The following patch patches the `Init()` method in any `NoteController`. The patch gets a reference to the instance of
+the object by injecting the `__instance` variable in the patch params.
+
+Since `NoteController` is a type that has many inheritors, we can get what type of note controller it is. If you run
+this in a map that also has bombs and chains, you will see their types get listed in the logs too.
+
+```c#
+[HarmonyPatch(typeof(NoteController), "Init")]
+public class ExamplePatch
+{
+ public static void Postfix(NoteController __instance)
+ {
+ Plugin.Log.Info($"A {__instance.GetType().Name} has been initialized.");
+ }
+}
+```
+
+### Prefix
+
+Another common patch, this is very similar to a postfix except it runs before the original method.
+
+- This allows you to decide dynamically decide whether the original implementation should run or not
+- Like with a postfix, you can also decide the result yourself
+- You can create a state variable that can be passed to a postfix of the same method
+
+The following example patches the `RefreshScore()` method in the `FlyingScoreEffect`, which is the MonoBehaviour
+attached to the text that displays your score when you cut a note. We get a reference to the instance, and also
+the original method params: `score` and `maxPossibleCutScore`.
+
+The patch is pretty self explanatory, but when you score the max possible score for a note - which is 115 for
+a normal note - the text will be replaced with `Hello World!`, and the original method will be ignored. If the score
+does not reach the max possible score, then the original method will be called instead.
+
+```c#
+[HarmonyPatch(typeof(FlyingScoreEffect), "RefreshScore")]
+public class ExamplePatch
+{
+ public static bool Prefix(FlyingScoreEffect __instance, int score, int maxPossibleCutScore)
+ {
+ if (score >= maxPossibleCutScore)
+ {
+ __instance._text.text = "Hello World!";
+ __instance._colorAMultiplier = 1f;
+
+ // Cancel the original method
+ return false;
+ }
+
+ // Run the original implementation
+ return true;
+ }
+}
+```
+
+### Transpiler
+
+The last commonly used patch we will mention is the transpiler. These are used to modify the
+[CIL](https://en.wikipedia.org/wiki/Common_Intermediate_Language) code of the game directly. With these, you can
+make changes in the middle of methods.
+
+This type of patch is much more complicated, and we won't provide an example here (for now), but we can recommend
+checking out transpilers from other mods. As always, if you want to learn more about transpilers, check the
+[documentation](https://harmony.pardeike.net/articles/patching-transpiler.html).
+
+## Accessing Private Code
+
+When making mods you often will need to alter `private` fields, or call `private` methods. Thankfully, in C# there are
+some methods that allow us to do this.
+
+### Publicizing Assemblies
+
+The easiest and recommended way to access `private` members is by utilizing the `BepInEx.AssemblyPublicizer.MSBuild`
+NuGet package. To add this to your project, do one of the following:
+
+- Navigate to your project dependencies in the assembly explorer, right click it, and select `Manage NuGet Packages`.
+ Then, search for "BepInEx.AssemblyPublicizer.MSBuild", right click it, and select install;
+- or navigate to the top bar and look for `Tools | NuGet | Manage NuGet Packages for Solution` and search for it there.
+
+Alternatively, you can add it manually in the `.csproj` project file manually by adding a `PackageReference`:
+
+```xml
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+```
+
+Once installed, all you have to do is add the `Publicize` property to an assembly reference like this:
+
+```xml
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll
+ false
+
+```
+
+Now, anything that was `private` or `protected` will be seen as `public` by the compiler, allowing you to bypass this
+restriction.
+
+The only restriction you will run in to now is with `readonly` fields and auto-computed properties (see below).
+If you want to set the value of these, you will have to use [reflection](#reflection).
+
+```c#
+public readonly float _field;
+public float Property { get; }
+
+```
+
+::: danger IMPORTANT
+**Do not use the assembly publicizer to publicize other mods**. This can cause some problems with the mod loader. Instead,
+use [reflection](#reflection) or make a request to the mod's maintainer to add a change if you need it.
+:::
+
+### Reflection
+
+Reflection is a special feature of C# that lets you read code at runtime by making types into objects which you can access
+members from. There is a lot you can do with reflection, and something that it is commonly used for is checking if certain
+parts of another mod's code are running without actually having to reference that mod's assembly.
+
+If you want to read more about reflection you can check
+[Microsoft's docs](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/).
+
+IPA provides some utilities to use reflection to get and set values of members, and invoke methods, even if they are private.
+
+```c#
+using IPA.Utilities;
+```
+
+Now we can use the `ReflectionUtil` class, which provides a couple extension methods to pretty easily access private members
+of an object.
+
+```c#
+public class SomeClass
+{
+ private float someValue = 0.25f;
+
+ public float SomeValue => someValue;
+}
+```
+
+Let's say we had a reference to an object of type `SomeClass`, we can access and set the private field by using `SetField`,
+this works even when the field is `readonly`.
+
+```c#
+var someClass = new SomeClass();
+someClass.SetField("someValue", 0.5f);
+```
+
+::: tip NOTE
+If you are just reading the values of members, accessing methods, or setting the values of **non-readonly** fields and properties,
+you should use the [Assembly Publicizer](#publicizing-assemblies), because it is easier to read, is faster, and creates less
+garbage.
+:::
+
+If you must set a field often or repetitively with reflection, you should use the `FieldAccessor` to reduce the
+performance cost. You create the accessor by providing the type of the object the field is on, and the backing type of
+the field, as well as the name of the field itself.
+
+```c#
+private static FieldAccessor.Accessor SomeValueAccessor { get; } =
+ FieldAccessor.GetAccessor("someValue");
+
+private SomeClass someClass = new();
+
+public void SomeMethod()
+{
+ SomeValueAccessor(ref someClass) = 1f;
+}
+```
diff --git a/docs/modding/pc/index.md b/docs/modding/pc/index.md
new file mode 100644
index 0000000..a4a2f8c
--- /dev/null
+++ b/docs/modding/pc/index.md
@@ -0,0 +1,74 @@
+---
+prev: false
+next: false
+description: Learn how to create your own PC mods!
+---
+
+# Making PC Mods
+
+Currently, all mods are made using the
+[BSIPA (Beat Saber Illusion Plugin Architecture)](https://github.com/nike4613/BeatSaber-IPA-Reloaded/)
+to inject plugins into the game. It makes the process of executing code in game much easier, and provides many useful
+tools, some of which will be covered in this section of the wiki.
+
+## List of contents
+
+- [Getting a setup ready for creating PC mods](#getting-started)
+- [Useful launch arguments](#launch-args)
+- [Using Runtime Unity Editor](./rue.md)
+- [Inspecting the game code with a decompiler](./decompiling.md)
+- [Harmony patching](./harmony-patching.md)
+- [Creating user interfaces with BeatSaberMarkupLanguage](./bsml.md)
+- [The essentials of Zenject through SiraUtil](./zenject.md)
+- [Writing a functioning mod step-by-step](./full-mod-guide.md)
+- [Other links](#other-links)
+
+## Getting Started
+
+If you are interested in creating a Beat Saber mod, but do not have a template or Visual Studio template set up,
+follow the [setup guide](./setup.md) to get your project all set up.
+
+If you have any questions at any point, the best place to ask is in the `#pc-mod-dev` channel on the
+[BSMG Discord](https://discord.gg/beatsabermods), another modder may be able to help you solve your problem.
+
+## Launch args
+
+Listed in the table below are numerous helpful launch arguments that will make modding / debugging easier.
+
+If you are using Steam, you can enter these by right-clicking the game in Steam, then `Properties...`, then `General`.
+
+If you are using BSManager, you can enter these by opening the `Advanced launch` option on the game launch section.
+BSManager also already provides FPFC and Debug modes, which correspond to `fpfc` and `--verbose` respectively.
+
+
+
+| Argument | Description |
+| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `--verbose` | Enables the output log window for IPA. This will show the debug console that mods use. This is a must-have for all modders. |
+| `--debug` | Enables 'debug' level logs to show up in the log output window. These would otherwise normally only show up in log files. |
+| `--trace` | Enables 'trace' level logs to show up in the log output window. These are typically reserved for overly-detailed logs. |
+| `fpfc` | The "First Person Flying Controller" is a base-game feature that allows you to use WASD and mouse to control the camera without VR. This makes for easy testing. |
+| `--auto_play` | A base-game feature since version 1.37.1, it enables a basic auto player. This is useful for testing gameplay without playing yourself. |
+| `-vrmode oculus` | Only works on versions 1.29.1 and older. Allows you to play without SteamVR when playing the game from Steam. |
+
+
+
+## Other Links
+
+Notable links mentioned in the PC modding wiki:
+
+- [BSIPA Documentation](https://nike4613.github.io/BeatSaber-IPA-Reloaded/articles/start-dev.html)
+- [JetBrains Rider](https://www.jetbrains.com/rider/)
+- [BSMT For JetBrains Rider](https://github.com/Fernthedev/BSMT-Rider/)
+- [BSMT For Visual Studio](https://github.com/Zingabopp/BeatSaberTemplates/)
+- [C# Documentation](https://learn.microsoft.com/en-us/dotnet/csharp/)
+- [Unity Scripting API](https://docs.unity3d.com/ScriptReference/index.html)
+- [Runtime Unity Editor](https://github.com/ManlyMarco/RuntimeUnityEditor)
+- [ILSpy](https://github.com/icsharpcode/ILSpy)
+- [dnSpy](https://github.com/dnSpyEx/dnSpy)
+- [Harmony](https://github.com/pardeike/Harmony)
+- [Harmony Documentation](https://harmony.pardeike.net/articles/patching.html)
+- [Zenject](https://github.com/Mathijs-Bakker/Extenject)
+- [BSMG Discord](https://discord.gg/beatsabermods)
+- [BeatMods](https://beatmods.com)
+- [BeatMods Approval Guidelines](https://docs.google.com/document/d/15RBVesZdS-U94AvesJ2DJqcnAtgh9E2PZOcbjrQle5Y/edit?usp=sharing)
diff --git a/docs/modding/pc/rue.md b/docs/modding/pc/rue.md
new file mode 100644
index 0000000..4475995
--- /dev/null
+++ b/docs/modding/pc/rue.md
@@ -0,0 +1,35 @@
+---
+prev: Setup Guide
+next: Decompiling
+---
+
+# Object Inspectors
+
+An essential tool for modding is a game object inspector.
+
+## Runtime Unity Editor
+
+[Runtime Unity Editor (RUE)](https://github.com/ManlyMarco/RuntimeUnityEditor) is a tool that we can use to look at different
+components in-game while playing. It will allow us to find objects by name, components attached to GameObjects, and
+tweak properties of these while the game is running.
+
+It's important to get used to using RUE because figuring out the game through code will take ten times as much trial-and-error.
+In order to get RUE, currently, you can download it from a pinned message you will find in the `#pc-mod-dev` channel in the
+[BSMG discord](https://discord.gg/beatsabermods).
+
+You will have to [manually install](../../pc-modding.md#manual-installation) RUE by dragging the Libs and Plugins within
+the zip into your game. Once installed, you can open the game in FPFC mode, then press `G` to open RUE.
+
+You can configure the keybinding in `/UserData/Runtime Unity Editor (BSIPA).json` which is recommended because `G` is also
+the default keybinding for SiraUtil's FPFC toggle feature.
+
+
+
+
+
+## UnityExplorer
+
+An alternative to Runtime Unity Editor is [UnityExplorer](https://github.com/yukieiji/UnityExplorer),
+which is also regularly used for Beat Saber modding. You can find all details on how to install UnityExplorer
+[here](https://github.com/yukieiji/UnityExplorer?tab=readme-ov-file#standalone), but, because we use BSIPA, you will
+have to either build it yourself or search around in BSMG for someone who has already done this.
diff --git a/docs/modding/pc/setup.md b/docs/modding/pc/setup.md
new file mode 100644
index 0000000..83b5f9a
--- /dev/null
+++ b/docs/modding/pc/setup.md
@@ -0,0 +1,183 @@
+---
+prev: false
+next: Runtime Unity Editor
+---
+
+# PC Mod Development Intro
+
+_Learn how to get started writing your own PC Mods._
+
+## Getting Started
+
+::: warning
+This guide is for making mods for the **PC** version of Beat Saber!
+
+If you want to develop mods for the **Quest Standalone** version of the game, visit
+the [Quest Mod Development Guide](../quest/intro.md)
+
+Make sure your game is modded before trying to make a mod.
+See instructions for [modding Beat Saber on PC.](../../pc-modding.md)
+
+This guide assumes you have a basic to intermediate understanding of C# and Unity.
+You may have difficulty understanding what is covered here if you do not have this foundation.
+:::
+
+Beat Saber is made in Unity 2022.3 using C# with .NET framework 4.7.2. To make writing and building mods as simple as
+possible you will need to download an IDE that supports Unity.
+
+This guide
+will be focused on [JetBrains Rider](https://www.jetbrains.com/rider/), however you can also
+use [Microsoft Visual Studio Community](https://visualstudio.microsoft.com/). Both of these are good options, however,
+the guide for Rider users is more up-to-date.
+
+We will now cover setting up Rider for modding. For Visual Studio users, refer to the
+[Visual Studio Setup](./vs-setup.md) page.
+
+## Modding Tools Setup
+
+We will be using the BeatSaberModdingTools (BSMT) extension in this tutorial, as it comes with modding templates and
+useful features, like saving your Beat Saber directory.
+
+Firstly, download and install [Rider](https://www.jetbrains.com/rider/) from their website. Rider is free for
+non-commercial use.
+
+The Rider extension can be downloaded from [GitHub](https://github.com/Fernthedev/BSMT-Rider/releases/latest). Download
+the BSMT Rider zip.
+
+Once you have installed Rider open it and, after signing in, you will be greeted by the welcome window. In the bottom
+left, click `Configure`, click `Settings`, then look for `Plugins`.
+
+Next to `Marketplace` and `Installed` there will be a settings icon, click this, and click
+`Install Plugin from Disk...`. From here, find the BSMT Rider zip you downloaded and select it, this will install the
+plugin in Rider.
+
+
+
+## Template setup
+
+BSMT comes with some working plugin templates to get you started as quickly as possible.
+
+Create a new solution and, if you installed BSMT correctly, you should be able to select a plugin template from the
+`Custom Templates` list. We are going to use the bare template in this example and, later on, we will be building
+[a functional mod completely from scratch](./full-mod-guide.md).
+
+
+
+Choose a name for your mod and the location you want to save it. Do not save the solution in your Beat Saber
+installation folder lest you lose it.
+
+Once you're done, click `Create` and the mod template will open. Next, you will receive a popup asking you to set your
+Beat Saber Directory.
+
+
+
+Select your Beat Saber game's installation, you can also use a BSManager instance here too. If you select
+`Store this beat saber folder in config`, BSMT will remember this directory whenever you reopen a project.
+
+At this point, **try and build the project**, and it should automatically find the
+references for you and the build should succeed if you set a valid Beat Saber installation directory. You can do this
+with the build hotkey or the button on the top bar.
+
+
+
+If you get any immediate errors, you may want to double-check the Beat Saber directory you provided. You can change it
+by navigating to the `Tools` section at the top of the Rider window, and locating the `BSMT Project Tools` option. If
+you still get errors, you can try restarting Rider.
+
+Once again, if you have any issues you can't resolve, you can always
+ask questions in the `#pc-mod-dev` channel in the BSMG discord.
+
+If you need to manually add Beat Saber assembly or other mod references, right click on `Dependencies` in the Project
+folder, then `Add Beat Saber assembly references`. This will let you search for Beat Saber assemblies, and it will add
+them to the `.csproj` for you.
+
+
+
+## Inspecting the Code
+
+Open the explorer on the right side of Rider and you should see all the project files.
+
+| Filename | About |
+| ------------------------ | ------------------------------------------------------------------------------- |
+| `PluginName.csproj` | This is the C# project that contains build information. |
+| `PluginName.csproj.user` | This is where the Beat Saber directory is saved. BSMT will manage this for you. |
+| `Plugin.cs` | The main file that is loaded for your mod. This is the entry point for BSIPA. |
+| `Directory.Build.props` | Contains metadata for your plugin like the version, links, dependencies etc. |
+
+## Edit your mod's manifest
+
+### Defining Metadata
+
+Open `Directory.Build.props` and fill in your mod's information in the Plugin Metadata `PropertyGroup`:
+
+- The `PluginId` and `PluginName` keys are used to identify your mod. Mods that will be uploaded to BeatMods typically
+ should have these be exactly the same and have no spaces.
+- The `Authors` is where you use your name.
+- The `Version` is the version of your mod. This follows [Semantic Versioning](https://semver.org).
+- The `GameVersion` is the exact version of the game you are making the mod for. It's recommended to make mods for the latest
+ version of the game with mod support.
+- In the `Description` provide a short sentence or two about what your mod is/does.
+
+There are also some optional properties you can add:
+
+- The `ProjectSource` is a URL to the source code of your mod. Most mods have their source code open on GitHub, for
+ instance.
+- The `ProjectHome` can be a URL to a website where your mod is downloaded from or hosted.
+- You can also specify a `Donate` URL, which if you want to, you can set up a way for people to support your modding
+ work.
+- The `PluginIcon` is a path to a `.png` file that can be pulled from your plugin.
+
+### Defining Dependencies
+
+Underneath the plugin metadata is an `ItemGroup` that declares which other mods are required for your mod to work.
+
+::: warning
+Do not remove the dependency on BSIPA. This is required by BSIPA itself.
+:::
+
+The template in this case only needs BSIPA to work. Add additional `DependsOn` members for each dependency.
+
+Some example mod libraries that are commonly used could be BeatSaberMarkupLanguage, which is used to generate custom
+menus in Beat Saber, or SiraUtil, which is used to interface with the game's Zenject system to easily access certain
+game objects and build robust large plugins. These will be briefly covered with some examples later on this wiki.
+
+### Additional Properties
+
+- If your mod breaks in the presence of another mod due to conflicting behavior, you should add it as a `ConflictsWith`
+ member, which will make your plugin not load if the specified conflicting mod is installed.
+- If your mod interacts with other mods but does not need them in order to function, consider adding `LoadAfter` to
+ ensure your mod doesn't try to interact with them before they are loaded by BSIPA.
+- Similarly, you can add `LoadBefore` members to make your mod load before the specified mod.
+- If you want to move `Plugin.cs` to somewhere else in the project, use `PluginHint` to specify where it is so that
+ BSIPA can find it.
+- You can add numerous `RequiredFile` properties to specify external files required by the mod, typically used for libraries.
+
+Once you've set all of this, BSMT will automatically generate an embedded `manifest.json` in your mod during build,
+which is required by BSIPA and can be used to pull information about the mod.
+
+This data can also be pulled from BSIPA to be used within your mod, and by other mods.
+
+## Compiling
+
+After running the build, your compiled DLL should automatically be copied to the `Plugins` folder in your Beat Saber
+directory, which will be done for both debug and release builds.
+
+When you are ready to release your mod, find the dropdown next to the build icon, and select the `Release` option to
+make a Release build of your mod. Building in `Release` mode will generate a packaged `.zip` file ready to distribute.
+This zip file should appear in `\bin\Release\net472\zip\` but you can always look at the build output tab to find the
+zip destination directory.
+
+## Testing your mod in-game
+
+To test if your mod is loaded in-game, you will need to launch Beat Saber with the BSIPA Console enabled. For more
+information on launch arguments, see [here](./index.md#launch-args).
+
+When you launch the game, you should see BSIPA load your mod in the console window.
+
+
+
+If you got this far, congratulations! You are now set up to create mods for Beat Saber.
+
+From here, you should consider checking out the other pages of this wiki to learn about some of the libraries modders
+use to add functionality to their mods, as well as learning to use some essential tools. If it helps, you can follow
+the [full mod guide](./full-mod-guide.md) too, which will cover designing a mod from scratch.
diff --git a/docs/modding/pc/testing.md b/docs/modding/pc/testing.md
new file mode 100644
index 0000000..76f1dfd
--- /dev/null
+++ b/docs/modding/pc/testing.md
@@ -0,0 +1,94 @@
+---
+prev: false
+next: false
+description: Learn how to create your own PC mods!
+---
+
+# Testing
+
+It's important to make sure your mod doesn't unintentionally break base game functionality or other mods.
+This page contains tips on how to test your mod properly.
+
+## Testing Checklist
+
+- If your mod affects something while playing a map, make sure to test all the modes (Solo, Campaign, Party, Online,
+ and Tutorial). There are variations between the modes so a class or an object you expect to exist might not be there
+ or could have a different name!
+- Check that things still work properly after an internal restart. The easiest way to make the game internally restart
+ is to go to Settings and press OK. The game will destroy and recreate various objects when this happens so you need to
+ make sure your mod is picking up the new instances properly.
+- Try to test with as many publicly available mods installed as you can. There might be some unexpected conflicts!
+- Use a debug Unity build while testing as explained below.
+
+## Using a Debug Unity Build
+
+::: warning IMPORTANT
+It is **highly recommended** to test your mod by using a debug Unity build, especially if you are doing any kind of
+multithreading. It helps identify issues that can result in hard crashes to desktop that are otherwise very hard
+to debug since Unity strips a lot of checks on release builds. Mods are tested using a debug build when being reviewed
+for approval on [BeatMods](https://beatmods.com) and any exception thrown by Unity is grounds for denial.
+:::
+
+First, download the version of Unity the game is using. We highly recommend using
+[Unity Hub](https://unity.com/unity-hub) to manage your Unity installations. The game's Unity version won't usually be
+available directly in the Hub application since it's usually an older LTS version, but you can find all Unity versions
+in [the Unity download archive](https://unity.com/releases/editor/archive). You can find the current version of Unity
+the game is using by checking your logs; it'll be right above the list of plugins:
+
+```log
+...
+[INFO @ 00:00:00 | IPA] Beat Saber
+[INFO @ 00:00:00 | IPA] Running on Unity 2022.3.33f1
+[INFO @ 00:00:00 | IPA] Game version 1.40.4
+[INFO @ 00:00:00 | IPA] -----------------------------
+[INFO @ 00:00:00 | IPA] Loading plugins from Plugins and found 1
+[INFO @ 00:00:00 | IPA] -----------------------------
+...
+```
+
+Once you've installed the required version of Unity, navigate to the install folder. In Unity Hub, you can do this
+by going to the _Installs_ tab, pressing the cog on the top right corner of the version's box, and pressing
+_Show in Explorer_.
+
+
+
+Once you've opened the folder, navigate to
+`Data\PlaybackEngines\windowsstandalonesupport\Variations\win64_player_development_mono`. The contents should look
+like below.
+
+
+
+Select and copy the `UnityCrashHandler`, `UnityPlayer`, `WindowsPlayer`, and `WinPixEventRuntime` files as shown above,
+then paste them into your Beat Saber installation's folder. This will overwrite some files; feel free to move or rename
+the files that would be overwritten out if you want to swap between the release & debug builds more easily. Once you've
+pasted the new files, delete/rename/move the `Beat Saber.exe` file, and rename `WindowsPlayer.exe` to `Beat Saber.exe`.
+
+::: details Using a Batch script to swap between debug and release
+If you want to swap between a release and a debug build often, you can use a batch script like the one below. Simply
+add `.bak` to `UnityPlayer.dll`, `UnityCrashHandler64.exe`, and `Beat Saber.exe` (make sure you have file extensions
+enabled in Windows Explorer or else this won't work properly), then copy the files from `win64_player_development_mono`
+as explained above. Once that's done, create a new file called `debug.bat` (or whatever name you want as long as it
+ends in `.bat`) and paste the contents below into that file. Double-click this new file to swap between the release
+and debug builds.
+
+```batch
+move UnityPlayer.dll UnityPlayer.dll.tmp
+move UnityPlayer.dll.bak UnityPlayer.dll
+move UnityPlayer.dll.tmp UnityPlayer.dll.bak
+
+move UnityCrashHandler64.exe UnityCrashHandler64.exe.tmp
+move UnityCrashHandler64.exe.bak UnityCrashHandler64.exe
+move UnityCrashHandler64.exe.tmp UnityCrashHandler64.exe.bak
+
+move "Beat Saber.exe" "Beat Saber.exe.tmp"
+move "Beat Saber.exe.bak" "Beat Saber.exe"
+move "Beat Saber.exe.tmp" "Beat Saber.exe.bak"
+```
+
+:::
+
+That's it! You should now be able to start the game as usual. If all went according to plan, you should see the
+"Development Build" text at the bottom right of the screen when in FPFC, and whenever an error occurs the development
+console will show up on the game window.
+
+
diff --git a/docs/modding/pc/vs-setup.md b/docs/modding/pc/vs-setup.md
new file mode 100644
index 0000000..a55f262
--- /dev/null
+++ b/docs/modding/pc/vs-setup.md
@@ -0,0 +1,76 @@
+---
+prev: false
+next: false
+---
+
+# Modding Tools Setup for Visual Studio
+
+We will be using the BeatSaberModdingTools (BSMT) extension in this tutorial,
+as it comes with modding templates and useful features, like saving your Beat Saber directory.
+
+The Visual Studio extension can be downloaded
+from [GitHub](https://github.com/Zingabopp/BeatSaberTemplates/releases/latest). You will need to download
+`BeatSaberModdingTools.vsix`. (Expand the Assets dropdown if you cannot find it)
+
+Once downloaded, open the `.vsix` and it will install itself as a Visual Studio Plugin.
+If you have any issues, consult the project's [README](https://github.com/Zingabopp/BeatSaberModdingTools#readme)
+and [WIKI](https://github.com/Zingabopp/BeatSaberModdingTools/wiki).
+
+## Template setup
+
+BSMT comes with some working plugin templates to get you started as quickly as possible.
+
+First, create a new project and find a template. We are going to use the `BSIPA4 Plugin (Core)` template, and we'll be
+calling our mod `BSPlugin1`.
+You should change the name to whatever you want to call your mod.
+
+
+
+
+You will then need to set your Beat Saber Directory in Visual Studio.
+Follow the instructions [on the template readme](https://github.com/Zingabopp/BeatSaberModdingTools#how-to-use),
+or see the screenshot below.
+
+
+
+At this point, **try and build the project**, and it should automatically find the
+references for you and the build should succeed.
+
+If your build does not succeed, check that you don't have any missing references.
+
+::: tip
+BeatSaberModdingTools will automatically handle references. If your references could not be
+found, [double-check the instructions](https://github.com/Zingabopp/BeatSaberModdingTools#beat-saber-modding-tools).
+
+If you need to manually add references, right click on `References` in the Project folder, then
+`Beat Saber Reference Manager...`.
+Select your references, then click "Apply".
+
+You can find more information about the reference
+manager [here](https://github.com/Zingabopp/BeatSaberModdingTools/wiki/Adding-References).
+:::
+
+## Inspecting the Code
+
+You should have 5 files open automatically with the template.
+
+| Filename | About |
+| ------------------------ | ------------------------------------------------------------------------------ |
+| `manifest.json` | Information about your mod for BSIPA. |
+| `Plugin.cs` | The main file that is loaded for your mod. |
+| `AssemblyInfo.cs` | File information about your mod. This is mostly managed by Modding Tools. |
+| `PluginConfig.cs` | A template for enabling config for your mod. This is commented out by default. |
+| `BSPlugin1Controller.cs` | A generic MonoBehaviour for your mod. |
+
+### Edit your mod's Manifest
+
+Fill out the `manifest.json` file with your information.
+The `name` and `id` keys are used to identify your mod.
+The ID should match the ID used when uploading your mod to BeatMods.
+
+::: warning
+Do **not** remove the dependency on BSIPA. As of BSIPA v4.1 this is required for your mod to load.
+:::
+
+After you're done with the setup, you can return to the main
+[PC mod dev intro page](./setup.md#compiling) to find out how to run your mod in game!
diff --git a/docs/modding/pc/zenject.md b/docs/modding/pc/zenject.md
new file mode 100644
index 0000000..fbde721
--- /dev/null
+++ b/docs/modding/pc/zenject.md
@@ -0,0 +1,517 @@
+---
+prev: Creating UI
+next: Step-by-step Mod Tutorial
+---
+
+# Zenject Introduction
+
+Zenject is what is called a Dependency Injection (DI) Framework, and Beat Saber's code uses it extensively. You can read
+more about DI on [Microsoft's docs](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) and
+on [Wikipedia](https://en.wikipedia.org/wiki/Dependency_injection).
+
+## What Is Dependency Injection
+
+Trying to explain dependency injection usually makes it sound a lot more complex than it is. In short, it's when you delegate
+the responsibility certain functionality in your code to "dependencies" and "injecting" them into objects upon their creation.
+
+That is all DI is, but let's look at a simple C# example:
+
+```c#
+public interface IService
+{
+ public int GetNumber();
+}
+
+internal class ServiceImplementation : IService
+{
+ public int GetNumber()
+ {
+ // Implement this method
+ }
+
+ // Some other private behaviour
+}
+```
+
+Now, we have an interface that provides the result of `GetNumber()`. Let's say we needed this behaviour in another object:
+
+```c#
+internal class SomeObject
+{
+ private readonly IService service;
+ private readonly List numbers = [];
+
+ public SomeObject(IService service)
+ {
+ this.service = service;
+ }
+
+ void Update()
+ {
+ if (numbers.Count > 5)
+ {
+ numbers.Clear();
+ }
+
+ int number = _service.GetNumber();
+ numbers.Add(number);
+ }
+}
+```
+
+As we can see, when we create `SomeObject` we have to provide an instance of `IService` because it depends on the service.
+The field `_numbers` is not a dependency in this case; it is just data that belongs to `SomeObject`.
+
+This is essentially all you need to know to understand dependency injection, but the
+[Zenject README](https://github.com/Mathijs-Bakker/Extenject?tab=readme-ov-file#what-is-dependency-injection)
+goes a bit more in-depth about the what and why of DI.
+
+By using dependency injection, you are able to more easily define the behaviour that each feature needs. If you need to
+make changes in the future, your code will have enough abstractness that you should not have to go into every part of
+the code to make everything work together.
+
+## What Is Zenject
+
+Now that you have some idea of what DI looks like, all Zenject does is makes the process of maintaining DI easy. Zenject
+has a lot of different features but it would be pointless to cover them all here, but you can always check the
+[GitHub README](https://github.com/Mathijs-Bakker/Extenject) to learn more about all of its features.
+
+Zenject lets create us objects by declaring their "contract binding" in what they call an `Installer`. We can give keys
+to dependencies, we can provide specific methods to create objects, we can declare multiple implementations of the same
+interface, and more.
+
+## Using Zenject In Mods
+
+In order to easily access the game's implementation of Zenject, we use a library called [SiraUtil](https://github.com/Auros/SiraUtil).
+This is used in a wide variety of mods and it allows us to take full advantage of dependency injection without much
+extra effort.
+
+Before doing anything, add an assembly reference to `SiraUtil`, `Zenject`, and `Zenject-usage`. Make sure you add
+`SiraUtil` as a dependency in your plugin metadata.
+
+### Implementing Zenject
+
+First, add a `Zenjector` param to your plugin class `[Init]` method:
+
+```c#
+[Init]
+public Plugin(Zenjector zenjector)
+{
+
+}
+```
+
+The `Zenjector` will allow you to access the game's `Installer`s and let you make your own bindings with them.
+
+Let's now look at the class we will be using to test Zenject:
+
+```C#
+internal class Test : IInitializable
+{
+ private readonly SiraLog log;
+
+ public Test(SiraLog log) { this.log = log; }
+
+ public void Initialize() => log.Info("Initializable test");
+}
+```
+
+We pass a `SiraLog` instance to this object in the constructor. This is a service provided by SiraUtil, and acts
+as an instance-based logger.
+
+This class implements [IInitializable](https://github.com/Mathijs-Bakker/Extenject?tab=readme-ov-file#iinitializable),
+which is an interface provided by Zenject. The `Initialize()` method gets called after all objects have been created,
+and on Unity's [Start](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/MonoBehaviour.Start.html) event.
+This is ideally where initialization logic for your object would go.
+In this test case, all it does is log a message when created.
+
+Let's make a binding to test this behaviour - we provide an installer and use the callback with the `DiContainer`
+to make a binding:
+
+```c#
+public Plugin(Zenjector zenjector)
+{
+ zenjector.Install(container =>
+ {
+ container.Bind().To().AsSingle();
+ });
+}
+```
+
+What we are doing here is binding `Test` with its `IInitializable` interface on the DiContainer for the
+`StandardGameplayInstaller`. The `AsSingle` method ensures only one instance of `Test` can be bound.
+
+If you build this now and play any map in solo, you will see the "Initializable test" message appear in the console
+when the scene transition ends.
+
+However, the `SiraLog` that we used doesn't have a base logger to use, so the
+source appears as `???`. In order to fix this, we can just provide the `Zenjector` with IPA's logger:
+
+```c#
+zenjector.UseLogger(logger);
+```
+
+### Cleaning Up
+
+It's recommended to organize your bindings in your own installers. Create an installer, and override the
+`InstallBindings()` method:
+
+```c#
+internal class TutorialInstaller : Installer
+{
+ public override void InstallBindings()
+ {
+ Container.BindInterfacesTo().AsSingle();
+ }
+}
+```
+
+We have also made use of the [BindInterfacesTo](https://github.com/Mathijs-Bakker/Extenject?tab=readme-ov-file#bindinterfacesto-and-bindinterfacesandselfto)
+method here, which is just a shortcut so you don't have to remember what interfaces your type implements. It is good to know
+the full expression in case you want to make it clear that you are implementing an interface that will be used as a
+dependency throughout your code.
+
+Now, we just specify the installer to the `Zenjector` with either a base installer to install upon, or by using the
+`location` enum argument to specify a common location:
+
+```c#
+public Plugin(Zenjector zenjector, IPALogger logger)
+{
+ zenjector.UseLogger(logger);
+ zenjector.Install(Location.StandardPlayer);
+}
+```
+
+By doing this we have made the `Plugin` class just responsible for defining the contexts in which the plugin operates
+in, whilst the installers declare the interface of the code.
+
+## Types Of Injection
+
+So far we've only covered injecting dependencies through a constructor, however, there are multiple ways to achieve this
+goal with Zenject.
+
+### Constructors
+
+As covered before, constructor injection is the main form of injection. They force the dependencies to only be resolved
+at object creation, the dependencies are immediately apparent, and they guarantee no circular dependencies which
+encourages better design.
+
+```c#
+internal class SomeObject
+{
+ private readonly IService service;
+
+ public SomeObject(IService service) { this.service = service; }
+}
+
+internal record SomeOtherObject(IService Service);
+```
+
+Unfortunately, MonoBehaviours cannot have constructors, so you are left with method and field injection for those.
+
+### Methods
+
+The `Inject` attribute can be used on methods, and with it we can treat methods just like constructors by supplying the
+dependencies in the params for the method.
+
+```c#
+internal class SomeBehavior : MonoBehaviour
+{
+ private IService service = null!;
+
+ [Inject]
+ public void Init(IService service) { this.service = service; }
+}
+```
+
+As you can see, this example is using a `MonoBehaviour`. Since MonoBehaviours cannot have constructors, this is the
+preferred way to do injection on them. It looks a lot like a constructor which makes the intention of this code
+slightly more clear. That being said, you can use field injection on MonoBehaviours too.
+
+A problem with this approach is that you can't make the field readonly. This can make the code's intent less clear,
+as a field that isn't readonly implies it might be open to changing; you usually aren't going to be changing the
+value of dependencies.
+
+### Fields And Properties
+
+Field and property injections occur directly after the constructor finishes. This is achieved by adding `[Inject]` to any
+field or property.
+
+```c#
+internal class SomeBehavior : MonoBehaviour
+{
+ [Inject]
+ private readonly IService service = null! // assigned by Zenject
+}
+```
+
+Since Zenject uses [reflection](https://learn.microsoft.com/en-us/dotnet/fundamentals/reflection/reflection)
+to set these fields, you can make them private and readonly. This is great for demonstrating the intention of the code,
+but field injection can look a bit cryptic for others looking at the code.
+
+## Common DiContainer Methods
+
+There are dozens of methods to create a binding as seen in the documentation, so let's highlight a few ways of creating
+bindings that you will be mostly using.
+
+
+
+| Name | Description |
+| ---------------------------------- | --------------------------------------------------------------------------------------------------- |
+| `Bind` | Registers the type `T` for injection for itself and other types |
+| `BindInstance` | Registers the type of the provided existing object instance |
+| `BindInterfacesTo` | Registers the interfaces for the type `T` for injection |
+| `BindInterfacesAndSelfTo` | A combination of `Bind` and `BindInterfacesTo` |
+| `AsCached` | The same instance of the object will be reused |
+| `AsSingle` | The same as `AsCached` but ensures only one binding can be made for the result type |
+| `AsTransient` | Instances of the result type will not be reused; a new one will be created each time it's requested |
+| `FromNewComponentOnNewGameObject` | Create an empty `GameObject` and add a new component of the result type on it |
+| `FromNewComponentAsViewController` | Provided by SiraUtil; creates a new view controller - result type must inherit `ViewController` |
+
+
+
+## Zenject With UI
+
+Once you have your SiraUtil setup, you can easily declare all menu-related code in a installer in the menu.
+
+```c#
+zenjector.Install(Location.Menu);
+```
+
+### Binding View Controllers
+
+SiraUtil provides a way to create view controllers easily using `FromNewComponentAsViewController`. You
+can also bind a flow coordinator, but since it is a `MonoBehaviour`, you should use `FromNewComponentOnNewGameObject`,
+or any compatible construction method.
+
+```c#
+internal class MenuInstaller : Installer
+{
+ public override void InstallBindings()
+ {
+ Container.Bind().FromNewComponentAsViewController().AsSingle();
+ Container.Bind().FromNewComponentOnNewGameObject().AsSingle();
+ Container.BindInterfacesTo().AsSingle();
+ }
+}
+```
+
+Now, we would be able to inject our view controllers into the flow coordinator, and we can also inject the
+`MainFlowCoordinator` to make use of it for the menu button.
+
+Additionally, as seen before with the `SiraLog`, we can use bindings made by other mods. Another case is the `MenuButtons`
+class from BSML:
+
+```c#
+internal class MenuButtonManager : IInitializable
+{
+ private readonly MenuButtons menuButtons;
+ private readonly MainFlowCoordinator mainFlowCoordinator;
+ private readonly TutorialFlowCoordinator tutorialFlowCoordinator;
+ private readonly MenuButton menuButton;
+
+ public MenuButtonManager(MenuButtons menuButtons, MainFlowCoordinator mainFlowCoordinator, TutorialFlowCoordinator tutorialFlowCoordinator)
+ {
+ this.menuButtons = menuButtons;
+ this.mainFlowCoordinator = mainFlowCoordinator;
+ this.tutorialFlowCoordinator = tutorialFlowCoordinator;
+ menuButton = new("Tutorial Mod", ShowFlowCoordinator);
+ }
+
+ public void Initialize()
+ {
+ menuButtons.RegisterButton(menuButton);
+ }
+
+ private void ShowFlowCoordinator()
+ {
+ mainFlowCoordinator.PresentFlowCoordinator(tutorialFlowCoordinator);
+ }
+}
+```
+
+This seems more complex than it would be without Zenject, however, Zenject will call `Initialize` for
+us on the first frame of the menu scene being loaded. Most importantly, this class is only responsible
+for doing one thing: managing the menu button.
+
+### Registering Custom Tags
+
+If you have some custom UI tags that you want to use, it's recommended to bind them using Zenject. You
+would bind them like this in a menu installer:
+
+```c#
+Container.Bind().To().AsSingle();
+Container.Bind().To().AsSingle();
+```
+
+## Affinity Patching
+
+SiraUtil provides a way to make non-static [Harmony patches](./harmony-patching.md) using the "Affinity
+API". Being able to make patch methods not static lets you make use of dependency injection for your
+patches.
+
+The syntax is mostly the same, however, Affinity is a lot more limited than Harmony. For the attributes, you must specify
+a `AffinityPatch` attribute on every patch method, and you need to specify a patch type using either `AffinityPostfix`,
+`AffinityPrefix`, or `AffinityTranspiler`. Do note - if you don't provide a patch type attribute then affinity will default
+to a postfix.
+
+### How To Affinity
+
+Below is an example of an affinity patch taken from the SiraUtil documentation. It injects the `PauseController` and causes
+the game to pause every 10 misses and cancels the miss by using a [prefix](./harmony-patching.md#prefix).
+
+```c#
+internal class PauseOnXMisses : IAffinity
+{
+ private readonly PauseController pauseController;
+
+ public PauseOnXMisses(PauseController pauseController)
+ {
+ this.pauseController = pauseController;
+ }
+
+ private int misses = 0;
+
+ [AffinityPrefix]
+ [AffinityPatch(typeof(ScoreController), nameof(ScoreController.HandleNoteWasMissed))]
+ private bool HandleNoteWasMissedPrefix(NoteController noteController)
+ {
+ if (noteController.colorType == ColorType.None && misses++ < 10)
+ {
+ return true;
+ }
+
+ pauseController.Pause();
+ misses = 0;
+ return false;
+ }
+}
+```
+
+As you can see, you just need to add the `IAffinity` interface to the patch class, then you need to bind it in a gameplay
+related installer so that you have access to the `PauseController`.
+
+```c#
+Container.BindInterfacesTo().AsSingle();
+```
+
+### Affinity's Limitations
+
+Affinity is maintained separately from Harmony, so it doesn't have nearly as many features as Harmony does.
+
+The main problem is the timing of the patch. Your patch will only be effective after the object graph is constructed,
+so you can't patch `Awake` methods or constructors, for instance.
+
+Secondly, your patches will be unapplied automatically when the DiContainer it was bound to is disposed, but this should
+be fine in almost all cases.
+
+## Custom Sabers
+
+SiraUtil provides a unified way to replace the vanilla saber model, such that mods do not fight over which saber model
+gets shown.
+
+### Registering A Saber Model
+
+Create a class which inherits from a `SaberModelController`, create the saber model registration, and bind it in a game
+installer. You will have to provide a priority too so SiraUtil can decide which registration to use when there are
+multiple.
+
+```c#
+internal class CustomSaberModelController : SaberModelController { }
+```
+
+```c#
+var registration = SaberModelRegistration.Create(0);
+Container.BindInstance(registration).AsSingle();
+```
+
+### Additional Interfaces
+
+`IColorable` will provide a property which receives a color when one is set by SiraUtil. This is primarily used by
+Chroma to set the color of sabers to the color of Chroma-colored notes.
+
+```c#
+internal class CustomSaberModelController : SaberModelController, IColorable
+{
+ public Color Color { get; set; } // Add behaviour on the setter
+}
+```
+
+`IPreSaberModelInit` and `IPostSaberModelInit` provide methods which will be called before and after the `Init()`
+method of the `SaberModelController` and also provide a reference to the original `Saber` and saber parent `Transform`.
+
+The return type of `PreInit()` is `bool`, and it works just like Harmony prefixes; you should return `true` if you
+want the original `Init` to run, otherwise return `false`.
+
+```c#
+internal class CustomSaberModelController
+ : SaberModelController, IPreSaberModelInit, IPostSaberModelInit
+{
+ public bool PreInit(Transform parent, Saber saber) => true;
+ public void PostInit(Transform parent, Saber saber) { }
+}
+```
+
+## Object Redecorating
+
+Similarly to registering saber models, SiraUtil provides a way to modify the prefabs for various GameObjects before
+they are bound in their installers.
+
+As well as a priority, you can decide if it should be chained, which is useful if your redecoration doesn't causes
+conflicts. SiraUtil will start at the registration with the highest priority, and if it has chaining, it will continue
+to the next highest priority registration until it encounters a registration that doesn't have chaining.
+
+The following example simply takes the `GameObject` of the `BombController` provided by the param of the `BombNoteRegistration`,
+and adds a `CustomBombBehaviour` to it.
+
+```c#
+var bombNoteRegistration = new BombNoteRegistration(
+ redecorateCall: bomb =>
+ {
+ bomb.gameObject.AddComponent();
+ return bombNoteController;
+ },
+ priority: int.MaxValue,
+ chain: true);
+
+Container.RegisterRedecorator(bombNoteRegistration);
+```
+
+Below is a collection of all possible redecorators provided by SiraUtil as of v3.1.14.
+
+### Notes
+
+| Name | Backing Prefab Type |
+| --------------------------------- | ---------------------------------------------- |
+| `BasicNoteRegistration` | `GameNoteController` |
+| `ProModeNoteRegistration` | `GameNoteController` |
+| `BurstSliderHeadNoteRegistration` | `GameNoteController` |
+| `BombNoteRegistration` | `BombNoteRegistration` |
+| `BurstSliderNoteRegistration` | `BurstSliderGameNoteController` |
+| `LongSliderNoteRegistration` | `SliderController` |
+| `MediumSliderNoteRegistration` | `SliderController` |
+| `ShortSliderNoteRegistration` | `SliderController` |
+| `ConnectedPlayerNoteRegistration` | `MultiplayerConnectedPlayerGameNoteController` |
+
+### Debris
+
+| Name | Backing Prefab Type |
+| ----------------------------------------- | ------------------- |
+| `NormalNoteDebrisHDRegistration` | `NoteDebris` |
+| `NormalNoteDebrisLWRegistration` | `NoteDebris` |
+| `BurstSliderHeadNoteDebrisHDRegistration` | `NoteDebris` |
+| `BurstSliderHeadNoteDebrisLWRegistration` | `NoteDebris` |
+| `BurstSliderElementNoteHDRegistration` | `NoteDebris` |
+| `BurstSliderElementNoteLWRegistration` | `NoteDebris` |
+
+### Multiplayer
+
+| Name | Backing Prefab Type |
+| ----------------------------------- | -------------------------------------- |
+| `LocalActivePlayerRegistration` | `MultiplayerLocalActivePlayerFacade` |
+| `LocalActivePlayerDuelRegistration` | `MultiplayerLocalActivePlayerFacade` |
+| `ConnectedPlayerRegistration` | `MultiplayerConnectedPlayerFacade` |
+| `ConnectedPlayerDuelRegistration` | `MultiplayerConnectedPlayerFacade` |
+| `LobbyAvatarPlaceRegistration` | `MultiplayerLobbyAvatarPlace` |
+| `LobbyAvatarRegistration` | `MultiplayerLobbyAvatarController` |
+| `LocalInactivePlayerRegistration` | `MultiplayerLocalInactivePlayerFacade` |
diff --git a/docs/modding/quest/config.md b/docs/modding/quest/config.md
new file mode 100644
index 0000000..5677c2c
--- /dev/null
+++ b/docs/modding/quest/config.md
@@ -0,0 +1,108 @@
+---
+prev: false
+next: false
+description: Learn how to create create mod configs for your Quest Mod!
+---
+
+# Quest Mod Configuration
+
+Most mods require a configuration to allow users to change the functionality of the mod.
+
+This section will guide you through the basics of using `config-utils` to create configuration for your mod.
+
+## Prerequisites
+
+- Install `config-utils` by running `qpm dependency add config-utils` in your project directory.
+
+Make sure to restore after adding the dependencies.
+
+## Declaring Your Configuration
+
+First, you will need to define what your configuration will be. Create a `modconfig.hpp` header file, this will contain
+the definition.
+
+In `modconfig.hpp`, you should put the following:
+
+```cpp
+#pragma once
+
+#include "config-utils/shared/config-utils.hpp"
+
+// Declare the mod config as "ModConfiguration" and declare all its values and functions.
+DECLARE_CONFIG(ModConfig,
+ // Declare "VariableA"
+ CONFIG_VALUE(VariableA, std::string, "Variable Name", "Variable Value");
+)
+```
+
+Here is an example that uses all the types except `const char*` and `char*`
+
+```cpp
+#pragma once
+
+#include "config-utils/shared/config-utils.hpp"
+#include "UnityEngine/Color.hpp"
+#include "UnityEngine/Vector2.hpp"
+#include "UnityEngine/Vector3.hpp"
+#include "UnityEngine/Vector4.hpp"
+
+DECLARE_CONFIG(ModConfig,
+ CONFIG_VALUE(VariableString, std::string, "String Example", "Var Value");
+ CONFIG_VALUE(VariableInteger, int, "Integer Example", 5);
+ CONFIG_VALUE(VariableFloat, float, "Float Example", 1.5f);
+ CONFIG_VALUE(VariableBoolean, bool, "Bool Example", false);
+ CONFIG_VALUE(VariableDouble, double, "Double Example", 0.39221);
+
+ // dividing by 255 in color constructor because UnityEngine::Color represents RGBA as values in the range of 0 to 1
+ CONFIG_VALUE(VariableColor, UnityEngine::Color, "Color Example", UnityEngine::Color(10.0/255, 155.0/255, 90.0/255, 0));
+ CONFIG_VALUE(VariableVector2, UnityEngine::Vector2, "Vector2 Example", UnityEngine::Vector2(1, 2));
+ CONFIG_VALUE(VariableVector3, UnityEngine::Vector3, "Vector3 Example", UnityEngine::Vector3(1, 2, 3));
+ CONFIG_VALUE(VariableVector4, UnityEngine::Vector4, "Vector4 Example", UnityEngine::Vector4(1, 2, 3, 4));
+)
+```
+
+## Loading your Config
+
+Make sure to initialize the config! If you attempt to get values from it before it's loaded, your game will crash.
+You can run this in `setup()`, `load()`, `late_load()`, or even anytime later if you really want to, but it only ever
+needs to be run once.
+
+```cpp
+#include "modconfig.hpp"
+
+// other code
+
+extern "C" void late_load() {
+ // Initialize and load the config
+ getModConfig().Init(modInfo);
+
+ // other code.
+}
+```
+
+## Using Your Configuration
+
+In the following examples, we will be using the example that uses all the types
+from [Declaring Your Configuration](#declaring-your-configuration)
+
+```cpp
+// Get VariableString
+getModConfig().VariableString.GetValue();
+
+// Set VariableString to "Eris cute"
+getModConfig().VariableString.SetValue("Eris cute");
+
+// Get VariableVector2 and store it as vec
+UnityEngine::Vector2 vec = getModConfig().VariableVector2.GetValue();
+
+// Add 30 to the x value.
+vec = vec + UnityEngine::Vector2(30, 0, 0);
+
+// Save VariableVector2 to the new vector
+getModConfig().VariableVector2.SetValue(vec);
+```
+
+Setting a config variable will automatically save the configuration file.
+
+The configuration file is usually stored at `~/ModData/com.beatgames.beatsaber/Configs/` on the Quest.
+Your mod id will be used to create the configuration file, eg: `qosmetics.json`.
diff --git a/docs/modding/quest/custom-types.md b/docs/modding/quest/custom-types.md
new file mode 100644
index 0000000..1a0cd38
--- /dev/null
+++ b/docs/modding/quest/custom-types.md
@@ -0,0 +1,338 @@
+---
+prev: false
+next: false
+description: Learn how to create C# macros for your Quest Mod!
+---
+
+# Quest Custom Types
+
+`custom-types` is a library that allows you to create (fake) C# types using macros. These types can extend classes such
+as `MonoBehaviour` and much more. `custom-types` also allows you to create [coroutines](https://docs.unity3d.com/Manual/Coroutines.html)
+and [delegates](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/).
+
+Custom Types are complex and requires knowledge of basic C#.
+
+## Prerequisites
+
+- Install `custom-types` by running `qpm dependency add custom-types` in your project directory.
+
+Make sure to restore after adding the dependency.
+
+## Basics
+
+To create a custom type, create a header file for your type. In this example, we'll make a type called `Counter`
+that extends `MonoBehavior`.
+
+In your header file, include the macros file.
+
+```cpp
+#pragma once
+
+#include "custom-types/shared/macros.hpp"
+```
+
+Since our `Counter` Custom Type will be extending `MonoBehaviour`, we need to include this too.
+
+```cpp
+#include "UnityEngine/MonoBehaviour.hpp"
+```
+
+### Declaring the Type
+
+With those includes, we can now declare our `Counter` type. Types are declared using macros, similarly to hooking.
+
+```cpp
+// parameters are (namespace, class name, parent class, contents)
+DECLARE_CLASS_CODEGEN(MyNamespace, Counter, UnityEngine::MonoBehaviour,
+ // DECLARE_INSTANCE_METHOD creates methods
+ DECLARE_INSTANCE_METHOD(void, Update);
+
+ // DECLARE_INSTANCE_FIELD creates fields
+ DECLARE_INSTANCE_FIELD(int, counts);
+)
+```
+
+In C#, this would translate to the following:
+
+```csharp
+namespace MyNamespace
+{
+ public class Counter : MonoBehaviour
+ {
+ public int counts;
+
+ public void Update()
+ {
+
+ }
+ }
+}
+```
+
+Note that only basic types, such as `int`, `bool`, etc, and C# types can be used as instance
+fields and method parameters declared with these macros. If you need something like a `std::vector`
+or a c++ struct in your type, you can declare it after all the C# fields the same way you would
+in a regular c++ struct or class.
+
+### Defining the Type
+
+Create a new source file - name it accordingly - and include your Custom Type header.
+
+To define the type, use the `DEFINE_TYPE(Namespace, Class)` macro.
+
+For our `Counter` type, this will look like so:
+
+```cpp
+#include "Counter.hpp"
+
+DEFINE_TYPE(MyNamespace, Counter);
+```
+
+We can now define the methods that we have declared:
+
+- `Update` - Unity's update method, declared by `DECLARE_INSTANCE_METHOD(void, Update);`
+
+Our `Counter.cpp` file now looks like this:
+
+```cpp
+#include "Counter.hpp"
+
+DEFINE_TYPE(MyNamespace, Counter);
+
+// Unity update method - runs every frame this component is enabled
+void MyNamespace::Counter::Update() {
+ // Add 5 to the counter field
+ counter = counter + 5;
+}
+```
+
+## Overriding methods
+
+We can also define methods that override those on parent types or interfaces, but we are limited to only overriding
+methods explicitly defined as `virtual` or `abstract` in the C# code. For non interfaces, it's not always clear whether
+this is the case for any given method if you don't have access to a decompiler and the PC game files, but an example of
+a virtual method that is commonly overriden is `HMUI::ViewController::DidActivate`:
+
+```cpp
+// don't forget to include the types you use!
+#include "HMUI/ViewController.hpp"
+
+DECLARE_CLASS_CODEGEN(MyNamespace, CustomMenu, HMUI::ViewController,
+ // to override a method, we need the MethodInfo* of the original
+ // there are two common ways to get it, but unfortunately both of them make for relatively long lines
+ DECLARE_OVERRIDE_METHOD(void, DidActivate,
+ il2cpp_utils::il2cpp_type_check::MetadataGetter<&HMUI::ViewController::DidActivate>::get(),
+ bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling);
+ // OR
+ DECLARE_OVERRIDE_METHOD(void, DidActivate,
+ il2cpp_utils::FindMethodUnsafe("HMUI", "ViewController", "DidActivate", 3),
+ bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling);
+ // note that both of these seem to be calling methods at the global level, outside of any functions or hooks,
+ // that you normally cannot call until at least after load() --
+ // but actually, since these are macros, the code is actually moved inside of internal functions
+ // that get called at the correct times for registration
+)
+```
+
+### Using Interfaces
+
+Sometimes you will want to have your custom type inherit from interfaces. Putting them as the parent type will not work,
+and instead there is a different macro for it:
+
+```cpp
+#include "HMUI/TableView_IDataSource.hpp"
+
+// if there is no required parent class, Il2CppObject can be used to equal a plain object with no parent
+// also, to inherit from multiple interfaces, they need to be wrapped with std::vector({ ... })
+// to prevent the macro from expanding them incorrectly
+DECLARE_CLASS_CODEGEN_INTERFACES(MyNamespace, TableData, Il2CppObject, { classof(HMUI::ISaberMovementData*) },
+ // rest of the custom type as normal
+)
+```
+
+## Constructors
+
+Some simple custom types do not necessarily need constructors, but there are a lot of cases where one does
+need to be defined. You can create a fully custom one with the `DECLARE_CTOR` macro:
+
+```cpp
+DECLARE_CLASS_CODEGEN(MyNamespace, Counter, UnityEngine::MonoBehaviour,
+ // other members
+
+ // can have arguments the same as any other method
+ // but the return type is always void so it is omitted from the macro
+ DECLARE_CTOR(ctor);
+)
+```
+
+And then define it just like any other method. However, in that definition, you should make sure to invoke the
+constructor of the base class with `INVOKE_BASE_CTOR`:
+
+```cpp
+void MyNamespace::Counter::ctor() {
+ INVOKE_BASE_CTOR(classof(UnityEngine::MonoBehaviour*), ...constructor arguments);
+ // initialize other things
+}
+```
+
+In the case of `MonoBehaviour`, this isn't necessary as it doesn't do anything in its constructor. If you inherit
+other types, though, not invoking their constructors can cause hard to track down bugs.
+
+Another case where the constructor would be used is if you use `DECLARE_INSTANCE_FIELD_DEFAULT` or have c++ style fields
+in your class that need special initialization, such as `std::vector` or something with a default value, ex:
+
+```cpp
+DECLARE_CLASS_CODEGEN(MyNamespace, Counter, UnityEngine::MonoBehaviour,
+ // C# members
+ public:
+ int counts = 5;
+)
+```
+
+In this case you define the constructor method the same way and include `INVOKE_CTOR()` in the method definition:
+
+```cpp
+void MyNamespace::Counter::ctor() {
+ // sets counts to 5
+ INVOKE_CTOR();
+ // initialize other things
+}
+```
+
+If you want these macros but have nothing else to do in the constructor, you can skip the method definition and
+just use `DECLARE_DEFAULT_CTOR`:
+
+```cpp
+DECLARE_CLASS_CODEGEN(MyNamespace, Counter, UnityEngine::MonoBehaviour,
+ // C# members
+
+ // invokes the MonoBehaviour constructor and sets counts to 5
+ DECLARE_DEFAULT_CTOR();
+
+ public:
+ int counts = 5;
+)
+```
+
+Destructors can be defined custom similarly to contructors with `DECLARE_DTOR`, and/or `DECLARE_SIMPLE_DTOR` to run
+the destructor for any c++ fields that need to have special behavior when being destroyed. You don't need to worry
+about running the base class destructor, though.
+
+::: warning
+To create a new object, _do not_ run `ctor` yourself or create it in c++ with `new` or any similar operator,
+but instead use `il2cpp_utils::New(...constructor arguments);`, `Counter::New_ctor(...constructor
+arguments);`, or any C# method that would
+create an object, such as `AddComponent`.
+:::
+
+### Registering
+
+You can register all the custom types you have created using the `custom_types::Register::AutoRegister()` method.
+
+This method should be put in your `load()` or `late_load()` like so:
+
+```cpp
+#include "custom-types/shared/register.hpp"
+
+// other code
+
+extern "C" void late_load() {
+ // make sure this is after il2cpp_functions::Init()
+ custom_types::Register::AutoRegister();
+
+ // other code
+}
+```
+
+To ensure correct behavior, make sure you install hooks _after_ you register your Custom Types!
+
+### Using the Type
+
+Custom Types can be used as if they were conventional C# types like you would find in the base game - for our `Counter` type,
+we can add it as a component to a `GameObject` as it inherits `MonoBehaviour`.
+
+```cpp
+#include "UnityEngine/GameObject.hpp"
+#include "Counter.hpp"
+
+// in a hook somewhere
+UnityEngine::GameObject* gameObject = UnityEngine::GameObject::New_ctor("CounterObject");
+gameObject->AddComponent();
+```
+
+## Coroutines
+
+In Unity, a coroutine is a method that can pause execution and return control to Unity but then continue where it left
+off on the following frame. [Unity Documentation](https://docs.unity3d.com/Manual/Coroutines.html)
+
+### Creating a Coroutine
+
+Using Custom Types, coroutines are pretty much the same as their C# counterparts. Take a look at this example:
+
+```cpp
+#include "custom-types/shared/coroutine.hpp"
+#include "UnityEngine/WaitForSeconds.hpp"
+#include "System/Collections/IEnumerator.hpp"
+
+custom_types::Helpers::Coroutine counterCoroutine() {
+
+ int secondsPassed = 0;
+
+ // loop 30 times
+ for (int i = 0; i < 30; i++) {
+ secondsPassed++;
+
+ // wait one second
+ // arguments passed to co_yield must be cast to this type
+ // you can also use co_yield nullptr; to wait a single frame
+ co_yield reinterpret_cast(UnityEngine::WaitForSeconds::New_ctor(1));
+ }
+ co_return;
+}
+```
+
+| C# | C++ |
+| -------------- | ----------- |
+| `yield return` | `co_yield` |
+| `yield` | `co_yield` |
+| `yield break` | `co_return` |
+
+`co_return` is used to end a coroutine. C# automatically handles this during compilation, but c++ does
+not, so make sure you have one at the end of all your coroutines.
+
+You can also use `co_return` to exit a coroutine early, just like `return` would in a typical function.
+
+Using normal `return` in a coroutine will not work.
+
+### Using the Coroutine
+
+You can start a coroutine on any `MonoBehaviour` using the `StartCoroutine` method just like in C#, however
+to create an actual coroutine from a function you need an extra call:
+
+```cpp
+#include "UnityEngine/GameObject.hpp"
+#include "custom-types/shared/coroutine.hpp"
+
+// in a hook somewhere
+auto gameObject = UnityEngine::GameObject::New_ctor("MyCoroutineRunner");
+// this is the example custom type we made earlier, but anything inheriting from a MonoBehaviour will work
+auto myMonoBehaviour = gameObject->AddComponent();
+// create the object that we can pass to StartCoroutine from our function
+auto coroutine = custom_types::Helpers::CoroutineHelper::New(counterCoroutine());
+myMonoBehaviour->StartCoroutine(coroutine);
+```
+
+You can use `SharedCoroutineStarter` to start a coroutine without the need of an instance like so:
+
+```cpp
+#include "GlobalNamespace/SharedCoroutineStarter.hpp"
+#include "custom-types/shared/coroutine.hpp"
+
+// in a hook somewhere
+auto coroutine = custom_types::Helpers::CoroutineHelper::New(counterCoroutine());
+GlobalNamespace::SharedCoroutineStarter::get_instance()->StartCoroutine(coroutine);
+```
+
+## Other
+
+Some extra information and recommended dos and don'ts can be found [here](https://github.com/sc2ad/Il2CppQuestTypePatching/wiki/FAQ).
diff --git a/docs/modding/quest/intro.md b/docs/modding/quest/intro.md
new file mode 100644
index 0000000..7a6f313
--- /dev/null
+++ b/docs/modding/quest/intro.md
@@ -0,0 +1,433 @@
+---
+prev: false
+next: false
+description: Learn how to create your own Quest mods!
+---
+
+# Quest Mod Development Intro
+
+_Learn how to get started writing your own Quest Mods._
+
+## Getting Started
+
+::: warning
+This guide is for making mods for the **Quest Standalone** version of Beat Saber!
+
+If you use Oculus Link or similar, you want to visit the [PC Mod Development Guide](../pc/index.md) as that uses
+the PC version of the game.
+:::
+
+This guide assumes you have a basic to intermediate understanding of the following:
+
+- [C++](https://www.w3schools.com/CPP/default.asp)
+- [CMake](https://cmake.org/cmake/help/latest/guide/tutorial/index.html)
+- [ADB](https://developer.android.com/studio/command-line/adb)
+- [Powershell](https://docs.microsoft.com/en-us/learn/modules/introduction-to-powershell/)
+
+You may have difficulty understanding what is covered here if you do not have this foundation.
+
+While this guide is for development on Windows, it is not dependent on an IDE. Instead you should configure your preferred
+IDE accordingly by referring to the documentation. For example, you would need to install C++ tools for VSCode or configure
+CMake for CLion.
+
+## Environment Setup
+
+The following pieces of software are needed to follow this guide.
+
+- [Powershell](#powershell-core) - Cross Platform utility scripts
+- [CMake](#cmake) - Build Automation
+- [QPM](#qpm) - Dependency Management
+- [Ninja](#ninja) - Build Tool
+- [Android NDK](#android-ndk) - Native Development Kit for Android Devices
+
+### Powershell Core
+
+::: warning
+You must download Powershell Core, the default windows Powershell will _not_ work.
+:::
+
+[Download the latest Powershell binary for your system](https://github.com/PowerShell/PowerShell/releases/latest) and add
+it to your PATH variable, or
+alternatively download and run the windows installer.
+
+### CMake
+
+[Download the latest CMake binary for your system](https://cmake.org/download/) and add it to your PATH variable, or
+alternatively download and run the windows installer.
+
+### QPM
+
+[Download the latest QPM binary for your system](https://github.com/QuestPackageManager/QPM.CLI) from the
+Actions tab, name it qpm.exe, and add it to your PATH variable, or alternatively download and run the Windows installer
+from the appropriate workflow.
+
+### Ninja
+
+Download ninja via qpm using `qpm download ninja`.
+
+Alternatively you can [Download the latest Ninja binary for your system](https://github.com/ninja-build/ninja/releases)
+from the Releases tab
+and add it to your PATH variable.
+
+### Android NDK
+
+Download the Andoid NDK via qpm using `qpm ndk download 27`, and add the extracted directory to a new environment variable
+called ANDROID_NDK_HOME.
+
+Alternatively you can run `qpm ndk pin 27` in a project directory to only apply the NDK in the current project.
+
+If you wish you can instead download the NDK manually from the [Android NDK Downloads page](https://developer.android.com/ndk/downloads).
+
+## Create a Project
+
+Once you have setup your environment you can now generate a mod template. The template this guide uses is one by
+[Lauriethefish](https://github.com/Lauriethefish/quest-mod-template). To start run the following command in Powershell.
+
+```powershell
+qpm templatr --git https://github.com/Lauriethefish/quest-mod-template.git
+```
+
+Templatr will then ask a series of questions to create a mod project.
+
+
+
+### Add and Update Dependencies
+
+Once the project has been generated, you should now update the following two dependencies, [beatsaber-hook](https://github.com/QuestPackageManager/beatsaber-hook/)
+and [bs-cordl](https://github.com/QuestPackageManager/bs-cordl), to the version best suited for the game version you are
+developing for.
+
+`beatsaber-hook` is a library that allows for modding il2cpp games. `bs-cordl` is a library that allows modders to
+interface with the game's code.
+
+To update these, open a Powershell terminal in the project directory then run the following commands to add the latest versions:
+
+```powershell
+qpm dependency add beatsaber-hook
+qpm dependency add bs-cordl
+```
+
+If the latest versions do match those for the version you are developing for, add `-v ^x.x.x` after the command with the
+correct version instead of running those commands. For example, for Beat Saber version 1.35.0, the correct codegen
+version is 3500.0.0:
+
+```powershell
+qpm dependency add bs-cordl -v ^3500.0.0
+```
+
+### Restore Dependencies
+
+Before you can open the project in an IDE, you must restore all of the dependencies. Consider this step similar to
+fully initializing the project.
+
+In a Powershell terminal in the project directory run:
+
+```powershell
+qpm restore
+```
+
+## Project Contents
+
+Your project should contain the following structure:
+
+```properties
+// Files in .gitignore have been excluded
+cmake/
+└── ... project cmake files
+extern/
+└── ... dependencies should be here
+include/
+└── main.hpp
+scripts/
+└── ... utility scripts
+shared
+src/
+└── main.cpp
+.gitignore
+CMakeLists.txt
+mod.template.json
+qpm.json
+README.md
+```
+
+### Code Breakdown
+
+#### src/main.cpp
+
+`main.cpp` contains the `setup()` and `late_load()` methods. These methods can exist in any source file as long as they are
+accessible by the modloader. Take a look inside of `main.cpp` for more information as Laurie has thankfully commented
+most of the code.
+
+#### shared
+
+The shared folder can be exposed by QPM to other mods and published to the QPM dependency registry. Useful if you want
+to make an API to let other mods control your mod in certain ways (for example Qosmetics has a model loading API).
+Speak to @Sc2ad if you want to publish something.
+
+#### extern
+
+The extern folder should be ignored (and/or in some cases excluded). It contains dependencies, similarly to
+`node_modules` (nodejs) or `packages` (.net core).
+
+### Script Breakdown
+
+It is recommended to run these scripts using Powershell Core (v7) - however, it is not required. All scripts can be run
+with the `--help` argument for a description of arguments and functionality. Scripts can be manually invoked from the
+`scripts` folder or via qpm scripts inside `qpm.json`
+
+#### build.ps1
+
+Usage: `qpm s build`
+
+Builds your mod. Does not produce a QMOD file.
+
+#### copy.ps1
+
+Usage: `qpm s copy`
+
+Builds your mod, then copies it to your quest and launches Beat Saber if your quest is connected with ADB.
+
+#### createqmod.ps1
+
+Usage: `qpm s qmod`
+
+Generates a QMOD file that can be parsed by BMBF and or QuestPatcher. Will use the most recently built version of your mod.
+
+#### pull-tombstone.ps1
+
+Usage: `qpm s tomb`
+
+Finds the most recently modified Beat Saber crash tombstone and copies it to your device. If the build on your quest matches
+what you have most recently built locally, the `-analyze` argument can be provided to generate the source file locations
+of any lines mentioned in the backtrace.
+
+#### restart-game.ps1
+
+Usage: `qpm s restart`
+
+Closes and reopens Beat Saber on your quest if it is connected. Mostly used inside of `copy.ps1`. Does not have help text.
+
+#### start-logging.ps1
+
+Usage: `qpm s logcat`
+
+Prints logs from Beat Saber, just your mod, or also crashes. Usage of `-self` is recommended.
+
+#### validate-modjson.ps1
+
+Usage: `qpm s validate`
+
+Generates a `mod.json` from `mod.template.json` if not present and verifies it against the QMOD schema. Mostly used
+inside of `createqmod.ps1`. Does not have help text.
+
+## Hooking
+
+Hooking is core to modding. `beatsaber-hook` provides a simple way of hooking methods and other miscellaneous stuff
+like constructors.
+
+> In computer programming, the term hooking covers a range of techniques used to alter or augment the behavior of an
+> operating system, of applications, or of other software components by intercepting function calls or messages or events
+> passed between software components. Code that handles such intercepted function calls, events or messages is called a hook.
+> [Wikipedia](https://en.wikipedia.org/wiki/Hooking#:~:text=In%20computer%20programming%2C%20the%20term,events%20passed%20between%20software%20components.&text=Hooking%20can%20also%20be%20used%20by%20malicious%20code.)
+
+To view a list of methods and classes you can hook, the most convenient option is to use a C# decompiler such as [IlSpy](https://github.com/icsharpcode/ILSpy)
+if you own the game on PC, as it provides not only the classes and member names, but also the full contents of most methods.
+If you only own the game on the Quest, then you can still view all the classes and methods in the `includes/codegen`
+directory in your `extern` folder.
+
+In this example, we will hook onto the initialization of the level screen and change the text on the play button to
+something funny.
+
+The level screen runs the event `DidActivate` when it is fully initialized. This is useful for us because we can hook
+this event and add our own functionality.
+
+Firstly, create your hook using the `MAKE_HOOK_MATCH` macro:
+
+
+
+```cpp
+// You can think of these as C# - using HMUI, UnityEngine, etc, but with individual classes
+// Classes without a namespace are assigned to the GlobalNamespace
+// If you use a class and do not include it, you may get unclear compiler errors, so make sure to include what you use
+#include "GlobalNamespace/StandardLevelDetailView.hpp"
+#include "GlobalNamespace/StandardLevelDetailViewController.hpp"
+#include "UnityEngine/UI/Button.hpp"
+#include "UnityEngine/GameObject.hpp"
+#include "HMUI/CurvedTextMeshPro.hpp"
+
+// Create a hook struct named LevelUIHook
+// targeting the method "StandardLevelDetailViewController::DidActivate", which takes the following arguments:
+// bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling
+// and returns void.
+
+// General format: MAKE_HOOK_MATCH(hook name, hooked method, method return type, method class pointer, arguments...) {
+// HookName(self, arguments...);
+// your code here
+// }
+
+
+MAKE_HOOK_MATCH(LevelUIHook, &GlobalNamespace::StandardLevelDetailViewController::DidActivate, void,
+ GlobalNamespace::StandardLevelDetailViewController* self, bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) {
+ // Run the original method before our code.
+ // Note that you can run the original method after our code or even in the middle
+ // if you want to change arguments or do something before it runs.
+ LevelUIHook(self, firstActivation, addedToHierarchy, screenSystemEnabling);
+
+ // Get the actionButton text object by accessing the actionButton field and some simple Unity methods.
+ // Note that auto can be used instead of declaring the full type in many cases.
+ GlobalNamespace::StandardLevelDetailView* standardLevelDetailView = self->_standardLevelDetailView;
+ UnityEngine::UI::Button* actionButton = standardLevelDetailView->actionButton;
+ UnityEngine::GameObject* gameObject = actionButton->get_gameObject();
+ HMUI::CurvedTextMeshPro* actionButtonText = gameObject->GetComponentInChildren();
+
+ // Set the text to "Skill Issue"
+ actionButtonText->set_text("Skill Issue");
+}
+```
+
+
+
+Now, you have to install your hook. Usually, hooks are installed in `load()` or `late_load()` in `main.cpp`:
+
+```cpp
+MOD_EXTERN_FUNC void late_load() {
+ il2cpp_functions::Init();
+
+ PaperLogger.info("Installing hooks...");
+
+ INSTALL_HOOK(PaperLogger, LevelUIHook);
+
+ PaperLogger.info("Installed all hooks!");
+}
+```
+
+You can now test to see if this was successful!
+
+## Testing your Mod
+
+### Without BMBF
+
+You can test your mod without BMBF quickly using [`copy.ps1`](#copy-ps1). This is recommended while developing
+for convenience. You should always test using a QMOD and BMBF if you're about to release your mod.
+
+What[`copy.ps1`](#copy-ps1) does specifically is copy the `libmodname.so` in the `build` folder to the correct place on your
+quest and then restart Beat Saber for you. You can also specify while launching to collect logs with the `-log` argument
+followed by any of the arguments supported by the `start-logging.ps1` script:
+
+```powershell
+copy.ps1 -log -self -file latest.log
+```
+
+### With BMBF
+
+Testing your mod with BMBF is useful to make sure BMBF shows and handles your QMOD correctly (copying files,
+version, cover, etc.)
+
+You will need to generate a QMOD file using [`createqmod.ps1`](#createqmod-ps1).
+
+You can then upload the generated QMOD file to BMBF and it should install your mod - it should appear on the mods list.
+
+You can still collect logs from your mod using the [`start-logging.ps1`](#start-logging-ps1) command after you launch
+the game.
+
+## Utilizing `mod.template.json`
+
+`mod.template.json` contains basic information on your mod. It can also allow you to define other features such as:
+
+- Cover Image (the preview image shown on the BMBF Mods tab)
+- File Copies (extract files from the QMOD to a location on the quest device)
+
+Some fields in it will be of the form `${x}` - those will be automatically filled by QPM based on the information in
+your `qpm.json` and written to the file `mod.json`. It's not recommended to edit the `mod.json` manually, and it can be
+updated at any time by running the command `qpm qmod build` (which only creates the `mod.json` file, not the QMOD itself.)
+
+### Cover Image
+
+A cover image is used by certain mods and BMBF to show a preview of your mod.
+
+To add a cover image, simply name the image `cover.png`, put it in your project directory, and add the following to your
+`mod.template.json`:
+
+```json
+"coverImage": "cover.png"
+```
+
+:::tip Cover Image Recommendations
+
+- 1024x512 (BMBF will resize/crop the image to be this size)
+- File format either png, jpg or gif
+- Under 2mb to prevent load lag (larger images will take longer to show with no advantage)
+ :::
+
+#### Example Cover Images
+
+Click on the arrow beside the mod name to see the image.
+
+
+Noodle Extensions
+
+
+
+
+
+
+Slice Details Quest
+
+
+
+
+
+
+### File Copies
+
+File copies is an array that can specify extra files in your QMOD to be copied to the quest, such as sabers included by
+default in Qosmetics. You can add files by editing `createqmod.ps1` and `mod.template.json`.
+
+#### Example
+
+This example will add `secret-data.json` to the QMOD and copy it to `/sdcard/ModData/com.beatgames.beatsaber/Mods/Secret/secret-data.json`
+
+Edit [createqmod.ps1](#createqmod-ps1) to include `secret-data.json`:
+
+```powershell
+# This is after line 59 of createqmod.ps1
+$filelist += "/path/to/secret-data.json"
+```
+
+Update the following in your `mod.template.json`:
+
+```json
+"fileCopies": [
+ {
+ "name": "secret-data.json",
+ "destination": "/sdcard/ModData/com.beatgames.beatsaber/Mods/Secret/secret-data.json"
+ }
+]
+```
+
+## Mod Configuration
+
+Most mods require a configuration to allow users to change the functionality of the mod.
+
+Visit the [Quest Mod Configuration](./config.md) page to learn the basics of using `config-utils` to create
+a configuration for your mod.
+
+## Custom Types
+
+`custom-types` is a library that allows you to create the equivalent of C# types using macros. These types can extend
+classes such as `MonoBehaviour` and much more. `custom-types` also allows you to create and use [coroutines](https://docs.unity3d.com/Manual/Coroutines.html)
+and [delegates](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/).
+
+Custom Types are complex and requires knowledge of basic C#. Visit the [Quest Custom Types](./custom-types.md)
+page to learn more about integrating this into your mod.
+
+## User Interface
+
+A user interface (UI) is used by many mods to show configuration options. Visit the [Quest User Interface](./ui.md)
+page to see how to use `bsml` to create a settings screen for your mod.
+
+## Credits
+
+Initial guide content was integrated from the Beat Saber Quest Modding Guide by [Calum](https://github.com/mineblock11)
+with contributions from [Raine](https://github.com/raineio), [Pangwen](https://github.com/PangwenE), and [Metalit](https://github.com/Metalit/).
+Integration and editing was done by [Bloodcloak](/about/staff.md#bloodcloak).
diff --git a/docs/modding/quest/ui.md b/docs/modding/quest/ui.md
new file mode 100644
index 0000000..adc96b4
--- /dev/null
+++ b/docs/modding/quest/ui.md
@@ -0,0 +1,81 @@
+---
+prev: false
+next: false
+description: Learn how to create a UI for your Quest Mod!
+---
+
+# Quest User Interface
+
+:::warning
+This is a stub page, content is a work in progress! Ask in `#quest-mod-dev` if you want more info!
+:::
+
+UI is used by many mods to show configuration options. In this section, we'll show you how to use `bsml` to create a
+settings screen for your mod using code. `bsml` also supports creating UI with xml which can be found on the [BSML docs](https://redbrumbler.github.io/Quest-BSML-Docs/).
+
+## Prerequisites
+
+- Install `bsml` by running `qpm dependency add bsml` in your project directory.
+- You also need to install `custom-types` even if you don't use it in your mod: `qpm dependency add custom-types`
+
+Make sure to restore after adding the dependencies.
+
+## Creating a `DidActivate` method
+
+`DidActivate` is a method you can register with `bsml` that allows you to make a simple mod settings page.
+
+Take a look at this example:
+
+- You should only create your components on first activation to prevent duplication.
+- You can utilize containers (such as Scrollable, HorizontalLayout and VerticalLayout) to manipulate the locations of components.
+
+```cpp
+#include "bsml/shared/BSML.hpp"
+
+void DidActivate(HMUI::ViewController* self, bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) {
+ // Create our UI elements only when shown for the first time.
+ if(firstActivation) {
+ // Create a container that has a scroll bar
+ UnityEngine::GameObject* container = BSML::Lite::CreateScrollableSettingsContainer(self->get_transform());
+
+ // Create a text that says "Hello World!" and set the parent to the container.
+ BSML::Lite::CreateText(container->get_transform(), "Hello World!");
+ }
+}
+```
+
+There are too many UI components and methods to document in this guide. However, the files in the `BSML-Lite/Creation`
+folder have comments that document almost all the methods.
+
+## Registering `DidActivate`
+
+`bsml` contains a few locations you can register to:
+
+- Main Menu Mod Tabs
+ 
+- Mod Settings
+ 
+- Gameplay Setup
+ 
+
+For `bsml` to use your `DidActivate` method, you will need to register it using the `BSML::Register` class in your
+`late_load()` method.
+
+```cpp
+#include "bsml/shared/BSML.hpp"
+
+// other code
+
+extern "C" void late_load() {
+ // make sure this is after il2cpp_functions::Init()
+ BSML::Init();
+ BSML::Register::RegisterMainMenuViewControllerMethod(title, text, hoverHint, DidActivate);
+
+ // other code
+}
+```
+
+The gameplay setup location requires a slightly different function signature than the other two, with the arguments
+being just `UnityEngine::GameObject* self, bool firstActivation`.
+
+All the register functions can be found in the `BSML.hpp` file.
diff --git a/docs/pc-modding.md b/docs/pc-modding.md
index ca30838..19f89fd 100644
--- a/docs/pc-modding.md
+++ b/docs/pc-modding.md
@@ -417,3 +417,4 @@ decompiled/
- BSManager (recommended Linux installer):
- BSIPA repo — Unity mod injector (reference): [`~/src/nike4613/BeatSaber-IPA-Reloaded`](../../../src/nike4613/BeatSaber-IPA-Reloaded)
- Song Core — plugin for handling custom song additions [`~/src/Kylemc1413/SongCore`](../../../src/Kylemc1413/SongCore)
+- The BSMG wiki's [Modding](./modding/index.md) section is the workspace for reference.