Split Beat Saber plugin agent skills

This commit is contained in:
pleb
2026-06-29 10:57:11 -07:00
parent e0616de6c3
commit 6117173507
5 changed files with 282 additions and 17 deletions
@@ -0,0 +1,94 @@
---
name: beatsaber-plugin-builder
description: Build, test-compile, or package Beat Saber PC BSIPA plugin source on Linux from local checkouts, GitHub branches, or pull requests. Use when asked to build a Beat Saber plugin, compile a plugin PR, produce a DLL/zip artifact, configure BeatSaberDir/local refs for dotnet builds, or diagnose Linux build failures for net472 BSIPA projects.
---
# Build Beat Saber Plugin
Use this skill to compile PC BSIPA plugin projects on this Linux host, especially from the `plugin-helper` repo. The workflow is adapted from the Setlist repo's Linux/Cursor build notes.
For detailed Linux/BSMT behavior, read [linux-bsipa-build.md](references/linux-bsipa-build.md) when you need to configure a project, fix missing references, package artifacts, or explain a failure.
## Core Workflow
1. Confirm context.
```bash
pwd
git status --short
```
In `plugin-helper`, run commands from repo root and keep temporary source checkouts under `.state/build/<name>` unless the user asks for another location. Do not disturb unrelated dirty files.
2. Resolve source.
For a GitHub PR, clone or reuse a checkout under `.state/build`, add/fetch the upstream remote if needed, and check out the PR head:
```bash
git clone https://github.com/<owner>/<repo>.git .state/build/<name>
git -C .state/build/<name> fetch origin pull/<pr>/head:pr-<pr>
git -C .state/build/<name> checkout pr-<pr>
```
If the PR is from a fork and the repo already has a fork remote, preserve it. Never overwrite local source changes without explicit approval.
3. Inspect build shape.
```bash
find <checkout> -maxdepth 3 -name '*.sln' -o -name '*.csproj' -o -name 'manifest.json' -o -name 'Directory.Build.props'
sed -n '1,240p' <project>.csproj
```
Note target framework, `BeatSaberDir`, `LocalRefsDir`, package references, and whether `DisableCopyToPlugins` is already set.
4. Choose Beat Saber references.
Prefer a BSManager instance matching the plugin or manifest `gameVersion`. Common local roots:
```text
/home/pleb/.local/share/BSManager/BSInstances/<version>
/home/pleb/Windows/Users/pleb/BSManager/BSInstances/<version>
```
Use a local `.csproj.user` or MSBuild properties rather than committing machine paths. For test builds, pass `-p:DisableCopyToPlugins=True` and, for BSMT projects that expose it, `-p:DisableCopyToGame=True` so compilation does not mutate the game install.
5. Restore and build.
Prefer the solution when present; otherwise build the plugin `.csproj`:
```bash
dotnet restore <solution-or-project>
dotnet build <solution-or-project> -c Release -p:DisableCopyToPlugins=True -p:DisableCopyToGame=True
```
If the project lacks .NET Framework reference assemblies on Linux, add or pass `Microsoft.NETFramework.ReferenceAssemblies.net472` as described in the reference file.
6. Collect artifacts.
Find produced DLLs and release zips:
```bash
find <checkout> -path '*/bin/*' \( -name '*.dll' -o -name '*.zip' \) -print
```
Verify the DLL name, version, and manifest. If the result is meant for `plugin-helper`, place a copy under `.state/instances/<instance>/downloads/<plugin-id>/` and use the helper plan/apply workflow rather than hand-copying into a BSManager instance.
7. Validate.
For skill edits inside this repo, run:
```bash
python /home/pleb/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/beatsaber-plugin-builder
PYTHONPATH=src python -m compileall -q src tests
PYTHONPATH=src python -m unittest discover -s tests
```
For a plugin build, at minimum report the exact `dotnet build` result and artifact paths. For live game validation, use `docs/SMOKETEST.md` and tear down Beat Saber processes afterward.
## Failure Triage
- Missing `Microsoft.NETFramework.ReferenceAssemblies`: add the net472 reference-assemblies package or pass an equivalent MSBuild/package restore fix.
- Missing `Main.dll`, `HMUI.dll`, `IPA.Loader.dll`, `BSML.dll`, `SongCore.dll`, or similar: `BeatSaberDir` points at the wrong/unmodded instance, or dependencies are absent from `Plugins/`/`Libs/`.
- BSMT copies to `IPA/Pending` or `Plugins` during build: rebuild with `-p:DisableCopyToPlugins=True -p:DisableCopyToGame=True` unless the user explicitly wants deployment.
- NuGet package restore fails because a source is missing: inspect `NuGet.config` and installed package sources; use repo-local configuration where possible.
- API compile errors from a PR: inspect the target game/dependency versions before changing code. Prefer one target version rather than compatibility branches unless the user asks for multi-version support.
@@ -0,0 +1,4 @@
interface:
display_name: "Build Beat Saber Plugin"
short_description: "Build BSIPA plugins on Linux"
default_prompt: "Use $beatsaber-plugin-builder to build this Beat Saber plugin PR on Linux."
@@ -0,0 +1,131 @@
# Linux BSIPA Build Reference
## Source Notes
This workflow comes from `/home/pleb/ops/beatsaber/setlist/docs/pc-modding.md` and `/home/pleb/ops/beatsaber/setlist/docs/bootstrap.md`. Prefer those local files and local clones under `~/src` over web copies when deeper reference material is needed.
## Toolchain
- PC BSIPA plugins are usually .NET Framework `net472` class libraries.
- Linux `dotnet` SDK 6+ can build them because output DLLs are platform-agnostic CIL loaded by Beat Saber under Proton.
- `BeatSaberModdingTools.Tasks` supplies the MSBuild targets normally driven by Visual Studio/Rider BSMT extensions.
- On this host, `dotnet --list-sdks` should show a usable SDK. NuGet is available for package inspection.
## Game References
Point `BeatSaberDir` at a modded BSManager instance containing:
```text
Beat Saber.exe
Beat Saber_Data/Managed/
IPA/
Libs/
Plugins/
winhttp.dll
```
Preferred local managed instance root:
```text
/home/pleb/.local/share/BSManager/BSInstances/<version>
```
Windows mirror root:
```text
/home/pleb/Windows/Users/pleb/BSManager/BSInstances/<version>
```
Use `BeatSaberVersion.txt` for the exact game version. The manifest `gameVersion` normally uses the `major.minor.patch` prefix, not the build suffix.
## Project Configuration
Prefer machine-local configuration in `<Project>.csproj.user`:
```xml
<Project>
<PropertyGroup>
<BeatSaberDir>/home/pleb/.local/share/BSManager/BSInstances/1.40.8</BeatSaberDir>
</PropertyGroup>
</Project>
```
Some projects use `LocalRefsDir` and set:
```xml
<BeatSaberDir>$(LocalRefsDir)</BeatSaberDir>
```
For those, pass `-p:LocalRefsDir=/path/to/instance` or add a local `.csproj.user` property if the project imports it.
When changing a project file for Linux portability, prefer the smallest explicit fix:
- Add `Microsoft.NETFramework.ReferenceAssemblies.net472` when MSBuild reports missing .NET Framework reference assemblies.
- Set or pass `DisableCopyToPlugins=True` for artifact-only builds.
- Keep hint paths rooted at `$(BeatSaberDir)` where possible.
- Do not add broad multi-version compatibility logic unless requested.
## Build Commands
From the plugin checkout:
```bash
dotnet restore <solution-or-project>
dotnet build <solution-or-project> -c Release -p:DisableCopyToPlugins=True
```
For a Debug smoke build:
```bash
dotnet build <solution-or-project> -c Debug -p:DisableCopyToPlugins=True
```
Expected outputs:
```text
bin/Debug/<Plugin>.dll
bin/Release/<Plugin>.dll
bin/Release/zip/<Plugin>-<version>.zip
bin/<Configuration>/Artifact/Plugins/<Plugin>.dll
```
The exact layout depends on BSMT version and project customization.
## BSMT Copy Behavior
`BeatSaberModdingTools.Tasks` may copy built DLLs into the game directory. On Unix hosts, its process check can behave differently and some projects add a custom target to copy into `<BeatSaberDir>/Plugins/`.
For builds that should not alter a live instance, pass both property names; different BSMT projects/targets surface different copy switches:
```bash
-p:DisableCopyToPlugins=True -p:DisableCopyToGame=True
```
If the purpose is to install the built artifact, copy it into plugin-helper's state downloads and use `plugin-helper plan/apply`, or follow `docs/SMOKETEST.md` for intentional live validation.
## Common Reference Failures
- `MSB3644` or missing `.NETFramework,Version=v4.7.2` reference assemblies: add `Microsoft.NETFramework.ReferenceAssemblies.net472`.
- Missing game assemblies such as `Main.dll`, `HMUI.dll`, `UnityEngine.CoreModule.dll`: `BeatSaberDir` is wrong or incomplete.
- Missing mod dependencies such as `BSML.dll`, `SongCore.dll`, `SiraUtil.dll`, `BeatSaberPlaylistsLib.dll`: install or point at an instance containing those plugins, or fetch the dependency DLL from its verified release only when appropriate.
- `IPA.Loader.dll` missing: BSIPA is not bootstrapped in that instance.
## Artifact Handoff To plugin-helper
For a built plugin DLL intended for a managed instance:
```bash
mkdir -p .state/instances/<instance>/downloads/<plugin-id>
cp <checkout>/<path>/bin/Release/<Plugin>.dll .state/instances/<instance>/downloads/<plugin-id>/<Plugin>.dll
sha256sum .state/instances/<instance>/downloads/<plugin-id>/<Plugin>.dll
```
Then update registry/lock data only if the user asked to manage/install the artifact, and use:
```bash
PYTHONPATH=src python -m plugin_helper --state-dir .state check --instance <instance>
PYTHONPATH=src python -m plugin_helper --state-dir .state plan --instance <instance> --plugin <plugin-id>
PYTHONPATH=src python -m plugin_helper --state-dir .state apply <plan-path>
```
Inspect the generated plan before applying.
@@ -1,5 +1,5 @@
---
name: install-beatsaber-plugin
name: beatsaber-plugin-manager
description: Install or update a Beat Saber plugin in the plugin-helper repo by using the local helper workflow. Use when the user asks to add, install, update, bump, lock, bootstrap BSIPA, or manage a Beat Saber plugin release for a BSManager instance. Prefer upstream GitHub release artifacts for normal plugins; use BeatMods primarily as compatibility/dependency metadata, with CDN artifacts only for inaccessible upstream assets, BeatMods-only packages, or framework/library dependencies.
---
@@ -12,11 +12,16 @@ Use the repository's own `plugin-helper` commands to manage plugins for BSManage
## Hard Guardrail
For ordinary GitHub-hosted plugins, require an explicit GitHub release URL from
the user's prompt before selecting any release or repository.
For ordinary GitHub-hosted plugins, require an explicit GitHub repository or
release URL from the user's prompt or from a user-provided local planning note
before selecting any release.
- If the prompt does not contain a GitHub release URL, stop and ask the user for it.
- Do not search the web to discover a repository or "correct" release URL.
- If the user has provided a GitHub repository URL but not a release URL, use
that exact repository's release API and choose the most appropriate
non-draft, non-prerelease release/asset for the target Beat Saber instance.
- If the user has provided no GitHub repository or release URL for the plugin,
stop and ask the user for one.
- Do not search the web to discover a repository or "correct" project URL.
- Do not substitute a similar repo, fork, project, or package name.
- If the provided URL is a general releases page, use that repo's release API and choose the latest non-draft, non-prerelease release unless the user asks for a specific tag/version.
- If the provided URL is a tag URL, use that exact tag.
@@ -75,15 +80,38 @@ https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>
For BeatMods bootstrap or verified packages, query BeatMods with a browser-like user agent:
```bash
python - <<'PY'
PYTHONPATH=src python - <<'PY'
import json, urllib.request
from plugin_helper.beatmods import by_version_id, normalize_mods
game_version = "<instance>"
url = f"https://beatmods.com/api/mods?status=verified&gameVersion={game_version}&gameName=BeatSaber&platform=steampc"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 plugin-helper"})
with urllib.request.urlopen(req, timeout=20) as response:
data = json.load(response)
mods = data["mods"] if isinstance(data, dict) and "mods" in data else data
print(json.dumps(mods, indent=2)[:20000])
mods = normalize_mods(data)
mods_by_version_id = by_version_id(mods)
print(json.dumps(
[
{
"name": mod.name,
"modId": mod.mod_id,
"gitUrl": mod.git_url,
"category": mod.category,
"versionId": mod.version_id,
"modVersion": mod.mod_version,
"zipHash": mod.zip_hash,
"dependencies": mod.dependencies,
"dependencyNames": [
mods_by_version_id[dep].name
for dep in mod.dependencies
if dep in mods_by_version_id
],
}
for mod in mods
],
indent=2,
)[:20000])
PY
```
@@ -103,7 +131,8 @@ https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>
the smoketest. This creates/copies the root `winhttp.dll` and root `Libs/`
substrate that IPA needs.
For GitHub URLs, resolve the release from the user-provided URL only.
For GitHub URLs, resolve the release from the user-provided repository or
release URL only.
Derive `<owner>/<repo>` and optional `<tag>` from the URL. Query the GitHub API directly for metadata:
@@ -189,13 +218,20 @@ https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>
Use `PYTHONPATH=src`; plain `python -m unittest` may fail in this source-layout repo.
For live Beat Saber validation, follow `docs/SMOKETEST.md`. Do not rely on
`timeout` to kill the full game process tree. Prefer the documented
foreground Proton launch with a background watchdog that sleeps for the smoke
window, then terminates Beat Saber by process name. Confirm `Logs/_latest.log`
has the expected IPA/plugin lines. If the game remains open after the
watchdog cleanup, say so and ask the user to close it manually rather than
leaving the turn with Beat Saber running.
After any successful apply that changes a live BSManager instance, always
run the documented live smoketest before the final response unless the user
explicitly says not to. Do not stop at helper check, unit tests, compile
checks, or file-hash verification for live installs. For live Beat Saber
validation, follow `docs/SMOKETEST.md`. Before starting the launch, announce
in agent chat how long the smoketest window will run for, using the current
duration from `docs/SMOKETEST.md` unless the user requested a different
duration. Do not rely on `timeout` to kill the full game process tree.
Prefer the documented foreground Proton launch with a background watchdog
that sleeps for the smoke window, then terminates Beat Saber by process
name. Confirm `Logs/_latest.log` has the expected IPA/plugin lines and
enough menu/UI initialization evidence for the plugin under test. If the
game remains open after the watchdog cleanup, say so and ask the user to
close it manually rather than leaving the turn with Beat Saber running.
For BSIPA/SongCore bootstrap, expected successful log lines include:
@@ -1,4 +1,4 @@
interface:
display_name: "Install Beat Saber Plugin"
short_description: "Update Beat Saber plugins via helper"
default_prompt: "Use $install-beatsaber-plugin to install or update a Beat Saber plugin from this release URL: <url>."
default_prompt: "Use $beatsaber-plugin-manager to install or update a Beat Saber plugin from this release URL: <url>."