19 KiB
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,
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:
- Install Beat Saber + BSIPA via BSManager (Linux-friendly).
- Install the .NET SDK and (optionally) Mono on the host.
- Copy one of the BSMT project templates from
UnityModdingTools.Templates.BeatSaberinto a new repo and substitute the$placeholders$. - Edit
csproj.userto pointBeatSaberDirat your install. dotnet build— theBeatSaberModdingTools.TasksMSBuild package generates the embeddedmanifest.json, copies the DLL intoBeat Saber/Plugins/, and (onRelease) zips it for distribution.- 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 <Reference Include="…"><HintPath> 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:
BeatSaberModdingTools.Tasksis just a NuGet package. It hooks into MSBuild viabuild/targets shipped in the package, so anydotnet buildgets the same manifest generation, output copy, and release zipping that VS / Rider do. See its references inBareProjectTemplate.csprojandCoreProjectTemplate.csproj.- Plugin DLLs are CIL, not native. Beat Saber's Unity Mono runtime loads
any
net472-compatible assembly. Whether the assembly was produced bycsc.exeon Windows ordotnet buildon 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 lists three
options; BSManager is what BSMG currently recommends.
# 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:
- Add
WINEDLLOVERRIDES="winhttp=native,builtin" %command%to Beat Saber's Steam launch options. - Drop
BSIPA-x64-Net4.zipinto the game folder, then runwine IPA.exe -nfrom inside it. - Verify a
Plugins/folder is created next toBeat Saber.exe.
The full BSIPA install reference is at https://nike4613.github.io/BeatSaber-IPA-Reloaded/articles/start-user.html.
::: 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:
# 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:
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:
nugetCLI (for inspecting / restoring packages outsidedotnet).ilspycmdorILSpyfor 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
https://open-vsx.org/ and use Install from VSIX….
Settings worth tweaking in settings.json:
{
// 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) |
Smallest possible BSIPA plugin — Plugin.cs + manifest.json. |
BSIPA Plugin (Core) |
Adds a MonoBehaviour controller and a commented config example. |
BSIPA Plugin (Disableable) |
Plugin that can be enabled/disabled at runtime. |
The walkthrough below uses Core and a fictional plugin called MyPlugin.
5.1 Copy the template
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:
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):
<Project>
<PropertyGroup>
<BeatSaberDir>/home/you/.local/share/Steam/steamapps/common/Beat Saber</BeatSaberDir>
</PropertyGroup>
</Project>
If you mod a BSManager-managed copy instead of the Steam copy, point at that
folder (typically under ~/BSManager/versions/<version>/). All the
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\…</HintPath> 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.
{
"$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
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(andMyPlugin.pdb).- A copy of the same DLL inside
<BeatSaberDir>/Plugins/. This is done by the BSMT MSBuild targetCopyToPlugins. 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
dotnet build -c Release
This produces bin/Release/MyPlugin.dll plus a distributable
bin/Release/zip/MyPlugin-<version>.zip shaped to drop into the game folder.
6. Iteration loop
- Edit C# in Cursor; the OmniSharp-style language server gives you completion
against the game's own DLLs (it follows the
<HintPath>entries). dotnet build— the DLL lands in<BeatSaberDir>/Plugins/automatically.- Launch Beat Saber from BSManager with the
--verbose --debugarguments 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. - Watch the BSIPA console window or
Logs/_latest.login the game folder.
Useful launch arguments (full list in the BSMG index page):
| 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:
- Add
--mono-debuggerto Beat Saber's launch arguments. Unity's Mono will open a debugger socket on127.0.0.1:55555by default. - In Cursor, install MonoDebug (
MetinSeylan.mono-debug) or usednSpyEx's Mono debugger from a separate window. - The plugin must be built with
<DebugType>portable</DebugType>(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 or the CLI:
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.
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 runilspycmd …/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.jsonis the source of truth for runtime dependencies. When your agent adds ausing BeatSaberMarkupLanguage…, it must also add"BeatSaberMarkupLanguage": "^1.x"todependsOnand a corresponding<Reference>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-
<Nullable>enable</Nullable>. Either keep that style or flip the switch inMyPlugin.csprojand 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
gameVersionand 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:
bin/
obj/
*.csproj.user
decompiled/
.vs/
.idea/
References
- BSMG wiki — Linux modding: https://bsmg.wiki/linux-modding.html
- BSMG wiki — PC mod dev intro:
setup.md - BSMG wiki — VS variant of the setup:
vs-setup.md - BSMG wiki — launch flags etc.:
index.md - BSIPA install reference: https://nike4613.github.io/BeatSaber-IPA-Reloaded/articles/start-user.html
BeatSaberModdingTools.Tasks(the MSBuild engine that powers the templates): https://www.nuget.org/packages/BeatSaberModdingTools.Tasks- BSMT VS extension source (read-only reference for what the IDE plugin
automates):
~/src/denpadokei/BeatSaberModdingTools - BSMT project templates (the ones we copy from):
~/src/Zingabopp/UnityModdingTools.Templates.BeatSaber - BSManager (recommended Linux installer): https://github.com/Zagrios/bs-manager
- BSIPA repo — Unity mod injector (reference):
~/src/nike4613/BeatSaber-IPA-Reloaded - Song Core — plugin for handling custom song additions
~/src/Kylemc1413/SongCore - The BSMG wiki's Modding section is the workspace for reference.