# Beat Saber Plugin Development on Linux with Cursor This guide is a 2026 take on the BSMG "PC Mod Development Intro" wiki, adapted for: - Cursor IDE (instead of Visual Studio or Rider) - Linux (instead of Windows) - LLM-assisted development The official wiki ([`vs-setup.md`](../../../src/bsmg/wiki/wiki/modding/pc/vs-setup.md), [`setup.md`](../../../src/bsmg/wiki/wiki/modding/pc/setup.md)) assumes a Visual Studio or Rider extension (BSMT) does most of the project plumbing for you. None of those extensions exist for Cursor / VSCode, so we drive the same toolchain by hand from the `dotnet` CLI. The good news: BSMT's heavy lifting actually lives in an MSBuild-time NuGet package (`BeatSaberModdingTools.Tasks`), so once a project is bootstrapped, the IDE is just a code editor. ## TL;DR A Beat Saber plugin is a .NET Framework 4.7.2 class library that BSIPA loads at runtime. The compiled `.dll` is platform-agnostic CIL bytecode, so **you can build it on Linux and Beat Saber (running through Proton) will load it unmodified**. The workflow is: 1. Install Beat Saber + BSIPA via BSManager (Linux-friendly). 2. Install the .NET SDK and (optionally) Mono on the host. 3. Copy one of the BSMT project templates from [`UnityModdingTools.Templates.BeatSaber`](../../../src/Zingabopp/UnityModdingTools.Templates.BeatSaber) into a new repo and substitute the `$placeholders$`. 4. Edit `csproj.user` to point `BeatSaberDir` at your install. 5. `dotnet build` — the `BeatSaberModdingTools.Tasks` MSBuild package generates the embedded `manifest.json`, copies the DLL into `Beat Saber/Plugins/`, and (on `Release`) zips it for distribution. 6. Launch Beat Saber from BSManager (or Steam with `--verbose`) and watch the BSIPA console. The rest of this doc walks each step in detail. ## 1. Toolchain landscape | Concern | Windows / VS-Rider workflow | Linux / Cursor workflow | | -------------------------- | -------------------------------------- | -------------------------------------------------------------------- | | Project templates | BSMT VSIX or BSMT-Rider plugin | Copy the template files manually from the BSMT template repo | | `manifest.json` generation | `BeatSaberModdingTools.Tasks` (MSBuild) | Same — pure NuGet, IDE-independent | | Reference resolution | BSMT "Beat Saber Reference Manager" UI | Hand-edit `` entries in `.csproj` | | Build | IDE "Build" button (msbuild) | `dotnet build` (or `msbuild` from Mono) | | Deploy to game | BSMT post-build copy step | Same — driven by `BeatSaberModdingTools.Tasks` | | Run / debug | VS / Rider attach to `Beat Saber.exe` | Inspect BSIPA console + log files; remote-debug via Mono is possible | The two key insights that make Cursor-on-Linux viable: 1. **`BeatSaberModdingTools.Tasks` is just a NuGet package.** It hooks into MSBuild via `build/` targets shipped in the package, so any `dotnet build` gets the same manifest generation, output copy, and release zipping that VS / Rider do. See its references in [`BareProjectTemplate.csproj`](../../../src/Zingabopp/UnityModdingTools.Templates.BeatSaber/BSIPA%20Plugin%20%28Bare%29/BareProjectTemplate.csproj) and [`CoreProjectTemplate.csproj`](../../../src/Zingabopp/UnityModdingTools.Templates.BeatSaber/BSIPA%20Plugin%20%28Core%29/CoreProjectTemplate.csproj). 2. **Plugin DLLs are CIL, not native.** Beat Saber's Unity Mono runtime loads any `net472`-compatible assembly. Whether the assembly was produced by `csc.exe` on Windows or `dotnet build` on Linux makes no difference at runtime. ## 2. Install Beat Saber + BSIPA on Linux Beat Saber has no native Linux binary — it runs through Proton in your Steam prefix at `~/.local/share/Steam/steamapps/common/Beat Saber/`. The BSMG [Linux Modding Guide](https://bsmg.wiki/linux-modding.html) lists three options; **BSManager is what BSMG currently recommends**. ```bash # 1. Install BSManager (AppImage, .deb, or AUR — see its release page) # https://github.com/Zagrios/bs-manager/releases # Linux install notes: # https://github.com/Zagrios/bs-manager/wiki/Linux#installation # 2. Launch Beat Saber once via Steam *before* modding it (creates the prefix). # 3. In BSManager: # - Download a "Recommended" Beat Saber version into a *managed* copy # (do NOT mod the Steam copy directly — Steam updates will break mods). # - Open that version → Mods tab → install at minimum BSIPA. # - Always launch the modded version from BSManager. ``` If you would rather wire up BSIPA by hand (e.g. for a CI machine), the manual recipe is: 1. Add `WINEDLLOVERRIDES="winhttp=native,builtin" %command%` to Beat Saber's Steam launch options. 2. Drop `BSIPA-x64-Net4.zip` into the game folder, then run `wine IPA.exe -n` from inside it. 3. Verify a `Plugins/` folder is created next to `Beat Saber.exe`. The full BSIPA install reference is at . ::: tip Pin a known game version Steam will silently update Beat Saber and break your mod. Either let BSManager hold a pinned download (preferred) or set the Steam app to "Only update this game when I launch it" and skip launching from Steam. ::: ## 3. Host toolchain Install once on the host: ```bash # Pick whichever your distro provides (examples for the most common ones) sudo pacman -S dotnet-sdk # Arch sudo apt install dotnet-sdk-9.0 # Debian/Ubuntu sudo dnf install dotnet-sdk-9.0 # Fedora ``` Verify: ```bash dotnet --list-sdks # any 6.0+ SDK can build net472 targets ``` You do **not** need Mono for building. The `dotnet` SDK plus the NuGet `Microsoft.NETFramework.ReferenceAssemblies` package (pulled in transitively by `BeatSaberModdingTools.Tasks`) is sufficient to produce `net472` assemblies on Linux. Install Mono only if you want to run / debug `.NET Framework` test harnesses outside the game. Optional but recommended: - `nuget` CLI (for inspecting / restoring packages outside `dotnet`). - `ilspycmd` or `ILSpy` for reading decompiled game code. ## 4. Cursor extensions for C\# Cursor's marketplace doesn't carry Microsoft's official `C#` / `C# Dev Kit` extensions (their license restricts them to VS Code / VS). Workable replacements that ship through Open VSX / sideloads: | Need | Extension | | ----------------------------- | -------------------------------------------------------- | | Language server (LSP) | `muhammad-sammy.csharp` (community fork of OmniSharp) | | or | `Ionide.csharp` / `csharp-language-server` if preferred | | Debugger | `vsdbg` is MS-licensed; on Linux use `netcoredbg` via | | | `muhammad-sammy.csharp`'s built-in debugger | | `.csproj` / MSBuild awareness | Comes with the language-server extension above | | XAML / `.bsml` | Plain XML support is enough; no dedicated BSML extension | Install from Cursor: `Ctrl+Shift+X` → search the names above. If a package is not in the marketplace, grab the `.vsix` from and use **Install from VSIX…**. Settings worth tweaking in `settings.json`: ```jsonc { // Force OmniSharp/Roslyn to load the right SDK "dotnet.server.useOmnisharp": true, // Prevent the language server from chewing on the Beat Saber game DLLs "files.watcherExclude": { "**/Refs/**": true, "**/bin/**": true, "**/obj/**": true } } ``` ## 5. Bootstrap a new plugin project The BSMT templates are designed to be expanded by Visual Studio's templating engine — they contain `$safeprojectname$`, `$guid1$`, and `$targetframeworkversion$` placeholders. We expand them by hand. Pick the template that matches what you want: | Template | When to use | | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | [`BSIPA Plugin (Bare)`](../../../src/Zingabopp/UnityModdingTools.Templates.BeatSaber/BSIPA%20Plugin%20%28Bare%29) | Smallest possible BSIPA plugin — `Plugin.cs` + `manifest.json`. | | [`BSIPA Plugin (Core)`](../../../src/Zingabopp/UnityModdingTools.Templates.BeatSaber/BSIPA%20Plugin%20%28Core%29) | Adds a `MonoBehaviour` controller and a commented config example. | | [`BSIPA Plugin (Disableable)`](../../../src/Zingabopp/UnityModdingTools.Templates.BeatSaber/BSIPA%20Plugin%20%28Disableable%29) | Plugin that can be enabled/disabled at runtime. | The walkthrough below uses **Core** and a fictional plugin called `MyPlugin`. ### 5.1 Copy the template ```bash SRC="$HOME/src/Zingabopp/UnityModdingTools.Templates.BeatSaber/BSIPA Plugin (Core)" mkdir -p ~/src/yourname/MyPlugin && cd ~/src/yourname/MyPlugin cp "$SRC"/Plugin.cs Plugin.cs cp "$SRC"/MonobehaviourTemplate.cs MyPluginController.cs cp "$SRC"/PluginConfig.cs Configuration/PluginConfig.cs mkdir -p Properties cp "$SRC"/AssemblyInfo.cs Properties/AssemblyInfo.cs cp "$SRC"/manifest.json manifest.json cp "$SRC"/CoreProjectTemplate.csproj MyPlugin.csproj cp "$SRC"/csproj.user.template MyPlugin.csproj.user cp "$SRC"/Directory.Build.props.template Directory.Build.props ``` ### 5.2 Substitute placeholders The VS template engine would normally do this. Replace these tokens across every file: | Token | Replace with | | ------------------------------ | ------------------------------------------------------------------------- | | `$safeprojectname$` | `MyPlugin` | | `$projectname$` | `My Plugin` (display name; can equal the safe name) | | `$guid1$` | A fresh GUID — `uuidgen` then wrap as `{XXXXXXXX-…}` | | `$targetframeworkversion$` | `4.7.2` | | Any reference to `$safeprojectname$Controller` | `MyPluginController` (in `Plugin.cs` + the controller filename) | A one-shot rewrite: ```bash GUID="{$(uuidgen | tr a-f A-F)}" find . -type f \( -name '*.cs' -o -name '*.csproj' -o -name '*.user' -o -name 'manifest.json' \) \ -exec sed -i \ -e "s|\$safeprojectname\$|MyPlugin|g" \ -e "s|\$projectname\$|My Plugin|g" \ -e "s|\$guid1\$|$GUID|g" \ -e "s|\$targetframeworkversion\$|4.7.2|g" {} + ``` ### 5.3 Tell MSBuild where Beat Saber lives Edit `MyPlugin.csproj.user` (this file should be `.gitignore`'d — paths are machine-specific): ```xml /home/you/.local/share/Steam/steamapps/common/Beat Saber ``` If you mod a BSManager-managed copy instead of the Steam copy, point at that folder (typically under `~/BSManager/versions//`). All the `$(BeatSaberDir)\Beat Saber_Data\Managed\…` entries in `MyPlugin.csproj` resolve through that variable. Backslashes work fine — MSBuild normalises them on Linux. ### 5.4 Fill in your manifest Open `manifest.json` and set `id`, `name`, `author`, `version`, `description`, and (importantly) `gameVersion` — the value here is what BSIPA shows the user when your plugin is loaded against a different game version. ```jsonc { "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json", "id": "MyPlugin", "name": "My Plugin", "author": "you", "version": "0.1.0", "description": "What this plugin does in one or two sentences.", "gameVersion": "1.42.3", "dependsOn": { "BSIPA": "^4.3.0" } } ``` `gameVersion` should match the major.minor.patch of the game you are targeting (check `BeatSaberVersion.txt` in the install dir — yours is `1.42.3_15380`, i.e. `1.42.3`). Add other plugins (e.g. `BeatSaberMarkupLanguage`, `SiraUtil`) to `dependsOn` as needed. ### 5.5 Initial build ```bash cd ~/src/yourname/MyPlugin dotnet restore dotnet build -c Debug ``` The first restore pulls down `BeatSaberModdingTools.Tasks` and the .NET Framework reference assemblies. After the build succeeds you should see: - `bin/Debug/MyPlugin.dll` (and `MyPlugin.pdb`). - A copy of the same DLL inside `/Plugins/`. This is done by the BSMT MSBuild target `CopyToPlugins`. Disable it on CI by passing `-p:DisableCopyToPlugins=True`. If the build fails complaining about missing `Main.dll`, `HMUI.dll`, etc., your `BeatSaberDir` is wrong or pointing at an unmodded game (the Managed folder must contain those DLLs). ### 5.6 Release builds ```bash dotnet build -c Release ``` This produces `bin/Release/MyPlugin.dll` plus a distributable `bin/Release/zip/MyPlugin-.zip` shaped to drop into the game folder. ## 6. Iteration loop 1. Edit C# in Cursor; the OmniSharp-style language server gives you completion against the game's own DLLs (it follows the `` entries). 2. `dotnet build` — the DLL lands in `/Plugins/` automatically. 3. Launch Beat Saber from BSManager with the `--verbose --debug` arguments set under "Advanced launch", or from Steam with the same flags appended to the launch options. BSManager already exposes a "Debug mode" toggle that adds `--verbose`. 4. Watch the BSIPA console window or `Logs/_latest.log` in the game folder. Useful launch arguments (full list in the BSMG [index page](../../../src/bsmg/wiki/wiki/modding/pc/index.md)): | Flag | Effect | | -------------- | ------------------------------------------------------- | | `--verbose` | Open the BSIPA console window. | | `--debug` | Promote `Debug.Log(…)` calls to console output. | | `--trace` | Even noisier — BSIPA-internal traces. | | `fpfc` | First-Person Flying Controller — play without VR. | | `--auto_play` | Built-in autoplayer (handy for testing UI/menu mods). | For a tighter loop, point your Steam compatibility tool at GE-Proton (newer than the Valve build) and add `PROTON_LOG=1` so Wine logs land in `~/steam-MyPlugin.log`. ## 7. Debugging True breakpoint debugging requires attaching Mono Soft Debugger to the Unity Mono runtime that ships with Beat Saber. The procedure: 1. Add `--mono-debugger` to Beat Saber's launch arguments. Unity's Mono will open a debugger socket on `127.0.0.1:55555` by default. 2. In Cursor, install **MonoDebug** (`MetinSeylan.mono-debug`) or use `dnSpyEx`'s Mono debugger from a separate window. 3. The plugin must be built with `portable` (the templates already do this for Debug). In practice most mod authors lean on `Plugin.Log.Info(…)` + the BSIPA console because it survives Proton's quirks better than the Mono debugger. For inspecting the game's own assemblies, use [ILSpy](https://github.com/icsharpcode/ILSpy) or the CLI: ```bash dotnet tool install -g ilspycmd ilspycmd "$HOME/.local/share/Steam/steamapps/common/Beat Saber/Beat Saber_Data/Managed/Main.dll" \ -o ~/decompiled/ ``` The BSMG wiki has a dedicated [decompiling guide](../../../src/bsmg/wiki/wiki/modding/pc/decompiling.md). ## 8. Tips for LLM-assisted development A few things make this codebase friendlier than average to LLM agents: - **The reference graph is closed.** All Beat Saber types live under `Beat Saber_Data/Managed/`. Have your agent run `ilspycmd …/Managed/Main.dll -o decompiled/` once and grep the result when it needs to know a method signature. Cache it in the repo (gitignored). - **`manifest.json` is the source of truth for runtime dependencies**. When your agent adds a `using BeatSaberMarkupLanguage…`, it must also add `"BeatSaberMarkupLanguage": "^1.x"` to `dependsOn` and a corresponding `` in the `.csproj` — otherwise BSIPA will refuse to load the plugin or the build will fail to find the type. - **No nullable reference types in templates.** The BSMT templates are pre-`enable`. Either keep that style or flip the switch in `MyPlugin.csproj` and let the agent maintain `?`/`!` annotations consistently. - **Harmony patches are runtime-reflective.** Static analysis can't catch a typo in a `[HarmonyPatch(typeof(Foo), nameof(Foo.Bar))]` attribute — add a smoke test that asserts the patch attached at startup (BSIPA logs an error if a patch fails to apply, so a log-line check works). - **Game updates change DLL ABIs.** Pin a `gameVersion` and verify it on startup; tell the agent to bail out loudly rather than silently wrap an exception when the API drift is too large. ## 9. Repo layout suggestion ``` MyPlugin/ ├─ .gitignore # ignore bin/, obj/, *.csproj.user, decompiled/ ├─ Directory.Build.props # BSMT switches (ImportBSMTTargets, BSMTProjectType) ├─ MyPlugin.csproj # references + BeatSaberModdingTools.Tasks ├─ MyPlugin.csproj.user # local BeatSaberDir — gitignored ├─ manifest.json # BSIPA metadata (embedded into DLL at build time) ├─ Plugin.cs # BSIPA entry point ├─ MyPluginController.cs # MonoBehaviour singleton ├─ Configuration/ │ └─ PluginConfig.cs # BSIPA Generated config (uncomment to enable) ├─ Properties/ │ └─ AssemblyInfo.cs └─ Refs/ # optional fallback for Beat Saber DLLs # (LocalRefsDir wins over BeatSaberDir if present) ``` A starter `.gitignore`: ```gitignore bin/ obj/ *.csproj.user decompiled/ .vs/ .idea/ ``` ## References - BSMG wiki — Linux modding: - BSMG wiki — PC mod dev intro: [`setup.md`](../../../src/bsmg/wiki/wiki/modding/pc/setup.md) - BSMG wiki — VS variant of the setup: [`vs-setup.md`](../../../src/bsmg/wiki/wiki/modding/pc/vs-setup.md) - BSMG wiki — launch flags etc.: [`index.md`](../../../src/bsmg/wiki/wiki/modding/pc/index.md) - BSIPA install reference: - `BeatSaberModdingTools.Tasks` (the MSBuild engine that powers the templates): - BSMT VS extension source (read-only reference for what the IDE plugin automates): [`~/src/denpadokei/BeatSaberModdingTools`](../../../src/denpadokei/BeatSaberModdingTools) - BSMT project templates (the ones we copy from): [`~/src/Zingabopp/UnityModdingTools.Templates.BeatSaber`](../../../src/Zingabopp/UnityModdingTools.Templates.BeatSaber) - 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.