diff --git a/docs/notes.md b/docs/notes.md new file mode 100644 index 0000000..514ea5a --- /dev/null +++ b/docs/notes.md @@ -0,0 +1,19 @@ +# Notes + +Gamedir + +```sh +cd ~/.local/share/BSManager/BSInstances/1.40.8 +``` + +## Logs + +```sh +tail -f Logs/_latest.log +``` + +## FPFC Mode + +Press `g` to release the mouse input from the game + +Mouse input is broken because Wayland is not yet supported. diff --git a/docs/pc-modding.md b/docs/pc-modding.md new file mode 100644 index 0000000..ca30838 --- /dev/null +++ b/docs/pc-modding.md @@ -0,0 +1,419 @@ +# 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)