Compare commits
45 Commits
13b1840ba0
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ed71a6d081 | |||
| 4dea85b869 | |||
| 544e4cbd29 | |||
| e48c677cc0 | |||
| 1d9698fcbc | |||
| 54803c5c5b | |||
| f0d7955ea0 | |||
| 17d50e1239 | |||
| 4b544e0858 | |||
| c7d629644a | |||
| f40f3ddc56 | |||
| bcfada09f9 | |||
| 861ce587f3 | |||
| 77a15daeda | |||
| 538feae811 | |||
| 26def9609d | |||
| f4fedaeb34 | |||
| 8419d3a5c9 | |||
| 74f1b292f2 | |||
| 8bbca1a872 | |||
| 3154d71988 | |||
| ef673a76de | |||
| 1dbe80a36f | |||
| 8ff20bd4e4 | |||
| a94a1ed000 | |||
| 545d407116 | |||
| ea1e2ad61e | |||
| 53ad6e2c0d | |||
| 1858f76766 | |||
| 1b4087c5b5 | |||
| 74ac0a1c1d | |||
| 400e69289c | |||
| b75d8ccc57 | |||
| c86e51b85a | |||
| f160e4b349 | |||
| dbc4c7e0b4 | |||
| c67878cac0 | |||
| 6af1b03c35 | |||
| 7f0fcc8d20 | |||
| 407abfe6ec | |||
| 1be1353835 | |||
| c0a167dc67 | |||
| eed12376c6 | |||
| 69f9dbd9b1 | |||
| 13d9481168 |
@@ -1,12 +1,19 @@
|
||||
---
|
||||
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.
|
||||
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 .NET Framework BSIPA projects.
|
||||
---
|
||||
|
||||
# Build Beat Saber Plugin
|
||||
# Beat Saber Plugin Builder
|
||||
|
||||
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.
|
||||
|
||||
Before building, read the shared policy references:
|
||||
|
||||
- [repo-workflow.md](../references/repo-workflow.md) for repo root, `.venv`, `PYTHONPATH=src`, dirty worktree handling, and validation commands.
|
||||
- [state-and-profiles.md](../references/state-and-profiles.md) for `.state`, profiles, install identity, instance selection, and checkout locations.
|
||||
- [artifact-policy.md](../references/artifact-policy.md) when handing a built artifact to `plugin-helper`.
|
||||
- [live-validation.md](../references/live-validation.md) when validating a built plugin in-game.
|
||||
|
||||
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
|
||||
@@ -16,21 +23,26 @@ For detailed Linux/BSMT behavior, read [linux-bsipa-build.md](references/linux-b
|
||||
```bash
|
||||
pwd
|
||||
git status --short
|
||||
sed -n '1,220p' plugin-helper.local.toml
|
||||
```
|
||||
|
||||
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.
|
||||
In `plugin-helper`, run commands from repo root. Treat shared profile and
|
||||
dirty-worktree policy as binding.
|
||||
|
||||
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:
|
||||
For a GitHub PR, clone or reuse a checkout according to the shared source
|
||||
checkout policy, 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>
|
||||
git clone https://github.com/<owner>/<repo>.git <state_dir>/build/<name>
|
||||
git -C <state_dir>/build/<name> fetch origin pull/<pr>/head:pr-<pr>
|
||||
git -C <state_dir>/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.
|
||||
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.
|
||||
|
||||
@@ -43,14 +55,21 @@ For detailed Linux/BSMT behavior, read [linux-bsipa-build.md](references/linux-b
|
||||
|
||||
4. Choose Beat Saber references.
|
||||
|
||||
Prefer a BSManager instance matching the plugin or manifest `gameVersion`. Common local roots:
|
||||
Prefer a BSManager instance matching the plugin or manifest `gameVersion`.
|
||||
If the user did not specify an instance and the plugin does not require a
|
||||
narrower version, use the latest available BSInstance. Read
|
||||
`plugin-helper.local.toml` and select the intended profile, then use that
|
||||
profile's `instances_root` and matching `state_dir` instead of searching
|
||||
default BSManager paths manually:
|
||||
|
||||
```text
|
||||
/home/pleb/.local/share/BSManager/BSInstances/<version>
|
||||
/home/pleb/Windows/Users/pleb/BSManager/BSInstances/<version>
|
||||
```bash
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --profile <profile-id> instances
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -61,9 +80,25 @@ For detailed Linux/BSMT behavior, read [linux-bsipa-build.md](references/linux-b
|
||||
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.
|
||||
If the project lacks .NET Framework reference assemblies on Linux, add or
|
||||
pass the package matching the project's target framework, usually
|
||||
`Microsoft.NETFramework.ReferenceAssemblies.net48` for current BSIPA
|
||||
projects, as described in the reference file.
|
||||
|
||||
6. Collect artifacts.
|
||||
6. Verify Beat Saber API changes before patching gameplay code.
|
||||
|
||||
When a Beat Saber upgrade breaks a type/member reference, first determine
|
||||
whether the API moved assemblies before substituting another type. Inspect
|
||||
the target game DLLs and nearby mod sources:
|
||||
|
||||
```bash
|
||||
strings "<BeatSaberDir>/Beat Saber_Data/Managed/Main.dll" | rg 'TypeOrMemberName'
|
||||
strings "<BeatSaberDir>/Beat Saber_Data/Managed/HMLib.dll" | rg 'TypeOrMemberName'
|
||||
ilspycmd -t TypeName "<BeatSaberDir>/Beat Saber_Data/Managed/Main.dll" | sed -n '1,220p'
|
||||
rg -n 'TypeOrMemberName|NearbyConcept' ~/src/<owner>/<repo> ~/src/Auros/SiraUtil ~/src/nike4613/BeatSaber-IPA-Reloaded
|
||||
```
|
||||
|
||||
7. Collect artifacts.
|
||||
|
||||
Find produced DLLs and release zips:
|
||||
|
||||
@@ -71,23 +106,27 @@ For detailed Linux/BSMT behavior, read [linux-bsipa-build.md](references/linux-b
|
||||
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.
|
||||
Verify the DLL name, version, and manifest. If the result is meant for
|
||||
`plugin-helper`, place a copy under
|
||||
`<state_dir>/instances/<instance>/downloads/<plugin-id>/` for the selected
|
||||
profile and use the helper plan/apply workflow rather than hand-copying into
|
||||
a BSManager instance.
|
||||
|
||||
7. Validate.
|
||||
8. Validate.
|
||||
|
||||
For skill edits inside this repo, run:
|
||||
For skill edits inside this repo, run the shared skill validation plus:
|
||||
|
||||
```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
|
||||
PYTHONPATH=src .venv/bin/python -m compileall -q src tests
|
||||
PYTHONPATH=src .venv/bin/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 `Microsoft.NETFramework.ReferenceAssemblies`: add the package matching the target framework, usually `Microsoft.NETFramework.ReferenceAssemblies.net48` for current projects, 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.
|
||||
|
||||
@@ -6,7 +6,8 @@ This workflow comes from `/home/pleb/ops/beatsaber/setlist/docs/pc-modding.md` a
|
||||
|
||||
## Toolchain
|
||||
|
||||
- PC BSIPA plugins are usually .NET Framework `net472` class libraries.
|
||||
- PC BSIPA plugins are .NET Framework class libraries; current projects
|
||||
commonly target `net48`, while older projects may still target `net472`.
|
||||
- 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.
|
||||
@@ -24,16 +25,13 @@ Plugins/
|
||||
winhttp.dll
|
||||
```
|
||||
|
||||
Preferred local managed instance root:
|
||||
In `plugin-helper`, prefer the profile selected from
|
||||
`plugin-helper.local.toml` as the source of truth for the managed instance root
|
||||
and state directory:
|
||||
|
||||
```text
|
||||
/home/pleb/.local/share/BSManager/BSInstances/<version>
|
||||
```
|
||||
|
||||
Windows mirror root:
|
||||
|
||||
```text
|
||||
/home/pleb/Windows/Users/pleb/BSManager/BSInstances/<version>
|
||||
```bash
|
||||
sed -n '1,220p' plugin-helper.local.toml
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --profile <profile-id> instances
|
||||
```
|
||||
|
||||
Use `BeatSaberVersion.txt` for the exact game version. The manifest `gameVersion` normally uses the `major.minor.patch` prefix, not the build suffix.
|
||||
@@ -45,7 +43,7 @@ Prefer machine-local configuration in `<Project>.csproj.user`:
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<BeatSaberDir>/home/pleb/.local/share/BSManager/BSInstances/1.40.8</BeatSaberDir>
|
||||
<BeatSaberDir>/path/from/selected/profile/instances_root/selected-instance</BeatSaberDir>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
@@ -60,7 +58,10 @@ For those, pass `-p:LocalRefsDir=/path/to/instance` or add a local `.csproj.user
|
||||
|
||||
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.
|
||||
- Add the `Microsoft.NETFramework.ReferenceAssemblies.*` package matching the
|
||||
project target framework, usually `Microsoft.NETFramework.ReferenceAssemblies.net48`
|
||||
for current projects, 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.
|
||||
@@ -105,7 +106,9 @@ If the purpose is to install the built artifact, copy it into plugin-helper's st
|
||||
|
||||
## Common Reference Failures
|
||||
|
||||
- `MSB3644` or missing `.NETFramework,Version=v4.7.2` reference assemblies: add `Microsoft.NETFramework.ReferenceAssemblies.net472`.
|
||||
- `MSB3644` or missing `.NETFramework,Version=v4.x` reference assemblies: add
|
||||
the matching `Microsoft.NETFramework.ReferenceAssemblies.*` package, usually
|
||||
`Microsoft.NETFramework.ReferenceAssemblies.net48` for current projects.
|
||||
- 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.
|
||||
@@ -115,17 +118,17 @@ If the purpose is to install the built artifact, copy it into plugin-helper's st
|
||||
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
|
||||
mkdir -p <state_dir>/instances/<instance>/downloads/<plugin-id>
|
||||
cp <checkout>/<path>/bin/Release/<Plugin>.dll <state_dir>/instances/<instance>/downloads/<plugin-id>/<Plugin>.dll
|
||||
sha256sum <state_dir>/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>
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --profile <profile-id> check --instance <instance>
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --profile <profile-id> plan --instance <instance> --plugin <plugin-id>
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --profile <profile-id> apply <plan-path>
|
||||
```
|
||||
|
||||
Inspect the generated plan before applying.
|
||||
|
||||
@@ -3,36 +3,16 @@ 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.
|
||||
---
|
||||
|
||||
# Install Beat Saber Plugin
|
||||
# Beat Saber Plugin Installer
|
||||
|
||||
Use the repository's own `plugin-helper` commands to manage plugins for BSManager instances whenever the helper supports the operation.
|
||||
|
||||
## Hard Guardrail
|
||||
Before acting, read the shared policy references:
|
||||
|
||||
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 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.
|
||||
|
||||
Exception: if the user explicitly asks to bootstrap a Beat Saber version or
|
||||
install verified mods without providing GitHub URLs, use BeatMods metadata to
|
||||
identify compatible versions and dependency closure. Still prefer upstream
|
||||
GitHub release artifacts when BeatMods exposes a `gitUrl` and a matching
|
||||
release/asset can be found. Use BeatMods CDN artifacts only when the upstream
|
||||
artifact is inaccessible, no matching upstream release asset exists, the package
|
||||
is effectively BeatMods-only, or the package is a framework/library dependency
|
||||
such as .NET assemblies. Record the artifact source plus BeatMods `modVersion`,
|
||||
version id, `zipHash`, dependencies, and supported game version in the repo
|
||||
notes/lock data.
|
||||
- [repo-workflow.md](../references/repo-workflow.md) for repo root, `.venv`, `PYTHONPATH=src`, dirty worktree handling, and validation commands.
|
||||
- [state-and-profiles.md](../references/state-and-profiles.md) for `.state`, profiles, install identity, instance selection, and checkout locations.
|
||||
- [artifact-policy.md](../references/artifact-policy.md) for GitHub/BeatMods/private-source/checksum/bootstrap policy.
|
||||
- [live-validation.md](../references/live-validation.md) for smoke tests, logs, and process cleanup.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -58,10 +38,12 @@ notes/lock data.
|
||||
|
||||
3. Determine the instance.
|
||||
|
||||
Prefer the instance the user names. If omitted and the working context clearly points at one lockfile, use that instance. Otherwise run:
|
||||
Prefer the instance the user names. If omitted, use the latest available
|
||||
BSInstance unless the current task context clearly points at another
|
||||
instance:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src python -m plugin_helper instances
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper instances
|
||||
```
|
||||
|
||||
4. Resolve the release source.
|
||||
@@ -69,7 +51,7 @@ notes/lock data.
|
||||
For BeatMods bootstrap or verified packages, query BeatMods with a browser-like user agent:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src python - <<'PY'
|
||||
PYTHONPATH=src .venv/bin/python - <<'PY'
|
||||
import json, urllib.request
|
||||
from plugin_helper.beatmods import by_version_id, normalize_mods
|
||||
|
||||
@@ -104,22 +86,13 @@ notes/lock data.
|
||||
PY
|
||||
```
|
||||
|
||||
BeatMods dependency entries are mod-version ids. Resolve the selected mod's
|
||||
dependency closure before downloading. For each resolved package, prefer its
|
||||
upstream `gitUrl` release artifacts when a matching release asset exists.
|
||||
Fall back to BeatMods CDN only for inaccessible/missing upstream assets,
|
||||
BeatMods-only packages, or framework/library dependencies. CDN URLs are:
|
||||
Follow the artifact policy for GitHub-first sourcing, BeatMods exceptions,
|
||||
and dependency closure. BeatMods CDN URLs are:
|
||||
|
||||
```text
|
||||
https://beatmods.com/cdn/mod/<zipHash>.zip
|
||||
```
|
||||
|
||||
For BSIPA bootstrap, expect the archive to contain root-relative `IPA/` and
|
||||
`IPA.exe` files whether sourced from GitHub or BeatMods. Extract it into the
|
||||
instance root and run `IPA.exe -n` under the same Proton environment used by
|
||||
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 repository or
|
||||
release URL only.
|
||||
|
||||
@@ -148,13 +121,7 @@ release URL only.
|
||||
unzip -l .state/instances/<instance>/downloads/<plugin-id>/<asset-name>
|
||||
```
|
||||
|
||||
Strategy guide:
|
||||
|
||||
- `dll-to-plugins`: asset is a single `.dll` that belongs in `Plugins/`.
|
||||
- `bsipa-zip`: zip top-level paths are only `IPA/`, `Libs/`, or `Plugins/`.
|
||||
- `root-zip`: zip contains valid game-root paths outside the BSIPA top-level set. Use this for BSIPA/bootstrap archives because `IPA.exe`, `IPA.runtimeconfig*.json`, and root `winhttp.dll` are game-root files.
|
||||
- `zip-to-pending`: only when the release is intended for `IPA/Pending/`.
|
||||
- `manual`: do not use for installable releases.
|
||||
Use the install strategy guide in the artifact policy.
|
||||
|
||||
6. Update the registry and lockfile.
|
||||
|
||||
@@ -188,9 +155,9 @@ release URL only.
|
||||
Always pass `--state-dir .state` so the helper uses the repo-local downloaded asset:
|
||||
|
||||
```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 <generated-plan-path>
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state check --instance <instance>
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state plan --instance <instance> --plugin <plugin-id>
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state apply <generated-plan-path>
|
||||
```
|
||||
|
||||
Before applying, read or summarize the generated plan enough to confirm it changes only the intended plugin files.
|
||||
@@ -200,41 +167,15 @@ release URL only.
|
||||
Confirm the installed file hashes match the plan or archive members:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src python -m plugin_helper --state-dir .state state --instance <instance>
|
||||
PYTHONPATH=src python -m plugin_helper --state-dir .state check --instance <instance>
|
||||
PYTHONPATH=src python -m unittest discover -s tests
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state state --instance <instance>
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state check --instance <instance>
|
||||
PYTHONPATH=src .venv/bin/python -m compileall -q src tests
|
||||
PYTHONPATH=src .venv/bin/python -m unittest discover -s tests
|
||||
```
|
||||
|
||||
Use `PYTHONPATH=src`; plain `python -m unittest` may fail in this source-layout repo.
|
||||
|
||||
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:
|
||||
|
||||
```text
|
||||
Game version <version>
|
||||
Loading plugins from Plugins and found <n>
|
||||
Beat Saber IPA (BSIPA): <version>
|
||||
SongCore (SongCore): <version>
|
||||
```
|
||||
|
||||
Warnings about older mod target game-version metadata can be acceptable when
|
||||
BeatMods verified that exact package for the target Beat Saber version, but
|
||||
record them in the tracker or roadmap. Also record when a BeatMods CDN
|
||||
artifact was used so it can be migrated to upstream GitHub later if possible.
|
||||
For live Beat Saber validation, follow the live-validation reference. Also
|
||||
record when a BeatMods CDN artifact was used so it can be migrated to
|
||||
upstream GitHub later if possible.
|
||||
|
||||
9. Final response.
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: beatsaber-plugin-update-auditor
|
||||
description: Audit Beat Saber plugin-helper locks for available plugin updates and follow-up work. Use when the user asks to check all Beat Saber plugins for updates, compare locked mods against GitHub or BeatMods, review plugin/version compatibility tracking notes, identify PR/local/manual builds that need follow-up, or skip paid/private Patreon and Discord plugin sources during update checks.
|
||||
---
|
||||
|
||||
# Beat Saber Plugin Update Auditor
|
||||
|
||||
Use this skill from the `plugin-helper` repo to produce an update audit, not to
|
||||
blindly update plugins. Keep public, private, and experimental sources distinct.
|
||||
|
||||
Before auditing, read the shared policy references:
|
||||
|
||||
- [repo-workflow.md](../references/repo-workflow.md) for repo root, `.venv`, `PYTHONPATH=src`, dirty worktree handling, and validation commands.
|
||||
- [state-and-profiles.md](../references/state-and-profiles.md) for `.state`, profiles, install identity, instance selection, and checkout locations.
|
||||
- [artifact-policy.md](../references/artifact-policy.md) for GitHub/BeatMods/private-source/checksum policy.
|
||||
- [live-validation.md](../references/live-validation.md) only when reviewing smoke-test notes or launch failures.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm repo context:
|
||||
|
||||
```bash
|
||||
test -f pyproject.toml && test -d src/plugin_helper && test -d locks && test -d registry/plugins
|
||||
git status --short
|
||||
```
|
||||
|
||||
2. Choose the instance from the user request. If omitted, use the latest
|
||||
available BSInstance unless the current task context clearly points at
|
||||
another instance:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper instances
|
||||
```
|
||||
|
||||
3. Run the update audit with repo-local state unless the user explicitly targets
|
||||
another configured profile:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state updates --instance <instance>
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state updates --instance <instance> --json
|
||||
```
|
||||
|
||||
The command checks ordinary GitHub locks, `beatmods-*` locks via BeatMods
|
||||
verified metadata, skips `patreon/` and `discord/` sources, and marks local,
|
||||
PR, manual, failed-trial, or pending-smoke entries as review items.
|
||||
|
||||
4. Read the active compatibility tracker before proposing updates for review
|
||||
items. For the current migration notes, inspect:
|
||||
|
||||
```bash
|
||||
sed -n '1,380p' docs/notes/install-and-verify-plugins-1.44.1.md
|
||||
sed -n '1,220p' docs/notes/naluluna-mod-assistant.md
|
||||
```
|
||||
|
||||
Follow up on rows marked failed, omitted, currently failed, smoke blocked,
|
||||
manual resmoke pending, local build, PR build, or local/private. Do not
|
||||
reinstall failed or omitted plugins unless the notes identify a newer
|
||||
compatible source.
|
||||
|
||||
5. For each public update candidate, inspect source notes before changing the
|
||||
lock. Follow the shared artifact policy for GitHub-first sourcing and
|
||||
BeatMods fallback cases.
|
||||
|
||||
6. Do not attempt automated public update checks for:
|
||||
|
||||
- `patreon/NalulunaModAssistant` or Naluluna bundles
|
||||
- `discord/BeatSaberPlus` or BeatSaberPlus/ChatPlexSDK bundles
|
||||
- other paid or closed-source plugin zips
|
||||
|
||||
Report them as skipped/manual follow-up only.
|
||||
|
||||
7. If the user asks to apply an update, switch to the `beatsaber-plugin-manager`
|
||||
workflow for download, lock update, planning, apply, and smoketest.
|
||||
|
||||
## Output
|
||||
|
||||
Summarize in four groups:
|
||||
|
||||
- Public updates found: plugin, current version, candidate version, source.
|
||||
- Current public locks: count only unless the user asks for every row.
|
||||
- Review follow-ups: local builds, PR builds, failed trials, blocked smokes,
|
||||
or notes saying compatibility work is pending.
|
||||
- Skipped private sources: Patreon/Discord paid or closed-source packages.
|
||||
|
||||
Mention BeatMods/GitHub API errors separately from "no update found"; those are
|
||||
unknowns, not proof that the plugin is current.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Beat Saber Plugin Update Auditor"
|
||||
short_description: "Audit plugin locks for GitHub and BeatMods updates."
|
||||
default_prompt: "Check the locked Beat Saber plugins for updates and review items."
|
||||
@@ -0,0 +1,85 @@
|
||||
# Beat Saber Artifact Policy
|
||||
|
||||
Use this policy for plugin install/update/bootstrap tasks and for audit reports
|
||||
that discuss candidate artifacts.
|
||||
|
||||
## Source Priority
|
||||
|
||||
- Prefer upstream GitHub release artifacts for ordinary repository-backed
|
||||
plugins.
|
||||
- Use BeatMods primarily as compatibility, dependency, and verification
|
||||
metadata.
|
||||
- Use BeatMods CDN artifacts only for inaccessible upstream assets,
|
||||
BeatMods-only packages, framework/library dependencies, or cases where no
|
||||
matching upstream release asset exists.
|
||||
- Skip paid/private Patreon, Discord, and closed-source sources during automated
|
||||
public update checks. Report them as manual follow-up unless the user provides
|
||||
the artifact and asks to manage it.
|
||||
|
||||
## Repository Guardrail
|
||||
|
||||
- For ordinary GitHub-hosted plugins, require an explicit GitHub repository,
|
||||
release URL, or user-provided local planning note before selecting a release.
|
||||
- If the user provided a repo URL but no release URL, query that exact repo's
|
||||
releases and choose the most appropriate non-draft, non-prerelease release for
|
||||
the target instance.
|
||||
- If the user provided no repo or release URL for a plugin install/update, stop
|
||||
and ask for one.
|
||||
- Do not discover a different repository from web search, substitute a fork, or
|
||||
infer the "correct" project from a similar package name.
|
||||
- If the URL is a releases page, use that repo's release API and choose the
|
||||
latest non-draft, non-prerelease release unless the user asks for a tag.
|
||||
- If the URL is a tag URL, use that exact tag.
|
||||
|
||||
## BeatMods Exceptions
|
||||
|
||||
When the user explicitly asks to bootstrap a Beat Saber instance or install
|
||||
verified mods without GitHub URLs, use BeatMods verified metadata to identify
|
||||
compatible versions and dependency closure. Still prefer upstream GitHub assets
|
||||
when BeatMods exposes a `gitUrl` and a matching release asset can be found.
|
||||
|
||||
Record the artifact source plus BeatMods `modVersion`, version id, `zipHash`,
|
||||
dependencies, and supported game version in repo notes or lock data when
|
||||
BeatMods metadata drives the selection.
|
||||
|
||||
BeatMods dependency entries are mod-version ids. Resolve the selected mod's
|
||||
dependency closure before downloading.
|
||||
|
||||
## Checksums And Inspection
|
||||
|
||||
- Download artifacts into the selected helper state directory, normally:
|
||||
|
||||
```bash
|
||||
<state_dir>/instances/<instance>/downloads/<plugin-id>/
|
||||
```
|
||||
|
||||
- Record SHA-256 checksums for downloaded or built artifacts.
|
||||
- Match the checksum against GitHub's `digest` when available.
|
||||
- For BeatMods CDN artifacts, preserve BeatMods `zipHash` metadata and verify
|
||||
the downloaded archive against the expected hash when the helper supports it.
|
||||
- Inspect archive contents before selecting install strategy:
|
||||
|
||||
```bash
|
||||
unzip -l <artifact>
|
||||
```
|
||||
|
||||
## Install Strategy Guide
|
||||
|
||||
- `dll-to-plugins`: asset is a single `.dll` that belongs in `Plugins/`.
|
||||
- `bsipa-zip`: zip top-level paths are only `IPA/`, `Libs/`, or `Plugins/`.
|
||||
- `root-zip`: zip contains valid game-root paths outside the BSIPA top-level
|
||||
set. Use this for BSIPA/bootstrap archives because `IPA.exe`,
|
||||
`IPA.runtimeconfig*.json`, and root `winhttp.dll` are game-root files.
|
||||
- `zip-to-pending`: only when the release is intended for `IPA/Pending/`.
|
||||
- `manual`: do not use for installable releases.
|
||||
|
||||
## BSIPA Bootstrap
|
||||
|
||||
Treat BSIPA as a bootstrap phase. `bootstrap` installs the locked BSIPA archive
|
||||
and records generated files. Ordinary plugin plans should depend on healthy
|
||||
bootstrap state.
|
||||
|
||||
For BSIPA bootstrap archives, expect root-relative `IPA/` and `IPA.exe` files
|
||||
whether sourced from GitHub or BeatMods. Extract into the instance root and run
|
||||
`IPA.exe -n` under the same Proton environment used by the smoke test. This
|
||||
creates or copies root `winhttp.dll` and root `Libs/` substrate files.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Beat Saber Live Validation
|
||||
|
||||
Use this policy when a task changes or verifies a live BSManager instance.
|
||||
|
||||
## Smoke Test
|
||||
|
||||
- Follow `docs/SMOKETEST.md` for live game validation.
|
||||
- Before starting Beat Saber, announce how long the smoke window will run, using
|
||||
the current duration in `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.
|
||||
- After the run, confirm no Beat Saber process remains. If cleanup fails, say so
|
||||
and ask the user to close it manually.
|
||||
|
||||
## Required After Live Apply
|
||||
|
||||
After any successful helper `apply` that changes a live BSManager instance, run
|
||||
the documented live smoke test before the final response unless the user
|
||||
explicitly says not to. Helper `check`, unit tests, compile checks, and file-hash
|
||||
verification are useful but do not replace live validation.
|
||||
|
||||
## Logs
|
||||
|
||||
Inspect `Logs/_latest.log`, Unity `Player.log` when relevant, and the live
|
||||
process command line before calling a black screen or launch failure a plugin
|
||||
failure. Duplicate launch args such as repeated `--no-yeet fpfc` can trigger
|
||||
fatal command-line parsing after BSIPA/plugin loading succeeds.
|
||||
|
||||
Expected successful BSIPA/SongCore lines include:
|
||||
|
||||
```text
|
||||
Game version <version>
|
||||
Loading plugins from Plugins and found <n>
|
||||
Beat Saber IPA (BSIPA): <version>
|
||||
SongCore (SongCore): <version>
|
||||
```
|
||||
|
||||
Warnings about older mod target game-version metadata can be acceptable when
|
||||
BeatMods verified that exact package for the target Beat Saber version. Record
|
||||
them in the tracker or roadmap rather than treating them as automatic install
|
||||
failure.
|
||||
@@ -0,0 +1,52 @@
|
||||
# plugin-helper Repo Workflow
|
||||
|
||||
Use these rules for all Beat Saber `plugin-helper` skills.
|
||||
|
||||
## Repo Context
|
||||
|
||||
- Work from the `plugin-helper` repo root.
|
||||
- Confirm context before acting:
|
||||
|
||||
```bash
|
||||
test -f pyproject.toml && test -d src/plugin_helper && test -d registry && test -d locks
|
||||
git status --short
|
||||
```
|
||||
|
||||
- The worktree may already be dirty. Treat existing changes as user work:
|
||||
preserve them, do not revert them, and avoid unrelated formatting or metadata
|
||||
churn.
|
||||
- Use `rg`/`rg --files` for search.
|
||||
|
||||
## Python Commands
|
||||
|
||||
- Run helper commands with `PYTHONPATH=src`.
|
||||
- Prefer `.venv/bin/python` when `.venv` exists; otherwise use `python`.
|
||||
- For human-style inspection, prefer repo-local state:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state menu
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
- After code changes in this repo, run:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src .venv/bin/python -m compileall -q src tests
|
||||
PYTHONPATH=src .venv/bin/python -m unittest discover -s tests
|
||||
```
|
||||
|
||||
- For skill-only edits, also run `quick_validate.py` for each edited skill:
|
||||
|
||||
```bash
|
||||
python /home/pleb/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/<skill-name>
|
||||
```
|
||||
|
||||
- Plain `python -m unittest` can fail in this source-layout repo without
|
||||
`PYTHONPATH=src`.
|
||||
|
||||
## Final Reporting
|
||||
|
||||
Report the commands actually run and their results. When a task changes repo
|
||||
files, include a concise commit message suggestion unless the user already asked
|
||||
for a commit.
|
||||
@@ -0,0 +1,53 @@
|
||||
# plugin-helper State And Profiles
|
||||
|
||||
Use these rules when selecting BSManager instances, state directories, and source
|
||||
checkout locations.
|
||||
|
||||
## Instance Selection
|
||||
|
||||
- Prefer the Beat Saber instance the user names.
|
||||
- If the user omits an instance, use the latest available BSInstance unless the
|
||||
current task context or user notes clearly point at another instance.
|
||||
- Discover available instances with the helper rather than assuming a hard-coded
|
||||
game version:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper instances
|
||||
```
|
||||
|
||||
## State Directories
|
||||
|
||||
- Prefer repo-local `.state` for planned installs, update audits, downloaded
|
||||
artifacts, and generated plans unless the user explicitly targets live default
|
||||
state or another configured profile.
|
||||
- For mounted Windows installs, prefer the shared Windows-partition state
|
||||
directory configured in `plugin-helper.local.toml` when both roots contain the
|
||||
same instance name.
|
||||
- Keep target-specific managed state with the selected target root. Do not mix a
|
||||
Linux install's `installed.json` with a Windows install's state unless the user
|
||||
intentionally selected that shared state.
|
||||
|
||||
## Profiles
|
||||
|
||||
- Read `plugin-helper.local.toml` before choosing a configured profile:
|
||||
|
||||
```bash
|
||||
sed -n '1,220p' plugin-helper.local.toml
|
||||
PYTHONPATH=src .venv/bin/python -m plugin_helper --profile <profile-id> instances
|
||||
```
|
||||
|
||||
- Treat the selected profile's `instances_root` and `state_dir` as a pair.
|
||||
- When using `--profile`, prefer it over manually passing default BSManager paths.
|
||||
|
||||
## Source Checkouts
|
||||
|
||||
- Keep plugin source checkouts under `~/src/<owner>/<repo>` when a locked or
|
||||
registry plugin has a GitHub source repo.
|
||||
- Prefer checking out upstream `owner/repo` first, with `origin` pointing at
|
||||
upstream.
|
||||
- If a personal fork checkout already exists, preserve it as a remote named
|
||||
`github` and set or add `origin` to the upstream repo instead of replacing
|
||||
local work.
|
||||
- For temporary PR/build work tied to a selected profile, use that profile's
|
||||
`<state_dir>/build/<name>` when the skill explicitly calls for disposable
|
||||
build checkouts.
|
||||
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{md,markdown}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{nix,yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
@@ -0,0 +1,17 @@
|
||||
# Normalize text files to LF in the repository.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Windows scripts should stay CRLF when present.
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Binary assets
|
||||
*.dll binary
|
||||
*.exe binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.jpeg binary
|
||||
*.jpg binary
|
||||
*.png binary
|
||||
*.zip binary
|
||||
@@ -1,5 +1,7 @@
|
||||
/.state/
|
||||
/.state-*/
|
||||
/plugin-helper.local.toml
|
||||
/plugin-helper.windows.toml
|
||||
/.pytest_cache/
|
||||
/build/
|
||||
/dist/
|
||||
@@ -7,3 +9,4 @@
|
||||
/src/*.egg-info/
|
||||
/__pycache__/
|
||||
*.pyc
|
||||
.venv
|
||||
|
||||
@@ -5,35 +5,23 @@ Guidance for coding agents working in this repo.
|
||||
## Project Shape
|
||||
|
||||
- This repo manages Beat Saber plugins for BSManager instances.
|
||||
- Default instance roots are:
|
||||
- `~/Windows/Users/pleb/BSManager/BSInstances`
|
||||
- `~/.local/share/BSManager/BSInstances`
|
||||
- A local BSManager source checkout may be available at
|
||||
`~/src/Zagrios/bs-manager`. Use it as a read-only reference when
|
||||
investigating launch behavior, inherited Steam arguments, instance layout, or
|
||||
Proton environment details unless the user explicitly asks for BSManager code
|
||||
changes.
|
||||
- Keep plugin source checkouts under `~/src/<owner>/<repo>` when a
|
||||
locked or registry plugin has a GitHub source repo. Prefer checking out the
|
||||
upstream author/repo first, with `origin` pointing at upstream. If the user has
|
||||
an existing personal fork checkout, preserve it as a remote named `github`
|
||||
and set/add `origin` to the upstream repo instead of replacing local work.
|
||||
- Prefer repo-local state for planned installs unless the task explicitly
|
||||
targets the user's live default state. Use `--state-dir .state` for the local
|
||||
Linux install and `--state-dir .state-windows` for the mounted Windows install
|
||||
when both roots contain the same instance name.
|
||||
targets the user's live default state. For mounted Windows installs, prefer
|
||||
the shared Windows-partition state directory configured in
|
||||
`plugin-helper.local.toml` when both roots contain the same instance name.
|
||||
|
||||
## Workflow Rules
|
||||
|
||||
- Run commands from the repo root with `PYTHONPATH=src`.
|
||||
- A repo-local Python virtualenv is normally available at `.venv`; prefer
|
||||
`.venv/bin/python` for helper commands and tests when it exists.
|
||||
- For human-style inspection, prefer the menu with repo-local state:
|
||||
`PYTHONPATH=src python -m plugin_helper --state-dir .state menu`.
|
||||
- When targeting the local Linux BSManager install, pass
|
||||
`--instances-root ~/.local/share/BSManager/BSInstances` and normally
|
||||
`--state-dir .state`.
|
||||
- When targeting the mounted Windows BSManager install, pass
|
||||
`--instances-root ~/Windows/Users/pleb/BSManager/BSInstances` and
|
||||
normally `--state-dir .state-windows`.
|
||||
`PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state menu`.
|
||||
- Use the helper commands instead of manually copying plugin files into an
|
||||
instance.
|
||||
- When adding, updating, building, or investigating a GitHub-hosted plugin,
|
||||
@@ -43,33 +31,18 @@ Guidance for coding agents working in this repo.
|
||||
- Treat BSIPA as a bootstrap phase:
|
||||
- `bootstrap` installs the locked BSIPA archive and records generated files.
|
||||
- ordinary plugin plans should depend on healthy bootstrap state.
|
||||
- Be careful with duplicate instance names across Windows and local roots. Use
|
||||
the menu or pass `--instances-root` explicitly when targeting one install, and
|
||||
keep install/bootstrap state separate per target root.
|
||||
- After completing repo changes, suggest a concise commit message in the final
|
||||
response unless the user already asked you to commit.
|
||||
|
||||
## Validation
|
||||
|
||||
- Run `PYTHONPATH=src python -m unittest discover -s tests` after code changes.
|
||||
- Run `PYTHONPATH=src python -m compileall -q src tests` for syntax/import
|
||||
checks.
|
||||
- Run `PYTHONPATH=src .venv/bin/python -m unittest discover -s tests` after
|
||||
code changes when `.venv` exists; otherwise use `python`.
|
||||
- Run `PYTHONPATH=src .venv/bin/python -m compileall -q src tests` for
|
||||
syntax/import checks when `.venv` exists; otherwise use `python`.
|
||||
- For live game validation, follow `docs/SMOKETEST.md` and tear down Beat Saber
|
||||
processes afterward.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- When the user asks for a PR against an upstream plugin, compose and submit a
|
||||
polite pull request using the user's existing `gh` session.
|
||||
- Keep upstream PRs small, focused, and easy for the maintainer to verify. Favor
|
||||
narrow compatibility fixes, clear commit messages, and minimal formatting or
|
||||
project-file churn.
|
||||
- Work from the upstream checkout under `~/src/<owner>/<repo>` when available.
|
||||
If the upstream remote is not writable, create or reuse the user's fork with
|
||||
`gh repo fork`, push a topic branch there, and open the PR against upstream.
|
||||
- Include concise verification notes in the PR body, especially the exact build
|
||||
or smoke-test command and any generated artifact name. Mention local-only
|
||||
build configuration separately if it was needed, and do not commit machine
|
||||
paths or helper state unless the user explicitly asks for it.
|
||||
|
||||
## Launch Notes
|
||||
|
||||
- BSManager may inherit Beat Saber launch arguments configured in Steam.
|
||||
|
||||
@@ -6,83 +6,196 @@ The first implementation focuses on safe local workflows:
|
||||
|
||||
- discover BSManager instances
|
||||
- scan existing `Plugins/` and `Libs/` files
|
||||
- read checked-in registry and per-version lockfiles
|
||||
- read the checked-in plugin catalog and per-version locks
|
||||
- generate a machine-readable install plan from local release assets
|
||||
- apply exactly that plan and record install state
|
||||
- uninstall only files recorded in install state
|
||||
- apply exactly that plan and record managed install state
|
||||
- uninstall only files recorded in managed install state
|
||||
|
||||
Default BSManager instance roots:
|
||||
Default BSManager instance root:
|
||||
|
||||
```text
|
||||
/home/pleb/Windows/Users/pleb/BSManager/BSInstances
|
||||
/home/pleb/.local/share/BSManager/BSInstances
|
||||
```
|
||||
|
||||
Override with `--instances-root` or `PLUGIN_HELPER_INSTANCES_ROOT`. To search
|
||||
multiple explicit roots, separate them with `:`.
|
||||
|
||||
## Managing Multiple Installs
|
||||
|
||||
The helper is intended to manage both the local Linux BSManager install and the
|
||||
mounted Windows install. Lockfiles and registry entries are shared by Beat Saber
|
||||
version, but install state is target-specific. When the same instance name
|
||||
exists under both roots, such as `1.44.1`, use an explicit `--instances-root`
|
||||
and a separate state directory for each target.
|
||||
|
||||
Suggested repo-local convention:
|
||||
Default plugin-helper state directory:
|
||||
|
||||
```text
|
||||
.state/ local Linux BSManager state
|
||||
.state-windows/ mounted Windows BSManager state
|
||||
$XDG_STATE_HOME/plugin-helper
|
||||
```
|
||||
|
||||
Examples:
|
||||
If `XDG_STATE_HOME` is not set, the state directory defaults to:
|
||||
|
||||
```text
|
||||
~/.local/state/plugin-helper
|
||||
```
|
||||
|
||||
Override the instance root with `--instances-root`,
|
||||
`PLUGIN_HELPER_INSTANCES_ROOT`, or `plugin-helper.local.toml`. To search
|
||||
multiple explicit roots, separate them with `:` on Linux/macOS or `;` on
|
||||
Windows.
|
||||
|
||||
Override the state directory with `--state-dir`, `PLUGIN_HELPER_STATE_DIR`, or
|
||||
`plugin-helper.local.toml`.
|
||||
|
||||
## Local Configuration
|
||||
|
||||
This checkout is intended to manage the local Linux BSManager install. If you
|
||||
also manage a Windows install, use a separate clone on that partition and point
|
||||
both clones at the same state directory only when you intentionally want one
|
||||
shared source of truth.
|
||||
|
||||
Copy the example config and adjust paths if needed:
|
||||
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python -m plugin_helper \
|
||||
--instances-root /home/pleb/.local/share/BSManager/BSInstances \
|
||||
--state-dir .state \
|
||||
installed --instance 1.44.1
|
||||
|
||||
PYTHONPATH=src python -m plugin_helper \
|
||||
--instances-root /home/pleb/Windows/Users/pleb/BSManager/BSInstances \
|
||||
--state-dir .state-windows \
|
||||
installed --instance 1.44.1
|
||||
cp plugin-helper.toml.example plugin-helper.local.toml
|
||||
```
|
||||
|
||||
Do not reuse the same state directory for both targets when their instance names
|
||||
match. The current state layout is keyed by instance name, so sharing one state
|
||||
directory would mix bootstrap records, generated plans, backups, and installed
|
||||
file records for different game trees.
|
||||
`plugin-helper.local.toml` is ignored by git and uses top-level fields:
|
||||
|
||||
```toml
|
||||
instances_root = "~/.local/share/BSManager/BSInstances"
|
||||
state_dir = "~/.local/state/plugin-helper"
|
||||
```
|
||||
|
||||
For repo-local state, set:
|
||||
|
||||
```toml
|
||||
state_dir = ".state"
|
||||
```
|
||||
|
||||
For a shared Windows-partition state directory, set the same `state_dir` in both
|
||||
clones, for example:
|
||||
|
||||
```toml
|
||||
state_dir = "~/Windows/Users/pleb/ops/plugin-helper/.state"
|
||||
```
|
||||
|
||||
CLI flags override environment variables, environment variables override local
|
||||
config, and local config overrides built-in defaults.
|
||||
|
||||
## Native Windows
|
||||
|
||||
For a separate checkout on a Windows partition, copy the Windows example config
|
||||
and adjust paths if needed:
|
||||
|
||||
```powershell
|
||||
Copy-Item plugin-helper.windows.toml.example plugin-helper.windows.toml
|
||||
```
|
||||
|
||||
`plugin-helper.windows.toml` is ignored by git. It uses profile entries:
|
||||
|
||||
```toml
|
||||
[[profiles]]
|
||||
id = "windows"
|
||||
label = "Native Windows BSManager"
|
||||
instances_root = "~/BSManager/BSInstances"
|
||||
state_dir = ".state"
|
||||
```
|
||||
|
||||
Run commands with `--config` and `--profile` so native Windows state stays in
|
||||
that checkout's `.state/` directory. If `plugin-helper.windows.toml` has only
|
||||
one `[[profiles]]` entry, that profile is selected automatically when
|
||||
`--profile` is omitted:
|
||||
|
||||
From the repo root, double-click or run `menu.bat` to create `.venv` on first
|
||||
use, install the package, and open the Textual menu.
|
||||
|
||||
Or set up manually:
|
||||
|
||||
```powershell
|
||||
py -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
py -m pip install -e .
|
||||
py -m plugin_helper
|
||||
py -m plugin_helper --config plugin-helper.windows.toml --profile windows installed --instance 1.44.1
|
||||
```
|
||||
|
||||
On native Windows, `bootstrap` runs `IPA.exe "Beat Saber.exe" -n` directly
|
||||
instead of through Proton. `bootstrap-check` accepts a recorded native bootstrap
|
||||
without requiring `Logs/_latest.log` when `IPA.exe "Beat Saber.exe" -n`
|
||||
completed successfully.
|
||||
|
||||
When no config file is present on Windows, defaults are
|
||||
`~/BSManager/BSInstances` and `%LOCALAPPDATA%/plugin-helper`.
|
||||
|
||||
## Building plugins
|
||||
|
||||
PC BSIPA plugins target .NET Framework (`net48`). On native Windows, install the
|
||||
reference assemblies before `dotnet build`:
|
||||
|
||||
```powershell
|
||||
winget install -e --id Microsoft.DotNet.Framework.DeveloperPack_4
|
||||
```
|
||||
|
||||
Point `BeatSaberDir` at your BSManager instance and pass
|
||||
`-p:DisableCopyToPlugins=True` so builds do not mutate the live install. See
|
||||
[`.agents/skills/beatsaber-plugin-builder/`](.agents/skills/beatsaber-plugin-builder/)
|
||||
for the full workflow.
|
||||
|
||||
## Commands
|
||||
|
||||
For normal use, run the menu from the repo root. Use repo-local state so the
|
||||
menu sees the same plans, downloads, and install records used by the helper
|
||||
workflow:
|
||||
For normal use, run the Textual menu from the repo root:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python -m plugin_helper --state-dir .state menu
|
||||
PYTHONPATH=src python -m plugin_helper
|
||||
```
|
||||
|
||||
That is equivalent to `PYTHONPATH=src python -m plugin_helper menu` when run
|
||||
from an interactive terminal.
|
||||
|
||||
The menu reads `plugin-helper.local.toml` when present, shows each discovered
|
||||
Beat Saber install with its resolved state directory, and lists plugins from
|
||||
that instance's version lock. Managed install state decides whether each row is
|
||||
currently checked or unchecked. In the plugin table, use `d` to disable all
|
||||
currently enabled plugins (except `bsipa`) and `e` to enable all currently
|
||||
unchecked plugins. With a single Beat Saber installation, the menu opens the
|
||||
plugin table directly. Re-enabling a plugin auto-bootstraps BSIPA when needed.
|
||||
|
||||
Use `s` to save the set of currently enabled plugins as that instance's
|
||||
known-good set, and `g` to restore it later: plugins outside the saved set are
|
||||
disabled and any saved plugins that are currently disabled/missing are
|
||||
re-enabled from local assets. This is handy after a newly added mod pulls in
|
||||
extra dependencies you don't actually want — press `g` to snap back to
|
||||
the combination you know works. Only one known-good set is kept per instance;
|
||||
saving again overwrites it. The equivalent automation commands are
|
||||
`save-known-good` and `restore-known-good`.
|
||||
|
||||
The individual subcommands are mostly for automation and debugging. If you use
|
||||
them, pass `--state-dir .state` unless you intentionally want the default live
|
||||
state outside this repo or are intentionally targeting the Windows install with
|
||||
`.state-windows`.
|
||||
them, pass `--state-dir` directly only when you intentionally want to override
|
||||
the configured state directory for one command.
|
||||
|
||||
Install assets are currently expected to already exist locally, usually under:
|
||||
|
||||
```text
|
||||
.state/instances/<instance>/downloads/<plugin-id>/
|
||||
<state-dir>/cache/downloads/<instance>/<plugin-id>/
|
||||
```
|
||||
|
||||
For a second target-specific state directory, copy or re-download the same
|
||||
locked assets under that state root before planning. For example:
|
||||
Lock entries may include `download_url`, the exact public zip or DLL URL for the
|
||||
locked artifact. The URL is currently audit/recovery metadata; `plan`, `check`,
|
||||
and `apply` still use local assets and do not fetch missing downloads
|
||||
automatically.
|
||||
|
||||
```text
|
||||
.state-windows/instances/<instance>/downloads/<plugin-id>/
|
||||
Composite local assets should have an explicit reconstruction recipe checked in
|
||||
with the helper. For example, Cinema's external Windows downloader tools are a
|
||||
small deterministic bundle rebuilt from upstream executables with:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python -m plugin_helper.recipes.cinema_tools --instance 1.44.1
|
||||
```
|
||||
|
||||
Use `updates` to audit the version lock against obvious public update sources:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python -m plugin_helper --state-dir .state updates --instance 1.44.1
|
||||
```
|
||||
|
||||
The command checks GitHub release assets for ordinary repository-backed locks
|
||||
and BeatMods verified metadata for `beatmods-*` locks. Patreon and Discord
|
||||
paid/private sources are skipped, and local builds, PR builds, failed
|
||||
compatibility trials, and manual installs are marked for review instead of being
|
||||
treated as routine updates.
|
||||
|
||||
## Beat Saber Data Backups
|
||||
|
||||
`backup-userdata` copies the mounted Windows `UserData` folder and Beat Saber
|
||||
@@ -153,10 +266,11 @@ custom content or non-obvious user choices rather than pure cache data.
|
||||
arguments such as `--no-yeet fpfc` can make the game fail command-line
|
||||
parsing after BSIPA and plugins have already loaded.
|
||||
- BSIPA is managed as a first-class bootstrap phase. The `bootstrap` command
|
||||
applies the locked `bsipa` root archive, runs `IPA.exe -n` through Proton, and
|
||||
records every bootstrap-relevant file under root `IPA.exe*`, `winhttp.dll`,
|
||||
`Libs/`, and `IPA/`, including backups created during patching.
|
||||
- If an instance lockfile includes `bsipa`, ordinary plugin plans require a
|
||||
applies the locked `bsipa` root archive, runs `IPA.exe "Beat Saber.exe" -n`
|
||||
(natively on Windows or through Proton on Linux), and records every
|
||||
bootstrap-relevant file under root `IPA.exe*`, `winhttp.dll`, `Libs/`, and
|
||||
`IPA/`, including backups created during patching.
|
||||
- If an instance version lock includes `bsipa`, ordinary plugin plans require a
|
||||
recorded bootstrap state plus a `Logs/_latest.log` that shows BSIPA startup.
|
||||
Use `bootstrap-check` before planning a batch when you want a quick gate.
|
||||
- Use [`docs/SMOKETEST.md`](docs/SMOKETEST.md) after installing or removing a
|
||||
|
||||
+16
-13
@@ -59,7 +59,9 @@ plugin-helper/
|
||||
DESIGN.md
|
||||
README.md
|
||||
registry/
|
||||
plugins.toml
|
||||
plugins/
|
||||
songcore.toml
|
||||
bsipa.toml
|
||||
locks/
|
||||
1.40.8.lock.toml
|
||||
src/plugin_helper/
|
||||
@@ -94,11 +96,11 @@ For early development, a `--state-dir` option is useful so plans and manifests c
|
||||
When managing both local Linux and mounted Windows installs, install state must
|
||||
be separated by target root as well as by instance name. The current state
|
||||
layout is keyed by instance name, so two `1.44.1` installs should not share one
|
||||
state directory. A practical repo-local convention is:
|
||||
state directory. A practical convention is:
|
||||
|
||||
```text
|
||||
.state/ local Linux BSManager state
|
||||
.state-windows/ mounted Windows BSManager state
|
||||
.state/ local Linux BSManager state
|
||||
~/Windows/Users/pleb/ops/plugin-helper/.state mounted Windows BSManager state
|
||||
```
|
||||
|
||||
The registry and lockfile remain shared for a Beat Saber version. Downloads may
|
||||
@@ -124,21 +126,15 @@ Artifact source preference:
|
||||
Example:
|
||||
|
||||
```toml
|
||||
[[plugins]]
|
||||
id = "songcore"
|
||||
name = "SongCore"
|
||||
repo = "Kylemc1413/SongCore"
|
||||
asset_patterns = ["*SongCore*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[plugins.compatibility]
|
||||
source = "metadata-or-release-notes"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bs-utils"
|
||||
constraint = ">=1.0"
|
||||
required = true
|
||||
dependencies = [
|
||||
{ id = "bs-utils", constraint = ">=1.0" },
|
||||
]
|
||||
```
|
||||
|
||||
Important fields:
|
||||
@@ -166,6 +162,7 @@ id = "songcore"
|
||||
repo = "Kylemc1413/SongCore"
|
||||
tag = "v4.3.0"
|
||||
asset = "SongCore-4.3.0.zip"
|
||||
download_url = "https://github.com/Kylemc1413/SongCore/releases/download/v4.3.0/SongCore-4.3.0.zip"
|
||||
sha256 = "..."
|
||||
reason = "Pinned until dependent mods support newer SongCore"
|
||||
|
||||
@@ -174,9 +171,15 @@ id = "some-ui-mod"
|
||||
repo = "example/some-ui-mod"
|
||||
tag = "v1.2.1"
|
||||
asset = "SomeUIMod.dll"
|
||||
download_url = "https://github.com/example/some-ui-mod/releases/download/v1.2.1/SomeUIMod.dll"
|
||||
sha256 = "..."
|
||||
```
|
||||
|
||||
`download_url` is the optional concrete URL for the locked artifact. It should
|
||||
point at the exact zip or DLL when the artifact is publicly downloadable, and
|
||||
should be omitted for local builds, private/commercial packages, and packages
|
||||
that must be reconstructed from source or a recipe.
|
||||
|
||||
The lockfile should be the source of truth for reproducible installs. `check` may propose newer versions, but `apply` should install what is present in a plan generated from the lockfile or from an explicit update command.
|
||||
|
||||
## Install State
|
||||
|
||||
+8
-1
@@ -88,6 +88,13 @@ For a batch install, check for:
|
||||
- there are no repeated unhandled exceptions from newly installed plugins.
|
||||
- the game reaches the main menu.
|
||||
|
||||
Several `ERROR` and `WARNING` lines are benign on a healthy 1.44.1 stack (older
|
||||
manifest warnings, BeatLeader optional-interop probes, brief
|
||||
`AudioTimeSyncController` resume noise, custom-map `Leaderboard id ... not
|
||||
found`, transient KeyRemapper controller reconnects, and similar). See
|
||||
[`docs/notes/install-and-verify-plugins-1.44.1.md`](notes/install-and-verify-plugins-1.44.1.md#known-ipa-log-noise)
|
||||
before treating every red log line as a plugin failure.
|
||||
|
||||
After the first successful launch, `plugin-helper bootstrap-check --instance
|
||||
<version>` should pass. Ordinary plugin plans for lockfiles that include BSIPA
|
||||
depend on that recorded bootstrap state and the latest IPA log.
|
||||
@@ -99,7 +106,7 @@ grep Setlist Logs/_latest.log
|
||||
```
|
||||
|
||||
Expected Setlist signals include `platformUserId=...`, playlist lines with
|
||||
`hasSyncUrl=...`, and `beatLeaderOwnerConfirmed=...`. When testing real playlist
|
||||
`hasSyncUrl=...`, and `ownerMatchesPlatform=...`. When testing real playlist
|
||||
sync, adding a map to an owned BeatLeader-synced playlist should log an HTTP
|
||||
success line.
|
||||
|
||||
|
||||
@@ -83,6 +83,34 @@ Suggested log checks:
|
||||
- custom songs still enumerate
|
||||
- playlist and downloader UI still opens when relevant
|
||||
|
||||
Treat the patterns in [Known IPA log noise](#known-ipa-log-noise) as non-blocking
|
||||
unless they appear together with a startup failure, missing plugin load line, or
|
||||
broken in-game behavior.
|
||||
|
||||
## Known IPA Log Noise
|
||||
|
||||
These messages show up in healthy 1.44.1 Windows VR sessions and do not, by
|
||||
themselves, block continuing the install pass. They were confirmed on
|
||||
`Logs/_latest.log` from 2026-07-05 after FailButton and the current gameplay
|
||||
stack loaded successfully.
|
||||
|
||||
| Pattern | Example | Why it is usually harmless |
|
||||
| --- | --- | --- |
|
||||
| Older manifest game version | `[IPA/Loader] Mod <name> developed for game version 1.42.0, so it may not work properly.` | IPA warns when a plugin manifest predates 1.44.1. Common across the stack; only actionable if the plugin misbehaves in VR. |
|
||||
| BeatLeader optional interop missing | `[BeatLeader] Plugin ScoreSaber not found, ScoreSaberInterop will not be loaded` | BeatLeader probes for optional companion mods. Expected when those mods are not installed. |
|
||||
| ScoreSaber Harmony stub when ScoreSaber absent | `[Harmony] Could not find type named ScoreSaber.Core.ReplaySystem.HarmonyPatches.PatchHandleHMDUnmounted` | Harmless when ScoreSaber is not in the instance. |
|
||||
| Vanilla custom-map leaderboard lookup | `[UnityEngine] Leaderboard id for custom_level_<hash><diff> not found` | Unity/official leaderboard plumbing on custom songs. BeatLeader API calls can still succeed in the same session. |
|
||||
| Brief audio resume desync | `[UnityEngine] AudioTimeSyncController: audio should be playing ... isPlaying=False` | Short burst when resuming from pause or squat gate; not the same class of failure as HitsoundTweaks' `InvalidProgramException` on an affinity patch. |
|
||||
| Transient XR controller drop | `[KeyRemapper/UnityXRInputManager] Left controller not found.` | Headset/controller reconnect noise. Controllers typically reappear a line or two later. |
|
||||
| SiraUtil duplicate expose | `[SiraUtil] Could not expose NoteCutSoundEffectManager. It is already binded.` | Seen when multiple installers touch the same gameplay binding. |
|
||||
| BSML duplicate type handlers | `[BeatSaberMarkupLanguage] Registering type handler BeatLeader.UI.BSML_Addons...` | BeatLeader registers BSML extensions that overlap core handlers. Cosmetic warning only so far. |
|
||||
| BeatLeader SteamVR console | `[BeatLeader] SteamVR console connection failed: Unable to connect to the remote server` | Optional SteamVR console integration; unrelated to BeatLeader login/API traffic. |
|
||||
| SongCore WIP cache folder | `[SongCore] Folder: '...CustomWIPLevels\Cache' is missing Info.dat file!` | Empty or placeholder WIP cache directory. |
|
||||
| Settings migration noise | `[SettingsIO] Decode: Unknown property 'quality.depth_texture'` | Stale graphics setting key in saved settings. |
|
||||
| DLC promo assets | `[DlcPromoPanelModel] No PromoPanel assets discovered` | Normal when promo panel content is absent. |
|
||||
| SquatToBegin lifecycle notice | `[IPA/Loader] Plugin SquatToBegin has no methods marked [OnStart] or [OnEnable]` | Upstream plugin shape; gate still arms from gameplay scene setup. |
|
||||
| IPA feature deprecation | `[IPA/Loader/Features] Encountered old features used` / `No such feature SiraLocalizer.LocalizedPlugin` | Loader metadata drift; not a load failure. |
|
||||
|
||||
## Status Legend
|
||||
|
||||
- <span style="color:#8b949e; font-weight:600">todo</span>: not attempted
|
||||
@@ -103,7 +131,7 @@ Track the tool work discovered while using it for 1.44.1.
|
||||
| Model BSIPA/bootstrap installation separately | <span style="color:#8b949e; font-weight:600">todo</span> | BSManager creates root `IPA/`, `IPA.exe`, `winhttp.dll`, `Libs/`, `Logs`, `UserData`, and root `Plugins/`. |
|
||||
| Resolve BeatMods dependency closure | <span style="color:#8b949e; font-weight:600">todo</span> | Use as metadata/advisory input even when downloading plugin artifacts from upstream GitHub. |
|
||||
| Install BeatMods library payloads into `Libs/` | <span style="color:#8b949e; font-weight:600">todo</span> | Include framework-library cases when required; these are likely exceptions to GitHub-preferred sourcing. |
|
||||
| Support local/private plugin payloads | <span style="color:#8b949e; font-weight:600">todo</span> | Needed for paid closed-source and manual plugins. |
|
||||
| Support local/private plugin payloads | <span style="color:#8b949e; font-weight:600">todo</span> | Needed for paid closed-source and manual plugins. BeatSaberPlus 6.4.5 was installed manually from a Discord-only release zip on 2026-06-29. |
|
||||
| Record install state for every copied file | <span style="color:#8b949e; font-weight:600">todo</span> | Required for rollback and clean omission testing. |
|
||||
| Add a batch install workflow or documented command sequence | <span style="color:#8b949e; font-weight:600">todo</span> | Useful for two-or-three-at-a-time validation. |
|
||||
| Add IPA log inspection helper | <span style="color:#8b949e; font-weight:600">todo</span> | Nice-to-have; manual log watching via `docs/SMOKETEST.md` is acceptable today. |
|
||||
@@ -170,7 +198,7 @@ stable.
|
||||
|
||||
| Plugin | Upstream | Status | Source/version | Verification notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| ScoreSaber | [github](https://github.com/ScoreSaber/pc-mod) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `ScoreSaber/pc-mod` tag `v3.3.27`, asset `ScoreSaber-v3.3.27-bs1.42.0-to-1.44.0-9b4cfcf.zip`; GitHub release digest matched downloaded asset | IPA loaded ScoreSaber 3.3.27 and synchronized its clock. Warning: FPFC/no-VR smoke logged `openxr_loader not found` while trying to get HMD info. |
|
||||
| ScoreSaber | [github](https://github.com/ScoreSaber/pc-mod) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `ScoreSaber/pc-mod` tag `v3.3.27`, asset `ScoreSaber-v3.3.27-bs1.42.0-to-1.44.0-9b4cfcf.zip`; GitHub release digest matched downloaded asset | IPA loads 3.3.27, but auth fails on 1.44.1: `/api/v2/game/auth` returns 403 `Official build does not match upload version hash` because upstream only registers upload-trust hashes through 1.44.0. |
|
||||
| BeatLeader + LeaderboardCore | [github](https://github.com/BeatLeader/beatleader-mod) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `BeatLeader/beatleader-mod` tag `v0.10.0`, asset `BeatLeader-0.10.0-bs1.42+.zip`; GitHub release digest matched downloaded asset; archive bundles `Plugins/LeaderboardCore.dll`, superseding the standalone BeatMods CDN trial | IPA loaded BeatLeader 0.10.0 and bundled LeaderboardCore 1.7.0, made BeatLeader API requests, and patched 13 ScoreSaber installers. Warnings: optional interop plugins missing; FPFC/no-VR smoke logged OpenXR session not running; bundled LeaderboardCore logged a Harmony patch error for `PanelView_SetIsLoaded`. |
|
||||
| AccSaber | [github](https://github.com/not-dexter/accsaber-reloaded-plugin) | <span style="color:#f85149; font-weight:600">failed compatibility trial</span> | GitHub `not-dexter/accsaber-reloaded-plugin` tag `v1.1.3`, asset `1.40.8.zip`; GitHub release digest matched downloaded asset; latest release has no 1.44.x asset | IPA loaded AccSaber 1.1.3, but the smoke log hit `TypeLoadException: Invalid type AccSaber.Managers.AccSaberStore` for `AccSaberMissionScreen`. Removed from the live instance after the failed trial. |
|
||||
| SongRankedBadge | [github](https://github.com/qe201020335/SongRankedBadge) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `qe201020335/SongRankedBadge` tag `v1.0.6`, asset `SongRankedBadge-1.0.6-bs1.40.0-88ee233.zip`; BeatMods version id 2267, zipHash `c6944b8a4b00b0c0bb1d44f273b3bb18` | IPA loaded SongRankedBadge 1.0.6, resolved SongDetailsCache, loaded song details, and initialized. Warning: manifest targets Beat Saber 1.40.0. |
|
||||
@@ -198,12 +226,12 @@ Purpose: restore song-list, menu, and visualization conveniences.
|
||||
|
||||
| Plugin | Upstream | Status | Source/version | Verification notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BetterSongList | [github](https://github.com/kinsi55/BeatSaber_BetterSongList) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `kinsi55/BeatSaber_BetterSongList` tag `v0.4.3`, asset `BetterSongList.dll`; GitHub release digest matched downloaded asset | IPA loaded BetterSongList 0.4.3 and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.42.0. |
|
||||
| BetterSongList | [github](https://github.com/kinsi55/BeatSaber_BetterSongList) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `kinsi55/BeatSaber_BetterSongList` tag `v0.4.3`, asset `BetterSongList.dll`; GitHub release digest matched downloaded asset | IPA loaded BetterSongList 0.4.3 and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.42.0. SongDetailsCache is optional but recommended for cached song-detail lookups; the registry lists it as a non-required dependency. |
|
||||
| HitScoreVisualizer | [github](https://github.com/ErisApps/HitScoreVisualizer) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `ErisApps/HitScoreVisualizer` tag `3.7.3`, asset `HitScoreVisualizer-3.7.3-bs1.42.0-a565cbb.zip`; GitHub release digest matched downloaded asset | IPA loaded HitScoreVisualizer 3.7.3, installed app/menu installers, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.42.0. |
|
||||
| DiTails | [github](https://github.com/Auros/DiTails) | <span style="color:#d29922; font-weight:600">installed; smoke blocked</span> | Local build from `/home/pleb/src/Auros/DiTails` commit `601e3c4` on branch `fix-1.44-artwork-initialization`, asset `DiTails-1.1.3-bs1.44.1-601e3c4.zip`, SHA-256 `b8735f24545f1a50392865bf3013b930bee8e4ba7feac824a2482e2f1deeba1e`; replaces failed GitHub `Auros/DiTails` tag `1.1.3`, asset `DiTails-v1.1.3-g1.42.0-271d394.zip` and BeatMods 1.1.3 trial | Helper installed `Plugins/DiTails.dll` SHA-256 `5c856ffdfeab54982b84cb2e3033c970622668c553396460499d2343363d1d9d` in the mounted Windows instance. IPA loaded DiTails 1.1.3 and the previous `DetailContextManager.Initialize()` `NullReferenceException` did not recur before startup was blocked by `SteamAPI Init failed` because Steam was not running. |
|
||||
| HideTheLogo | [github](https://github.com/TheBlackParrot/HideTheLogo) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `TheBlackParrot/HideTheLogo` tag `1.0.3`, asset `HideTheLogo-1.0.3-bs1.40.3-c968d91.zip` | IPA loaded HideTheLogo 1.0.3, logged `yeet`, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.40.3. |
|
||||
| SongChartVisualizer | [github](https://github.com/NuggoDEV/SongChartVisualizer), [beatmods zip](https://beatmods.com/cdn/mod/5d3fc025fe098277667fc0846e1b8fe3.zip) | <span style="color:#d29922; font-weight:600">verified with warning</span> | BeatMods 1.1.11, version id 2249, zipHash `5d3fc025fe098277667fc0846e1b8fe3`; GitHub releases API returned no releases on 2026-06-29 | IPA loaded SongChartVisualizer 1.1.11, installed app/menu installers, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.39.1. |
|
||||
| Setlist | | <span style="color:#f85149; font-weight:600">skipped after local-build trial</span> | Local build from `/home/pleb/ops/beatsaber/setlist` commit `14d21ad` with working tree modifications; first SHA-256 `01ecba3cfa697488faddf6eb8bfcc1aedff5d95f4b5a9f673f70b0ff150f5ab9`, rebuilt SHA-256 `57f07f5d99505ee35d45b3914484434fed98113c84cb94e74c6b362abd2216a1` after manifest dependency fix | First smoke ignored Setlist because `BeatLeader@^0.9.0` did not accept installed BeatLeader 0.10.0; after widening to `>=0.9.0`, IPA loaded Setlist 0.1.0 but only logged `No playlists loaded` and did not produce the expected `platformUserId`/playlist ownership lines. Removed from the live instance per skip instruction. |
|
||||
| Setlist | | <span style="color:#3fb950; font-weight:600">verified</span> | Local build from `/home/pleb/ops/beatsaber/setlist` commit `14d21ad` with working tree modifications; asset `Setlist.dll`, SHA-256 `9806d878951958960e82582d219af360e5d67ec2806867900e1374659bfdec2e`; version 0.1.1, manifest `gameVersion` 1.44.1 | Replaces the earlier skipped local builds (`01ecba3c...` and `57f07f5d...`). Fixes the startup scan by waiting briefly for BeatSaberPlaylistsLib playlists instead of returning after an empty early read; dependency gate remains widened to `BeatLeader >=0.9.0`. Smoke on 2026-07-19 loaded Setlist 0.1.1, resolved `platformUserId=76561199407393962`, logged four owned BeatLeader playlists with `ownerMatchesPlatform=true`, and reached `MainSystemInit`. Real add-to-playlist POST sync was not exercised. |
|
||||
|
||||
### Batch 7: Cosmetic, Camera, and Lighting
|
||||
|
||||
@@ -216,39 +244,45 @@ Purpose: add visual and stream-facing mods after functional mods are stable.
|
||||
| PitchBlack | [github](https://github.com/Loloppe/BeatSaber_PitchBlack), [beatmods zip](https://beatmods.com/cdn/mod/65656bf33b2a0b356c381132387ea7ea.zip) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `Loloppe/BeatSaber_PitchBlack` tag `0.03`, asset `PitchBlack-v0.0.3-bs1.39.1.zip`; BeatMods version id 2233, zipHash `65656bf33b2a0b356c381132387ea7ea`; GitHub asset is byte-identical to the BeatMods CDN zip | IPA loaded PitchBlack 0.0.3, generated config, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.39.1. In-song lighting behavior not exercised in FPFC smoke. |
|
||||
| Dimmer | | <span style="color:#8b949e; font-weight:600">todo</span> | TBD | Manual install candidate. |
|
||||
| ReeCamera | [github](https://github.com/Reezonate/ReeCamera) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `Reezonate/ReeCamera` tag `v0.0.5`, asset `ReeCamera.1.42.0.zip`; GitHub release digest matched downloaded asset | IPA loaded ReeCamera 0.0.5, logged Spout load success, installed app/menu installers, and the game reached `MainSystemInit`. Warnings: manifest targets Beat Saber 1.42.0; first launch logged missing `UserData/ReeCamera.json` until the mod creates it on exit per upstream docs. Archive also replaced bundled `Plugins/CameraUtils.dll`. Camera presets not exercised in FPFC smoke. |
|
||||
| Cinema | [github](https://github.com/MisterKelley/BeatSaberCinema/tree/bs-1.42.3) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Local build from `MisterKelley/BeatSaberCinema` branch `bs-1.42.3` commit `010ce6e01728af1e21774ccbb8d14405773f5b02`, asset `BeatSaberCinema-1.7.13-bs1.42.3-010ce6e.zip`, SHA-256 `cd128597b0b0aab2b152eda16849677dd0b52b6f0aebfdaae3ce0c07809d83d7`; source checkout has a local Linux build fix for `BGLib.AppFlow.dll` hint-path casing. | Helper installed `Plugins/BeatSaberCinema.dll` SHA-256 `1dfa5d885ddd1713f8161a266062b413743198961adae87ead45c8ace066ef61` and enabled locked BetterSongList 0.4.3. IPA loaded Cinema 1.7.13 and BetterSongList 0.4.3; Cinema logged hardware info, Steam initialized, and startup reached `MainSystemInit` plus menu installers. Warnings: manifest targets Beat Saber 1.42.3; Cinema failed to register its BetterSongList filter (`Object of type 'BeatSaberCinema.HasVideoFilter' cannot be converted to type 'T'`); Cinema reported missing `yt-dlp`/`ffmpeg` libraries under `Libs`, so video downloading will not work until those are added. |
|
||||
| wipbot | [github](https://github.com/Danielduel/wipbot) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Local build from `C:\Users\pleb\src\Danielduel\wipbot` branch `bs-1.44.1-compat` commit `c717a84`, asset `wipbot.dll`, SHA-256 `c895d45b8e842f1d408b9f53864fc4fc1c2bb6dda4b992d0bb3a6fdc12c0e0a8`; replaces upstream GitHub `1.21.0` (labeled for 1.40.8). Changes: manifest `gameVersion` `1.44.0` / version `1.22.0`; Harmony `PatchAll` scoped to the patch type; CatCore compile-optional so ChatPlex-only installs avoid `ReflectionTypeLoadException`. | Helper installed `Plugins/wipbot.dll` into Windows BSManager `1.44.1` on 2026-07-16. IPA loaded wipbot 1.22.0 and logged `Using ChatPlexSDK for chat`; the prior CatCore Harmony `ReflectionTypeLoadException` did not recur. Warning: manifest targets Beat Saber 1.44.0. Smoke did not reach the main menu because Steam was not running (`SteamAPI` / platform restart). Twitch `!wip` download/queue UI not exercised. |
|
||||
|
||||
### Batch 8: Paid or Closed-Source Plugins
|
||||
|
||||
Purpose: restore private plugin set only after public/dependency-heavy mods are
|
||||
known good.
|
||||
|
||||
See `docs/notes/naluluna-mod-assistant.md` for how NalulunaModAssistant sources
|
||||
Patreon-only Naluluna packages, where it installs them by default, and how to
|
||||
treat its output as local/private payload input for `plugin-helper`.
|
||||
|
||||
| Plugin | Upstream | Status | Source/version | Verification notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| ReeSabers | | <span style="color:#8b949e; font-weight:600">todo</span> | paid/private | Verify saber visuals in VR when practical. |
|
||||
| BeatSaberPlus_Chat | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup and module UI. |
|
||||
| BeatSaberPlus_ChatEmoteRain | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| BeatSaberPlus_ChatIntegrations | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| BeatSaberPlus_ChatRequest | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| BeatSaberPlus_GameTweaker | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| BeatSaberPlus_MenuMusic | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| BeatSaberPlus_Multiplayer | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| BeatSaberPlus_NoteTweaker | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| BeatSaberPlus_SongChartVisualizer | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| BeatSaberPlus_SongOverlay | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| NalulunaMenu | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup and menu. |
|
||||
| NalulunaCounters | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify in-song counters. |
|
||||
| NalulunaLevelDetail | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify song detail panel. |
|
||||
| NalulunaSliceVisualizer | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify in-song visuals. |
|
||||
| NalulunaSongPreview | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify song preview. |
|
||||
| NalulunaMissIndicator | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify in-song visuals. |
|
||||
| NalulunaEnergy | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify in-song HUD. |
|
||||
| NalulunaFps | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify overlay. |
|
||||
| NalulunaPPCoin | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| NalulunaRewinder | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify startup. |
|
||||
| NalulunaAvatars | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify avatar load. |
|
||||
| NalulunaShaders | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify dependency for Naluluna visuals. |
|
||||
| NalulunaSkybox | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify skybox sample manifest and skybox load. |
|
||||
| NalulunaUtils | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Verify dependency load. |
|
||||
| ReeSabers | [beatleader patreon](https://www.patreon.com/beatleader) | <span style="color:#d29922; font-weight:600">installed; smoke pending</span> | User-provided Patreon zip `ReeSabers_1.42.0.zip` (original SHA-256 `258b28b27e8a9274a15b22c91ed9a611a85367d9a9f31b575fd992ecd4bdc3ee`); staged helper payload SHA-256 `3f4b08fc450b1dc29f4fc3282c5099d3e09578e3fa2734dfa3c5d670dddfb219` omits bundled `CameraUtils.dll` to preserve locked CameraUtils 1.0.8 | Helper installed `Plugins/ReeSabers.dll` and `UserData/ReeSabers/` presets/textures on 2026-07-12. Live smoke not run yet; verify saber visuals in VR when practical. |
|
||||
| BeatSaberPlus_Chat | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Discord release zip `BeatSaberPlus_v6.4.5-bs-1.44.0.zip`, SHA-256 `21ad51edea5703485779b851e7e86aac11c3d6e3b1de2460b2c3c3b4b5704657`; manual install into Windows BSManager `1.44.1` root `Plugins/` | IPA loaded BeatSaberPlus_Chat 6.4.5, ChatPlexSDK initialized all modules and added a menu button, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.44.0. Chat/Twitch module UI not exercised in FPFC smoke. |
|
||||
| BeatSaberPlus_ChatEmoteRain | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_ChatEmoteRain 6.4.5 and ChatPlexSDK initialized the Chat Emote Rain module; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| BeatSaberPlus_ChatIntegrations | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_ChatIntegrations 6.4.5 and ChatPlexSDK initialized the Chat Integrations module; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| BeatSaberPlus_ChatRequest | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_ChatRequest 6.4.5 and ChatPlexSDK initialized the Chat Request module; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| BeatSaberPlus_GameTweaker | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_GameTweaker 6.4.5 and applied Harmony patches; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| BeatSaberPlus_MenuMusic | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_MenuMusic 6.4.5 and applied Harmony patches; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| BeatSaberPlus_Multiplayer | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_Multiplayer 6.4.5, applied Harmony patches, and added a menu button; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| BeatSaberPlus_NoteTweaker | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_NoteTweaker 6.4.5 and applied Harmony patches; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| BeatSaberPlus_SongChartVisualizer | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_SongChartVisualizer 6.4.5 and ChatPlexSDK initialized the Song Chart Visualizer module; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| BeatSaberPlus_SongOverlay | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Same Discord bundle as BeatSaberPlus_Chat | IPA loaded BeatSaberPlus_SongOverlay 6.4.5 and ChatPlexSDK initialized the Song Overlay module; same FPFC smoketest as BeatSaberPlus_Chat on 2026-06-29. |
|
||||
| NalulunaMenu | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaMenu` 1.2.0 archive `NalulunaMenu-1.2.0-bs1.44.1.zip`; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaMenu 1.2.0, logged `Init`, `OnMenu`, `PlayDefaultMenuMusic`, and registered its menu button; the game reached `MainSystemInit`. Warning: IPA logged missing `NalulunaMenu.resources`. |
|
||||
| NalulunaCounters | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaCounters` 1.3.2; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaCounters 1.3.2, logged `BeatLeader: available`, initialized its menu button, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.43.0. In-song counters not exercised in FPFC smoke. |
|
||||
| NalulunaLevelDetail | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaLevelDetail` 2.0.0; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaLevelDetail 2.0.0, controller `Awake`, menu button registration, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.39.1. Song detail panel not exercised in FPFC smoke. |
|
||||
| NalulunaSliceVisualizer | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Not present in Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29. Verify in-song visuals after install. |
|
||||
| NalulunaSongPreview | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaSongPreview` 1.0.1; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaSongPreview 1.0.1, controller `Awake`, menu button registration, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.40.4. Song preview not exercised in FPFC smoke. |
|
||||
| NalulunaMissIndicator | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Not present in Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29. Verify in-song visuals after install. |
|
||||
| NalulunaEnergy | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaEnergy` 1.1.0; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaEnergy 1.1.0, controller `Awake`, menu button registration, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.42.0. In-song HUD not exercised in FPFC smoke. |
|
||||
| NalulunaFps | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Not present in Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29. Verify overlay after install. |
|
||||
| NalulunaPPCoin | | <span style="color:#8b949e; font-weight:600">todo</span> | local/private | Not present in Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29. Verify startup after install. |
|
||||
| NalulunaRewinder | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaRewinder` 1.2.0; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaRewinder 1.2.0, config loaded, menu button registered, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.42.2. Rewind behavior not exercised in FPFC smoke. |
|
||||
| NalulunaAvatars | [patreon](https://www.patreon.com/naluluna/posts/nalulunaavatars-52013165) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaAvatars` 1.10.0 plus bundled `AliciaSolid` 1.10.0 manifest; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaAvatars 1.10.0, logged `Init`, started OSC face tracking, and registered its menu button; the game reached `MainSystemInit`. Warnings: configured avatar `2299336039803240.vrm` missing; IPA logged missing `NalulunaAvatars.resources`; one `NullReferenceException` in `HMUI.CurvedCanvasSettings.Start` during menu setup. VR avatar load not exercised. |
|
||||
| NalulunaShaders | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaShaders` 2.1.3 archive `NalulunaShaders-2.1.3-bs1.39.1.zip`; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaShaders 2.1.3, logged `Assets loaded`, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.39.1. |
|
||||
| NalulunaSkybox | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaSkybox` 2.0.0 plus `NalulunaSkyboxSamples` 1.1.0 manifest; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaSkybox 2.0.0 and NalulunaSkyboxSamples 1.1.0, controller `Awake`, menu button registration, and the game reached `MainSystemInit`. Warning: Skybox manifest targets Beat Saber 1.39.1. Skybox swap not exercised in FPFC smoke. |
|
||||
| NalulunaUtils | [patreon](https://www.patreon.com/c/naluluna/posts) | <span style="color:#d29922; font-weight:600">verified with warning</span> | Patreon via NalulunaModAssistant; `NalulunaUtils` 5.7.0 archive `NalulunaUtils-5.7.0-bs1.44.0.zip` plus bundled `OpenVRHelper` 0.2.3 manifest; manual install into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded NalulunaUtils 5.7.0 and OpenVRHelper 0.2.3, logged `Awake`, registered dependent Naluluna menu buttons, and `OnMenuSceneActive: Succeeded`; the game reached `MainSystemInit`. Warnings: manifest targets Beat Saber 1.44.0; IPA logged missing `NalulunaUtils.resources` and optional libs (SteamVR, DynamicBone, FinalIK, CameraPlus, BGNet). |
|
||||
|
||||
## Shared Libraries and Dependency Packages
|
||||
|
||||
@@ -268,6 +302,7 @@ are user-facing features.
|
||||
| SongDetailsCache | [github](https://github.com/kinsi55/BeatSaber_SongDetails) | <span style="color:#d29922; font-weight:600">verified with warning</span> | BetterSongList, SongRankedBadge | GitHub `kinsi55/BeatSaber_SongDetails` tag `v1.4.0`, asset `SongDetailsCache.BS.Lib.zip`; BeatMods version id 2226, zipHash `e1167b64cd3eff7e3651ec2dbbe50d81` | IPA loaded SongDetailsCache 1.4.0 and SongRankedBadge used it to load song details. Warning: manifest targets Beat Saber 1.13.2. |
|
||||
| System.IO.Compression | [beatmods zip](https://beatmods.com/cdn/mod/a4e9e26f61967e56168e08eecb01ab88.zip) | <span style="color:#d29922; font-weight:600">verified with warning</span> | BeatSaberPlaylistsLib, System.IO.Compression.FileSystem | BeatMods 4.6.57, version id 1763, zipHash `a4e9e26f61967e56168e08eecb01ab88` | IPA loaded System.IO.Compression 4.6.57; logged a duplicate-library notice because the game also ships this assembly. |
|
||||
| System.IO.Compression.FileSystem | [beatmods zip](https://beatmods.com/cdn/mod/e19f6fd395d54de7bfcbbbe3084dea28.zip) | <span style="color:#3fb950; font-weight:600">verified</span> | BeatSaverDownloader | BeatMods 4.7.3056, version id 1762, zipHash `e19f6fd395d54de7bfcbbbe3084dea28` | IPA loaded System.IO.Compression.FileSystem 4.7.3056. |
|
||||
| ChatPlexSDK_BS | [discord](https://discord.com/channels/723117082111246416/768683248804167690) | <span style="color:#d29922; font-weight:600">verified with warning</span> | BeatSaberPlus | Bundled in Discord release zip `BeatSaberPlus_v6.4.5-bs-1.44.0.zip`; manually installed into Windows BSManager `1.44.1` root `Plugins/` on 2026-06-29 | IPA loaded ChatPlexSDK_BS 6.4.5, initialized all BeatSaberPlus modules, added a menu button, and cached Twitch/BTTV/FFZ/7TV emotes during FPFC smoke on 2026-06-29. Warning: manifest targets Beat Saber 1.44.0. |
|
||||
| Dynamic Bone | | <span style="color:#dbab79; font-weight:600">defer</span> | TBD | TBD | Include only if a selected 1.44.1 mod requires it. |
|
||||
| Final IK | | <span style="color:#dbab79; font-weight:600">defer</span> | TBD | TBD | Include only if a selected 1.44.1 mod requires it. |
|
||||
|
||||
@@ -296,4 +331,5 @@ Record plugins skipped for this 1.44.1 pass. This is not a fix list for today.
|
||||
| 5 | | <span style="color:#8b949e; font-weight:600">todo</span> | | |
|
||||
| 6 | | <span style="color:#8b949e; font-weight:600">todo</span> | | |
|
||||
| 7 | 2026-06-29 | <span style="color:#d29922; font-weight:600">verified with warning</span> | Installed and smoke-tested AdBlocker 1.0.5, HighlightBombs 1.0.3, PitchBlack 0.0.3, and ReeCamera 0.0.5. All four loaded and the game reached `MainSystemInit`. HighlightBombs installed its app installer and loaded QuickOutline material. ReeCamera logged Spout load success and installed app/menu installers. | Warnings: all four manifests target older Beat Saber versions (1.34.2, 1.32.0, 1.39.1, 1.42.0). AdBlocker and PitchBlack used BeatMods CDN or byte-identical GitHub assets because JonnyVR1/AdBlocker exposes no GitHub releases and HighlightBombs BeatMods 1.0.3 is newer than GitHub v1.0.1. ReeCamera first launch logged missing `UserData/ReeCamera.json` until mod creates it on exit; archive replaced bundled `CameraUtils.dll`. In-song bomb/lighting/camera visuals were not exercised in FPFC smoke. Pre-existing LeaderboardCore and PlaylistManager errors still appear in the log. |
|
||||
| 8 | | <span style="color:#8b949e; font-weight:600">todo</span> | | |
|
||||
| 7b | 2026-07-16 | <span style="color:#d29922; font-weight:600">verified with warning</span> | Local-built wipbot 1.22.0 installed into Windows BSManager `1.44.1`; IPA loaded wipbot and logged `Using ChatPlexSDK for chat` with no CatCore Harmony `ReflectionTypeLoadException`. | Warning: manifest targets 1.44.0. Steam was not running, so the smoke did not reach `MainSystemInit`; Twitch `!wip` / WIP button not exercised. |
|
||||
| 8 | 2026-06-29 | <span style="color:#d29922; font-weight:600">verified with warning</span> | FPFC smoketest `Logs/2026.06.29.15.32.05.log`: BeatSaberPlus 6.4.5 bundle and ChatPlexSDK_BS loaded and initialized all modules; ten Patreon Naluluna packages loaded (`NalulunaUtils`, `NalulunaMenu`, `NalulunaAvatars`, `NalulunaSkybox`, `NalulunaShaders`, `NalulunaCounters`, `NalulunaEnergy`, `NalulunaLevelDetail`, `NalulunaSongPreview`, `NalulunaRewinder`); NalulunaUtils registered menu buttons and `OnMenuSceneActive: Succeeded`; NalulunaMenu played default menu music; the game reached `MainSystemInit`. | Warnings: BeatSaberPlus/ChatPlex and several Naluluna manifests target older Beat Saber versions; NalulunaUtils logged missing satellite resources and optional libs; NalulunaAvatars referenced a missing configured VRM and hit one menu `NullReferenceException`; pre-existing LeaderboardCore Harmony patch error still appears; slice/miss/fps/ppcoin Naluluna mods remain uninstalled; in-song Naluluna features and VR avatar load were not exercised. |
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# NalulunaModAssistant Source and Install Notes
|
||||
|
||||
This note records how NalulunaModAssistant behaves when used to install
|
||||
Patreon-only Naluluna mods. It is intended to guide future `plugin-helper`
|
||||
support for private/local payloads without checking private artifacts,
|
||||
session-specific URLs, or account data into the repo.
|
||||
|
||||
Observed run:
|
||||
|
||||
- Date: 2026-06-29
|
||||
- Assistant path: `C:\Users\pleb\Apps\Naluluna\NalulunaModAssistant.exe`
|
||||
- Assistant version: `1.5.1`
|
||||
- Runtime UI: WebView2 `149.0.4022.80`
|
||||
- Log path: `C:\Users\pleb\Apps\Naluluna\Logs\NalulunaModAssistant\_latest.log`
|
||||
|
||||
## High-Level Flow
|
||||
|
||||
NalulunaModAssistant is a WebView2 desktop app. It authenticates the user through
|
||||
an embedded browser, fetches a versioned mod catalog, then downloads selected
|
||||
plugin archives into a temporary working directory before extracting them into
|
||||
the configured Beat Saber directory.
|
||||
|
||||
The observed Patreon flow was:
|
||||
|
||||
1. Initialize WebView2 and a temporary working directory.
|
||||
2. Load the initial catalog JSON named `start`.
|
||||
3. Navigate to Patreon at `https://www.patreon.com/c/naluluna/posts`.
|
||||
4. Resolve the NalulunaModAssistant files post:
|
||||
`https://www.patreon.com/naluluna/posts/nalulunamodassis-74489236`.
|
||||
5. Confirm the logged-in Patreon account has access.
|
||||
6. Detect Beat Saber version `1.44.1`.
|
||||
7. Select mod list version `1.42.0`.
|
||||
8. Fetch the catalog JSON named `1-42-0`.
|
||||
9. Download selected archives and extract their payloads into the configured
|
||||
Beat Saber tree.
|
||||
|
||||
The assistant binary also contains a `GetJsonFromGoogleSiteAsync` code path and
|
||||
Fanbox login/status paths. Those appear to be alternate or older source flows.
|
||||
The 2026-06-29 run used Patreon, not Google Sites, GitHub, BeatMods, or Fanbox.
|
||||
|
||||
## Source Model
|
||||
|
||||
Naluluna plugin artifacts are not normal public GitHub or BeatMods releases.
|
||||
For Patreon installs, the authoritative source is the private
|
||||
NalulunaModAssistant Patreon post and its attachment metadata.
|
||||
|
||||
The assistant downloads files through two forms:
|
||||
|
||||
- Relative catalog paths such as `nma/files/NalulunaUtils-5.7.0-bs1.44.0.zip`
|
||||
and `nma/imgs/NalulunaCounters.jpg`.
|
||||
- Patreon file endpoints such as `https://www.patreon.com/file?...`, which
|
||||
resolve to tokenized `patreonusercontent.com` CDN URLs after authentication.
|
||||
|
||||
Do not record tokenized CDN URLs, cookies, WebView profile data, or private
|
||||
account identifiers in repo files. They are session-specific and may grant access
|
||||
to paid artifacts. For lockfile-style notes, record the Patreon post, the
|
||||
assistant-visible archive name, the mod version, the Beat Saber target string,
|
||||
the installed paths, and local file hashes when available.
|
||||
|
||||
## Catalog Versioning
|
||||
|
||||
In the observed run, the game was `1.44.1` but the assistant selected
|
||||
`modListVersion: 1.42.0` and fetched catalog node `1-42-0`.
|
||||
|
||||
That means the assistant's catalog version should not be assumed to exactly
|
||||
match the game version. Treat it as an upstream compatibility bucket. For
|
||||
documentation and locks, record both values:
|
||||
|
||||
- actual game version reported by the assistant
|
||||
- mod list version selected by the assistant
|
||||
|
||||
## Install Target
|
||||
|
||||
The assistant reads and writes a configured Beat Saber directory, not a
|
||||
BSManager instance root chosen by `plugin-helper`.
|
||||
|
||||
Observed config:
|
||||
|
||||
- Beat Saber directory:
|
||||
`C:\Program Files (x86)\Steam\steamapps\common\Beat Saber`
|
||||
- Working directory:
|
||||
`C:\Users\pleb\AppData\Local\Temp\tmp97E.tmp`
|
||||
|
||||
This matters because the observed run installed into the Steam Beat Saber tree,
|
||||
not the BSManager `1.44.1` instance at
|
||||
`C:\Users\pleb\BSManager\BSInstances\1.44.1`.
|
||||
|
||||
Before using NalulunaModAssistant as a source for a BSManager instance, either:
|
||||
|
||||
- point the assistant at the intended BSManager instance root, if the UI allows
|
||||
it, or
|
||||
- treat the Steam install as a staging source and copy exact installed files into
|
||||
the target instance through a `plugin-helper` local/private payload workflow.
|
||||
|
||||
Do not manually copy files into the live instance as a substitute for
|
||||
`plugin-helper` state tracking unless this is an explicit one-off recovery step.
|
||||
|
||||
## Extraction Behavior
|
||||
|
||||
Archives are extracted relative to the Beat Saber directory. The observed
|
||||
payloads wrote to these top-level areas:
|
||||
|
||||
- `Plugins/`
|
||||
- `Libs/Native/`
|
||||
- `UserData/NalulunaUtils/`
|
||||
- `UserData/NalulunaSkybox/`
|
||||
- `NalulunaAvatars/`
|
||||
|
||||
Examples from the observed run:
|
||||
|
||||
- `Plugins/NalulunaUtils.dll`
|
||||
- `Plugins/NalulunaAvatars.dll`
|
||||
- `Plugins/NalulunaSkybox.dll`
|
||||
- `Plugins/NalulunaSkyboxSamples.manifest`
|
||||
- `Libs/Native/OVRLipSync.dll`
|
||||
- `UserData/NalulunaUtils/OpenVRHelper/OpenVRHelper.exe`
|
||||
- `UserData/NalulunaSkybox/*.jpgx`
|
||||
- `NalulunaAvatars/AliciaSolid.vrm`
|
||||
|
||||
Some packages install manifests or data assets instead of a plugin DLL. A
|
||||
`plugin-helper` implementation should record every extracted file, not just the
|
||||
primary DLL.
|
||||
|
||||
## Observed Install Batch
|
||||
|
||||
The 2026-06-29 run installed the following selected packages:
|
||||
|
||||
- `NalulunaShaders` `2.1.3`, archive
|
||||
`NalulunaShaders-2.1.3-bs1.39.1.zip`
|
||||
- `NalulunaUtils` `5.7.0`, archive
|
||||
`NalulunaUtils-5.7.0-bs1.44.0.zip`
|
||||
- `AliciaSolid` `1.10.0`, archive
|
||||
`AliciaSolid-1.10.0-bs1.26.0.zip`
|
||||
- `NalulunaAvatars` `1.10.0`
|
||||
- `NalulunaSkybox` `2.0.0`
|
||||
- `OpenVRHelper` `0.2.3`
|
||||
- `NalulunaRewinder` `1.2.0`
|
||||
- `NalulunaCounters` `1.3.2`
|
||||
- `NalulunaEnergy` `1.1.0`
|
||||
- `NalulunaLevelDetail` `2.0.0`
|
||||
- `NalulunaSongPreview` `1.0.1`
|
||||
- `NalulunaSkyboxSamples` `1.1.0`
|
||||
|
||||
The first `NalulunaUtils` attempt downloaded successfully but failed extraction
|
||||
with a Windows file-lock error:
|
||||
|
||||
```text
|
||||
The process cannot access the file
|
||||
'C:\Users\pleb\AppData\Local\Temp\tmp97E.tmp\NalulunaUtils-5.7.0-bs1.44.0.zip'
|
||||
because it is being used by another process.
|
||||
```
|
||||
|
||||
A later retry succeeded and extracted `Plugins/NalulunaUtils.dll`.
|
||||
|
||||
Several downloads log `download interrupted: FileSecurityCheckFailed` after a
|
||||
successful install. In the observed run, those messages did not necessarily mean
|
||||
the mod failed; the assistant logged `Installed: ...` first. Treat this message
|
||||
as a warning to cross-check against extracted files and the assistant's final
|
||||
success/failure count.
|
||||
|
||||
## Implications for plugin-helper
|
||||
|
||||
Private Naluluna payloads should be modeled as local/private artifacts rather
|
||||
than first-class remote fetches until there is an explicit supported way to
|
||||
authenticate and download them safely.
|
||||
|
||||
Recommended near-term workflow:
|
||||
|
||||
1. Use NalulunaModAssistant to download/install into a staging Beat Saber tree.
|
||||
2. Inspect the assistant log for selected package names, versions, archive names,
|
||||
and extracted paths.
|
||||
3. Hash the installed files or preserved archives.
|
||||
4. Add a `plugin-helper` local/private payload entry for each package.
|
||||
5. Apply that payload to the intended BSManager instance with normal install
|
||||
state recording.
|
||||
6. Smoketest the BSManager instance and record results in
|
||||
`docs/notes/install-and-verify-plugins-1.44.1.md`.
|
||||
|
||||
Longer term, `plugin-helper` could add a "local archive import" command that
|
||||
accepts a zip from the assistant's working directory or a saved local cache,
|
||||
normalizes the extracted paths, computes hashes, and creates an auditable
|
||||
install plan without needing to understand Patreon authentication.
|
||||
|
||||
## What Not To Commit
|
||||
|
||||
Do not commit:
|
||||
|
||||
- downloaded private plugin archives
|
||||
- tokenized `patreonusercontent.com` URLs
|
||||
- WebView2 profile data
|
||||
- Patreon or Fanbox cookies
|
||||
- full `_patreon.log` / `_fanbox.log` captures
|
||||
- account identifiers from logs
|
||||
|
||||
Safe to commit:
|
||||
|
||||
- assistant version
|
||||
- Patreon post URL
|
||||
- package names and versions
|
||||
- non-tokenized archive names shown by the assistant
|
||||
- extracted relative paths
|
||||
- hashes of local artifacts or installed files
|
||||
- compatibility and smoketest notes
|
||||
@@ -0,0 +1,103 @@
|
||||
# Windows Compatibility Tracker
|
||||
|
||||
Started: 2026-07-01
|
||||
|
||||
This note tracks the changes needed to run `plugin-helper` natively on Windows
|
||||
11 with Python 3.13, separate from the Linux helper that manages mounted
|
||||
Windows instances.
|
||||
|
||||
## Target Setup
|
||||
|
||||
- Install Python with:
|
||||
|
||||
```powershell
|
||||
winget install --id Python.Python.3.13 --exact
|
||||
```
|
||||
|
||||
- Use a separate checkout or working copy on the Windows partition.
|
||||
- Copy and adjust the Windows config template:
|
||||
|
||||
```powershell
|
||||
Copy-Item plugin-helper.windows.toml.example plugin-helper.windows.toml
|
||||
```
|
||||
|
||||
- Use a Windows-specific config file:
|
||||
|
||||
```powershell
|
||||
py -3.13 -m plugin_helper --config plugin-helper.windows.toml --profile windows instances
|
||||
```
|
||||
|
||||
- Keep native Windows state local to that checkout, such as `.state/`, so it
|
||||
does not mix with Linux-managed state directories.
|
||||
|
||||
## Current Compatibility Notes
|
||||
|
||||
- Core scan, plan, apply, uninstall, disable, and enable flows are mostly
|
||||
platform-neutral. They use `pathlib`, `zipfile`, `shutil`, JSON/TOML, and
|
||||
local file hashes.
|
||||
- Native Windows defaults do not reuse Linux-mounted paths such as
|
||||
`/home/pleb/Windows/...`. A Windows-specific TOML file handles this for normal
|
||||
use.
|
||||
- Multiple `--instances-root` values use `os.pathsep`; that means `;` on
|
||||
Windows and `:` on Linux. README documents this platform-specific separator.
|
||||
- The Textual TUI dependency supports Windows and Python 3.13, but the best
|
||||
terminal target is Windows Terminal or a modern PowerShell host.
|
||||
|
||||
## Work Items
|
||||
|
||||
### Done
|
||||
|
||||
- Add native Windows bootstrap support.
|
||||
- `bootstrap` auto-detects Windows and runs `IPA.exe "Beat Saber.exe" -n` directly.
|
||||
- `--native` forces native mode; `--proton` remains for Linux/Proton installs.
|
||||
- Timeout cleanup uses `process.terminate()` / `process.kill()` on Windows
|
||||
instead of POSIX process groups.
|
||||
- Bootstrap state records `bootstrapMode: "native"` or `"proton"`.
|
||||
- `bootstrap-check` accepts recorded native Windows bootstrap state without
|
||||
`Logs/_latest.log` when `IPA.exe "Beat Saber.exe" -n` completed successfully
|
||||
(`ipaExitCode == 0`, not timed out).
|
||||
- Add Windows-aware default paths when no config is present:
|
||||
`~/BSManager/BSInstances` and `%LOCALAPPDATA%/plugin-helper`.
|
||||
Normal native use still prefers `--config plugin-helper.windows.toml`.
|
||||
- Add `--config` and `--profile` CLI flags with `[[profiles]]` TOML support.
|
||||
- Add `plugin-helper.windows.toml.example` as the committed template.
|
||||
- Update README examples for PowerShell, profiles, and the `;` path separator.
|
||||
- Add tests for profile config resolution, native bootstrap command
|
||||
construction, native bootstrap health checks, and platform-gated IPA timeout
|
||||
behavior.
|
||||
- Review backup and restore helpers on native Windows.
|
||||
- `sync_windows_data_repo` and `restore_windows_data_repo` work with ordinary
|
||||
Windows paths; `infer_windows_appdata_path` now keys off `BSManager` rather
|
||||
than the first `Users` path segment.
|
||||
- `backup_userdata` no longer uses `NamedTemporaryFile` while the tar archive
|
||||
is open, which fixes Windows permission errors.
|
||||
|
||||
### Remaining
|
||||
|
||||
- Run the first manual smoke on a native Windows checkout (see below).
|
||||
- Consider whether the menu TUI needs Windows-specific setup hints when
|
||||
`plugin-helper.windows.toml` is present but `--profile` was omitted.
|
||||
|
||||
## First Manual Smoke
|
||||
|
||||
From the Windows checkout:
|
||||
|
||||
```powershell
|
||||
py -3.13 -m pip install -e .
|
||||
py -3.13 -m plugin_helper --config plugin-helper.windows.toml --profile windows instances
|
||||
py -3.13 -m plugin_helper --config plugin-helper.windows.toml --profile windows installed --instance 1.44.1
|
||||
```
|
||||
|
||||
If instance discovery fails, verify the BSManager instance root in
|
||||
`plugin-helper.windows.toml`.
|
||||
|
||||
## Implementation Map
|
||||
|
||||
| Area | Files |
|
||||
| --- | --- |
|
||||
| Config profiles and Windows defaults | `src/plugin_helper/config.py` |
|
||||
| Native bootstrap and process handling | `src/plugin_helper/bootstrap.py` |
|
||||
| Native bootstrap health gate | `src/plugin_helper/bsipa.py` |
|
||||
| CLI flags | `src/plugin_helper/cli.py` |
|
||||
| Windows config template | `plugin-helper.windows.toml.example` |
|
||||
| Tests | `tests/test_plugin_helper.py` |
|
||||
@@ -0,0 +1,12 @@
|
||||
```powershell
|
||||
cd C:\Program Files (x86)\Steam\steamapps\common\Beat Saber\logs>
|
||||
Get-Content .\_latest.log -Wait -Tail 50
|
||||
```
|
||||
|
||||
`-Tail 50` shows the last 50 lines; `-Wait` keeps following new output (like `tail -f`).
|
||||
|
||||
Shorter alias:
|
||||
|
||||
```powershell
|
||||
gc .\_latest.log -Wait -Tail 50
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
**Findings**
|
||||
|
||||
- Resolved: install state is now keyed by concrete `install_id`, and reusable plugin assets live under the shared download cache instead of per-install state.
|
||||
|
||||
- Medium-high: dependency handling is intentionally thin. `Dependency.constraint` exists in [models.py](/home/pleb/ops/beatsaber/plugin-helper/src/plugin_helper/models.py:10), but [planner.py](/home/pleb/ops/beatsaber/plugin-helper/src/plugin_helper/planner.py:68) only expands required dependency IDs and does not enforce constraints, conflicts, `loadAfter`, `loadBefore`, BeatMods version-id closures, or “provides” relationships. That is the next big unlock for package-manager behavior.
|
||||
|
||||
- Medium: the agent skills have a few stale or conflicting operational rules. The manager skill still tells agents to inspect/edit `registry/plugins.toml` in [.agents/skills/beatsaber-plugin-manager/SKILL.md](/home/pleb/ops/beatsaber/plugin-helper/.agents/skills/beatsaber-plugin-manager/SKILL.md:54), while the project has moved to `registry/plugins/*.toml`. The builder skill says PR checkouts go under `<state_dir>/build` in [.agents/skills/beatsaber-plugin-builder/SKILL.md](/home/pleb/ops/beatsaber/plugin-helper/.agents/skills/beatsaber-plugin-builder/SKILL.md:24), while `AGENTS.md` says GitHub plugin source checkouts should live under `~/src/<owner>/<repo>`. That will make agents inconsistent.
|
||||
|
||||
**Skills**
|
||||
|
||||
I would not collapse the three skills into one. They are conceptually different:
|
||||
|
||||
- `beatsaber-plugin-manager`: mutating install/update/bootstrap workflow.
|
||||
- `beatsaber-plugin-update-auditor`: read-mostly audit/report workflow.
|
||||
- `beatsaber-plugin-builder`: source build workflow.
|
||||
|
||||
But I would consolidate shared policy into references, then have each skill import that mental model:
|
||||
|
||||
- `references/repo-workflow.md`: repo root, `.venv`, `PYTHONPATH=src`, dirty worktree rules, validation commands.
|
||||
- `references/state-and-profiles.md`: `.state`, profiles, install identity, source checkout location.
|
||||
- `references/artifact-policy.md`: GitHub first, BeatMods metadata/fallback, private sources, checksum policy.
|
||||
- `references/live-validation.md`: smoketest and process cleanup.
|
||||
|
||||
That removes duplicated drift while preserving good trigger boundaries.
|
||||
|
||||
**Design Direction**
|
||||
|
||||
Yes, the script design is useful. The best parts are exactly the right bones for a real package manager: registry, locks, dry-run plans, hash checks, managed install state, bootstrap as its own phase, update audit, and known-good sets.
|
||||
|
||||
The next shape I’d aim for is:
|
||||
|
||||
- `packages/registry`: identity, source aliases, install strategy, metadata extraction rules.
|
||||
- `versions/locks`: selected package versions/artifacts/evidence for each Beat Saber version.
|
||||
- `cache/downloads`: content-addressed artifacts reusable across installs.
|
||||
- `installs/<install_id>`: installed packages, transactions, bootstrap, known-good generations, backups.
|
||||
- commands like `resolve`, `fetch`, `plan`, `apply`, `rollback`, `audit`, `verify`.
|
||||
|
||||
Beat Saber plugins are heterogeneous in packaging, but homogeneous enough in runtime shape (`Plugins`, `Libs`, `IPA/Pending`, root BSIPA files) that this can become a nice small package manager. The trick is to normalize artifacts into a planned file tree before touching the game.
|
||||
|
||||
Validation: `compileall` passed, and `unittest discover` passed: 75 tests, 1 skipped.
|
||||
@@ -1,4 +1,4 @@
|
||||
# plugin-helper Roadmap
|
||||
# Roadmap 1
|
||||
|
||||
This roadmap tracks ideas that are useful but not part of the first safe CLI slice.
|
||||
|
||||
@@ -23,6 +23,39 @@ installations even when they share an instance name. Lockfiles can stay keyed by
|
||||
Beat Saber version, but bootstrap state, generated plans, backups, and
|
||||
`installed.json` need to stay target-specific.
|
||||
|
||||
Asset downloads are currently colocated with target-specific state under the
|
||||
same state root. That is convenient for the first CLI slice, but it makes
|
||||
dual-boot use awkward: pointing Linux at the Windows state root reuses the asset
|
||||
downloads, but also reuses the Windows `installed.json`. A later state layout
|
||||
should separate a reusable asset cache from per-install managed state, for
|
||||
example:
|
||||
|
||||
```text
|
||||
cache/
|
||||
downloads/<beat-saber-version>/<plugin-id>/<asset>
|
||||
|
||||
instances/
|
||||
<install-id>/
|
||||
installed.json
|
||||
bootstrap.json
|
||||
plans/
|
||||
backups/
|
||||
```
|
||||
|
||||
There is no urgent need to migrate the layout before the rest of the helper
|
||||
settles, but new code should avoid assuming that downloads and per-install
|
||||
state must always live together.
|
||||
|
||||
The version lock includes an optional `download_url` for publicly downloadable
|
||||
artifacts. That makes the fetch path explicit for both GitHub release assets and
|
||||
BeatMods CDN fallbacks, while local builds and private/commercial packages can
|
||||
leave it unset. A future helper command can use this field to fetch missing
|
||||
downloads itself. Hashes should remain useful audit metadata and a warning
|
||||
signal, but the UX needs a recovery path for replaced upstream assets: report
|
||||
the mismatch, show the expected and actual hashes, and let the user
|
||||
intentionally refresh or re-lock after inspection instead of treating every
|
||||
mismatch as an unrecoverable dead end.
|
||||
|
||||
## Future: Nix-Orchestrated Plugin Sets
|
||||
|
||||
Once Beat Saber is running on Linux through Steam Proton, it may make sense to let Nix orchestrate the plugin payload itself.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Roadmap 2
|
||||
|
||||
## Package Manager Features
|
||||
|
||||
From a review:
|
||||
|
||||
- High: `apply_plan` is not transactional. It backs up and writes files one by one, then saves state at the end in [installer.py](/home/pleb/ops/beatsaber/plugin-helper/src/plugin_helper/installer.py:39). If extraction/copy fails mid-plan, the game tree can be half-mutated without matching install state. A future package manager wants transactions/generations: stage, apply, record transaction, and have a first-class rollback command.
|
||||
|
||||
- Medium-high: installed state records files but not the installed package identity. [installer.py](/home/pleb/ops/beatsaber/plugin-helper/src/plugin_helper/installer.py:63) stores `path`, `sha256`, and `size`, but not repo/tag/asset/source URL/source hash/reason. The report then uses the current lockfile to describe installed versions, which can drift after a lock update. Record the lock identity into install state at apply time.
|
||||
+139
-7
@@ -6,6 +6,7 @@ id = "bsipa"
|
||||
repo = "nike4613/BeatSaber-IPA-Reloaded"
|
||||
tag = "4.3.7"
|
||||
asset = "BSIPA-net472-x64.zip"
|
||||
download_url = "https://github.com/nike4613/BeatSaber-IPA-Reloaded/releases/download/4.3.7/BSIPA-net472-x64.zip"
|
||||
sha256 = "899b8a1dda91935bd5c19a211fa48e44e32f7be9ab82b6fc796709c753b7b2bc"
|
||||
install_strategy = "root-zip"
|
||||
reason = "BeatMods verified BSIPA 4.3.7 for Beat Saber 1.44.1 as version id 2561, zipHash 947774ef1010ff809ae05e345e269a90. GitHub asset is byte-identical to the BeatMods CDN zip used for initial bootstrap."
|
||||
@@ -15,6 +16,7 @@ id = "beatsabermarkuplanguage"
|
||||
repo = "monkeymanboy/BeatSaberMarkupLanguage"
|
||||
tag = "v1.14.1"
|
||||
asset = "BeatSaberMarkupLanguage-v1.14.1+bs.1.41.1-RELEASE.zip"
|
||||
download_url = "https://github.com/monkeymanboy/BeatSaberMarkupLanguage/releases/download/v1.14.1/BeatSaberMarkupLanguage-v1.14.1%2Bbs.1.41.1-RELEASE.zip"
|
||||
sha256 = "816613459853955e2b7c02123ed42f8e5bdbd946cc774fd5fcc4e80a6a09120a"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified BeatSaberMarkupLanguage 1.14.1 for Beat Saber 1.44.1 as version id 2567, zipHash 46149d03f8549e07f2c88fefde4337b2. GitHub asset is byte-identical to the BeatMods CDN zip used for initial bootstrap."
|
||||
@@ -24,6 +26,7 @@ id = "sirautil"
|
||||
repo = "Auros/SiraUtil"
|
||||
tag = "v3.3.1"
|
||||
asset = "SiraUtil-v3.3.1+bs.1.42.0.zip"
|
||||
download_url = "https://github.com/Auros/SiraUtil/releases/download/v3.3.1/SiraUtil-v3.3.1%2Bbs.1.42.0.zip"
|
||||
sha256 = "2253e5c31324e1e01b23480acba5ceee6b67eb8f16a079f58bc2f6ce7b12051f"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified SiraUtil 3.3.1 for Beat Saber 1.44.1 as version id 2565, zipHash ae14f7d3192a919d5d996c802fbde037. GitHub asset is byte-identical to the BeatMods CDN zip used for initial bootstrap."
|
||||
@@ -33,6 +36,7 @@ id = "songcore"
|
||||
repo = "Kylemc1413/SongCore"
|
||||
tag = "beatmods-3.16.0"
|
||||
asset = "SongCore-3.16.0.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/0af9c0a03074c17ca15c1b667a0e30c8.zip"
|
||||
sha256 = "f9be5d66426d795c6c8af2a4da0150a298d2d6a65fbabcab316d379088a40019"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified SongCore 3.16.0 for Beat Saber 1.44.1 as version id 2564, zipHash 0af9c0a03074c17ca15c1b667a0e30c8. BeatMods points to Kylemc1413/SongCore, but that GitHub repo currently exposes releases only through 3.10.4, so this remains a BeatMods CDN fallback until a matching upstream release artifact is found."
|
||||
@@ -40,17 +44,19 @@ reason = "BeatMods verified SongCore 3.16.0 for Beat Saber 1.44.1 as version id
|
||||
[[plugins]]
|
||||
id = "customjsondata"
|
||||
repo = "Aeroluna/CustomJSONData"
|
||||
tag = "v2.6.8"
|
||||
asset = "CustomJSONData-2.6.8+1.40.0-bs1.40.0-7c2c32c.zip"
|
||||
tag = "beatmods-2.6.8+1.40.0"
|
||||
asset = "CustomJSONData-2.6.8+1.40.0.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/fed31638bbb678580ef760ec83cfd486.zip"
|
||||
sha256 = "30555c77485d2837bd294608fe4e23c34415b89413997be67a6150318090b216"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified CustomJSONData 2.6.8+1.40.0 for Beat Saber 1.44.1 as version id 2327, zipHash fed31638bbb678580ef760ec83cfd486. GitHub asset is byte-identical to the BeatMods CDN zip."
|
||||
reason = "BeatMods verified CustomJSONData 2.6.8+1.40.0 for Beat Saber 1.44.1 as version id 2327, zipHash fed31638bbb678580ef760ec83cfd486. Downloaded from BeatMods CDN and verified SHA-256 30555c77485d2837bd294608fe4e23c34415b89413997be67a6150318090b216."
|
||||
|
||||
[[plugins]]
|
||||
id = "heck"
|
||||
repo = "Aeroluna/Heck"
|
||||
tag = "v2026-05-02"
|
||||
asset = "Heck-1.8.3+1.42.1-bs1.42.1-3ebc6a2.zip"
|
||||
download_url = "https://github.com/Aeroluna/Heck/releases/download/v2026-05-02/Heck-1.8.3%2B1.42.1-bs1.42.1-3ebc6a2.zip"
|
||||
sha256 = "d6c6a2b7e54285c8e7ee0515546f8cec274033afaa11324164803d003d9afab4"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "No BeatMods verified 1.44.1 entry was returned on 2026-06-28. Use latest upstream GitHub Heck asset targeting the highest available Beat Saber version, bs1.42.1; GitHub release digest matched the downloaded asset."
|
||||
@@ -60,6 +66,7 @@ id = "chroma"
|
||||
repo = "Aeroluna/Heck"
|
||||
tag = "v2026-02-23"
|
||||
asset = "Chroma-2.9.22+1.42.1-bs1.42.1-b38d924.zip"
|
||||
download_url = "https://github.com/Aeroluna/Heck/releases/download/v2026-02-23/Chroma-2.9.22%2B1.42.1-bs1.42.1-b38d924.zip"
|
||||
sha256 = "5438d039336ec91573818b7dbd8d5fce30454d16a66f3e70d6eedf5bff6c32bf"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "No BeatMods verified 1.44.1 entry was returned on 2026-06-28. Use latest upstream GitHub Chroma asset targeting the highest available Beat Saber version, bs1.42.1; GitHub release digest matched the downloaded asset."
|
||||
@@ -69,6 +76,7 @@ id = "lookupid"
|
||||
repo = "Aeroluna/Heck"
|
||||
tag = "chroma-v2.5.10"
|
||||
asset = "LookupID-1.0.1.zip"
|
||||
download_url = "https://github.com/Aeroluna/Heck/releases/download/chroma-v2.5.10/LookupID-1.0.1.zip"
|
||||
sha256 = "5fc838c9bf3693a8e3728056d95c07c5b0e13f944c1f9f49607f701b58e3d05d"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "Required by Chroma. BeatMods verified LookupID 1.0.1 for Beat Saber 1.44.1 as version id 1527, zipHash 5f5655b91193602c0311b8b6d1b4c71c. GitHub exposes the same version as an upstream release asset."
|
||||
@@ -78,6 +86,7 @@ id = "noodleextensions"
|
||||
repo = "Aeroluna/Heck"
|
||||
tag = "v2026-05-09"
|
||||
asset = "NoodleExtensions-1.7.21+1.42.1-bs1.42.1-3bbcaf6.zip"
|
||||
download_url = "https://github.com/Aeroluna/Heck/releases/download/v2026-05-09/NoodleExtensions-1.7.21%2B1.42.1-bs1.42.1-3bbcaf6.zip"
|
||||
sha256 = "116d5a56c52faf652cecee2dd0b9a02c1798d893fb5adef4145a1fa555ef727f"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "No BeatMods verified 1.44.1 entry was returned on 2026-06-28. Use latest upstream GitHub Noodle Extensions asset targeting the highest available Beat Saber version, bs1.42.1; GitHub release digest matched the downloaded asset."
|
||||
@@ -87,6 +96,7 @@ id = "camerautils"
|
||||
repo = "Reezonate/CameraUtils"
|
||||
tag = "beatmods-1.0.8"
|
||||
asset = "CameraUtils-1.0.8.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/38507e3a9ff8486c75b002cc47226f43.zip"
|
||||
sha256 = "7066ec311dc599e1d654d70557abc6c0809b72b5f4d695c8cfb85bb0bf2e2366"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "Required by Vivify. BeatMods verified CameraUtils 1.0.8 for Beat Saber 1.44.1 as version id 2576, zipHash 38507e3a9ff8486c75b002cc47226f43. Upstream GitHub latest release is 1.0.7, so use the newer BeatMods CDN artifact."
|
||||
@@ -96,6 +106,7 @@ id = "assetbundleloadingtools"
|
||||
repo = "nicoco007/AssetBundleLoadingTools"
|
||||
tag = "v1.1.13"
|
||||
asset = "AssetBundleLoadingTools-v1.1.13+bs.1.41.1.zip"
|
||||
download_url = "https://github.com/nicoco007/AssetBundleLoadingTools/releases/download/v1.1.13/AssetBundleLoadingTools-v1.1.13%2Bbs.1.41.1.zip"
|
||||
sha256 = "356b1aa4722ecca6f13199ad378429ece9873c365d9cf013e095fd5c0757a127"
|
||||
install_strategy = "root-zip"
|
||||
reason = "Required by Vivify. BeatMods verified AssetBundleLoadingTools 1.1.13 for Beat Saber 1.44.1 as version id 2590, zipHash a381a964cb69d26be8348edf04fabbc7. GitHub asset is the same version and its release digest matched the downloaded asset."
|
||||
@@ -105,6 +116,7 @@ id = "vivify"
|
||||
repo = "Aeroluna/Vivify"
|
||||
tag = "v1.1.0"
|
||||
asset = "Vivify-1.1.0+1.42.1-bs1.42.1-f83aa3c.zip"
|
||||
download_url = "https://github.com/Aeroluna/Vivify/releases/download/v1.1.0/Vivify-1.1.0%2B1.42.1-bs1.42.1-f83aa3c.zip"
|
||||
sha256 = "0e8799a7243496925aa527bc4ca7d3bcab770f4e828bbceeed066539f90a0902"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "No BeatMods verified 1.44.1 entry was returned on 2026-06-28. Use latest upstream GitHub Vivify asset targeting the highest available Beat Saber version, bs1.42.1; GitHub release digest matched the downloaded asset."
|
||||
@@ -114,6 +126,7 @@ id = "iniparser"
|
||||
repo = "rickyah/ini-parser"
|
||||
tag = "beatmods-2.5.9"
|
||||
asset = "IniParser-2.5.9.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/5df74ad1c6b120fecdc615dd55f15b88.zip"
|
||||
sha256 = "b761293bea73ff3cb9998f404594aade617638aba4318203320189baf3449ff1"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified Ini Parser 2.5.9 for Beat Saber 1.44.1 as version id 1352, zipHash 5df74ad1c6b120fecdc615dd55f15b88. Use BeatMods CDN as a framework/library dependency payload."
|
||||
@@ -123,6 +136,7 @@ id = "bs-utils"
|
||||
repo = "Kylemc1413/Beat-Saber-Utils"
|
||||
tag = "beatmods-1.14.3"
|
||||
asset = "BSUtils-1.14.3.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/918d13ac2821a3a17b2819f8861453e9.zip"
|
||||
sha256 = "b450fa4561da5b5ad834a3dcf6f127c2d73e5f971aa6c986514a6b185d68edfe"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified BS Utils 1.14.3 for Beat Saber 1.44.1 as version id 2563, zipHash 918d13ac2821a3a17b2819f8861453e9. Upstream GitHub releases do not expose a matching 1.14.3 asset, so this remains a BeatMods CDN fallback."
|
||||
@@ -131,6 +145,7 @@ reason = "BeatMods verified BS Utils 1.14.3 for Beat Saber 1.44.1 as version id
|
||||
id = "system-io-compression"
|
||||
tag = "beatmods-4.6.57"
|
||||
asset = "System.IO.Compression-4.6.57.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/a4e9e26f61967e56168e08eecb01ab88.zip"
|
||||
sha256 = "9067ddaf52c077f38a27ace6180a8134ceaaf1ecba752d08de81b4cc0c13aa43"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified System.IO.Compression 4.6.57 for Beat Saber 1.44.1 as version id 1763, zipHash a4e9e26f61967e56168e08eecb01ab88. Use BeatMods CDN as a framework/library dependency payload."
|
||||
@@ -139,6 +154,7 @@ reason = "BeatMods verified System.IO.Compression 4.6.57 for Beat Saber 1.44.1 a
|
||||
id = "system-io-compression-filesystem"
|
||||
tag = "beatmods-4.7.3056"
|
||||
asset = "System.IO.Compression.FileSystem-4.7.3056.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/e19f6fd395d54de7bfcbbbe3084dea28.zip"
|
||||
sha256 = "1b6f7a33a69980344717a37240ed80b022903a52968d4968f307cf754e3a03f7"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified System.IO.Compression.FileSystem 4.7.3056 for Beat Saber 1.44.1 as version id 1762, zipHash e19f6fd395d54de7bfcbbbe3084dea28. Use BeatMods CDN as a framework/library dependency payload."
|
||||
@@ -148,6 +164,7 @@ id = "imagesharp"
|
||||
repo = "SixLabors/ImageSharp"
|
||||
tag = "beatmods-2.0.0"
|
||||
asset = "ImageSharp-2.0.0.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/b642fec88b0f84a0643ebd401d08da35.zip"
|
||||
sha256 = "b2bf6d195f25e1298199a389e381ab42f163b284105ae6181259d74f2528301b"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified ImageSharp 2.0.0 for Beat Saber 1.44.1 as version id 1428, zipHash b642fec88b0f84a0643ebd401d08da35. Use BeatMods CDN as a framework/library dependency payload."
|
||||
@@ -157,6 +174,7 @@ id = "beatsaberplaylistslib"
|
||||
repo = "Meivyn/BeatSaberPlaylistsLib"
|
||||
tag = "beatmods-1.7.2"
|
||||
asset = "BeatSaberPlaylistsLib-1.7.2.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/a3418b75ed7294a3856f3eca12bbd672.zip"
|
||||
sha256 = "dfb26cf90cb73834c405727418c0121d8546cad0403abd35a09437dab766bd32"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified BeatSaberPlaylistsLib 1.7.2 for Beat Saber 1.44.1 as version id 2175, zipHash a3418b75ed7294a3856f3eca12bbd672. Upstream GitHub exposes no release assets through the releases API, so this remains a BeatMods CDN fallback."
|
||||
@@ -166,6 +184,7 @@ id = "beatsaversharp"
|
||||
repo = "Auros/BeatSaverSharper"
|
||||
tag = "beatmods-3.4.5"
|
||||
asset = "BeatSaverSharp-3.4.5.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/be37e13e93d9ac7da4efbdc3f514fa8f.zip"
|
||||
sha256 = "f5f37b27438e9b2fa1d9fbdf51a4f015f44ae04979cbdd9b90f6ae18583a6911"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified BeatSaverSharp 3.4.5 for Beat Saber 1.44.1 as version id 1831, zipHash be37e13e93d9ac7da4efbdc3f514fa8f. Source repository is now Auros/BeatSaverSharper; this remains a BeatMods CDN fallback for the locked 3.4.5 package."
|
||||
@@ -174,6 +193,7 @@ reason = "BeatMods verified BeatSaverSharp 3.4.5 for Beat Saber 1.44.1 as versio
|
||||
id = "scoresabersharp"
|
||||
tag = "beatmods-0.1.0"
|
||||
asset = "ScoreSaberSharp-0.1.0.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/8713168c598577ee7c73fa3cf0e26f5c.zip"
|
||||
sha256 = "7f30be996f8f0e997f2d848e34de1d03ef6dc744ffc32d3fe505881ca22c6cd3"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified ScoreSaberSharp 0.1.0 for Beat Saber 1.44.1 as version id 445, zipHash 8713168c598577ee7c73fa3cf0e26f5c. BeatMods lists scoresaber.com rather than a GitHub release source, so this remains a BeatMods CDN fallback."
|
||||
@@ -181,17 +201,19 @@ reason = "BeatMods verified ScoreSaberSharp 0.1.0 for Beat Saber 1.44.1 as versi
|
||||
[[plugins]]
|
||||
id = "scoresaber"
|
||||
repo = "ScoreSaber/pc-mod"
|
||||
tag = "v3.3.27"
|
||||
asset = "ScoreSaber-v3.3.27-bs1.42.0-to-1.44.0-9b4cfcf.zip"
|
||||
sha256 = "8ad509ab38353dfb4d92d127ba4e40917552f700580a7384509cafa70e62c096"
|
||||
tag = "v3.4.1"
|
||||
asset = "ScoreSaber-v3.4.1-bs1.42.0-to-1.44.1-4a3f9ff.zip"
|
||||
download_url = "https://github.com/ScoreSaber/pc-mod/releases/download/v3.4.1/ScoreSaber-v3.4.1-bs1.42.0-to-1.44.1-4a3f9ff.zip"
|
||||
sha256 = "6a74de62103b9b6afb1e02a3740d800cf696a88beee68e55d56de24751d5f8de"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided GitHub releases URL https://github.com/ScoreSaber/pc-mod/releases. Latest non-draft, non-prerelease release v3.3.27 includes this highest compatible asset, labeled for Beat Saber 1.42.0 through 1.44.0; GitHub release digest matched the downloaded asset."
|
||||
reason = "Updated from GitHub ScoreSaber/pc-mod tag v3.4.1 release https://github.com/ScoreSaber/pc-mod/releases/tag/v3.4.1, asset ScoreSaber-v3.4.1-bs1.42.0-to-1.44.1-4a3f9ff.zip. GitHub release digest matched 6a74de62103b9b6afb1e02a3740d800cf696a88beee68e55d56de24751d5f8de."
|
||||
|
||||
[[plugins]]
|
||||
id = "protobuf-net"
|
||||
repo = "protobuf-net/protobuf-net"
|
||||
tag = "beatmods-3.0.102"
|
||||
asset = "protobuf-net-3.0.102.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/1f55ae4b80b747b5f03fa18337ead864.zip"
|
||||
sha256 = "8bf4a361038172eab2c0c2e6737773d6c9a47fc6504a97304a35f697d5166414"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "Required by SongDetailsCache. BeatMods verified protobuf-net 3.0.102 for Beat Saber 1.44.1 as version id 958, zipHash 1f55ae4b80b747b5f03fa18337ead864. Use BeatMods CDN as a framework/library dependency payload."
|
||||
@@ -201,6 +223,7 @@ id = "songdetailscache"
|
||||
repo = "kinsi55/BeatSaber_SongDetails"
|
||||
tag = "v1.4.0"
|
||||
asset = "SongDetailsCache.BS.Lib.zip"
|
||||
download_url = "https://github.com/kinsi55/BeatSaber_SongDetails/releases/download/v1.4.0/SongDetailsCache.BS.Lib.zip"
|
||||
sha256 = "6ccc816dd40752b46d5e7e1cfb41e5af9d915d1a032be51ce1e52fb154daa28c"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "Required by SongRankedBadge. BeatMods verified SongDetailsCache 1.4.0 for Beat Saber 1.44.1 as version id 2226, zipHash e1167b64cd3eff7e3651ec2dbbe50d81. GitHub asset is the same version selected from kinsi55/BeatSaber_SongDetails v1.4.0."
|
||||
@@ -210,6 +233,7 @@ id = "songrankedbadge"
|
||||
repo = "qe201020335/SongRankedBadge"
|
||||
tag = "v1.0.6"
|
||||
asset = "SongRankedBadge-1.0.6-bs1.40.0-88ee233.zip"
|
||||
download_url = "https://github.com/qe201020335/SongRankedBadge/releases/download/v1.0.6/SongRankedBadge-1.0.6-bs1.40.0-88ee233.zip"
|
||||
sha256 = "6028f2392dd5e228f477837a7e32d95f0ca8af4f1b697b17c842ea62c050edff"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified SongRankedBadge 1.0.6 for Beat Saber 1.44.1 as version id 2267, zipHash c6944b8a4b00b0c0bb1d44f273b3bb18. GitHub release asset v1.0.6 was used."
|
||||
@@ -219,15 +243,27 @@ id = "beatleader"
|
||||
repo = "BeatLeader/beatleader-mod"
|
||||
tag = "v0.10.0"
|
||||
asset = "BeatLeader-0.10.0-bs1.42+.zip"
|
||||
download_url = "https://github.com/BeatLeader/beatleader-mod/releases/download/v0.10.0/BeatLeader-0.10.0-bs1.42%2B.zip"
|
||||
sha256 = "0b7c2adcf4db9806cdc765164788a9394bca2ee12179fad1b8ed255da1b7104e"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided GitHub releases URL https://github.com/BeatLeader/beatleader-mod/releases. Latest non-draft, non-prerelease release v0.10.0 includes this highest compatible asset, labeled for Beat Saber 1.42+; GitHub release digest matched the downloaded asset. The archive bundles Plugins/LeaderboardCore.dll, so the standalone LeaderboardCore lock entry is intentionally omitted."
|
||||
|
||||
[[plugins]]
|
||||
id = "accsaber-reloaded"
|
||||
repo = "not-dexter/accsaber-reloaded-plugin"
|
||||
tag = "v1.20"
|
||||
asset = "1.42.0.zip"
|
||||
download_url = "https://github.com/not-dexter/accsaber-reloaded-plugin/releases/download/v1.20/1.42.0.zip"
|
||||
sha256 = "0fccbc51001ef16ef1fe1ca89db6729670e831e05c709542c27e3ff756c520d8"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided GitHub releases URL https://github.com/not-dexter/accsaber-reloaded-plugin/releases. Latest release v1.20 (v1.2.0) has no 1.44.x asset; use highest available game-version asset 1.42.0.zip. GitHub release digest matched the downloaded asset. Depends on BSIPA, BeatSaberMarkupLanguage, SiraUtil, SongCore, and LeaderboardCore (satisfied by BeatLeader-bundled LeaderboardCore)."
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsaverdownloader"
|
||||
repo = "Top-Cat/BeatSaverDownloader"
|
||||
tag = "beatmods-6.0.7"
|
||||
asset = "BeatSaverDownloader-6.0.7.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/a740c6e68a9b5d1dfda3cc8e81f7cf06.zip"
|
||||
sha256 = "4108b11eae11d09f8c4e838dde1dc36108f1f3fd4fff31b951e7c551fa59f5f1"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified BeatSaverDownloader 6.0.7 for Beat Saber 1.44.1 as version id 2217, zipHash a740c6e68a9b5d1dfda3cc8e81f7cf06. Upstream GitHub exposes no release assets through the releases API, so this remains a BeatMods CDN fallback."
|
||||
@@ -237,15 +273,35 @@ id = "beatsaverupdater"
|
||||
repo = "ibillingsley/BeatSaverUpdater"
|
||||
tag = "1.2.11"
|
||||
asset = "BeatSaverUpdater-1.2.11-bs1.39.1-3698f98.zip"
|
||||
download_url = "https://github.com/ibillingsley/BeatSaverUpdater/releases/download/1.2.11/BeatSaverUpdater-1.2.11-bs1.39.1-3698f98.zip"
|
||||
sha256 = "6237da05cbc044e99211cb0e569c917fe61bb0da25e42c61a6d438371548ebba"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified BeatSaverUpdater 1.2.11 for Beat Saber 1.44.1 as version id 2352, zipHash d9ea8dd0cbaac66cbb02fa59a548e42b. GitHub asset is byte-identical to the BeatMods CDN zip."
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsavervoting"
|
||||
repo = "Top-Cat/BeatSaverVoting"
|
||||
tag = "localbuild-bs-1.44.1-2.4.8"
|
||||
asset = "BeatSaverVoting.dll"
|
||||
sha256 = "2e35638d29676e01a21b854209a74eaa12a347faab3c8b789442d05554658e98"
|
||||
install_strategy = "dll-to-plugins"
|
||||
reason = "Local build from ~/src/Top-Cat/BeatSaverVoting branch bs-1.44.1 (based on upstream 5616be4 / 2.4.7 IPlatform migration). Bumps to 2.4.8 for Beat Saber 1.44.1, retargets the LevelListTableCell.SetDataFromLevelAsync Harmony patch to the 1.44 signature, and drops the unused PlatformUserModel reference. Replaces failed BeatMods 2.4.6 trial that TypeLoadException'd on IPlatformUserModel."
|
||||
|
||||
[[plugins]]
|
||||
id = "playlistmanager"
|
||||
repo = "rithik-b/PlaylistManager"
|
||||
tag = "localbuild-pr82-da1ad17-1.44"
|
||||
asset = "PlaylistManager-1.7.4-bs1.44.0-da1ad17.zip"
|
||||
sha256 = "cb86029a2defca846e2e1ebdf75bbe1368797fbf1b8ce509615155080b9ffbf9"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "Local build from GitHub PR https://github.com/rithik-b/PlaylistManager/pull/82 at commit da1ad17. Use this PR build instead of upstream releases, which still target older Beat Saber versions and fail on 1.44.x with IPlatformUserModel / PlatformUserModel resolution errors."
|
||||
|
||||
[[plugins]]
|
||||
id = "failbutton"
|
||||
repo = "qe201020335/FailButton"
|
||||
tag = "v0.0.4"
|
||||
asset = "FailButton-0.0.4-bs1.39.0-b6415fb.zip"
|
||||
download_url = "https://github.com/qe201020335/FailButton/releases/download/v0.0.4/FailButton-0.0.4-bs1.39.0-b6415fb.zip"
|
||||
sha256 = "bb163170f400191592af1689e7b7557ec82e45e36de36e6eaaae16b4273e595b"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided GitHub repository URL https://github.com/qe201020335/FailButton. Latest non-draft, non-prerelease release v0.0.4 exposes this asset; no newer 1.44.x-specific asset was available."
|
||||
@@ -255,15 +311,34 @@ id = "easyoffset"
|
||||
repo = "Reezonate/EasyOffset"
|
||||
tag = "v2.1.16"
|
||||
asset = "EasyOffset.dll"
|
||||
download_url = "https://github.com/Reezonate/EasyOffset/releases/download/v2.1.16/EasyOffset.dll"
|
||||
sha256 = "6b9c63e5a4ae0ebc103fc11f442917b8fae12d62369faed1aca7470899a2bbbf"
|
||||
install_strategy = "dll-to-plugins"
|
||||
reason = "User-provided GitHub repository URL https://github.com/Reezonate/EasyOffset. Latest non-draft, non-prerelease release v2.1.16 exposes this direct DLL asset; GitHub release digest matched the downloaded asset."
|
||||
|
||||
[[plugins]]
|
||||
id = "cinema"
|
||||
repo = "MisterKelley/BeatSaberCinema"
|
||||
tag = "localbuild-010ce6e-bs-1.42.3"
|
||||
asset = "BeatSaberCinema-1.7.13-bs1.42.3-010ce6e.zip"
|
||||
sha256 = "f31d186444908df121b8b1e7491e557a6acfc501ba9a92f06989e7dd31444fad"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided fork/branch URL https://github.com/MisterKelley/BeatSaberCinema/tree/bs-1.42.3. Rebuilt locally on Windows from commit 010ce6e01728af1e21774ccbb8d14405773f5b02 against the 1.44.1 instance (Release DLL sha256 ef23c5ffff6d93aaf00cb1f5661873e945b3e59fc5d475c5616f6ce6a09148ae packaged as Plugins/BeatSaberCinema.dll). Manifest still targets Beat Saber 1.42.3; HasVideoFilter uses reflection against BetterSongList."
|
||||
|
||||
[[plugins]]
|
||||
id = "cinema-tools"
|
||||
tag = "yt-dlp-2026.07.04-ffmpeg-8.1.2"
|
||||
asset = "CinemaTools-yt-dlp-2026.07.04-ffmpeg-8.1.2.zip"
|
||||
sha256 = "4d299e40f747abb9777838542ca68ec450e0b86e6eb11af341e6bd568f47edf2"
|
||||
install_strategy = "root-zip"
|
||||
reason = "Helper-managed Windows executable bundle for Cinema's hardcoded Proton paths Libs/yt-dlp.exe and Libs/ffmpeg.exe. Rebuild with PYTHONPATH=src python -m plugin_helper.recipes.cinema_tools --instance 1.44.1. Sources: https://github.com/yt-dlp/yt-dlp/releases/download/2026.07.04/yt-dlp.exe with sha256 52fe3c26dcf71fbdc85b528589020bb0b8e383155cfa81b64dd447bbe35e24b8, and https://github.com/GyanD/codexffmpeg/releases/download/8.1.2/ffmpeg-8.1.2-essentials_build.zip with sha256 db580001caa24ac104c8cb856cd113a87b0a443f7bdf47d8c12b1d740584a2ec; the recipe extracts */bin/ffmpeg.exe with sha256 1326dde4c84ff1f96fe6b8916c5bed29e163e9b5dccf995f6f3db069d143ec5e and writes a deterministic root zip."
|
||||
|
||||
[[plugins]]
|
||||
id = "gottagofast"
|
||||
repo = "kinsi55/CS_BeatSaber_GottaGoFast"
|
||||
tag = "v0.2.5"
|
||||
asset = "GottaGoFast.dll"
|
||||
download_url = "https://github.com/kinsi55/CS_BeatSaber_GottaGoFast/releases/download/v0.2.5/GottaGoFast.dll"
|
||||
sha256 = "c9cd0e29dd7540027cdd3b9138335d81f93d69daeb8460d2fdfef31199e0e44e"
|
||||
install_strategy = "dll-to-plugins"
|
||||
reason = "User-provided GitHub repository URL https://github.com/kinsi55/CS_BeatSaber_GottaGoFast. Latest non-draft, non-prerelease release v0.2.5 is labeled for Beat Saber 1.42.0+ and exposes this direct DLL asset; GitHub release digest matched the downloaded asset."
|
||||
@@ -273,6 +348,7 @@ id = "keepmyoverridespls"
|
||||
repo = "qqrz997/KeepMyOverridesPls"
|
||||
tag = "v1.1.3-b"
|
||||
asset = "KeepMyOverridesPls-1.1.3-bs1.40.6-487d417.zip"
|
||||
download_url = "https://github.com/qqrz997/KeepMyOverridesPls/releases/download/v1.1.3-b/KeepMyOverridesPls-1.1.3-bs1.40.6-487d417.zip"
|
||||
sha256 = "4c2da7349778eba98eb02ba518a1a056a784b719b974055c877aa6ce3e735fbc"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided GitHub repository URL https://github.com/qqrz997/KeepMyOverridesPls. Latest non-draft, non-prerelease release v1.1.3-b exposes this asset; GitHub release digest matched the downloaded asset."
|
||||
@@ -300,6 +376,7 @@ id = "introskip"
|
||||
repo = "Loloppe/Intro-Skip"
|
||||
tag = "beatmods-4.0.8"
|
||||
asset = "IntroSkip-4.0.8.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/7d98ae6049251eb4e3226a5c8ac675b3.zip"
|
||||
sha256 = "319bce38f69661a3b0997071b85d57e5b4295b9699ac5432877b0fed72aa48b9"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified IntroSkip 4.0.8 for Beat Saber 1.44.1 as version id 2570, zipHash 7d98ae6049251eb4e3226a5c8ac675b3. Use BeatMods CDN after the older GitHub 4.0.5 asset failed compatibility on this instance."
|
||||
@@ -309,6 +386,7 @@ id = "soundreplacer"
|
||||
repo = "Meivyn/SoundReplacer"
|
||||
tag = "beatmods-2.0.1"
|
||||
asset = "SoundReplacer-2.0.1.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/7d7a869996e10249d1f85f95e060319b.zip"
|
||||
sha256 = "c14cc99cb0bd4ea4c3ab47345692489cbdfed8b2b577925200b1a8f6f4f93512"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified SoundReplacer 2.0.1 for Beat Saber 1.44.1 as version id 2213, zipHash 7d7a869996e10249d1f85f95e060319b. GitHub releases API returned no releases on 2026-06-29, so use the BeatMods CDN artifact."
|
||||
@@ -318,6 +396,7 @@ id = "bettersonglist"
|
||||
repo = "kinsi55/BeatSaber_BetterSongList"
|
||||
tag = "v0.4.3"
|
||||
asset = "BetterSongList.dll"
|
||||
download_url = "https://github.com/kinsi55/BeatSaber_BetterSongList/releases/download/v0.4.3/BetterSongList.dll"
|
||||
sha256 = "50993315f41e5ca8e83ce7ded6be7780cfbe62ce248023849bd898068a4e25c1"
|
||||
install_strategy = "dll-to-plugins"
|
||||
reason = "User-provided GitHub repository URL https://github.com/kinsi55/BeatSaber_BetterSongList. Latest non-draft, non-prerelease release v0.4.3 exposes this direct DLL asset; GitHub release digest matched the downloaded asset."
|
||||
@@ -327,6 +406,7 @@ id = "hitscorevisualizer"
|
||||
repo = "ErisApps/HitScoreVisualizer"
|
||||
tag = "3.7.3"
|
||||
asset = "HitScoreVisualizer-3.7.3-bs1.42.0-a565cbb.zip"
|
||||
download_url = "https://github.com/ErisApps/HitScoreVisualizer/releases/download/3.7.3/HitScoreVisualizer-3.7.3-bs1.42.0-a565cbb.zip"
|
||||
sha256 = "b889355cd47cc3b09a352f081110b8130ac6e1d285fa58776466117d585814db"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided GitHub repository URL https://github.com/ErisApps/HitScoreVisualizer. Latest non-draft, non-prerelease release 3.7.3 exposes this asset for Beat Saber 1.42.0; GitHub release digest matched the downloaded asset."
|
||||
@@ -345,6 +425,7 @@ id = "hidethelogo"
|
||||
repo = "TheBlackParrot/HideTheLogo"
|
||||
tag = "1.0.3"
|
||||
asset = "HideTheLogo-1.0.3-bs1.40.3-c968d91.zip"
|
||||
download_url = "https://github.com/TheBlackParrot/HideTheLogo/releases/download/1.0.3/HideTheLogo-1.0.3-bs1.40.3-c968d91.zip"
|
||||
sha256 = "f7177cf28add03ddd73fdbd720628755c7787f515523f2b92e0041c63e83d6ca"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided GitHub repository URL https://github.com/TheBlackParrot/HideTheLogo. Latest non-draft, non-prerelease release 1.0.3 exposes this asset for Beat Saber 1.40.3; GitHub release did not expose a digest."
|
||||
@@ -354,6 +435,7 @@ id = "songchartvisualizer"
|
||||
repo = "NuggoDEV/SongChartVisualizer"
|
||||
tag = "beatmods-1.1.11"
|
||||
asset = "SongChartVisualizer-1.1.11.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/5d3fc025fe098277667fc0846e1b8fe3.zip"
|
||||
sha256 = "290a55ff87d8769f3ae7c5d0b44a67e9e6e89e8ef83a2b1bb988ebea28f89da4"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified SongChartVisualizer 1.1.11 for Beat Saber 1.44.1 as version id 2249, zipHash 5d3fc025fe098277667fc0846e1b8fe3. User-provided GitHub repository URL https://github.com/NuggoDEV/SongChartVisualizer returned no releases through the GitHub releases API, so use the BeatMods CDN artifact."
|
||||
@@ -363,6 +445,7 @@ id = "adblocker"
|
||||
repo = "JonnyVR1/AdBlocker"
|
||||
tag = "beatmods-1.0.5"
|
||||
asset = "AdBlocker-1.0.5.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/cd397e93b1a03f163534483462edf768.zip"
|
||||
sha256 = "0ee298563760d8c7f6ba7c8134982f9d1c2e554fe142648a3465051ac6468487"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified AdBlocker 1.0.5 for Beat Saber 1.44.1 as version id 1872, zipHash cd397e93b1a03f163534483462edf768. User-provided GitHub repository URL https://github.com/JonnyVR1/AdBlocker returned no releases through the GitHub releases API, so use the BeatMods CDN artifact."
|
||||
@@ -372,6 +455,7 @@ id = "highlightbombs"
|
||||
repo = "Meivyn/HighlightBombs"
|
||||
tag = "beatmods-1.0.3"
|
||||
asset = "HighlightBombs-1.0.3.zip"
|
||||
download_url = "https://beatmods.com/cdn/mod/4bedaa80ce5dda8414fea7d914fb94ad.zip"
|
||||
sha256 = "fba7750632079dbce846ba57c2e3284fc72cb95671bff08394246e6664fe8113"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified HighlightBombs 1.0.3 for Beat Saber 1.44.1 as version id 2066, zipHash 4bedaa80ce5dda8414fea7d914fb94ad. GitHub latest release v1.0.1 is older than the BeatMods-verified 1.0.3 package, so use the BeatMods CDN artifact."
|
||||
@@ -381,6 +465,7 @@ id = "pitchblack"
|
||||
repo = "Loloppe/BeatSaber_PitchBlack"
|
||||
tag = "0.03"
|
||||
asset = "PitchBlack-v0.0.3-bs1.39.1.zip"
|
||||
download_url = "https://github.com/Loloppe/BeatSaber_PitchBlack/releases/download/0.03/PitchBlack-v0.0.3-bs1.39.1.zip"
|
||||
sha256 = "ae7269b4f40fea8bee3d2b213a67c3e779ff8bfbcb4f4deabcb9e9c21bfadafb"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "BeatMods verified PitchBlack 0.0.3 for Beat Saber 1.44.1 as version id 2233, zipHash 65656bf33b2a0b356c381132387ea7ea. User-provided GitHub repository URL https://github.com/Loloppe/BeatSaber_PitchBlack tag 0.03 asset is byte-identical to the BeatMods CDN zip."
|
||||
@@ -390,6 +475,7 @@ id = "reecamera"
|
||||
repo = "Reezonate/ReeCamera"
|
||||
tag = "v0.0.5"
|
||||
asset = "ReeCamera.1.42.0.zip"
|
||||
download_url = "https://github.com/Reezonate/ReeCamera/releases/download/v0.0.5/ReeCamera.1.42.0.zip"
|
||||
sha256 = "76fb8efa1103ed18d7913c63818cb2699785fb3718d394176d5e2a6938842c24"
|
||||
install_strategy = "root-zip"
|
||||
reason = "User-provided GitHub repository URL https://github.com/Reezonate/ReeCamera. Latest non-draft, non-prerelease release v0.0.5 offers ReeCamera.1.42.0.zip as the highest Beat Saber version asset for instance 1.44.1; GitHub release digest matched the downloaded asset. Archive bundles Plugins/ReeCamera.dll, bundled CameraUtils.dll, and UserData/ReeCamera presets."
|
||||
@@ -402,3 +488,49 @@ asset = "JDFixer.dll"
|
||||
sha256 = "16b7dad9906d838dab40ce48a9b304be4847f18e700ddd31f2293d1065f4529d"
|
||||
install_strategy = "dll-to-plugins"
|
||||
reason = "Local build from GitHub PR https://github.com/zeph-yr/JDFixer/pull/26 at commit 3fce6ce465911bdd5e8e00411bc4672c54a317f7. Use this PR build instead of the failed upstream v.7.4.0 release asset, which loaded but failed OnEnable on Beat Saber 1.44.1."
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsaberplus"
|
||||
repo = "discord/BeatSaberPlus"
|
||||
tag = "v6.4.5-bs-1.44.0"
|
||||
asset = "BeatSaberPlus_v6.4.5-bs-1.44.0.zip"
|
||||
sha256 = "21ad51edea5703485779b851e7e86aac11c3d6e3b1de2460b2c3c3b4b5704657"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided Discord release zip C:\\Users\\pleb\\Downloads\\BeatSaberPlus_v6.4.5-bs-1.44.0.zip. Bundles BeatSaberPlus modules, ChatPlexSDK_BS, and license files for Beat Saber 1.44.0/1.44.1."
|
||||
|
||||
[[plugins]]
|
||||
id = "naluluna"
|
||||
repo = "patreon/NalulunaModAssistant"
|
||||
tag = "installed-1.44.1-utils-5.8.0"
|
||||
asset = "Naluluna-1.44.1-installed-bundle.zip"
|
||||
sha256 = "1d05575a1dd5c0d95aa001384c9f5a2b854361b937cefac13891b1a586c3f2ed"
|
||||
install_strategy = "root-zip"
|
||||
reason = "Local bundle recaptured from the Windows BSManager 1.44.1 instance on 2026-07-11 after NalulunaModAssistant installed NalulunaUtils 5.8.0 (archive NalulunaUtils-5.8.0-bs1.40.8.zip). Only Plugins/NalulunaUtils.dll changed versus the prior 2026-07-01 bundle; other Naluluna payloads, OpenVRHelper, skybox samples, AliciaSolid, and Libs/Native/OVRLipSync.dll are unchanged."
|
||||
|
||||
[[plugins]]
|
||||
id = "reesabers"
|
||||
repo = "patreon/ReeSabers"
|
||||
tag = "1.42.0"
|
||||
asset = "ReeSabers_1.42.0.zip"
|
||||
sha256 = "3f4b08fc450b1dc29f4fc3282c5099d3e09578e3fa2734dfa3c5d670dddfb219"
|
||||
install_strategy = "root-zip"
|
||||
reason = "User-provided BeatLeader Patreon zip C:\\Users\\pleb\\Downloads\\ReeSabers_1.42.0.zip (original SHA-256 258b28b27e8a9274a15b22c91ed9a611a85367d9a9f31b575fd992ecd4bdc3ee). Staged install payload omits bundled Plugins/CameraUtils.dll so it does not downgrade the locked CameraUtils 1.0.8 already present on 1.44.1; installs Plugins/ReeSabers.dll plus UserData/ReeSabers presets and textures."
|
||||
|
||||
[[plugins]]
|
||||
id = "wipbot"
|
||||
repo = "Danielduel/wipbot"
|
||||
tag = "localbuild-c717a84-1.44"
|
||||
asset = "wipbot.dll"
|
||||
sha256 = "c895d45b8e842f1d408b9f53864fc4fc1c2bb6dda4b992d0bb3a6fdc12c0e0a8"
|
||||
install_strategy = "dll-to-plugins"
|
||||
reason = "Local build from C:\\Users\\pleb\\src\\Danielduel\\wipbot commit c717a84 on branch bs-1.44.1-compat. Builds cleanly against Beat Saber 1.44.1; bumps manifest to gameVersion 1.44.0 / version 1.22.0; Harmony PatchAll scoped to patch type and CatCore compile-optional so ChatPlex-only installs avoid ReflectionTypeLoadException."
|
||||
|
||||
[[plugins]]
|
||||
id = "setlist"
|
||||
repo = "https://git.plebsaber.stream/pleb/setlist"
|
||||
tag = "v0.1.2"
|
||||
asset = "Setlist-0.1.2-bs1.44.1-e44e5e8.zip"
|
||||
download_url = "https://git.plebsaber.stream/pleb/setlist/releases/download/v0.1.2/Setlist-0.1.2-bs1.44.1-e44e5e8.zip"
|
||||
sha256 = "242677be9e5673100f7b737349698c8ffdffd95d5f23fdbc816ffd625f678e74"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "Gitea release https://git.plebsaber.stream/pleb/setlist/releases/tag/v0.1.2 for Beat Saber 1.44.1 (commit e44e5e8). Replaces the earlier local 0.1.0 trial build that was removed after an incomplete playlist-load smoke."
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
set "VENV_PY=.venv\Scripts\python.exe"
|
||||
set "STAMP_FILE=.venv\.last-install"
|
||||
set "NEED_INSTALL=0"
|
||||
|
||||
if not exist "%VENV_PY%" (
|
||||
echo Creating virtual environment...
|
||||
py -m venv .venv
|
||||
if errorlevel 1 (
|
||||
echo Failed to create virtual environment.
|
||||
exit /b 1
|
||||
)
|
||||
set "NEED_INSTALL=1"
|
||||
) else if not exist "%STAMP_FILE%" (
|
||||
set "NEED_INSTALL=1"
|
||||
) else (
|
||||
"%VENV_PY%" -c "import pathlib, sys, time; p=pathlib.Path(r'%STAMP_FILE%'); sys.exit(0 if time.time()-p.stat().st_mtime >= 20*3600 else 1)"
|
||||
if not errorlevel 1 set "NEED_INSTALL=1"
|
||||
)
|
||||
|
||||
if "%NEED_INSTALL%"=="1" (
|
||||
echo Installing plugin-helper...
|
||||
"%VENV_PY%" -m pip install -e .
|
||||
if errorlevel 1 (
|
||||
echo Failed to install plugin-helper.
|
||||
exit /b 1
|
||||
)
|
||||
type nul > "%STAMP_FILE%"
|
||||
)
|
||||
|
||||
"%VENV_PY%" -m plugin_helper
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
|
||||
endlocal & exit /b %EXIT_CODE%
|
||||
@@ -0,0 +1,8 @@
|
||||
instances_root = "~/.local/share/BSManager/BSInstances"
|
||||
state_dir = "~/.local/state/plugin-helper"
|
||||
|
||||
# To keep state inside this checkout instead:
|
||||
# state_dir = ".state"
|
||||
#
|
||||
# To share one state directory between Linux and a Windows-partition checkout:
|
||||
# state_dir = "~/Windows/Users/pleb/ops/plugin-helper/.state"
|
||||
@@ -0,0 +1,5 @@
|
||||
[[profiles]]
|
||||
id = "windows"
|
||||
label = "Native Windows BSManager"
|
||||
instances_root = "~/BSManager/BSInstances"
|
||||
state_dir = ".state"
|
||||
+3
-1
@@ -10,7 +10,9 @@ readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = "MIT"
|
||||
authors = [{ name = "plugin-helper contributors" }]
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"textual>=8.2,<9",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
plugin-helper = "plugin_helper.cli:main"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Plugin Registry
|
||||
|
||||
Each plugin lives in its own TOML file under `plugins/`.
|
||||
|
||||
```toml
|
||||
id = "example"
|
||||
name = "Example"
|
||||
repo = "owner/example"
|
||||
asset_patterns = ["Example-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
```
|
||||
|
||||
Dependencies are inline arrays on the plugin that owns them, so there is no
|
||||
implicit relationship with a preceding TOML table.
|
||||
@@ -1,939 +0,0 @@
|
||||
# Human-maintained plugin registry.
|
||||
#
|
||||
# Example:
|
||||
# [[plugins]]
|
||||
# id = "songcore"
|
||||
# name = "SongCore"
|
||||
# repo = "Kylemc1413/SongCore"
|
||||
# asset_patterns = ["*SongCore*.zip"]
|
||||
# install_strategy = "bsipa-zip"
|
||||
# category = "library"
|
||||
#
|
||||
# [[plugins.dependencies]]
|
||||
# id = "bs-utils"
|
||||
# constraint = ">=1.0"
|
||||
# required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "accsaber-reloaded"
|
||||
name = "AccSaber Reloaded"
|
||||
repo = "not-dexter/accsaber-reloaded-plugin"
|
||||
asset_patterns = ["1.40.8.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "leaderboard"
|
||||
|
||||
[[plugins]]
|
||||
id = "bsipa"
|
||||
name = "BSIPA"
|
||||
repo = "nike4613/BeatSaber-IPA-Reloaded"
|
||||
asset_patterns = ["BSIPA-net472-x64.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "core"
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
name = "BeatSaberMarkupLanguage"
|
||||
repo = "monkeymanboy/BeatSaberMarkupLanguage"
|
||||
asset_patterns = ["*RELEASE.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "bettersonglist"
|
||||
name = "BetterSongList"
|
||||
repo = "kinsi55/BeatSaber_BetterSongList"
|
||||
asset_patterns = ["BetterSongList.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "ui"
|
||||
|
||||
[[plugins]]
|
||||
id = "hitscorevisualizer"
|
||||
name = "HitScoreVisualizer"
|
||||
repo = "ErisApps/HitScoreVisualizer"
|
||||
asset_patterns = ["HitScoreVisualizer-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "ditails"
|
||||
name = "DiTails"
|
||||
repo = "Auros/DiTails"
|
||||
asset_patterns = ["DiTails-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsaversharp"
|
||||
constraint = ">=3.4.5"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "hidethelogo"
|
||||
name = "HideTheLogo"
|
||||
repo = "TheBlackParrot/HideTheLogo"
|
||||
asset_patterns = ["HideTheLogo-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
|
||||
[[plugins]]
|
||||
id = "songchartvisualizer"
|
||||
name = "SongChartVisualizer"
|
||||
repo = "NuggoDEV/SongChartVisualizer"
|
||||
asset_patterns = ["SongChartVisualizer-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "sirautil"
|
||||
name = "SiraUtil"
|
||||
repo = "Auros/SiraUtil"
|
||||
asset_patterns = ["SiraUtil-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "songcore"
|
||||
name = "SongCore"
|
||||
repo = "Kylemc1413/SongCore"
|
||||
asset_patterns = ["SongCore-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "core"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "customjsondata"
|
||||
name = "CustomJSONData"
|
||||
repo = "Aeroluna/CustomJSONData"
|
||||
asset_patterns = ["CustomJSONData-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "heck"
|
||||
name = "Heck"
|
||||
repo = "Aeroluna/Heck"
|
||||
asset_patterns = ["Heck-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "customjsondata"
|
||||
constraint = ">=2.6.8"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "lookupid"
|
||||
name = "LookupID"
|
||||
repo = "Aeroluna/Heck"
|
||||
asset_patterns = ["LookupID-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins]]
|
||||
id = "chroma"
|
||||
name = "Chroma"
|
||||
repo = "Aeroluna/Heck"
|
||||
asset_patterns = ["Chroma-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "mapping"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "heck"
|
||||
constraint = ">=1.8.3"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "customjsondata"
|
||||
constraint = ">=2.6.8"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "lookupid"
|
||||
constraint = ">=1.0.1"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "noodleextensions"
|
||||
name = "Noodle Extensions"
|
||||
repo = "Aeroluna/Heck"
|
||||
asset_patterns = ["NoodleExtensions-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "mapping"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "heck"
|
||||
constraint = ">=1.8.3"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "customjsondata"
|
||||
constraint = ">=2.6.8"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "camerautils"
|
||||
name = "CameraUtils"
|
||||
repo = "Reezonate/CameraUtils"
|
||||
asset_patterns = ["CameraUtils-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "assetbundleloadingtools"
|
||||
name = "AssetBundleLoadingTools"
|
||||
repo = "nicoco007/AssetBundleLoadingTools"
|
||||
asset_patterns = ["AssetBundleLoadingTools-*.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "system-io-compression"
|
||||
constraint = ">=4.6.57"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "system-io-compression-filesystem"
|
||||
constraint = ">=4.7.3056"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "vivify"
|
||||
name = "Vivify"
|
||||
repo = "Aeroluna/Vivify"
|
||||
asset_patterns = ["Vivify-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "mapping"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "heck"
|
||||
constraint = ">=1.8.3"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "customjsondata"
|
||||
constraint = ">=2.6.8"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "camerautils"
|
||||
constraint = ">=1.0.8"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "assetbundleloadingtools"
|
||||
constraint = ">=1.1.13"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "iniparser"
|
||||
name = "Ini Parser"
|
||||
repo = "rickyah/ini-parser"
|
||||
asset_patterns = ["IniParser-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins]]
|
||||
id = "bs-utils"
|
||||
name = "BS Utils"
|
||||
repo = "Kylemc1413/Beat-Saber-Utils"
|
||||
asset_patterns = ["BSUtils-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "iniparser"
|
||||
constraint = ">=2.5.9"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "system-io-compression"
|
||||
name = "System.IO.Compression"
|
||||
asset_patterns = ["System.IO.Compression-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins]]
|
||||
id = "system-io-compression-filesystem"
|
||||
name = "System.IO.Compression.FileSystem"
|
||||
asset_patterns = ["System.IO.Compression.FileSystem-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "system-io-compression"
|
||||
constraint = ">=4.6.57"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "imagesharp"
|
||||
name = "ImageSharp"
|
||||
repo = "SixLabors/ImageSharp"
|
||||
asset_patterns = ["ImageSharp-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsaberplaylistslib"
|
||||
name = "BeatSaberPlaylistsLib"
|
||||
repo = "Meivyn/BeatSaberPlaylistsLib"
|
||||
asset_patterns = ["BeatSaberPlaylistsLib-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "imagesharp"
|
||||
constraint = ">=2.0.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "system-io-compression"
|
||||
constraint = ">=4.6.57"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsaversharp"
|
||||
name = "BeatSaverSharp"
|
||||
repo = "Auros/BeatSaverSharper"
|
||||
asset_patterns = ["BeatSaverSharp-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins]]
|
||||
id = "scoresabersharp"
|
||||
name = "ScoreSaberSharp"
|
||||
asset_patterns = ["ScoreSaberSharp-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins]]
|
||||
id = "scoresaber"
|
||||
name = "ScoreSaber"
|
||||
repo = "ScoreSaber/pc-mod"
|
||||
asset_patterns = ["ScoreSaber-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "leaderboard"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "leaderboardcore"
|
||||
name = "LeaderboardCore"
|
||||
repo = "NSGolova/LeaderboardCore"
|
||||
asset_patterns = ["LeaderboardCore-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "protobuf-net"
|
||||
name = "protobuf-net"
|
||||
repo = "protobuf-net/protobuf-net"
|
||||
asset_patterns = ["protobuf-net-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins]]
|
||||
id = "songdetailscache"
|
||||
name = "SongDetailsCache"
|
||||
repo = "kinsi55/BeatSaber_SongDetails"
|
||||
asset_patterns = ["SongDetailsCache.BS.Lib.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "protobuf-net"
|
||||
constraint = ">=3.0.102"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "songrankedbadge"
|
||||
name = "SongRankedBadge"
|
||||
repo = "qe201020335/SongRankedBadge"
|
||||
asset_patterns = ["SongRankedBadge-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songdetailscache"
|
||||
constraint = ">=1.4.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "beatleader"
|
||||
name = "BeatLeader"
|
||||
repo = "BeatLeader/beatleader-mod"
|
||||
asset_patterns = ["BeatLeader-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "leaderboard"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bs-utils"
|
||||
constraint = ">=1.14.3"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsaverdownloader"
|
||||
name = "BeatSaverDownloader"
|
||||
repo = "Top-Cat/BeatSaverDownloader"
|
||||
asset_patterns = ["BeatSaverDownloader-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "downloader"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bs-utils"
|
||||
constraint = ">=1.14.3"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "scoresabersharp"
|
||||
constraint = ">=0.1.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "system-io-compression-filesystem"
|
||||
constraint = ">=4.7.3056"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsaversharp"
|
||||
constraint = ">=3.4.5"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsaverupdater"
|
||||
name = "BeatSaverUpdater"
|
||||
repo = "ibillingsley/BeatSaverUpdater"
|
||||
asset_patterns = ["BeatSaverUpdater-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "downloader"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsaversharp"
|
||||
constraint = ">=3.4.5"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "playlistmanager"
|
||||
name = "PlaylistManager"
|
||||
repo = "rithik-b/PlaylistManager"
|
||||
asset_patterns = ["PlaylistManager-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "playlist"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsaberplaylistslib"
|
||||
constraint = ">=1.7.2"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "system-io-compression-filesystem"
|
||||
constraint = ">=4.7.3056"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsaversharp"
|
||||
constraint = ">=3.4.5"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "system-io-compression"
|
||||
constraint = ">=4.6.57"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsavervoting"
|
||||
name = "BeatSaverVoting"
|
||||
repo = "Top-Cat/BeatSaverVoting"
|
||||
asset_patterns = ["BeatSaverVoting-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "downloader"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bs-utils"
|
||||
constraint = ">=1.14.3"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "songcore"
|
||||
constraint = ">=3.16.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "setlist"
|
||||
name = "Setlist"
|
||||
asset_patterns = ["Setlist.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "ui"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatleader"
|
||||
constraint = ">=0.9.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "playlistmanager"
|
||||
constraint = ">=1.7.0"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsaberplaylistslib"
|
||||
constraint = ">=1.7.0"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "introskip"
|
||||
name = "IntroSkip"
|
||||
repo = "Loloppe/Intro-Skip"
|
||||
asset_patterns = ["IntroSkip-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "failbutton"
|
||||
name = "FailButton"
|
||||
repo = "qe201020335/FailButton"
|
||||
asset_patterns = ["FailButton-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins]]
|
||||
id = "easyoffset"
|
||||
name = "EasyOffset"
|
||||
repo = "Reezonate/EasyOffset"
|
||||
asset_patterns = ["EasyOffset.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins]]
|
||||
id = "gottagofast"
|
||||
name = "GottaGoFast"
|
||||
repo = "kinsi55/CS_BeatSaber_GottaGoFast"
|
||||
asset_patterns = ["GottaGoFast.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins]]
|
||||
id = "hitsoundtweaks"
|
||||
name = "HitsoundTweaks"
|
||||
repo = "GalaxyMaster2/HitsoundTweaks"
|
||||
asset_patterns = ["HitsoundTweaks-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins]]
|
||||
id = "keepmyoverridespls"
|
||||
name = "KeepMyOverridesPls"
|
||||
repo = "qqrz997/KeepMyOverridesPls"
|
||||
asset_patterns = ["KeepMyOverridesPls-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins]]
|
||||
id = "soundreplacer"
|
||||
name = "SoundReplacer"
|
||||
repo = "Meivyn/SoundReplacer"
|
||||
asset_patterns = ["SoundReplacer-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "keyremapper"
|
||||
name = "KeyRemapper"
|
||||
repo = "lyyQwQ/KeyRemapper"
|
||||
asset_patterns = ["KeyRemapper-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "squattobegin"
|
||||
name = "SquatToBegin"
|
||||
repo = "kinsi55/BeatSaber_SquatToBegin"
|
||||
asset_patterns = ["SquatToBegin.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "adblocker"
|
||||
name = "AdBlocker"
|
||||
repo = "JonnyVR1/AdBlocker"
|
||||
asset_patterns = ["AdBlocker-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "cosmetic"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "highlightbombs"
|
||||
name = "HighlightBombs"
|
||||
repo = "Meivyn/HighlightBombs"
|
||||
asset_patterns = ["HighlightBombs-*.zip", "HighlightBombs.dll"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "cosmetic"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "pitchblack"
|
||||
name = "PitchBlack"
|
||||
repo = "Loloppe/BeatSaber_PitchBlack"
|
||||
asset_patterns = ["PitchBlack-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "lighting"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "reecamera"
|
||||
name = "ReeCamera"
|
||||
repo = "Reezonate/ReeCamera"
|
||||
asset_patterns = ["ReeCamera.*.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "camera"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "camerautils"
|
||||
constraint = ">=1.0.8"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
|
||||
[[plugins]]
|
||||
id = "jdfixer"
|
||||
name = "JDFixer"
|
||||
repo = "zeph-yr/JDFixer"
|
||||
asset_patterns = ["JDFixer.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "gameplay"
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "beatsabermarkuplanguage"
|
||||
constraint = ">=1.14.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "sirautil"
|
||||
constraint = ">=3.3.1"
|
||||
required = true
|
||||
|
||||
[[plugins.dependencies]]
|
||||
id = "bsipa"
|
||||
constraint = ">=4.3.7"
|
||||
required = true
|
||||
@@ -0,0 +1,13 @@
|
||||
id = "accsaber-reloaded"
|
||||
name = "AccSaber Reloaded"
|
||||
repo = "not-dexter/accsaber-reloaded-plugin"
|
||||
asset_patterns = ["1.*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "leaderboard"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.2.2" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.6.10" },
|
||||
{ id = "sirautil", constraint = ">=3.1.2" },
|
||||
{ id = "songcore", constraint = ">=3.10.2" },
|
||||
{ id = "beatleader", constraint = ">=0.10.0" },
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "adblocker"
|
||||
name = "AdBlocker"
|
||||
repo = "JonnyVR1/AdBlocker"
|
||||
asset_patterns = ["AdBlocker-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "cosmetic"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
id = "assetbundleloadingtools"
|
||||
name = "AssetBundleLoadingTools"
|
||||
repo = "nicoco007/AssetBundleLoadingTools"
|
||||
asset_patterns = ["AssetBundleLoadingTools-*.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "system-io-compression", constraint = ">=4.6.57" },
|
||||
{ id = "system-io-compression-filesystem", constraint = ">=4.7.3056" },
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
id = "beatleader"
|
||||
name = "BeatLeader"
|
||||
repo = "BeatLeader/beatleader-mod"
|
||||
asset_patterns = ["BeatLeader-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "leaderboard"
|
||||
dependencies = [
|
||||
{ id = "bs-utils", constraint = ">=1.14.3" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "system-io-compression", constraint = ">=4.6.0" },
|
||||
{ id = "system-io-compression-filesystem", constraint = ">=4.7.3056" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "beatsabermarkuplanguage"
|
||||
name = "BeatSaberMarkupLanguage"
|
||||
repo = "monkeymanboy/BeatSaberMarkupLanguage"
|
||||
asset_patterns = ["*RELEASE.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
id = "beatsaberplaylistslib"
|
||||
name = "BeatSaberPlaylistsLib"
|
||||
repo = "Meivyn/BeatSaberPlaylistsLib"
|
||||
asset_patterns = ["BeatSaberPlaylistsLib-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "imagesharp", constraint = ">=2.0.0" },
|
||||
{ id = "system-io-compression", constraint = ">=4.6.57" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "beatsaberplus"
|
||||
name = "BeatSaberPlus"
|
||||
repo = "discord/BeatSaberPlus"
|
||||
asset_patterns = ["BeatSaberPlus_*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "streaming"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
id = "beatsaverdownloader"
|
||||
name = "BeatSaverDownloader"
|
||||
repo = "Top-Cat/BeatSaverDownloader"
|
||||
asset_patterns = ["BeatSaverDownloader-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "downloader"
|
||||
dependencies = [
|
||||
{ id = "bs-utils", constraint = ">=1.14.3" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
{ id = "scoresabersharp", constraint = ">=0.1.0" },
|
||||
{ id = "system-io-compression-filesystem", constraint = ">=4.7.3056" },
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "beatsaversharp", constraint = ">=3.4.5" },
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "beatsaversharp"
|
||||
name = "BeatSaverSharp"
|
||||
repo = "Auros/BeatSaverSharper"
|
||||
asset_patterns = ["BeatSaverSharp-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
@@ -0,0 +1,13 @@
|
||||
id = "beatsaverupdater"
|
||||
name = "BeatSaverUpdater"
|
||||
repo = "ibillingsley/BeatSaverUpdater"
|
||||
asset_patterns = ["BeatSaverUpdater-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "downloader"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "beatsaversharp", constraint = ">=3.4.5" },
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
id = "beatsavervoting"
|
||||
name = "BeatSaverVoting"
|
||||
repo = "Top-Cat/BeatSaverVoting"
|
||||
asset_patterns = ["BeatSaverVoting.dll", "BeatSaverVoting-*.zip"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "downloader"
|
||||
dependencies = [
|
||||
{ id = "bs-utils", constraint = ">=1.14.3" },
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "bettersonglist"
|
||||
name = "BetterSongList"
|
||||
repo = "kinsi55/BeatSaber_BetterSongList"
|
||||
asset_patterns = ["BetterSongList.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "ui"
|
||||
dependencies = [
|
||||
{ id = "songdetailscache", constraint = ">=1.4.0", required = false },
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
id = "bs-utils"
|
||||
name = "BS Utils"
|
||||
repo = "Kylemc1413/Beat-Saber-Utils"
|
||||
asset_patterns = ["BSUtils-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "iniparser", constraint = ">=2.5.9" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "bsipa"
|
||||
name = "BSIPA"
|
||||
repo = "nike4613/BeatSaber-IPA-Reloaded"
|
||||
asset_patterns = ["BSIPA-net472-x64.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "core"
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "camerautils"
|
||||
name = "CameraUtils"
|
||||
repo = "Reezonate/CameraUtils"
|
||||
asset_patterns = ["CameraUtils-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "chroma"
|
||||
name = "Chroma"
|
||||
repo = "Aeroluna/Heck"
|
||||
asset_patterns = ["Chroma-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "mapping"
|
||||
dependencies = [
|
||||
{ id = "heck", constraint = ">=1.8.3" },
|
||||
{ id = "customjsondata", constraint = ">=2.6.8" },
|
||||
{ id = "lookupid", constraint = ">=1.0.1" },
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
id = "cinema-tools"
|
||||
name = "Cinema Tools"
|
||||
asset_patterns = ["CinemaTools-yt-dlp-*-ffmpeg-*.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "dependency"
|
||||
@@ -0,0 +1,13 @@
|
||||
id = "cinema"
|
||||
name = "Cinema"
|
||||
repo = "MisterKelley/BeatSaberCinema"
|
||||
asset_patterns = ["BeatSaberCinema-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "visual"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage" },
|
||||
{ id = "bs-utils" },
|
||||
{ id = "songcore" },
|
||||
{ id = "bettersonglist" },
|
||||
{ id = "cinema-tools" },
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "customjsondata"
|
||||
name = "CustomJSONData"
|
||||
repo = "Aeroluna/CustomJSONData"
|
||||
asset_patterns = ["CustomJSONData-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
id = "ditails"
|
||||
name = "DiTails"
|
||||
repo = "Auros/DiTails"
|
||||
asset_patterns = ["DiTails-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
dependencies = [
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "beatsaversharp", constraint = ">=3.4.5" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "easyoffset"
|
||||
name = "EasyOffset"
|
||||
repo = "Reezonate/EasyOffset"
|
||||
asset_patterns = ["EasyOffset.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "gameplay"
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "failbutton"
|
||||
name = "FailButton"
|
||||
repo = "qe201020335/FailButton"
|
||||
asset_patterns = ["FailButton-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "gottagofast"
|
||||
name = "GottaGoFast"
|
||||
repo = "kinsi55/CS_BeatSaber_GottaGoFast"
|
||||
asset_patterns = ["GottaGoFast.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "gameplay"
|
||||
@@ -0,0 +1,10 @@
|
||||
id = "heck"
|
||||
name = "Heck"
|
||||
repo = "Aeroluna/Heck"
|
||||
asset_patterns = ["Heck-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
{ id = "customjsondata", constraint = ">=2.6.8" },
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "hidethelogo"
|
||||
name = "HideTheLogo"
|
||||
repo = "TheBlackParrot/HideTheLogo"
|
||||
asset_patterns = ["HideTheLogo-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
@@ -0,0 +1,10 @@
|
||||
id = "highlightbombs"
|
||||
name = "HighlightBombs"
|
||||
repo = "Meivyn/HighlightBombs"
|
||||
asset_patterns = ["HighlightBombs-*.zip", "HighlightBombs.dll"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "cosmetic"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
id = "hitscorevisualizer"
|
||||
name = "HitScoreVisualizer"
|
||||
repo = "ErisApps/HitScoreVisualizer"
|
||||
asset_patterns = ["HitScoreVisualizer-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "hitsoundtweaks"
|
||||
name = "HitsoundTweaks"
|
||||
repo = "GalaxyMaster2/HitsoundTweaks"
|
||||
asset_patterns = ["HitsoundTweaks-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "imagesharp"
|
||||
name = "ImageSharp"
|
||||
repo = "SixLabors/ImageSharp"
|
||||
asset_patterns = ["ImageSharp-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "iniparser"
|
||||
name = "Ini Parser"
|
||||
repo = "rickyah/ini-parser"
|
||||
asset_patterns = ["IniParser-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "introskip"
|
||||
name = "IntroSkip"
|
||||
repo = "Loloppe/Intro-Skip"
|
||||
asset_patterns = ["IntroSkip-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "jdfixer"
|
||||
name = "JDFixer"
|
||||
repo = "zeph-yr/JDFixer"
|
||||
asset_patterns = ["JDFixer.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "gameplay"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "keepmyoverridespls"
|
||||
name = "KeepMyOverridesPls"
|
||||
repo = "qqrz997/KeepMyOverridesPls"
|
||||
asset_patterns = ["KeepMyOverridesPls-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "keyremapper"
|
||||
name = "KeyRemapper"
|
||||
repo = "lyyQwQ/KeyRemapper"
|
||||
asset_patterns = ["KeyRemapper-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "leaderboardcore"
|
||||
name = "LeaderboardCore"
|
||||
repo = "NSGolova/LeaderboardCore"
|
||||
asset_patterns = ["LeaderboardCore-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "lookupid"
|
||||
name = "LookupID"
|
||||
repo = "Aeroluna/Heck"
|
||||
asset_patterns = ["LookupID-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "naluluna"
|
||||
name = "Naluluna"
|
||||
repo = "patreon/NalulunaModAssistant"
|
||||
asset_patterns = ["Naluluna-*-installed-bundle.zip", "NalulunaUtils-*.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "cosmetic"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
id = "noodleextensions"
|
||||
name = "Noodle Extensions"
|
||||
repo = "Aeroluna/Heck"
|
||||
asset_patterns = ["NoodleExtensions-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "mapping"
|
||||
dependencies = [
|
||||
{ id = "heck", constraint = ">=1.8.3" },
|
||||
{ id = "customjsondata", constraint = ">=2.6.8" },
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
id = "pitchblack"
|
||||
name = "PitchBlack"
|
||||
repo = "Loloppe/BeatSaber_PitchBlack"
|
||||
asset_patterns = ["PitchBlack-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "lighting"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
id = "playlistmanager"
|
||||
name = "PlaylistManager"
|
||||
repo = "rithik-b/PlaylistManager"
|
||||
asset_patterns = ["PlaylistManager-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "playlist"
|
||||
dependencies = [
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "beatsaberplaylistslib", constraint = ">=1.7.2" },
|
||||
{ id = "system-io-compression-filesystem", constraint = ">=4.7.3056" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
{ id = "beatsaversharp", constraint = ">=3.4.5" },
|
||||
{ id = "system-io-compression", constraint = ">=4.6.57" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "protobuf-net"
|
||||
name = "protobuf-net"
|
||||
repo = "protobuf-net/protobuf-net"
|
||||
asset_patterns = ["protobuf-net-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
@@ -0,0 +1,10 @@
|
||||
id = "reecamera"
|
||||
name = "ReeCamera"
|
||||
repo = "Reezonate/ReeCamera"
|
||||
asset_patterns = ["ReeCamera.*.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "camera"
|
||||
dependencies = [
|
||||
{ id = "camerautils", constraint = ">=1.0.8" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "reesabers"
|
||||
name = "ReeSabers"
|
||||
repo = "patreon/ReeSabers"
|
||||
asset_patterns = ["ReeSabers_*.zip"]
|
||||
install_strategy = "root-zip"
|
||||
category = "cosmetic"
|
||||
dependencies = [
|
||||
{ id = "camerautils", constraint = ">=1.0.8" },
|
||||
{ id = "beatleader", constraint = ">=0.10.0" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
id = "scoresaber"
|
||||
name = "ScoreSaber"
|
||||
repo = "ScoreSaber/pc-mod"
|
||||
asset_patterns = ["ScoreSaber-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "leaderboard"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "protobuf-net", constraint = ">=3.0.102" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
id = "scoresabersharp"
|
||||
name = "ScoreSaberSharp"
|
||||
asset_patterns = ["ScoreSaberSharp-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "setlist"
|
||||
name = "Setlist"
|
||||
repo = "https://git.plebsaber.stream/pleb/setlist"
|
||||
asset_patterns = ["Setlist-*.zip", "Setlist.dll"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
dependencies = [
|
||||
{ id = "beatleader", constraint = ">=0.9.0" },
|
||||
{ id = "playlistmanager", constraint = ">=1.7.0" },
|
||||
{ id = "beatsaberplaylistslib", constraint = ">=1.7.0" },
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "sirautil"
|
||||
name = "SiraUtil"
|
||||
repo = "Auros/SiraUtil"
|
||||
asset_patterns = ["SiraUtil-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "songchartvisualizer"
|
||||
name = "SongChartVisualizer"
|
||||
repo = "NuggoDEV/SongChartVisualizer"
|
||||
asset_patterns = ["SongChartVisualizer-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "songcore"
|
||||
name = "SongCore"
|
||||
repo = "Kylemc1413/SongCore"
|
||||
asset_patterns = ["SongCore-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "core"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "songdetailscache"
|
||||
name = "SongDetailsCache"
|
||||
repo = "kinsi55/BeatSaber_SongDetails"
|
||||
asset_patterns = ["SongDetailsCache.BS.Lib.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "protobuf-net", constraint = ">=3.0.102" },
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
id = "songrankedbadge"
|
||||
name = "SongRankedBadge"
|
||||
repo = "qe201020335/SongRankedBadge"
|
||||
asset_patterns = ["SongRankedBadge-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "ui"
|
||||
dependencies = [
|
||||
{ id = "songcore", constraint = ">=3.16.0" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "songdetailscache", constraint = ">=1.4.0" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "soundreplacer"
|
||||
name = "SoundReplacer"
|
||||
repo = "Meivyn/SoundReplacer"
|
||||
asset_patterns = ["SoundReplacer-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "gameplay"
|
||||
dependencies = [
|
||||
{ id = "sirautil", constraint = ">=3.3.1" },
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.14.1" },
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
id = "squattobegin"
|
||||
name = "SquatToBegin"
|
||||
repo = "kinsi55/BeatSaber_SquatToBegin"
|
||||
asset_patterns = ["SquatToBegin.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "gameplay"
|
||||
dependencies = [
|
||||
{ id = "bsipa", constraint = ">=4.3.7" },
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
id = "system-io-compression-filesystem"
|
||||
name = "System.IO.Compression.FileSystem"
|
||||
asset_patterns = ["System.IO.Compression.FileSystem-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
dependencies = [
|
||||
{ id = "system-io-compression", constraint = ">=4.6.57" },
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
id = "system-io-compression"
|
||||
name = "System.IO.Compression"
|
||||
asset_patterns = ["System.IO.Compression-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
@@ -0,0 +1,12 @@
|
||||
id = "vivify"
|
||||
name = "Vivify"
|
||||
repo = "Aeroluna/Vivify"
|
||||
asset_patterns = ["Vivify-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "mapping"
|
||||
dependencies = [
|
||||
{ id = "heck", constraint = ">=1.8.3" },
|
||||
{ id = "customjsondata", constraint = ">=2.6.8" },
|
||||
{ id = "camerautils", constraint = ">=1.0.8" },
|
||||
{ id = "assetbundleloadingtools", constraint = ">=1.1.13" },
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
id = "wipbot"
|
||||
name = "wipbot"
|
||||
repo = "Danielduel/wipbot"
|
||||
asset_patterns = ["wipbot.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
category = "streaming"
|
||||
dependencies = [
|
||||
{ id = "beatsabermarkuplanguage", constraint = ">=1.8.0" },
|
||||
{ id = "songcore", constraint = ">=3.0.0" },
|
||||
{ id = "bsipa", constraint = ">=4.0.5" },
|
||||
]
|
||||
@@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -49,6 +52,24 @@ def normalize_mods(payload: Any) -> list[BeatModsEntry]:
|
||||
return [normalize_entry(entry) for entry in extract_mods(payload)]
|
||||
|
||||
|
||||
def fetch_verified_mods(game_version: str) -> list[dict[str, Any]]:
|
||||
query = urlencode(
|
||||
{
|
||||
"status": "verified",
|
||||
"gameVersion": game_version,
|
||||
"gameName": "BeatSaber",
|
||||
"platform": "steampc",
|
||||
}
|
||||
)
|
||||
request = Request(
|
||||
f"https://beatmods.com/api/mods?{query}",
|
||||
headers={"User-Agent": "Mozilla/5.0 plugin-helper"},
|
||||
)
|
||||
with urlopen(request, timeout=20) as response:
|
||||
data = json.loads(response.read().decode("utf-8"))
|
||||
return extract_mods(data)
|
||||
|
||||
|
||||
def by_version_id(entries: list[BeatModsEntry]) -> dict[int, BeatModsEntry]:
|
||||
return {entry.version_id: entry for entry in entries if entry.version_id is not None}
|
||||
|
||||
|
||||
+125
-26
@@ -8,7 +8,8 @@ from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from .bsipa import BSIPA_PLUGIN_ID, check_bsipa_health
|
||||
from .bsipa import BSIPA_PLUGIN_ID, bootstrap_health_error, check_bsipa_health, planning_requires_bootstrap
|
||||
from .config import is_windows
|
||||
from .fsutil import sha256_file
|
||||
from .installer import apply_plan
|
||||
from .models import LockedPlugin, Lockfile, Registry
|
||||
@@ -17,6 +18,9 @@ from .scanner import scan_bootstrap_files
|
||||
from .state import bootstrap_state_path, plugin_downloads_dir, save_bootstrap_state
|
||||
|
||||
|
||||
DEFAULT_IPA_TIMEOUT_SECONDS = 30
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
@@ -111,7 +115,7 @@ def _fetch_github_release_asset(locked: LockedPlugin, destination: Path) -> dict
|
||||
def fetch_locked_bsipa_archive(lockfile: Lockfile, state_root: Path) -> dict[str, Any]:
|
||||
locked = next((plugin for plugin in lockfile.plugins if plugin.id == BSIPA_PLUGIN_ID), None)
|
||||
if not locked:
|
||||
raise ValueError("lockfile does not include a bsipa entry")
|
||||
raise ValueError("version lock does not include a bsipa entry")
|
||||
if not locked.asset:
|
||||
raise ValueError("locked BSIPA entry has no asset")
|
||||
destination = plugin_downloads_dir(state_root, lockfile.instance, BSIPA_PLUGIN_ID) / locked.asset
|
||||
@@ -130,31 +134,64 @@ def fetch_locked_bsipa_archive(lockfile: Lockfile, state_root: Path) -> dict[str
|
||||
return result
|
||||
|
||||
|
||||
def build_bootstrap_command(
|
||||
ipa: Path,
|
||||
*,
|
||||
proton: Path | None = None,
|
||||
native: bool | None = None,
|
||||
) -> list[str]:
|
||||
beat_saber_exe = ipa.with_name("Beat Saber.exe")
|
||||
use_native = is_windows() if native is None else native
|
||||
if use_native:
|
||||
return [str(ipa), str(beat_saber_exe), "-n"]
|
||||
proton_path = proton or _default_proton()
|
||||
return [str(proton_path), "run", str(ipa), str(beat_saber_exe), "-n"]
|
||||
|
||||
|
||||
def _terminate_process(process: subprocess.Popen[str]) -> None:
|
||||
if is_windows():
|
||||
process.terminate()
|
||||
else:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
|
||||
|
||||
def _kill_process(process: subprocess.Popen[str]) -> None:
|
||||
if is_windows():
|
||||
process.kill()
|
||||
else:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
|
||||
|
||||
def _run_ipa(
|
||||
*,
|
||||
command: list[str],
|
||||
instance_path: Path,
|
||||
timeout_seconds: int,
|
||||
native: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=instance_path,
|
||||
env=_proton_env(instance_path),
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
start_new_session=True,
|
||||
)
|
||||
popen_kwargs: dict[str, Any] = {
|
||||
"cwd": instance_path,
|
||||
"text": True,
|
||||
"stdout": subprocess.PIPE,
|
||||
"stderr": subprocess.PIPE,
|
||||
}
|
||||
if native:
|
||||
popen_kwargs["env"] = os.environ.copy()
|
||||
else:
|
||||
popen_kwargs["env"] = _proton_env(instance_path)
|
||||
popen_kwargs["start_new_session"] = True
|
||||
|
||||
process = subprocess.Popen(command, **popen_kwargs)
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
||||
timed_out = False
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
_terminate_process(process)
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
_kill_process(process)
|
||||
stdout, stderr = process.communicate()
|
||||
return {
|
||||
"returncode": process.returncode,
|
||||
@@ -175,10 +212,15 @@ def run_bootstrap(
|
||||
state_root: Path,
|
||||
repo_root: Path,
|
||||
proton: Path | None = None,
|
||||
native: bool | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
ipa_timeout_seconds: int = 120,
|
||||
ipa_timeout_seconds: int = DEFAULT_IPA_TIMEOUT_SECONDS,
|
||||
install_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
tell = progress or (lambda _message: None)
|
||||
use_native = is_windows() if native is None else native
|
||||
bootstrap_mode = "native" if use_native else "proton"
|
||||
|
||||
tell("Fetching locked BSIPA archive")
|
||||
fetched = fetch_locked_bsipa_archive(lockfile, state_root)
|
||||
if fetched.get("cached"):
|
||||
@@ -199,6 +241,7 @@ def run_bootstrap(
|
||||
repo_root=repo_root,
|
||||
selected={BSIPA_PLUGIN_ID},
|
||||
require_bootstrap=False,
|
||||
install_id=install_id,
|
||||
)
|
||||
if not plan["changes"]:
|
||||
raise ValueError("BSIPA bootstrap plan has no changes")
|
||||
@@ -209,31 +252,38 @@ def run_bootstrap(
|
||||
ipa = instance_path / "IPA.exe"
|
||||
if not ipa.is_file():
|
||||
raise FileNotFoundError(f"BSIPA archive did not install IPA.exe: {ipa}")
|
||||
beat_saber_exe = instance_path / "Beat Saber.exe"
|
||||
if not beat_saber_exe.is_file():
|
||||
raise FileNotFoundError(f"Beat Saber executable not found: {beat_saber_exe}")
|
||||
|
||||
proton_path = proton or _default_proton()
|
||||
if not proton_path.is_file():
|
||||
raise FileNotFoundError(f"Proton executable not found: {proton_path}")
|
||||
command = build_bootstrap_command(ipa, proton=proton, native=use_native)
|
||||
if use_native:
|
||||
tell(f"Running IPA.exe \"Beat Saber.exe\" -n natively; timeout {ipa_timeout_seconds}s")
|
||||
else:
|
||||
proton_path = proton or _default_proton()
|
||||
if not proton_path.is_file():
|
||||
raise FileNotFoundError(f"Proton executable not found: {proton_path}")
|
||||
tell(f"Running IPA.exe \"Beat Saber.exe\" -n through Proton; timeout {ipa_timeout_seconds}s")
|
||||
|
||||
command = [str(proton_path), "run", str(ipa), "-n"]
|
||||
tell(f"Running IPA.exe -n through Proton; timeout {ipa_timeout_seconds}s")
|
||||
completed = _run_ipa(
|
||||
command=command,
|
||||
instance_path=instance_path,
|
||||
timeout_seconds=ipa_timeout_seconds,
|
||||
native=use_native,
|
||||
)
|
||||
tell("Scanning bootstrap files after IPA.exe -n")
|
||||
tell("Scanning bootstrap files after IPA.exe \"Beat Saber.exe\" -n")
|
||||
after = scan_bootstrap_files(instance_path)
|
||||
delta = _files_delta(before, after)
|
||||
state = {
|
||||
state: dict[str, Any] = {
|
||||
"schemaVersion": 1,
|
||||
"instance": instance,
|
||||
"beatSaberVersion": beat_saber_version,
|
||||
"bootstrappedAt": _now_iso(),
|
||||
"plugin": BSIPA_PLUGIN_ID,
|
||||
"bootstrapMode": bootstrap_mode,
|
||||
"archive": fetched,
|
||||
"planPath": str(plan_path),
|
||||
"applied": apply_result["applied"],
|
||||
"proton": str(proton_path),
|
||||
"command": command,
|
||||
"ipaExitCode": completed["returncode"],
|
||||
"ipaTimedOut": completed["timedOut"],
|
||||
@@ -244,12 +294,61 @@ def run_bootstrap(
|
||||
"delta": delta,
|
||||
"health": {},
|
||||
}
|
||||
save_bootstrap_state(state_root, instance, state)
|
||||
state["health"] = check_bsipa_health(instance_path, state_root, instance)
|
||||
save_bootstrap_state(state_root, instance, state)
|
||||
state["statePath"] = str(bootstrap_state_path(state_root, instance))
|
||||
if install_id:
|
||||
state["installId"] = install_id
|
||||
if not use_native:
|
||||
state["proton"] = str(proton or _default_proton())
|
||||
save_bootstrap_state(state_root, instance, state, install_id=install_id)
|
||||
state["health"] = check_bsipa_health(instance_path, state_root, instance, install_id=install_id)
|
||||
save_bootstrap_state(state_root, instance, state, install_id=install_id)
|
||||
state["statePath"] = str(bootstrap_state_path(state_root, instance, install_id=install_id))
|
||||
if completed["timedOut"]:
|
||||
raise TimeoutError(f"IPA.exe -n timed out after {ipa_timeout_seconds}s; state written to {state['statePath']}")
|
||||
if completed["returncode"] != 0:
|
||||
raise RuntimeError(f"IPA.exe -n failed with exit code {completed['returncode']}; state written to {state['statePath']}")
|
||||
return state
|
||||
|
||||
|
||||
def ensure_healthy_bootstrap(
|
||||
*,
|
||||
instance: str,
|
||||
instance_path: Path,
|
||||
beat_saber_version: str,
|
||||
registry: Registry,
|
||||
lockfile: Lockfile,
|
||||
state_root: Path,
|
||||
repo_root: Path,
|
||||
selected_ids: set[str],
|
||||
proton: Path | None = None,
|
||||
native: bool | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
ipa_timeout_seconds: int = DEFAULT_IPA_TIMEOUT_SECONDS,
|
||||
install_id: str | None = None,
|
||||
) -> None:
|
||||
if not planning_requires_bootstrap(lockfile.plugins, selected_ids):
|
||||
return
|
||||
|
||||
health = check_bsipa_health(instance_path, state_root, instance, install_id=install_id)
|
||||
if health["ok"]:
|
||||
return
|
||||
|
||||
tell = progress or (lambda _message: None)
|
||||
tell("BSIPA bootstrap is unhealthy; running bootstrap")
|
||||
run_bootstrap(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=beat_saber_version,
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=repo_root,
|
||||
proton=proton,
|
||||
native=native,
|
||||
progress=progress,
|
||||
ipa_timeout_seconds=ipa_timeout_seconds,
|
||||
install_id=install_id,
|
||||
)
|
||||
|
||||
health = check_bsipa_health(instance_path, state_root, instance, install_id=install_id)
|
||||
if not health["ok"]:
|
||||
raise ValueError(bootstrap_health_error(health))
|
||||
|
||||
@@ -10,12 +10,37 @@ from .state import bootstrap_state_path, load_bootstrap_state
|
||||
BSIPA_PLUGIN_ID = "bsipa"
|
||||
|
||||
|
||||
def planning_requires_bootstrap(lockfile_plugins: tuple[Any, ...] | list[Any], selected_ids: set[str]) -> bool:
|
||||
has_locked_bsipa = any(plugin.id == BSIPA_PLUGIN_ID for plugin in lockfile_plugins)
|
||||
planning_ordinary_plugins = any(plugin_id != BSIPA_PLUGIN_ID for plugin_id in selected_ids)
|
||||
return has_locked_bsipa and planning_ordinary_plugins
|
||||
|
||||
|
||||
def bootstrap_health_error(health: dict[str, Any]) -> str:
|
||||
joined = "; ".join(health["messages"])
|
||||
return f"BSIPA bootstrap is not healthy; run bootstrap first: {joined}"
|
||||
|
||||
|
||||
def latest_log_path(instance_path: Path) -> Path:
|
||||
return instance_path / "Logs" / "_latest.log"
|
||||
|
||||
|
||||
def check_bsipa_health(instance_path: Path, state_root: Path, instance: str) -> dict[str, Any]:
|
||||
state = load_bootstrap_state(state_root, instance)
|
||||
def _native_bootstrap_satisfied(state: dict[str, Any]) -> bool:
|
||||
return (
|
||||
state.get("bootstrapMode") == "native"
|
||||
and state.get("ipaExitCode") == 0
|
||||
and not state.get("ipaTimedOut")
|
||||
)
|
||||
|
||||
|
||||
def check_bsipa_health(
|
||||
instance_path: Path,
|
||||
state_root: Path,
|
||||
instance: str,
|
||||
*,
|
||||
install_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
state = load_bootstrap_state(state_root, instance, install_id=install_id)
|
||||
messages: list[str] = []
|
||||
|
||||
required = ["IPA.exe", "winhttp.dll"]
|
||||
@@ -27,21 +52,25 @@ def check_bsipa_health(instance_path: Path, state_root: Path, instance: str) ->
|
||||
messages.append(f"missing {rel}/")
|
||||
|
||||
log_path = latest_log_path(instance_path)
|
||||
native_bootstrap = _native_bootstrap_satisfied(state)
|
||||
if not log_path.is_file():
|
||||
messages.append("missing Logs/_latest.log")
|
||||
if not native_bootstrap:
|
||||
messages.append("missing Logs/_latest.log")
|
||||
else:
|
||||
text = log_path.read_text(encoding="utf-8", errors="replace")
|
||||
if "Beat Saber IPA (BSIPA):" not in text and "Beat Saber IPA" not in text:
|
||||
messages.append("Logs/_latest.log does not show BSIPA startup")
|
||||
if not native_bootstrap:
|
||||
messages.append("Logs/_latest.log does not show BSIPA startup")
|
||||
|
||||
if not state:
|
||||
messages.append(f"missing bootstrap state: {bootstrap_state_path(state_root, instance)}")
|
||||
messages.append(f"missing bootstrap state: {bootstrap_state_path(state_root, instance, install_id=install_id)}")
|
||||
|
||||
return {
|
||||
"ok": not messages,
|
||||
"messages": messages,
|
||||
"statePath": str(bootstrap_state_path(state_root, instance)),
|
||||
"statePath": str(bootstrap_state_path(state_root, instance, install_id=install_id)),
|
||||
"logPath": str(log_path),
|
||||
"logSha256": sha256_file(log_path) if log_path.is_file() else None,
|
||||
"bootstrapRecordedAt": state.get("updatedAt"),
|
||||
"bootstrapMode": state.get("bootstrapMode"),
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ def check_lock(
|
||||
messages: list[dict[str, str]] = []
|
||||
|
||||
if not registry_plugin:
|
||||
messages.append({"level": "warning", "message": "missing registry entry"})
|
||||
messages.append({"level": "warning", "message": "missing catalog entry"})
|
||||
if strategy == "manual":
|
||||
messages.append({"level": "error", "message": "manual install strategy cannot be applied"})
|
||||
if not locked.asset:
|
||||
|
||||
+239
-226
@@ -4,9 +4,10 @@ import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
from .config import instances_roots, repo_root, state_root
|
||||
from .config import repo_root, resolve_runtime_config
|
||||
from .beatmods import fetch_verified_mods
|
||||
from .bootstrap import run_bootstrap
|
||||
from .bsipa import check_bsipa_health
|
||||
from .checker import check_lock
|
||||
@@ -14,10 +15,12 @@ from .github import fetch_releases
|
||||
from .installer import apply_plan, disable_plugin, uninstall_plugin
|
||||
from .instances import get_instance, list_instances
|
||||
from .models import load_lockfile, load_registry
|
||||
from .models import Lockfile, Registry
|
||||
from .operations import enable_disabled_plugin, restore_known_good_set, save_known_good_set
|
||||
from .planner import create_plan
|
||||
from .reports import installed_plugins_report, print_installed_plugins
|
||||
from .scanner import scan_instance
|
||||
from .state import load_installed_state
|
||||
from .state import installation_id, load_installed_state
|
||||
from .update_runner import run_update
|
||||
from .updates import check_updates
|
||||
from .userdata import restore_windows_data_repo, sync_windows_data_repo
|
||||
|
||||
@@ -26,91 +29,20 @@ def _json(data: Any) -> None:
|
||||
print(json.dumps(data, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
def installed_plugins_report(
|
||||
*,
|
||||
installed_state: dict[str, Any],
|
||||
registry: Registry,
|
||||
lockfile: Lockfile,
|
||||
) -> dict[str, Any]:
|
||||
locked_by_id = {plugin.id: plugin for plugin in lockfile.plugins}
|
||||
plugins: list[dict[str, Any]] = []
|
||||
state_plugins = [
|
||||
(plugin_id, plugin_state, "enabled")
|
||||
for plugin_id, plugin_state in installed_state.get("plugins", {}).items()
|
||||
]
|
||||
state_plugins.extend(
|
||||
(plugin_id, plugin_state, "disabled")
|
||||
for plugin_id, plugin_state in installed_state.get("disabledPlugins", {}).items()
|
||||
)
|
||||
for plugin_id, plugin_state, status in sorted(state_plugins):
|
||||
registry_plugin = registry.get(plugin_id)
|
||||
locked = locked_by_id.get(plugin_id)
|
||||
files = plugin_state.get("files", [])
|
||||
plugins.append(
|
||||
{
|
||||
"id": plugin_id,
|
||||
"name": registry_plugin.name if registry_plugin else plugin_id,
|
||||
"version": locked.tag if locked and locked.tag else "(not locked)",
|
||||
"asset": locked.asset if locked and locked.asset else "(unknown)",
|
||||
"repo": (locked.repo if locked and locked.repo else None)
|
||||
or (registry_plugin.repo if registry_plugin else None)
|
||||
or "(unknown)",
|
||||
"installedAt": plugin_state.get("installedAt", "(unknown)"),
|
||||
"disabledAt": plugin_state.get("disabledAt"),
|
||||
"status": status,
|
||||
"fileCount": len(files),
|
||||
"files": files,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"instance": installed_state.get("instance", lockfile.instance),
|
||||
"beatSaberVersion": installed_state.get("beatSaberVersion", lockfile.beat_saber_version),
|
||||
"plugins": plugins,
|
||||
}
|
||||
|
||||
|
||||
def print_installed_plugins(report: dict[str, Any]) -> None:
|
||||
plugins = report["plugins"]
|
||||
print(f"{report['instance']} managed plugins ({len(plugins)})")
|
||||
if not plugins:
|
||||
print("No plugins have been installed by plugin-helper yet.")
|
||||
return
|
||||
|
||||
headers = ("Plugin", "Status", "Version", "Asset", "Files", "Installed")
|
||||
rows = [
|
||||
(
|
||||
f"{plugin['name']} ({plugin['id']})",
|
||||
plugin["status"],
|
||||
plugin["version"],
|
||||
plugin["asset"],
|
||||
str(plugin["fileCount"]),
|
||||
plugin["disabledAt"] or plugin["installedAt"],
|
||||
)
|
||||
for plugin in plugins
|
||||
]
|
||||
widths = [
|
||||
max(len(headers[index]), *(len(row[index]) for row in rows))
|
||||
for index in range(len(headers))
|
||||
]
|
||||
header = " ".join(label.ljust(widths[index]) for index, label in enumerate(headers))
|
||||
print(header)
|
||||
print(" ".join("-" * width for width in widths))
|
||||
for row in rows:
|
||||
print(" ".join(value.ljust(widths[index]) for index, value in enumerate(row)))
|
||||
|
||||
|
||||
def print_updates(report: dict[str, Any]) -> None:
|
||||
plugins = report["plugins"]
|
||||
summary = report["summary"]
|
||||
print(
|
||||
f"{report['instance']} updates: "
|
||||
f"{summary['updates']} available, {summary['current']} current, "
|
||||
f"{summary.get('reviews', 0)} review, {summary.get('skipped', 0)} skipped, "
|
||||
f"{summary['warnings']} warnings, {summary['errors']} errors"
|
||||
)
|
||||
|
||||
if not plugins:
|
||||
return
|
||||
|
||||
headers = ("Plugin", "Current", "Latest", "Asset", "Status")
|
||||
headers = ("Plugin", "Current", "Latest", "Asset", "Status", "Notes")
|
||||
rows = [
|
||||
(
|
||||
f"{plugin['name']} ({plugin['id']})",
|
||||
@@ -118,6 +50,7 @@ def print_updates(report: dict[str, Any]) -> None:
|
||||
plugin.get("latestTag") or "(unknown)",
|
||||
plugin.get("latestAsset") or plugin.get("currentAsset") or "(unknown)",
|
||||
plugin["status"],
|
||||
", ".join(plugin.get("reviewReasons") or plugin.get("messages") or ()),
|
||||
)
|
||||
for plugin in plugins
|
||||
]
|
||||
@@ -131,8 +64,24 @@ def print_updates(report: dict[str, Any]) -> None:
|
||||
print(" ".join(value.ljust(widths[index]) for index, value in enumerate(row)))
|
||||
|
||||
|
||||
def _installation_id_for(runtime: Any, instance_name: str, instance_path: Path) -> str:
|
||||
return installation_id(
|
||||
profile_id=getattr(runtime, "profile_id", None),
|
||||
root=instance_path.parent,
|
||||
instance=instance_name,
|
||||
instance_path=instance_path,
|
||||
)
|
||||
|
||||
|
||||
def _get_installation(runtime: Any, instance_name: str) -> tuple[Any, str]:
|
||||
instance = get_instance(runtime.instances_roots, instance_name)
|
||||
return instance, _installation_id_for(runtime, instance_name, instance.path)
|
||||
|
||||
|
||||
def _add_common(parser: argparse.ArgumentParser, *, suppress_default: bool = False) -> None:
|
||||
default = argparse.SUPPRESS if suppress_default else None
|
||||
parser.add_argument("--config", default=default, help="plugin-helper config TOML path")
|
||||
parser.add_argument("--profile", default=default, help="Config profile id from [[profiles]]")
|
||||
parser.add_argument("--instances-root", default=default, help="BSManager instances root")
|
||||
parser.add_argument("--state-dir", default=default, help="plugin-helper state directory")
|
||||
|
||||
@@ -140,7 +89,7 @@ def _add_common(parser: argparse.ArgumentParser, *, suppress_default: bool = Fal
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="plugin-helper")
|
||||
_add_common(parser)
|
||||
subcommands = parser.add_subparsers(dest="command", required=True)
|
||||
subcommands = parser.add_subparsers(dest="command")
|
||||
|
||||
subcommands.add_parser(
|
||||
"instances",
|
||||
@@ -165,40 +114,41 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
state = subcommands.add_parser(
|
||||
"state",
|
||||
help="Show recorded plugin-helper install state",
|
||||
help="Show recorded plugin-helper managed install state",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
state.add_argument("--instance", required=True)
|
||||
|
||||
installed = subcommands.add_parser(
|
||||
"installed",
|
||||
help="List plugins installed by plugin-helper with locked release versions",
|
||||
help="List plugins from the version lock with managed install state",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
installed.add_argument("--instance", required=True)
|
||||
installed.add_argument("--registry", default="registry/plugins.toml")
|
||||
installed.add_argument("--registry", default="registry/plugins")
|
||||
installed.add_argument("--lockfile")
|
||||
installed.add_argument("--json", action="store_true", help="Print full JSON output")
|
||||
|
||||
check = subcommands.add_parser(
|
||||
"check",
|
||||
help="Validate local registry, lockfile, and release asset readiness",
|
||||
help="Validate local catalog, version lock, and release asset readiness",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
check.add_argument("--instance", required=True)
|
||||
check.add_argument("--registry", default="registry/plugins.toml")
|
||||
check.add_argument("--registry", default="registry/plugins")
|
||||
check.add_argument("--lockfile")
|
||||
check.add_argument("--json", action="store_true", help="Print full JSON check output")
|
||||
|
||||
bootstrap = subcommands.add_parser(
|
||||
"bootstrap",
|
||||
help="Install locked BSIPA, run IPA.exe -n through Proton, and record bootstrap files",
|
||||
help="Install locked BSIPA, run IPA.exe -n, and record bootstrap files",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
bootstrap.add_argument("--instance", required=True)
|
||||
bootstrap.add_argument("--registry", default="registry/plugins.toml")
|
||||
bootstrap.add_argument("--registry", default="registry/plugins")
|
||||
bootstrap.add_argument("--lockfile")
|
||||
bootstrap.add_argument("--proton", help="Path to Proton executable")
|
||||
bootstrap.add_argument("--proton", help="Path to Proton executable (Linux/Proton installs)")
|
||||
bootstrap.add_argument("--native", action="store_true", help="Run IPA.exe -n natively instead of through Proton")
|
||||
bootstrap.add_argument("--json", action="store_true", help="Print full JSON bootstrap output")
|
||||
|
||||
bootstrap_check = subcommands.add_parser(
|
||||
@@ -211,23 +161,38 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
updates = subcommands.add_parser(
|
||||
"updates",
|
||||
help="Check GitHub for newer matching releases for locked plugins",
|
||||
help="Check GitHub and BeatMods for newer matching releases for locked plugins",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
updates.add_argument("--instance", required=True)
|
||||
updates.add_argument("--registry", default="registry/plugins.toml")
|
||||
updates.add_argument("--registry", default="registry/plugins")
|
||||
updates.add_argument("--lockfile")
|
||||
updates.add_argument("--plugin", action="append", help="Check only this locked plugin id; repeatable")
|
||||
updates.add_argument("--include-prerelease", action="store_true", help="Include prerelease GitHub releases")
|
||||
updates.add_argument("--json", action="store_true", help="Print full JSON update output")
|
||||
|
||||
update = subcommands.add_parser(
|
||||
"update",
|
||||
help="Download selected update candidates, update the lock, and create an install plan",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
update.add_argument("--instance", required=True)
|
||||
update.add_argument("--registry", default="registry/plugins")
|
||||
update.add_argument("--lockfile")
|
||||
update.add_argument("--plugin", action="append", required=True, help="Update this locked plugin id; repeatable")
|
||||
update.add_argument("--include-prerelease", action="store_true", help="Include prerelease GitHub releases")
|
||||
update.add_argument("--json", action="store_true", help="Print full JSON update output")
|
||||
update_mode = update.add_mutually_exclusive_group()
|
||||
update_mode.add_argument("--apply", action="store_true", help="Apply the generated update plan")
|
||||
update_mode.add_argument("--dry-run", action="store_true", help="Show selected updates without downloading or writing")
|
||||
|
||||
plan = subcommands.add_parser(
|
||||
"plan",
|
||||
help="Create a dry-run install plan from registry and lockfile",
|
||||
help="Create a dry-run install plan from the catalog and version lock",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
plan.add_argument("--instance", required=True)
|
||||
plan.add_argument("--registry", default="registry/plugins.toml")
|
||||
plan.add_argument("--registry", default="registry/plugins")
|
||||
plan.add_argument("--lockfile")
|
||||
plan.add_argument("--plugin", "--update", action="append", help="Plan only this locked plugin id; repeatable")
|
||||
|
||||
@@ -258,14 +223,32 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
enable = subcommands.add_parser(
|
||||
"enable",
|
||||
help="Reinstall a disabled locked plugin from local assets",
|
||||
help="Enable a version-locked plugin from local assets",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
enable.add_argument("--instance", required=True)
|
||||
enable.add_argument("--registry", default="registry/plugins.toml")
|
||||
enable.add_argument("--registry", default="registry/plugins")
|
||||
enable.add_argument("--lockfile")
|
||||
enable.add_argument("plugin")
|
||||
|
||||
save_known_good = subcommands.add_parser(
|
||||
"save-known-good",
|
||||
help="Record the currently enabled plugins as the known-good set for this instance",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
save_known_good.add_argument("--instance", required=True)
|
||||
save_known_good.add_argument("--json", action="store_true", help="Print full JSON output")
|
||||
|
||||
restore_known_good = subcommands.add_parser(
|
||||
"restore-known-good",
|
||||
help="Disable plugins outside the saved known-good set and re-enable any that are missing",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
restore_known_good.add_argument("--instance", required=True)
|
||||
restore_known_good.add_argument("--registry", default="registry/plugins")
|
||||
restore_known_good.add_argument("--lockfile")
|
||||
restore_known_good.add_argument("--json", action="store_true", help="Print full JSON output")
|
||||
|
||||
backup = subcommands.add_parser(
|
||||
"backup-userdata",
|
||||
help="Copy UserData and Windows AppData into the adjacent backups repo",
|
||||
@@ -295,120 +278,72 @@ def _common_parent() -> argparse.ArgumentParser:
|
||||
return parent
|
||||
|
||||
|
||||
def _ask_choice(
|
||||
def _run_menu(
|
||||
*,
|
||||
title: str,
|
||||
choices: list[tuple[str, str] | tuple[str, str, str]],
|
||||
input_func: Callable[[str], str] | None = None,
|
||||
) -> str | None:
|
||||
ask = input_func or input
|
||||
print()
|
||||
print(title)
|
||||
for index, choice in enumerate(choices, start=1):
|
||||
label = choice[1]
|
||||
print(f" {index}. {label}")
|
||||
if len(choice) > 2:
|
||||
print(f" {choice[2]}")
|
||||
print(" q. Quit")
|
||||
|
||||
while True:
|
||||
answer = ask("> ").strip().lower()
|
||||
if answer in {"q", "quit", "exit"}:
|
||||
return None
|
||||
if answer.isdigit():
|
||||
index = int(answer)
|
||||
if 1 <= index <= len(choices):
|
||||
return choices[index - 1][0]
|
||||
print("Choose a listed number, or q to quit.")
|
||||
|
||||
|
||||
def _run_menu(inst_roots: list[Path], st_root: Path, input_func: Callable[[str], str] | None = None) -> int:
|
||||
ask = input_func or input
|
||||
instances = list_instances(inst_roots)
|
||||
if not instances:
|
||||
print(f"No Beat Saber instances found under {', '.join(str(root) for root in inst_roots)}")
|
||||
runtime: Any,
|
||||
explicit_instances_root: bool,
|
||||
explicit_state_dir: bool,
|
||||
) -> int:
|
||||
try:
|
||||
from .tui import InstallationChoice, PluginHelperTui
|
||||
except ImportError as exc:
|
||||
print(f"Textual is required for the menu TUI: {exc}")
|
||||
print("Install project dependencies, for example: python -m pip install -e .")
|
||||
return 1
|
||||
|
||||
instances_by_choice = {str(index): item for index, item in enumerate(instances, start=1)}
|
||||
instance_choices = [(str(index), f"{item.name} {item.path}") for index, item in enumerate(instances, start=1)]
|
||||
action_choices = [
|
||||
("installed", "Show managed installs", "Lists plugins recorded in plugin-helper state with locked versions and files."),
|
||||
("updates", "Check locked plugin updates", "Looks at GitHub releases for newer assets matching locked plugins."),
|
||||
("scan", "Scan installed files", "Counts files currently present in Plugins/, Libs/, and IPA/Pending/."),
|
||||
("check", "Check lockfile and assets", "Validates registry entries, lockfile data, local assets, and SHA-256 values."),
|
||||
("bootstrap", "Bootstrap BSIPA", "Fetches the locked BSIPA archive, installs it, runs IPA.exe -n, and records bootstrap files."),
|
||||
("bootstrap-check", "Check BSIPA bootstrap", "Verifies recorded bootstrap state and the latest BSIPA log evidence."),
|
||||
("plan", "Create install plan", "Writes a dry-run JSON plan for locked plugin files before anything is applied."),
|
||||
("apply", "Apply a plan by path", "Installs exactly the file changes from a previously generated plan JSON."),
|
||||
("disable", "Disable managed plugin", "Removes one recorded plugin from the game while keeping helper state and assets."),
|
||||
("enable", "Enable managed plugin", "Reinstalls one disabled locked plugin from local assets."),
|
||||
("backup-userdata", "Back up UserData", "Copies UserData and AppData into the adjacent backups repo."),
|
||||
("restore-userdata", "Restore UserData", "Restores UserData and AppData from the backups repo into an instance."),
|
||||
("change", "Choose another version", "Returns to the Beat Saber version picker."),
|
||||
]
|
||||
|
||||
selected_instance_key = _ask_choice(
|
||||
title="Choose Beat Saber version",
|
||||
choices=instance_choices,
|
||||
input_func=ask,
|
||||
)
|
||||
if selected_instance_key is None:
|
||||
return 0
|
||||
selected_instance = instances_by_choice[selected_instance_key]
|
||||
|
||||
while True:
|
||||
selected_action = _ask_choice(
|
||||
title=f"Choose action for {selected_instance.name}",
|
||||
choices=action_choices,
|
||||
input_func=ask,
|
||||
)
|
||||
if selected_action is None:
|
||||
return 0
|
||||
if selected_action == "change":
|
||||
selected_instance_key = _ask_choice(
|
||||
title="Choose Beat Saber version",
|
||||
choices=instance_choices,
|
||||
input_func=ask,
|
||||
choices: list[InstallationChoice] = []
|
||||
for root in runtime.instances_roots:
|
||||
install_label = str(root) if len(runtime.instances_roots) > 1 else "Default"
|
||||
for instance in list_instances(root):
|
||||
install_id = _installation_id_for(runtime, instance.name, instance.path)
|
||||
choices.append(
|
||||
InstallationChoice(
|
||||
install_id=install_id,
|
||||
install_label=install_label,
|
||||
instance_name=instance.name,
|
||||
instance_path=instance.path,
|
||||
state_root=runtime.state_root,
|
||||
)
|
||||
)
|
||||
if selected_instance_key is None:
|
||||
return 0
|
||||
selected_instance = instances_by_choice[selected_instance_key]
|
||||
continue
|
||||
if not choices:
|
||||
searched = ", ".join(str(root) for root in runtime.instances_roots)
|
||||
print(f"No Beat Saber instances found under {searched}")
|
||||
return 1
|
||||
|
||||
command = [
|
||||
"--instances-root",
|
||||
str(selected_instance.path.parent),
|
||||
"--state-dir",
|
||||
str(st_root),
|
||||
selected_action,
|
||||
]
|
||||
if selected_action == "apply":
|
||||
plan_path = ask("Plan path> ").strip()
|
||||
if not plan_path:
|
||||
print("No plan path entered.")
|
||||
continue
|
||||
command.append(plan_path)
|
||||
elif selected_action in {"disable", "enable"}:
|
||||
plugin_id = ask("Plugin id> ").strip()
|
||||
if not plugin_id:
|
||||
print("No plugin id entered.")
|
||||
continue
|
||||
command.extend(["--instance", selected_instance.name, plugin_id])
|
||||
else:
|
||||
command.extend(["--instance", selected_instance.name])
|
||||
|
||||
print()
|
||||
status = run(command)
|
||||
print(f"Command exited with status {status}")
|
||||
setup_hint = None
|
||||
if not runtime.config_loaded and not explicit_instances_root and not explicit_state_dir:
|
||||
setup_hint = (
|
||||
f"No {runtime.config_path.name} found; using default roots and default state. "
|
||||
f"Copy plugin-helper.toml.example to {runtime.config_path.name} to set a custom state dir."
|
||||
)
|
||||
app = PluginHelperTui(choices=choices, repo_root=repo_root(), setup_hint=setup_hint)
|
||||
result = app.run()
|
||||
return int(result or 0)
|
||||
|
||||
|
||||
def run(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
inst_roots = instances_roots(getattr(args, "instances_root", None))
|
||||
st_root = state_root(getattr(args, "state_dir", None))
|
||||
|
||||
try:
|
||||
if args.command is None:
|
||||
if sys.stdin.isatty() and sys.stdout.isatty():
|
||||
args.command = "menu"
|
||||
else:
|
||||
parser.print_help()
|
||||
return 2
|
||||
|
||||
explicit_instances_root = getattr(args, "instances_root", None) is not None
|
||||
explicit_state_dir = getattr(args, "state_dir", None) is not None
|
||||
runtime = resolve_runtime_config(
|
||||
instances_root_value=getattr(args, "instances_root", None),
|
||||
state_dir_value=getattr(args, "state_dir", None),
|
||||
config_path_value=getattr(args, "config", None),
|
||||
profile_id=getattr(args, "profile", None),
|
||||
)
|
||||
inst_roots = runtime.instances_roots
|
||||
st_root = runtime.state_root
|
||||
|
||||
if args.command == "instances":
|
||||
found = list_instances(inst_roots)
|
||||
if not found:
|
||||
@@ -427,7 +362,11 @@ def run(argv: list[str] | None = None) -> int:
|
||||
return 0
|
||||
|
||||
if args.command == "menu":
|
||||
return _run_menu(inst_roots, st_root)
|
||||
return _run_menu(
|
||||
runtime=runtime,
|
||||
explicit_instances_root=explicit_instances_root,
|
||||
explicit_state_dir=explicit_state_dir,
|
||||
)
|
||||
|
||||
if args.command == "scan":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
@@ -443,17 +382,19 @@ def run(argv: list[str] | None = None) -> int:
|
||||
return 0
|
||||
|
||||
if args.command == "state":
|
||||
_json(load_installed_state(st_root, args.instance))
|
||||
_instance, install_id = _get_installation(runtime, args.instance)
|
||||
_json(load_installed_state(st_root, args.instance, install_id=install_id))
|
||||
return 0
|
||||
|
||||
if args.command == "installed":
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
root = repo_root()
|
||||
registry_path = (root / args.registry).resolve() if not Path(args.registry).is_absolute() else Path(args.registry)
|
||||
lock_path = Path(args.lockfile) if args.lockfile else root / "locks" / f"{args.instance}.lock.toml"
|
||||
if not lock_path.is_absolute():
|
||||
lock_path = (root / lock_path).resolve()
|
||||
result = installed_plugins_report(
|
||||
installed_state=load_installed_state(st_root, args.instance),
|
||||
installed_state=load_installed_state(st_root, instance.name, install_id=install_id),
|
||||
registry=load_registry(registry_path),
|
||||
lockfile=load_lockfile(lock_path),
|
||||
)
|
||||
@@ -502,6 +443,7 @@ def run(argv: list[str] | None = None) -> int:
|
||||
registry=load_registry(registry_path),
|
||||
lockfile=load_lockfile(lock_path),
|
||||
fetch_releases=fetch_releases,
|
||||
fetch_beatmods=fetch_verified_mods,
|
||||
selected=set(args.plugin) if args.plugin else None,
|
||||
include_prerelease=args.include_prerelease,
|
||||
)
|
||||
@@ -511,8 +453,53 @@ def run(argv: list[str] | None = None) -> int:
|
||||
print_updates(result)
|
||||
return 2 if result["summary"]["errors"] else 0
|
||||
|
||||
if args.command == "update":
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
root = repo_root()
|
||||
registry_path = (root / args.registry).resolve() if not Path(args.registry).is_absolute() else Path(args.registry)
|
||||
lock_path = Path(args.lockfile) if args.lockfile else root / "locks" / f"{args.instance}.lock.toml"
|
||||
if not lock_path.is_absolute():
|
||||
lock_path = (root / lock_path).resolve()
|
||||
result = run_update(
|
||||
instance=args.instance,
|
||||
instance_path=instance.path,
|
||||
registry=load_registry(registry_path),
|
||||
lockfile=load_lockfile(lock_path),
|
||||
lock_path=lock_path,
|
||||
state_root=st_root,
|
||||
repo_root=root,
|
||||
selected=set(args.plugin),
|
||||
fetch_releases=fetch_releases,
|
||||
fetch_beatmods=fetch_verified_mods,
|
||||
include_prerelease=args.include_prerelease,
|
||||
dry_run=args.dry_run,
|
||||
apply=args.apply,
|
||||
install_id=install_id,
|
||||
)
|
||||
if args.json:
|
||||
_json(result)
|
||||
else:
|
||||
action = "Would update" if result["dryRun"] else "Updated"
|
||||
print(f"{action}: {len(result['updated'])}")
|
||||
for item in result["updated"]:
|
||||
print(f" {item['plugin']}: {item.get('fromTag')} -> {item.get('toTag')} ({item.get('asset')})")
|
||||
if result["refused"]:
|
||||
print("Refused:")
|
||||
for item in result["refused"]:
|
||||
notes = ", ".join(item.get("reviewReasons") or item.get("messages") or ())
|
||||
suffix = f": {notes}" if notes else ""
|
||||
print(f" {item['plugin']}: {item['status']}{suffix}")
|
||||
if result.get("planPath"):
|
||||
print(f"Plan: {result['planPath']}")
|
||||
if not result["dryRun"] and result["updated"]:
|
||||
print(f"Lockfile: {result['lockfile']}")
|
||||
if args.apply:
|
||||
print(f"Applied: {len(result['applied'])}")
|
||||
print(f"State: {result.get('statePath')}")
|
||||
return 0 if not result["refused"] else 2
|
||||
|
||||
if args.command == "bootstrap":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
root = repo_root()
|
||||
registry_path = (root / args.registry).resolve() if not Path(args.registry).is_absolute() else Path(args.registry)
|
||||
lock_path = Path(args.lockfile) if args.lockfile else root / "locks" / f"{args.instance}.lock.toml"
|
||||
@@ -528,7 +515,9 @@ def run(argv: list[str] | None = None) -> int:
|
||||
state_root=st_root,
|
||||
repo_root=root,
|
||||
proton=Path(args.proton).expanduser() if args.proton else None,
|
||||
native=True if args.native else None,
|
||||
progress=lambda message: print(f" {message}", flush=True),
|
||||
install_id=install_id,
|
||||
)
|
||||
if args.json:
|
||||
_json(result)
|
||||
@@ -548,8 +537,8 @@ def run(argv: list[str] | None = None) -> int:
|
||||
return 0 if result["health"]["ok"] else 2
|
||||
|
||||
if args.command == "bootstrap-check":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
result = check_bsipa_health(instance.path, st_root, args.instance)
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
result = check_bsipa_health(instance.path, st_root, args.instance, install_id=install_id)
|
||||
if args.json:
|
||||
_json(result)
|
||||
else:
|
||||
@@ -561,7 +550,7 @@ def run(argv: list[str] | None = None) -> int:
|
||||
return 0 if result["ok"] else 2
|
||||
|
||||
if args.command == "plan":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
root = repo_root()
|
||||
registry_path = (root / args.registry).resolve() if not Path(args.registry).is_absolute() else Path(args.registry)
|
||||
lock_path = Path(args.lockfile) if args.lockfile else root / "locks" / f"{args.instance}.lock.toml"
|
||||
@@ -579,6 +568,7 @@ def run(argv: list[str] | None = None) -> int:
|
||||
state_root=st_root,
|
||||
repo_root=root,
|
||||
selected=selected,
|
||||
install_id=install_id,
|
||||
)
|
||||
print(f"Wrote plan: {path}")
|
||||
print(f"Changes: {len(plan['changes'])}")
|
||||
@@ -595,8 +585,8 @@ def run(argv: list[str] | None = None) -> int:
|
||||
return 0
|
||||
|
||||
if args.command == "uninstall":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
result = uninstall_plugin(args.instance, instance.path, st_root, args.plugin, force=args.force)
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
result = uninstall_plugin(args.instance, instance.path, st_root, args.plugin, force=args.force, install_id=install_id)
|
||||
print(f"Removed: {len(result['removed'])}")
|
||||
if result["skipped"]:
|
||||
print("Skipped:")
|
||||
@@ -605,8 +595,8 @@ def run(argv: list[str] | None = None) -> int:
|
||||
return 0 if result["stateUpdated"] else 2
|
||||
|
||||
if args.command == "disable":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
result = disable_plugin(args.instance, instance.path, st_root, args.plugin, force=args.force)
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
result = disable_plugin(args.instance, instance.path, st_root, args.plugin, force=args.force, install_id=install_id)
|
||||
print(f"Disabled: {args.plugin}")
|
||||
print(f"Removed: {len(result['removed'])}")
|
||||
if result["skipped"]:
|
||||
@@ -616,35 +606,58 @@ def run(argv: list[str] | None = None) -> int:
|
||||
return 0 if result["stateUpdated"] else 2
|
||||
|
||||
if args.command == "enable":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
installed_state = load_installed_state(st_root, args.instance)
|
||||
if args.plugin not in installed_state.get("disabledPlugins", {}):
|
||||
raise KeyError(f"plugin is not recorded as disabled: {args.plugin}")
|
||||
root = repo_root()
|
||||
registry_path = (root / args.registry).resolve() if not Path(args.registry).is_absolute() else Path(args.registry)
|
||||
lock_path = Path(args.lockfile) if args.lockfile else root / "locks" / f"{args.instance}.lock.toml"
|
||||
if not lock_path.is_absolute():
|
||||
lock_path = (root / lock_path).resolve()
|
||||
lockfile = load_lockfile(lock_path)
|
||||
if not any(plugin.id == args.plugin for plugin in lockfile.plugins):
|
||||
raise KeyError(f"plugin is disabled but not locked for this instance: {args.plugin}")
|
||||
plan, path = create_plan(
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
progress = (lambda message: print(f" {message}", flush=True))
|
||||
result = enable_disabled_plugin(
|
||||
instance=args.instance,
|
||||
instance_path=instance.path,
|
||||
beat_saber_version=lockfile.beat_saber_version,
|
||||
registry=load_registry(registry_path),
|
||||
lockfile=lockfile,
|
||||
state_root=st_root,
|
||||
repo_root=root,
|
||||
selected={args.plugin},
|
||||
plugin_id=args.plugin,
|
||||
registry=args.registry,
|
||||
lockfile=args.lockfile,
|
||||
progress=progress,
|
||||
install_id=install_id,
|
||||
)
|
||||
result = apply_plan(plan, st_root)
|
||||
print(f"Enabled: {args.plugin}")
|
||||
print(f"Plan: {path}")
|
||||
print(f"Plan: {result['planPath']}")
|
||||
print(f"Applied: {len(result['applied'])}")
|
||||
print(f"State: {result['statePath']}")
|
||||
return 0
|
||||
|
||||
if args.command == "save-known-good":
|
||||
_instance, install_id = _get_installation(runtime, args.instance)
|
||||
result = save_known_good_set(instance=args.instance, state_root=st_root, install_id=install_id)
|
||||
if args.json:
|
||||
_json(result)
|
||||
else:
|
||||
print(f"Saved known-good set for {args.instance}: {len(result['pluginIds'])} enabled plugins.")
|
||||
for plugin_id in result["pluginIds"]:
|
||||
print(f" {plugin_id}")
|
||||
return 0
|
||||
|
||||
if args.command == "restore-known-good":
|
||||
instance, install_id = _get_installation(runtime, args.instance)
|
||||
progress = (lambda message: print(f" {message}", flush=True))
|
||||
result = restore_known_good_set(
|
||||
instance=args.instance,
|
||||
instance_path=instance.path,
|
||||
state_root=st_root,
|
||||
registry=args.registry,
|
||||
lockfile=args.lockfile,
|
||||
progress=progress,
|
||||
install_id=install_id,
|
||||
)
|
||||
if args.json:
|
||||
_json(result)
|
||||
else:
|
||||
print(f"Disabled: {len(result['disabled'])}")
|
||||
print(f"Enabled: {len(result['enabled'])}")
|
||||
for item in result["disableErrors"]:
|
||||
print(f" disable error {item['plugin']}: {item['error']}")
|
||||
for item in result["enableErrors"]:
|
||||
print(f" enable error {item['plugin']}: {item['error']}")
|
||||
return 2 if result["disableErrors"] or result["enableErrors"] else 0
|
||||
|
||||
if args.command == "backup-userdata":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
root = repo_root()
|
||||
|
||||
+204
-9
@@ -1,33 +1,228 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tomllib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
WINDOWS_INSTANCES_ROOT = Path("/home/pleb/Windows/Users/pleb/BSManager/BSInstances")
|
||||
LOCAL_INSTANCES_ROOT = Path.home() / ".local/share/BSManager/BSInstances"
|
||||
DEFAULT_INSTANCES_ROOTS = (WINDOWS_INSTANCES_ROOT, LOCAL_INSTANCES_ROOT)
|
||||
DEFAULT_INSTANCES_ROOT = WINDOWS_INSTANCES_ROOT
|
||||
DEFAULT_INSTANCES_ROOT = LOCAL_INSTANCES_ROOT
|
||||
LOCAL_CONFIG_NAME = "plugin-helper.local.toml"
|
||||
WINDOWS_CONFIG_NAME = "plugin-helper.windows.toml"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Profile:
|
||||
id: str
|
||||
label: str | None
|
||||
instances_roots: list[Path]
|
||||
state_root: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LocalConfig:
|
||||
instances_roots: list[Path] | None
|
||||
state_root: Path | None
|
||||
profiles: tuple[Profile, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeConfig:
|
||||
instances_roots: list[Path]
|
||||
state_root: Path
|
||||
config_path: Path
|
||||
config_loaded: bool
|
||||
profile_id: str | None = None
|
||||
profile_label: str | None = None
|
||||
|
||||
|
||||
def is_windows() -> bool:
|
||||
return sys.platform == "win32"
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def instances_root(value: str | None = None) -> Path:
|
||||
return instances_roots(value)[0]
|
||||
|
||||
|
||||
def instances_roots(value: str | None = None) -> list[Path]:
|
||||
def instances_roots(value: str | None = None, *, base: Path | None = None) -> list[Path]:
|
||||
raw = value or os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT")
|
||||
if raw:
|
||||
return [Path(item).expanduser() for item in raw.split(os.pathsep) if item]
|
||||
return list(DEFAULT_INSTANCES_ROOTS)
|
||||
return _resolve_path_list(raw, base or repo_root())
|
||||
return _default_instances_roots()
|
||||
|
||||
|
||||
def state_root(value: str | None = None) -> Path:
|
||||
if value:
|
||||
return Path(value).expanduser()
|
||||
return _resolve_path(value, repo_root())
|
||||
env_state = os.environ.get("PLUGIN_HELPER_STATE_DIR")
|
||||
if env_state:
|
||||
return _resolve_path(env_state, repo_root())
|
||||
return _default_state_root()
|
||||
|
||||
|
||||
def default_config_path(root: Path | None = None) -> Path:
|
||||
repo = root or repo_root()
|
||||
if is_windows():
|
||||
windows_config = repo / WINDOWS_CONFIG_NAME
|
||||
if windows_config.is_file():
|
||||
return windows_config
|
||||
return repo / LOCAL_CONFIG_NAME
|
||||
|
||||
|
||||
def _resolve_path(value: str | Path, base: Path) -> Path:
|
||||
path = Path(value).expanduser()
|
||||
if path.is_absolute():
|
||||
return path
|
||||
return (base / path).resolve()
|
||||
|
||||
|
||||
def _resolve_path_list(value: str, base: Path) -> list[Path]:
|
||||
return [_resolve_path(item, base) for item in value.split(os.pathsep) if item]
|
||||
|
||||
|
||||
def _parse_profiles(data: dict[str, Any], base: Path) -> tuple[Profile, ...]:
|
||||
profiles: list[Profile] = []
|
||||
for entry in data.get("profiles", []):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
profile_id = entry.get("id")
|
||||
instances_value = entry.get("instances_root")
|
||||
state_value = entry.get("state_dir")
|
||||
if not profile_id or not instances_value or not state_value:
|
||||
raise ValueError(f"profile {profile_id!r} needs id, instances_root, and state_dir")
|
||||
profiles.append(
|
||||
Profile(
|
||||
id=str(profile_id),
|
||||
label=entry.get("label"),
|
||||
instances_roots=_resolve_path_list(str(instances_value), base),
|
||||
state_root=_resolve_path(str(state_value), base),
|
||||
)
|
||||
)
|
||||
return tuple(profiles)
|
||||
|
||||
|
||||
def load_local_config(config_path: str | Path | None = None, *, root: Path | None = None) -> tuple[LocalConfig, Path, bool]:
|
||||
repo = root or repo_root()
|
||||
path = _resolve_path(config_path, repo) if config_path else default_config_path(repo)
|
||||
if not path.exists():
|
||||
if config_path:
|
||||
raise FileNotFoundError(f"plugin-helper config not found: {path}")
|
||||
return LocalConfig(instances_roots=None, state_root=None, profiles=()), path, False
|
||||
|
||||
with path.open("rb") as handle:
|
||||
data: dict[str, Any] = tomllib.load(handle)
|
||||
|
||||
base = path.parent
|
||||
instances_value = data.get("instances_root")
|
||||
state_value = data.get("state_dir")
|
||||
profiles = _parse_profiles(data, base)
|
||||
return (
|
||||
LocalConfig(
|
||||
instances_roots=_resolve_path_list(instances_value, base) if instances_value else None,
|
||||
state_root=_resolve_path(state_value, base) if state_value else None,
|
||||
profiles=profiles,
|
||||
),
|
||||
path,
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def _env_instances_roots(root: Path) -> list[Path] | None:
|
||||
value = os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT")
|
||||
return _resolve_path_list(value, root) if value else None
|
||||
|
||||
|
||||
def _env_state_root(root: Path) -> Path | None:
|
||||
value = os.environ.get("PLUGIN_HELPER_STATE_DIR")
|
||||
return _resolve_path(value, root) if value else None
|
||||
|
||||
|
||||
def _default_state_root() -> Path:
|
||||
if is_windows():
|
||||
localappdata = os.environ.get("LOCALAPPDATA")
|
||||
if localappdata:
|
||||
return Path(localappdata) / "plugin-helper"
|
||||
return Path.home() / "AppData" / "Local" / "plugin-helper"
|
||||
xdg_state = os.environ.get("XDG_STATE_HOME")
|
||||
base = Path(xdg_state).expanduser() if xdg_state else Path.home() / ".local" / "state"
|
||||
return base / "plugin-helper"
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
def _default_instances_roots() -> list[Path]:
|
||||
if is_windows():
|
||||
return [Path.home() / "BSManager" / "BSInstances"]
|
||||
return [DEFAULT_INSTANCES_ROOT]
|
||||
|
||||
|
||||
def _select_profile(local_config: LocalConfig, profile_id: str | None) -> Profile | None:
|
||||
if profile_id:
|
||||
for profile in local_config.profiles:
|
||||
if profile.id == profile_id:
|
||||
return profile
|
||||
available = ", ".join(profile.id for profile in local_config.profiles) or "(none)"
|
||||
raise ValueError(f"unknown profile {profile_id!r}; available profiles: {available}")
|
||||
|
||||
if (
|
||||
local_config.profiles
|
||||
and local_config.instances_roots is None
|
||||
and local_config.state_root is None
|
||||
and len(local_config.profiles) == 1
|
||||
):
|
||||
return local_config.profiles[0]
|
||||
return None
|
||||
|
||||
|
||||
def resolve_runtime_config(
|
||||
*,
|
||||
instances_root_value: str | None = None,
|
||||
state_dir_value: str | None = None,
|
||||
config_path_value: str | None = None,
|
||||
profile_id: str | None = None,
|
||||
root: Path | None = None,
|
||||
) -> RuntimeConfig:
|
||||
repo = root or repo_root()
|
||||
local_config, loaded_path, loaded = load_local_config(config_path_value, root=repo)
|
||||
explicit_runtime_paths = bool(
|
||||
instances_root_value
|
||||
or state_dir_value
|
||||
or _env_instances_roots(repo)
|
||||
or _env_state_root(repo)
|
||||
)
|
||||
profile = (
|
||||
_select_profile(local_config, profile_id)
|
||||
if profile_id or not explicit_runtime_paths
|
||||
else None
|
||||
)
|
||||
|
||||
resolved_instances = (
|
||||
_resolve_path_list(instances_root_value, repo)
|
||||
if instances_root_value
|
||||
else _env_instances_roots(repo)
|
||||
or (profile.instances_roots if profile else None)
|
||||
or local_config.instances_roots
|
||||
or _default_instances_roots()
|
||||
)
|
||||
resolved_state = (
|
||||
_resolve_path(state_dir_value, repo)
|
||||
if state_dir_value
|
||||
else _env_state_root(repo)
|
||||
or (profile.state_root if profile else None)
|
||||
or local_config.state_root
|
||||
or _default_state_root()
|
||||
)
|
||||
|
||||
return RuntimeConfig(
|
||||
instances_roots=resolved_instances,
|
||||
state_root=resolved_state,
|
||||
config_path=loaded_path,
|
||||
config_loaded=loaded,
|
||||
profile_id=profile.id if profile else None,
|
||||
profile_label=profile.label if profile else None,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
from zipfile import ZipFile
|
||||
|
||||
from .fsutil import ensure_inside, ensure_relative, sha256_bytes, sha256_file
|
||||
from .state import backups_dir, load_installed_state, save_installed_state
|
||||
from .state import backups_dir, installed_state_path, load_installed_state, save_installed_state
|
||||
|
||||
|
||||
def _timestamp() -> str:
|
||||
@@ -26,12 +26,13 @@ def _backup_existing(instance_path: Path, backup_root: Path, rel_target: str) ->
|
||||
|
||||
def apply_plan(plan: dict[str, Any], state_root: Path) -> dict[str, Any]:
|
||||
instance = plan["instance"]
|
||||
install_id = plan.get("installId")
|
||||
instance_path = Path(plan["instancePath"])
|
||||
if not instance_path.is_dir():
|
||||
raise FileNotFoundError(f"instance path does not exist: {instance_path}")
|
||||
|
||||
backup_root = backups_dir(state_root, instance) / f"apply-{_timestamp()}"
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
backup_root = backups_dir(state_root, instance, install_id=install_id) / f"apply-{_timestamp()}"
|
||||
installed_state = load_installed_state(state_root, instance, install_id=install_id)
|
||||
installed_state.setdefault("beatSaberVersion", plan.get("beatSaberVersion"))
|
||||
installed_state.setdefault("plugins", {})
|
||||
|
||||
@@ -80,15 +81,23 @@ def apply_plan(plan: dict[str, Any], state_root: Path) -> dict[str, Any]:
|
||||
installed_state.setdefault("disabledPlugins", {}).pop(change["plugin"], None)
|
||||
applied.append({"path": rel_target, "plugin": change["plugin"], "backup": backup})
|
||||
|
||||
save_installed_state(state_root, instance, installed_state)
|
||||
return {"applied": applied, "statePath": str(state_root / "instances" / instance / "installed.json")}
|
||||
save_installed_state(state_root, instance, installed_state, install_id=install_id)
|
||||
return {"applied": applied, "statePath": str(installed_state_path(state_root, instance, install_id=install_id))}
|
||||
|
||||
|
||||
def uninstall_plugin(instance: str, instance_path: Path, state_root: Path, plugin_id: str, force: bool = False) -> dict[str, Any]:
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
def uninstall_plugin(
|
||||
instance: str,
|
||||
instance_path: Path,
|
||||
state_root: Path,
|
||||
plugin_id: str,
|
||||
force: bool = False,
|
||||
*,
|
||||
install_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
installed_state = load_installed_state(state_root, instance, install_id=install_id)
|
||||
plugin_state = installed_state.get("plugins", {}).get(plugin_id)
|
||||
if not plugin_state:
|
||||
raise KeyError(f"plugin is not recorded in install state: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in managed install state: {plugin_id}")
|
||||
|
||||
removed: list[str] = []
|
||||
skipped: list[dict[str, str]] = []
|
||||
@@ -109,17 +118,25 @@ def uninstall_plugin(instance: str, instance_path: Path, state_root: Path, plugi
|
||||
return {"removed": removed, "skipped": skipped, "stateUpdated": False}
|
||||
|
||||
installed_state.get("plugins", {}).pop(plugin_id, None)
|
||||
save_installed_state(state_root, instance, installed_state)
|
||||
save_installed_state(state_root, instance, installed_state, install_id=install_id)
|
||||
return {"removed": removed, "skipped": skipped, "stateUpdated": True}
|
||||
|
||||
|
||||
def disable_plugin(instance: str, instance_path: Path, state_root: Path, plugin_id: str, force: bool = False) -> dict[str, Any]:
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
def disable_plugin(
|
||||
instance: str,
|
||||
instance_path: Path,
|
||||
state_root: Path,
|
||||
plugin_id: str,
|
||||
force: bool = False,
|
||||
*,
|
||||
install_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
installed_state = load_installed_state(state_root, instance, install_id=install_id)
|
||||
plugin_state = installed_state.get("plugins", {}).get(plugin_id)
|
||||
if not plugin_state:
|
||||
if plugin_id in installed_state.get("disabledPlugins", {}):
|
||||
raise KeyError(f"plugin is already disabled: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in install state: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in managed install state: {plugin_id}")
|
||||
|
||||
removed: list[str] = []
|
||||
skipped: list[dict[str, str]] = []
|
||||
@@ -143,5 +160,5 @@ def disable_plugin(instance: str, instance_path: Path, state_root: Path, plugin_
|
||||
disabled_state["disabledAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
installed_state.setdefault("disabledPlugins", {})[plugin_id] = disabled_state
|
||||
installed_state.get("plugins", {}).pop(plugin_id, None)
|
||||
save_installed_state(state_root, instance, installed_state)
|
||||
save_installed_state(state_root, instance, installed_state, install_id=install_id)
|
||||
return {"removed": removed, "skipped": skipped, "stateUpdated": True}
|
||||
|
||||
+86
-23
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tomllib
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass, field, replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -42,6 +42,7 @@ class LockedPlugin:
|
||||
tag: str | None
|
||||
asset: str | None
|
||||
sha256: str | None
|
||||
download_url: str | None = None
|
||||
install_strategy: str | None = None
|
||||
reason: str | None = None
|
||||
|
||||
@@ -53,38 +54,99 @@ class Lockfile:
|
||||
plugins: tuple[LockedPlugin, ...]
|
||||
|
||||
|
||||
def _quote_toml_string(value: str) -> str:
|
||||
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def lockfile_to_toml(lockfile: Lockfile) -> str:
|
||||
lines = [
|
||||
f"beat_saber_version = {_quote_toml_string(lockfile.beat_saber_version)}",
|
||||
f"instance = {_quote_toml_string(lockfile.instance)}",
|
||||
"",
|
||||
]
|
||||
for plugin in lockfile.plugins:
|
||||
lines.append("[[plugins]]")
|
||||
lines.append(f"id = {_quote_toml_string(plugin.id)}")
|
||||
if plugin.repo is not None:
|
||||
lines.append(f"repo = {_quote_toml_string(plugin.repo)}")
|
||||
if plugin.tag is not None:
|
||||
lines.append(f"tag = {_quote_toml_string(plugin.tag)}")
|
||||
if plugin.asset is not None:
|
||||
lines.append(f"asset = {_quote_toml_string(plugin.asset)}")
|
||||
if plugin.download_url is not None:
|
||||
lines.append(f"download_url = {_quote_toml_string(plugin.download_url)}")
|
||||
if plugin.sha256 is not None:
|
||||
lines.append(f"sha256 = {_quote_toml_string(plugin.sha256)}")
|
||||
if plugin.install_strategy is not None:
|
||||
lines.append(f"install_strategy = {_quote_toml_string(plugin.install_strategy)}")
|
||||
if plugin.reason is not None:
|
||||
lines.append(f"reason = {_quote_toml_string(plugin.reason)}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_lockfile(path: Path, lockfile: Lockfile) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(lockfile_to_toml(lockfile), encoding="utf-8")
|
||||
|
||||
|
||||
def replace_locked_plugins(lockfile: Lockfile, replacements: dict[str, LockedPlugin]) -> Lockfile:
|
||||
return replace(
|
||||
lockfile,
|
||||
plugins=tuple(replacements.get(plugin.id, plugin) for plugin in lockfile.plugins),
|
||||
)
|
||||
|
||||
|
||||
def _load_toml(path: Path) -> dict[str, Any]:
|
||||
with path.open("rb") as handle:
|
||||
return tomllib.load(handle)
|
||||
|
||||
|
||||
def _registry_plugin_from_item(item: dict[str, Any], source: Path) -> RegistryPlugin:
|
||||
dependencies = tuple(
|
||||
Dependency(
|
||||
id=dep["id"],
|
||||
constraint=dep.get("constraint"),
|
||||
required=dep.get("required", True),
|
||||
)
|
||||
for dep in item.get("dependencies", [])
|
||||
)
|
||||
strategy = item.get("install_strategy", "manual")
|
||||
if strategy not in VALID_STRATEGIES:
|
||||
raise ValueError(f"{source}: invalid install_strategy for {item['id']}: {strategy}")
|
||||
return RegistryPlugin(
|
||||
id=item["id"],
|
||||
name=item.get("name", item["id"]),
|
||||
repo=item.get("repo"),
|
||||
asset_patterns=tuple(item.get("asset_patterns", [])),
|
||||
install_strategy=strategy,
|
||||
category=item.get("category"),
|
||||
dependencies=dependencies,
|
||||
)
|
||||
|
||||
|
||||
def _add_registry_plugin(plugins: dict[str, RegistryPlugin], plugin: RegistryPlugin, source: Path) -> None:
|
||||
if plugin.id in plugins:
|
||||
raise ValueError(f"{source}: duplicate plugin id: {plugin.id}")
|
||||
plugins[plugin.id] = plugin
|
||||
|
||||
|
||||
def load_registry(path: Path) -> Registry:
|
||||
if not path.exists():
|
||||
legacy_path = path.with_suffix(".toml")
|
||||
if path.name == "plugins" and legacy_path.exists():
|
||||
return load_registry(legacy_path)
|
||||
return Registry()
|
||||
data = _load_toml(path)
|
||||
plugins: dict[str, RegistryPlugin] = {}
|
||||
if path.is_dir():
|
||||
for item_path in sorted(path.glob("*.toml")):
|
||||
data = _load_toml(item_path)
|
||||
_add_registry_plugin(plugins, _registry_plugin_from_item(data, item_path), item_path)
|
||||
return Registry(plugins)
|
||||
|
||||
data = _load_toml(path)
|
||||
for item in data.get("plugins", []):
|
||||
dependencies = tuple(
|
||||
Dependency(
|
||||
id=dep["id"],
|
||||
constraint=dep.get("constraint"),
|
||||
required=dep.get("required", True),
|
||||
)
|
||||
for dep in item.get("dependencies", [])
|
||||
)
|
||||
strategy = item.get("install_strategy", "manual")
|
||||
if strategy not in VALID_STRATEGIES:
|
||||
raise ValueError(f"{path}: invalid install_strategy for {item['id']}: {strategy}")
|
||||
plugin = RegistryPlugin(
|
||||
id=item["id"],
|
||||
name=item.get("name", item["id"]),
|
||||
repo=item.get("repo"),
|
||||
asset_patterns=tuple(item.get("asset_patterns", [])),
|
||||
install_strategy=strategy,
|
||||
category=item.get("category"),
|
||||
dependencies=dependencies,
|
||||
)
|
||||
plugins[plugin.id] = plugin
|
||||
_add_registry_plugin(plugins, _registry_plugin_from_item(item, path), path)
|
||||
return Registry(plugins)
|
||||
|
||||
|
||||
@@ -97,6 +159,7 @@ def load_lockfile(path: Path) -> Lockfile:
|
||||
tag=item.get("tag"),
|
||||
asset=item.get("asset"),
|
||||
sha256=item.get("sha256"),
|
||||
download_url=item.get("download_url"),
|
||||
install_strategy=item.get("install_strategy"),
|
||||
reason=item.get("reason"),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .bootstrap import ensure_healthy_bootstrap
|
||||
from .config import repo_root
|
||||
from .installer import apply_plan, disable_plugin
|
||||
from .models import Lockfile, Registry, load_lockfile, load_registry
|
||||
from .planner import create_plan
|
||||
from .state import load_installed_state, load_known_good_state, save_known_good_state
|
||||
|
||||
|
||||
def _resolve_paths(
|
||||
*,
|
||||
instance: str,
|
||||
registry: str,
|
||||
lockfile: str | None,
|
||||
repo: Path | None,
|
||||
) -> tuple[Path, Path, Path, Lockfile, Registry]:
|
||||
root = repo or repo_root()
|
||||
registry_path = (root / registry).resolve() if not Path(registry).is_absolute() else Path(registry)
|
||||
lock_path = Path(lockfile) if lockfile else root / "locks" / f"{instance}.lock.toml"
|
||||
if not lock_path.is_absolute():
|
||||
lock_path = (root / lock_path).resolve()
|
||||
loaded_lockfile = load_lockfile(lock_path)
|
||||
loaded_registry = load_registry(registry_path)
|
||||
return root, registry_path, lock_path, loaded_lockfile, loaded_registry
|
||||
|
||||
|
||||
def enable_disabled_plugin(
|
||||
*,
|
||||
instance: str,
|
||||
instance_path: Path,
|
||||
state_root: Path,
|
||||
plugin_id: str,
|
||||
registry: str = "registry/plugins",
|
||||
lockfile: str | None = None,
|
||||
repo: Path | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
install_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
installed_state = load_installed_state(state_root, instance, install_id=install_id)
|
||||
if plugin_id in installed_state.get("plugins", {}):
|
||||
raise KeyError(f"plugin is already enabled: {plugin_id}")
|
||||
|
||||
root, registry_path, _lock_path, loaded_lockfile, loaded_registry = _resolve_paths(
|
||||
instance=instance,
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
repo=repo,
|
||||
)
|
||||
if not any(plugin.id == plugin_id for plugin in loaded_lockfile.plugins):
|
||||
raise KeyError(f"plugin is not locked for this instance: {plugin_id}")
|
||||
|
||||
tell = progress or (lambda _message: None)
|
||||
tell(f"Planning {plugin_id} from local assets")
|
||||
plan, path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected={plugin_id},
|
||||
require_bootstrap=False,
|
||||
install_id=install_id,
|
||||
)
|
||||
|
||||
ensure_healthy_bootstrap(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected_ids={plugin_id},
|
||||
progress=progress,
|
||||
install_id=install_id,
|
||||
)
|
||||
|
||||
tell(f"Applying {plugin_id}")
|
||||
result = apply_plan(plan, state_root)
|
||||
return {"planPath": str(path), **result}
|
||||
|
||||
|
||||
def enable_disabled_plugins(
|
||||
*,
|
||||
instance: str,
|
||||
instance_path: Path,
|
||||
state_root: Path,
|
||||
plugin_ids: list[str],
|
||||
registry: str = "registry/plugins",
|
||||
lockfile: str | None = None,
|
||||
repo: Path | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
install_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if not plugin_ids:
|
||||
return {"enabled": [], "errors": []}
|
||||
|
||||
root, _registry_path, _lock_path, loaded_lockfile, loaded_registry = _resolve_paths(
|
||||
instance=instance,
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
repo=repo,
|
||||
)
|
||||
installed_state = load_installed_state(state_root, instance, install_id=install_id)
|
||||
enabled_plugins = installed_state.get("plugins", {})
|
||||
locked_ids = {plugin.id for plugin in loaded_lockfile.plugins}
|
||||
|
||||
enabled: list[dict[str, Any]] = []
|
||||
errors: list[dict[str, str]] = []
|
||||
plannable_ids: list[str] = []
|
||||
for plugin_id in plugin_ids:
|
||||
if plugin_id in enabled_plugins:
|
||||
errors.append({"plugin": plugin_id, "error": f"plugin is already enabled: {plugin_id}"})
|
||||
continue
|
||||
if plugin_id not in locked_ids:
|
||||
errors.append({"plugin": plugin_id, "error": f"plugin is not locked for this instance: {plugin_id}"})
|
||||
continue
|
||||
plannable_ids.append(plugin_id)
|
||||
|
||||
if not plannable_ids:
|
||||
return {"enabled": enabled, "errors": errors}
|
||||
|
||||
selected_ids = set(plannable_ids)
|
||||
tell = progress or (lambda _message: None)
|
||||
tell(f"Planning {len(plannable_ids)} plugins from local assets")
|
||||
batch_plan, batch_path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected=selected_ids,
|
||||
require_bootstrap=False,
|
||||
install_id=install_id,
|
||||
)
|
||||
|
||||
ensure_healthy_bootstrap(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected_ids=selected_ids,
|
||||
progress=progress,
|
||||
install_id=install_id,
|
||||
)
|
||||
|
||||
tell(f"Applying {len(plannable_ids)} plugins")
|
||||
result = apply_plan(batch_plan, state_root)
|
||||
applied_by_plugin: dict[str, int] = {}
|
||||
for item in result["applied"]:
|
||||
plugin_id = item["plugin"]
|
||||
applied_by_plugin[plugin_id] = applied_by_plugin.get(plugin_id, 0) + 1
|
||||
|
||||
for plugin_id in plannable_ids:
|
||||
enabled.append({"plugin": plugin_id, "planPath": str(batch_path), "applied": applied_by_plugin.get(plugin_id, 0)})
|
||||
|
||||
return {"enabled": enabled, "errors": errors}
|
||||
|
||||
|
||||
def save_known_good_set(*, instance: str, state_root: Path, install_id: str | None = None) -> dict[str, Any]:
|
||||
"""Record the currently enabled plugin ids as the known-good set for this instance."""
|
||||
installed_state = load_installed_state(state_root, instance, install_id=install_id)
|
||||
plugin_ids = sorted(installed_state.get("plugins", {}))
|
||||
known_good = {
|
||||
"instance": instance,
|
||||
"beatSaberVersion": installed_state.get("beatSaberVersion"),
|
||||
"pluginIds": plugin_ids,
|
||||
}
|
||||
if install_id:
|
||||
known_good["installId"] = install_id
|
||||
save_known_good_state(state_root, instance, known_good, install_id=install_id)
|
||||
return known_good
|
||||
|
||||
|
||||
def restore_known_good_set(
|
||||
*,
|
||||
instance: str,
|
||||
instance_path: Path,
|
||||
state_root: Path,
|
||||
registry: str = "registry/plugins",
|
||||
lockfile: str | None = None,
|
||||
repo: Path | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
install_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Disable plugins outside the saved known-good set and re-enable the ones that are missing."""
|
||||
known_good = load_known_good_state(state_root, instance, install_id=install_id)
|
||||
known_good_ids = set(known_good.get("pluginIds", []))
|
||||
if not known_good:
|
||||
raise KeyError(f"no known-good set is saved for this instance: {instance}")
|
||||
|
||||
installed_state = load_installed_state(state_root, instance, install_id=install_id)
|
||||
currently_enabled = set(installed_state.get("plugins", {}))
|
||||
|
||||
to_disable = sorted(currently_enabled - known_good_ids)
|
||||
disabled: list[str] = []
|
||||
disable_errors: list[dict[str, str]] = []
|
||||
for plugin_id in to_disable:
|
||||
try:
|
||||
result = disable_plugin(instance, instance_path, state_root, plugin_id, False, install_id=install_id)
|
||||
if result["stateUpdated"]:
|
||||
disabled.append(plugin_id)
|
||||
else:
|
||||
skipped = "; ".join(f"{item['path']} {item['reason']}" for item in result["skipped"])
|
||||
disable_errors.append({"plugin": plugin_id, "error": skipped or "no files changed"})
|
||||
except Exception as exc:
|
||||
disable_errors.append({"plugin": plugin_id, "error": str(exc)})
|
||||
|
||||
installed_state = load_installed_state(state_root, instance, install_id=install_id)
|
||||
currently_enabled = set(installed_state.get("plugins", {}))
|
||||
to_enable = sorted(known_good_ids - currently_enabled)
|
||||
enable_result = enable_disabled_plugins(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
state_root=state_root,
|
||||
plugin_ids=to_enable,
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
repo=repo,
|
||||
progress=progress,
|
||||
install_id=install_id,
|
||||
)
|
||||
|
||||
return {
|
||||
"knownGood": known_good,
|
||||
"disabled": disabled,
|
||||
"disableErrors": disable_errors,
|
||||
"enabled": [item["plugin"] for item in enable_result["enabled"]],
|
||||
"enableErrors": enable_result["errors"],
|
||||
}
|
||||
@@ -9,8 +9,12 @@ from zipfile import ZipFile
|
||||
|
||||
from .fsutil import ensure_relative, sha256_bytes, sha256_file
|
||||
from .models import Lockfile, Registry, VALID_STRATEGIES
|
||||
from .bsipa import BSIPA_PLUGIN_ID, check_bsipa_health
|
||||
from .state import downloads_dir, plans_dir, plugin_downloads_dir
|
||||
from .bsipa import bootstrap_health_error, check_bsipa_health, planning_requires_bootstrap
|
||||
from .state import (
|
||||
downloads_dir,
|
||||
plans_dir,
|
||||
plugin_downloads_dir,
|
||||
)
|
||||
|
||||
|
||||
ALLOWED_BSIPA_TOP_LEVEL = {"IPA", "Libs", "Plugins"}
|
||||
@@ -65,6 +69,26 @@ def _asset_matches_patterns(name: str, patterns: tuple[str, ...]) -> bool:
|
||||
return not patterns or any(fnmatch.fnmatch(name, pattern) for pattern in patterns)
|
||||
|
||||
|
||||
def _expand_required_dependencies(selected_ids: set[str], registry: Registry, lockfile: Lockfile) -> set[str]:
|
||||
locked_ids = {plugin.id for plugin in lockfile.plugins}
|
||||
expanded = set(selected_ids)
|
||||
pending = list(selected_ids)
|
||||
while pending:
|
||||
plugin_id = pending.pop()
|
||||
registry_plugin = registry.get(plugin_id)
|
||||
if not registry_plugin:
|
||||
continue
|
||||
for dependency in registry_plugin.dependencies:
|
||||
if not dependency.required:
|
||||
continue
|
||||
if dependency.id not in locked_ids:
|
||||
raise ValueError(f"{plugin_id}: required dependency is not locked for this instance: {dependency.id}")
|
||||
if dependency.id not in expanded:
|
||||
expanded.add(dependency.id)
|
||||
pending.append(dependency.id)
|
||||
return expanded
|
||||
|
||||
|
||||
def create_plan(
|
||||
*,
|
||||
instance: str,
|
||||
@@ -76,18 +100,20 @@ def create_plan(
|
||||
repo_root: Path,
|
||||
selected: set[str] | None = None,
|
||||
require_bootstrap: bool = True,
|
||||
install_id: str | None = None,
|
||||
) -> tuple[dict[str, Any], Path]:
|
||||
selected_ids = selected or {plugin.id for plugin in lockfile.plugins}
|
||||
selected_ids = (
|
||||
_expand_required_dependencies(selected, registry, lockfile)
|
||||
if selected is not None
|
||||
else {plugin.id for plugin in lockfile.plugins}
|
||||
)
|
||||
changes: list[dict[str, Any]] = []
|
||||
warnings: list[str] = []
|
||||
|
||||
has_locked_bsipa = any(plugin.id == BSIPA_PLUGIN_ID for plugin in lockfile.plugins)
|
||||
planning_ordinary_plugins = any(plugin_id != BSIPA_PLUGIN_ID for plugin_id in selected_ids)
|
||||
if require_bootstrap and has_locked_bsipa and planning_ordinary_plugins:
|
||||
health = check_bsipa_health(instance_path, state_root, instance)
|
||||
if require_bootstrap and planning_requires_bootstrap(lockfile.plugins, selected_ids):
|
||||
health = check_bsipa_health(instance_path, state_root, instance, install_id=install_id)
|
||||
if not health["ok"]:
|
||||
joined = "; ".join(health["messages"])
|
||||
raise ValueError(f"BSIPA bootstrap is not healthy; run bootstrap first: {joined}")
|
||||
raise ValueError(bootstrap_health_error(health))
|
||||
|
||||
for locked in lockfile.plugins:
|
||||
if locked.id not in selected_ids:
|
||||
@@ -97,11 +123,11 @@ def create_plan(
|
||||
if strategy not in VALID_STRATEGIES:
|
||||
raise ValueError(f"{locked.id}: invalid install strategy: {strategy}")
|
||||
if strategy == "manual":
|
||||
raise ValueError(f"{locked.id}: install_strategy is manual; add a concrete registry rule first")
|
||||
raise ValueError(f"{locked.id}: install_strategy is manual; add a concrete catalog rule first")
|
||||
if not locked.asset:
|
||||
raise ValueError(f"{locked.id}: lock entry has no asset")
|
||||
if registry_plugin and not _asset_matches_patterns(Path(locked.asset).name, registry_plugin.asset_patterns):
|
||||
warnings.append(f"{locked.id}: asset does not match registry patterns")
|
||||
warnings.append(f"{locked.id}: asset does not match catalog patterns")
|
||||
|
||||
asset_path = _find_asset(locked.asset, state_root, instance, repo_root, locked.id)
|
||||
if not asset_path:
|
||||
@@ -154,7 +180,9 @@ def create_plan(
|
||||
"warnings": warnings,
|
||||
"changes": changes,
|
||||
}
|
||||
plan_path = plans_dir(state_root, instance) / f"plan-{_now_slug()}.json"
|
||||
if install_id:
|
||||
plan["installId"] = install_id
|
||||
plan_path = plans_dir(state_root, instance, install_id=install_id) / f"plan-{_now_slug()}.json"
|
||||
with plan_path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(plan, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Asset reconstruction recipes for helper-managed composite packages."""
|
||||
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from urllib.request import Request, urlopen
|
||||
from zipfile import ZIP_STORED, ZipFile, ZipInfo
|
||||
|
||||
|
||||
PLUGIN_ID = "cinema-tools"
|
||||
DEFAULT_INSTANCE = "1.44.1"
|
||||
|
||||
YT_DLP_VERSION = "2026.07.04"
|
||||
YT_DLP_URL = f"https://github.com/yt-dlp/yt-dlp/releases/download/{YT_DLP_VERSION}/yt-dlp.exe"
|
||||
YT_DLP_SHA256 = "52fe3c26dcf71fbdc85b528589020bb0b8e383155cfa81b64dd447bbe35e24b8"
|
||||
|
||||
FFMPEG_VERSION = "8.1.2"
|
||||
FFMPEG_URL = (
|
||||
f"https://github.com/GyanD/codexffmpeg/releases/download/{FFMPEG_VERSION}/"
|
||||
f"ffmpeg-{FFMPEG_VERSION}-essentials_build.zip"
|
||||
)
|
||||
FFMPEG_ZIP_SHA256 = "db580001caa24ac104c8cb856cd113a87b0a443f7bdf47d8c12b1d740584a2ec"
|
||||
FFMPEG_EXE_SHA256 = "1326dde4c84ff1f96fe6b8916c5bed29e163e9b5dccf995f6f3db069d143ec5e"
|
||||
|
||||
ASSET_NAME = f"CinemaTools-yt-dlp-{YT_DLP_VERSION}-ffmpeg-{FFMPEG_VERSION}.zip"
|
||||
BUNDLE_SHA256 = "4d299e40f747abb9777838542ca68ec450e0b86e6eb11af341e6bd568f47edf2"
|
||||
ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuiltAsset:
|
||||
path: Path
|
||||
sha256: str
|
||||
size: int
|
||||
|
||||
|
||||
def _default_state_root() -> Path:
|
||||
configured = os.environ.get("PLUGIN_HELPER_STATE_DIR")
|
||||
if configured:
|
||||
return Path(configured).expanduser()
|
||||
xdg_state_home = os.environ.get("XDG_STATE_HOME")
|
||||
if xdg_state_home:
|
||||
return Path(xdg_state_home).expanduser() / "plugin-helper"
|
||||
return Path("~/.local/state/plugin-helper").expanduser()
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _download(url: str, destination: Path) -> None:
|
||||
request = Request(url, headers={"User-Agent": "plugin-helper"})
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with urlopen(request, timeout=120) as response, destination.open("wb") as handle:
|
||||
shutil.copyfileobj(response, handle)
|
||||
|
||||
|
||||
def _verify(path: Path, expected_sha256: str, label: str) -> None:
|
||||
actual = _sha256_file(path)
|
||||
if actual != expected_sha256:
|
||||
raise ValueError(f"{label} sha256 mismatch: expected {expected_sha256}, got {actual}")
|
||||
|
||||
|
||||
def _read_ffmpeg_exe(archive_path: Path) -> bytes:
|
||||
with ZipFile(archive_path) as archive:
|
||||
matches = [
|
||||
info
|
||||
for info in archive.infolist()
|
||||
if not info.is_dir() and info.filename.replace("\\", "/").endswith("/bin/ffmpeg.exe")
|
||||
]
|
||||
if len(matches) != 1:
|
||||
raise ValueError(f"expected exactly one ffmpeg.exe under */bin/, found {len(matches)}")
|
||||
data = archive.read(matches[0])
|
||||
actual = hashlib.sha256(data).hexdigest()
|
||||
if actual != FFMPEG_EXE_SHA256:
|
||||
raise ValueError(f"ffmpeg.exe sha256 mismatch: expected {FFMPEG_EXE_SHA256}, got {actual}")
|
||||
return data
|
||||
|
||||
|
||||
def _write_member(archive: ZipFile, name: str, data: bytes) -> None:
|
||||
info = ZipInfo(name, date_time=ZIP_TIMESTAMP)
|
||||
info.compress_type = ZIP_STORED
|
||||
info.create_system = 3 # Unix; keep the bundle hash stable across hosts
|
||||
info.external_attr = 0o644 << 16
|
||||
archive.writestr(info, data)
|
||||
|
||||
|
||||
def build_asset(output: Path, *, work_dir: Path | None = None) -> BuiltAsset:
|
||||
output = output.expanduser()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=work_dir) as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
yt_dlp_path = tmp_path / "yt-dlp.exe"
|
||||
ffmpeg_zip_path = tmp_path / f"ffmpeg-{FFMPEG_VERSION}-essentials_build.zip"
|
||||
staged_output = tmp_path / ASSET_NAME
|
||||
|
||||
_download(YT_DLP_URL, yt_dlp_path)
|
||||
_verify(yt_dlp_path, YT_DLP_SHA256, "yt-dlp.exe")
|
||||
|
||||
_download(FFMPEG_URL, ffmpeg_zip_path)
|
||||
_verify(ffmpeg_zip_path, FFMPEG_ZIP_SHA256, "ffmpeg essentials zip")
|
||||
|
||||
yt_dlp_data = yt_dlp_path.read_bytes()
|
||||
ffmpeg_data = _read_ffmpeg_exe(ffmpeg_zip_path)
|
||||
|
||||
with ZipFile(staged_output, "w") as archive:
|
||||
_write_member(archive, "Libs/yt-dlp.exe", yt_dlp_data)
|
||||
_write_member(archive, "Libs/ffmpeg.exe", ffmpeg_data)
|
||||
|
||||
actual_bundle_sha256 = _sha256_file(staged_output)
|
||||
if actual_bundle_sha256 != BUNDLE_SHA256:
|
||||
raise ValueError(
|
||||
f"{ASSET_NAME} sha256 mismatch: expected {BUNDLE_SHA256}, got {actual_bundle_sha256}"
|
||||
)
|
||||
shutil.move(str(staged_output), output)
|
||||
|
||||
return BuiltAsset(path=output, sha256=_sha256_file(output), size=output.stat().st_size)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Rebuild the helper-managed Cinema tools bundle.")
|
||||
parser.add_argument("--instance", default=DEFAULT_INSTANCE, help="Beat Saber instance/version cache key")
|
||||
parser.add_argument(
|
||||
"--state-dir",
|
||||
type=Path,
|
||||
default=_default_state_root(),
|
||||
help="plugin-helper state root; defaults to PLUGIN_HELPER_STATE_DIR, XDG_STATE_HOME, or ~/.local/state/plugin-helper",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
help="Exact output path; defaults to <state-dir>/cache/downloads/<instance>/cinema-tools/<asset>",
|
||||
)
|
||||
parser.add_argument("--work-dir", type=Path, help="Parent directory for temporary downloads")
|
||||
return parser
|
||||
|
||||
|
||||
def run(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
output = args.output or args.state_dir / "cache" / "downloads" / args.instance / PLUGIN_ID / ASSET_NAME
|
||||
try:
|
||||
built = build_asset(output, work_dir=args.work_dir)
|
||||
except Exception as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
print(f"Wrote: {built.path}")
|
||||
print(f"Size: {built.size}")
|
||||
print(f"SHA-256: {built.sha256}")
|
||||
print("Members:")
|
||||
print(f" Libs/yt-dlp.exe {YT_DLP_SHA256}")
|
||||
print(f" Libs/ffmpeg.exe {FFMPEG_EXE_SHA256}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> None:
|
||||
raise SystemExit(run())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .models import Lockfile, Registry
|
||||
|
||||
|
||||
def installed_plugins_report(
|
||||
*,
|
||||
installed_state: dict[str, Any],
|
||||
registry: Registry,
|
||||
lockfile: Lockfile,
|
||||
) -> dict[str, Any]:
|
||||
locked_by_id = {plugin.id: plugin for plugin in lockfile.plugins}
|
||||
plugins: list[dict[str, Any]] = []
|
||||
enabled_plugins = installed_state.get("plugins", {})
|
||||
disabled_plugins = installed_state.get("disabledPlugins", {})
|
||||
plugin_ids = list(locked_by_id)
|
||||
plugin_ids.extend(
|
||||
sorted(
|
||||
plugin_id
|
||||
for plugin_id in set(enabled_plugins) | set(disabled_plugins)
|
||||
if plugin_id not in locked_by_id
|
||||
)
|
||||
)
|
||||
|
||||
for plugin_id in plugin_ids:
|
||||
if plugin_id in enabled_plugins:
|
||||
plugin_state = enabled_plugins[plugin_id]
|
||||
status = "enabled"
|
||||
else:
|
||||
plugin_state = disabled_plugins.get(plugin_id, {})
|
||||
status = "disabled"
|
||||
registry_plugin = registry.get(plugin_id)
|
||||
locked = locked_by_id.get(plugin_id)
|
||||
files = plugin_state.get("files", [])
|
||||
plugins.append(
|
||||
{
|
||||
"id": plugin_id,
|
||||
"name": registry_plugin.name if registry_plugin else plugin_id,
|
||||
"version": locked.tag if locked and locked.tag else "(not locked)",
|
||||
"asset": locked.asset if locked and locked.asset else "(unknown)",
|
||||
"repo": (locked.repo if locked and locked.repo else None)
|
||||
or (registry_plugin.repo if registry_plugin else None)
|
||||
or "(unknown)",
|
||||
"installedAt": plugin_state.get("installedAt", "(unknown)"),
|
||||
"disabledAt": plugin_state.get("disabledAt"),
|
||||
"status": status,
|
||||
"fileCount": len(files),
|
||||
"files": files,
|
||||
}
|
||||
)
|
||||
plugins.sort(key=lambda plugin: (plugin["name"].casefold(), plugin["id"]))
|
||||
return {
|
||||
"instance": installed_state.get("instance", lockfile.instance),
|
||||
"beatSaberVersion": installed_state.get("beatSaberVersion", lockfile.beat_saber_version),
|
||||
"plugins": plugins,
|
||||
}
|
||||
|
||||
|
||||
def print_installed_plugins(report: dict[str, Any]) -> None:
|
||||
plugins = report["plugins"]
|
||||
print(f"{report['instance']} version-locked plugins ({len(plugins)})")
|
||||
if not plugins:
|
||||
print("No plugins are listed in this instance's version lock.")
|
||||
return
|
||||
|
||||
headers = ("Plugin", "Status", "Version", "Asset", "Files", "Installed")
|
||||
rows = [
|
||||
(
|
||||
f"{plugin['name']} ({plugin['id']})",
|
||||
plugin["status"],
|
||||
plugin["version"],
|
||||
plugin["asset"],
|
||||
str(plugin["fileCount"]),
|
||||
plugin["disabledAt"] or plugin["installedAt"],
|
||||
)
|
||||
for plugin in plugins
|
||||
]
|
||||
widths = [
|
||||
max(len(headers[index]), *(len(row[index]) for row in rows))
|
||||
for index in range(len(headers))
|
||||
]
|
||||
header = " ".join(label.ljust(widths[index]) for index, label in enumerate(headers))
|
||||
print(header)
|
||||
print(" ".join("-" * width for width in widths))
|
||||
for row in rows:
|
||||
print(" ".join(value.ljust(widths[index]) for index, value in enumerate(row)))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user