Compare commits
3 Commits
6117173507
...
13b1840ba0
| Author | SHA1 | Date | |
|---|---|---|---|
| 13b1840ba0 | |||
| de0b20fa61 | |||
| fec4c82a5d |
@@ -5,10 +5,7 @@ description: Install or update a Beat Saber plugin in the plugin-helper repo by
|
||||
|
||||
# Install Beat Saber Plugin
|
||||
|
||||
Use the repository's own `plugin-helper` commands to manage plugins for BSManager instances whenever the helper supports the operation. Do not manually copy release files into the game instance except:
|
||||
|
||||
- to bootstrap BSIPA/core packages before the helper has a first-class bootstrap command
|
||||
- to undo your own mistaken install before rerunning the helper
|
||||
Use the repository's own `plugin-helper` commands to manage plugins for BSManager instances whenever the helper supports the operation.
|
||||
|
||||
## Hard Guardrail
|
||||
|
||||
@@ -37,14 +34,6 @@ 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.
|
||||
|
||||
Accepted URL shapes include:
|
||||
|
||||
```text
|
||||
https://github.com/<owner>/<repo>/releases
|
||||
https://github.com/<owner>/<repo>/releases/tag/<tag>
|
||||
https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm the workspace is the `plugin-helper` repo.
|
||||
|
||||
@@ -6,13 +6,18 @@ Guidance for coding agents working in this repo.
|
||||
|
||||
- This repo manages Beat Saber plugins for BSManager instances.
|
||||
- Default instance roots are:
|
||||
- `/home/pleb/Windows/Users/pleb/BSManager/BSInstances`
|
||||
- `/home/pleb/.local/share/BSManager/BSInstances`
|
||||
- `~/Windows/Users/pleb/BSManager/BSInstances`
|
||||
- `~/.local/share/BSManager/BSInstances`
|
||||
- A local BSManager source checkout may be available at
|
||||
`/home/pleb/src/Zagrios/bs-manager`. Use it as a read-only reference when
|
||||
`~/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
|
||||
@@ -24,13 +29,17 @@ Guidance for coding agents working in this repo.
|
||||
- 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 /home/pleb/.local/share/BSManager/BSInstances` and normally
|
||||
`--instances-root ~/.local/share/BSManager/BSInstances` and normally
|
||||
`--state-dir .state`.
|
||||
- When targeting the mounted Windows BSManager install, pass
|
||||
`--instances-root /home/pleb/Windows/Users/pleb/BSManager/BSInstances` and
|
||||
`--instances-root ~/Windows/Users/pleb/BSManager/BSInstances` and
|
||||
normally `--state-dir .state-windows`.
|
||||
- Use the helper commands instead of manually copying plugin files into an
|
||||
instance.
|
||||
- When adding, updating, building, or investigating a GitHub-hosted plugin,
|
||||
check for `~/src/<owner>/<repo>` and clone the upstream repo there if
|
||||
it is missing. Do not substitute forks or similar repos without explicit user
|
||||
direction.
|
||||
- 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.
|
||||
@@ -46,6 +55,21 @@ Guidance for coding agents working in this repo.
|
||||
- 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.
|
||||
|
||||
@@ -151,11 +151,11 @@ Purpose: restore in-game song discovery and playlist management.
|
||||
| Plugin | Upstream | Status | Source/version | Verification notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| BeatSaverDownloader | [beatmods zip](https://beatmods.com/cdn/mod/a740c6e68a9b5d1dfda3cc8e81f7cf06.zip) | <span style="color:#d29922; font-weight:600">verified with warning</span> | BeatMods 6.0.7, version id 2217, zipHash `a740c6e68a9b5d1dfda3cc8e81f7cf06`; BeatMods preferred repo `Top-Cat/BeatSaverDownloader` exposes no release assets through the GitHub releases API | IPA loaded BeatSaver Downloader 6.0.7 and started its internal webserver. Warning: it probed for missing `BetterSongList.dll` with IPA library-loader `CRITICAL` lines, then continued. |
|
||||
| PlaylistManager | [github](https://github.com/rithik-b/PlaylistManager) | <span style="color:#3fb950; font-weight:600">verified</span> | Local PR82 build from `.state/build/playlistmanager-pr82-skilltest`, artifact `PlaylistManager-1.7.4-bs1.44.0-da1ad17.zip`; replaces failed BeatMods 1.7.3 compatibility trial | IPA loaded PlaylistManager 1.7.4, installed `PlaylistManagerAppInstaller`, and reached `MainSystemInit` during smoketest. The old `IPlatformUserModel` / `PlatformUserModel` failure did not recur. |
|
||||
| PlaylistManager | [github](https://github.com/rithik-b/PlaylistManager) | <span style="color:#f85149; font-weight:600">failed compatibility trial</span> | Local PR82 build from `.state/build/playlistmanager-pr82-skilltest`, artifact `PlaylistManager-1.7.4-bs1.44.0-da1ad17.zip`; removed from the 1.44.1 lock after DiTails smoketest failure | IPA loaded PlaylistManager 1.7.4, but later smoketest coverage showed it was still incompatible with this stack. Removed from both live BS installs and do not reinstall until a compatible build is available. |
|
||||
| BeatSaverUpdater | [github](https://github.com/ibillingsley/BeatSaverUpdater) | <span style="color:#3fb950; font-weight:600">verified</span> | GitHub `ibillingsley/BeatSaverUpdater` tag `1.2.11`, asset `BeatSaverUpdater-1.2.11-bs1.39.1-3698f98.zip`; BeatMods version id 2352, zipHash `d9ea8dd0cbaac66cbb02fa59a548e42b` | GitHub asset is byte-identical to the BeatMods CDN zip. IPA loaded BeatSaverUpdater 1.2.11. |
|
||||
| BeatSaverVoting | [beatmods zip](https://beatmods.com/cdn/mod/bc002ed1a43e2c6d3a10d0750e5d94b4.zip) | <span style="color:#f85149; font-weight:600">failed compatibility trial</span> | BeatMods 2.4.6, version id 2159, zipHash `bc002ed1a43e2c6d3a10d0750e5d94b4`; most recent blessed entry found was for Beat Saber 1.40.8, with no BeatMods verified entry for 1.44.1, 1.44.0, 1.43.0, 1.42.0, or 1.41.1 | IPA discovered and loaded BeatSaverVoting 2.4.6, but BS Utils caught a menu event failure from `BeatSaverVoting` caused by `TypeLoadException` resolving `IPlatformUserModel` from `PlatformUserModel`. Removed from the live instance after the failed smoketest. |
|
||||
| BeatSaberPlaylistsLib | [beatmods zip](https://beatmods.com/cdn/mod/a3418b75ed7294a3856f3eca12bbd672.zip) | <span style="color:#3fb950; font-weight:600">verified</span> | BeatMods 1.7.2, version id 2175, zipHash `a3418b75ed7294a3856f3eca12bbd672`; BeatMods preferred repo `Meivyn/BeatSaberPlaylistsLib` exposes no release assets through the GitHub releases API | IPA loaded BeatSaberPlaylistsLib 1.7.2. |
|
||||
| BeatSaverSharp | [beatmods zip](https://beatmods.com/cdn/mod/be37e13e93d9ac7da4efbdc3f514fa8f.zip) | <span style="color:#3fb950; font-weight:600">verified</span> | BeatMods 3.4.5, version id 1831, zipHash `be37e13e93d9ac7da4efbdc3f514fa8f`; BeatMods preferred repo `lolPants/BeatSaverSharp` was inaccessible through the GitHub releases API | IPA loaded BeatSaverSharp 3.4.5. |
|
||||
| BeatSaverSharp | [github](https://github.com/Auros/BeatSaverSharper), [beatmods zip](https://beatmods.com/cdn/mod/be37e13e93d9ac7da4efbdc3f514fa8f.zip) | <span style="color:#3fb950; font-weight:600">verified</span> | BeatMods 3.4.5, version id 1831, zipHash `be37e13e93d9ac7da4efbdc3f514fa8f`; source repo updated to `Auros/BeatSaverSharper`; locked package remains the BeatMods CDN artifact | IPA loaded BeatSaverSharp 3.4.5. |
|
||||
| ScoreSaberSharp | [beatmods zip](https://beatmods.com/cdn/mod/8713168c598577ee7c73fa3cf0e26f5c.zip) | <span style="color:#d29922; font-weight:600">verified with warning</span> | BeatMods 0.1.0, version id 445, zipHash `8713168c598577ee7c73fa3cf0e26f5c`; BeatMods lists `scoresaber.com` rather than a GitHub release source | IPA loaded ScoreSaberSharp 0.1.0. Warning: bare manifest does not declare files. |
|
||||
| BS Utils | [beatmods zip](https://beatmods.com/cdn/mod/918d13ac2821a3a17b2819f8861453e9.zip) | <span style="color:#3fb950; font-weight:600">verified</span> | BeatMods 1.14.3, version id 2563, zipHash `918d13ac2821a3a17b2819f8861453e9`; BeatMods preferred repo `Kylemc1413/Beat-Saber-Utils` exposes no matching 1.14.3 GitHub release asset | IPA loaded BS Utils 1.14.3. |
|
||||
| Ini Parser | [beatmods zip](https://beatmods.com/cdn/mod/5df74ad1c6b120fecdc615dd55f15b88.zip) | <span style="color:#3fb950; font-weight:600">verified</span> | BeatMods 2.5.9, version id 1352, zipHash `5df74ad1c6b120fecdc615dd55f15b88` | IPA loaded INI Parser 2.5.9. |
|
||||
@@ -188,8 +188,8 @@ Purpose: add small gameplay helpers two or three at a time.
|
||||
| HitsoundTweaks | [github](https://github.com/GalaxyMaster2/HitsoundTweaks) | <span style="color:#f85149; font-weight:600">failed compatibility trial</span> | GitHub `GalaxyMaster2/HitsoundTweaks` tag `v1.1.9`, asset `HitsoundTweaks-1.1.9-bs1.40.3-4ad8461.zip`; GitHub release digest matched downloaded asset | IPA loaded HitsoundTweaks 1.1.9, but SiraUtil failed to apply the `AudioTimeSyncController_dspTimeOffset_Patch` affinity patch with `InvalidProgramException`. Removed from the live instance after the failed smoke. BeatMods verified search on 2026-06-29 found no `HitsoundTweaks` entry for Beat Saber 1.42.0, 1.42.1, 1.43.0, 1.44.0, or 1.44.1. |
|
||||
| KeepMyOverridesPls | [github](https://github.com/qqrz997/KeepMyOverridesPls) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `qqrz997/KeepMyOverridesPls` tag `v1.1.3-b`, asset `KeepMyOverridesPls-1.1.3-bs1.40.6-487d417.zip`; GitHub release digest matched downloaded asset | IPA loaded KeepMyOverridesPls 1.1.3, installed its app installer, wrote config, and the game reached `MainSystemInit`. Warnings: manifest targets Beat Saber 1.40.6 and plugin has no start/exit methods. |
|
||||
| SoundReplacer | [github](https://github.com/Meivyn/SoundReplacer), [beatmods zip](https://beatmods.com/cdn/mod/7d7a869996e10249d1f85f95e060319b.zip) | <span style="color:#d29922; font-weight:600">verified with warning</span> | BeatMods 2.0.1, version id 2213, zipHash `7d7a869996e10249d1f85f95e060319b`; GitHub releases API returned no releases on 2026-06-29 | IPA loaded SoundReplacer 2.0.1, installed its app and menu installers, and the game reached `MainSystemInit`. Warning: manifest targets Beat Saber 1.29.4. |
|
||||
| KeyRemapper | [github](https://github.com/lyyQwQ/KeyRemapper) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `lyyQwQ/KeyRemapper` tag `0.3.0`, asset `KeyRemapper-0.3.0-bs1.39.1-8e4c11a.zip`; GitHub release digest matched downloaded asset | IPA loaded KeyRemapper 0.3.0, initialized config, installed menu bindings, registered the Key Remapper button, and the game reached `MainSystemInit`. Warnings: manifest targets Beat Saber 1.39.1 and FPFC smoke logged dummy input manager because runtime was null. |
|
||||
| SquatToBegin | [github](https://github.com/kinsi55/BeatSaber_SquatToBegin) | <span style="color:#d29922; font-weight:600">verified with warning</span> | GitHub `kinsi55/BeatSaber_SquatToBegin` tag `v0.0.7`, asset `SquatToBegin.dll`; GitHub release digest matched downloaded asset | IPA loaded SquatToBegin 0.0.7 and the game reached `MainSystemInit`. Warnings: manifest reports Beat Saber 1.20.0 despite the GitHub release being labeled for 1.39.1+, and plugin has no start/exit methods. Verify squat gate behavior outside FPFC when practical. |
|
||||
| KeyRemapper | [github](https://github.com/lyyQwQ/KeyRemapper), [pr](https://github.com/lyyQwQ/KeyRemapper/pull/6) | <span style="color:#f85149; font-weight:600">currently failed</span> | Local build from `/home/pleb/src/lyyQwQ/KeyRemapper` commit `23bb523`, asset `KeyRemapper-0.3.0-bs1.44.0-8e4c11a.zip`, SHA-256 `b8aaaa72712ae10cec98f51f79b3b87fe7a588b0d3393dd087b7d84975246e6c`; PR 6 retargets the pause/menu Affinity patch from removed `UnityXRHelper` to `DevicelessVRHelper` and sets manifest gameVersion to 1.44.0. PR 6 was converted to draft after the failed manual smoke. | Replaces the prior GitHub `0.3.0` Beat Saber 1.39.1 asset. Helper installed `Plugins/KeyRemapper.dll` SHA-256 `07839dd28fa4bfbd88a3b72a8f77ebd22e70ac5acdc1239c47b9deabf775d8cd` and `Plugins/KeyRemapper.pdb` SHA-256 `e118c71519b632027f958d902d5ebcd9efb4851530091639cdbfaa96d43d4cb8`. User manual smoke on 2026-07-01 found the configured `UserData/KeyRemapper.json` pause bindings (`L_X`, `L_Y`, `R_A`, `R_B`) were not remapped. Windows logs show KeyRemapper loaded and read config without a KeyRemapper exception; `2026.07.01.09.53.35.log.gz` found both controllers and installed `KeyRemapper.Installers.GameplayInstaller`, but gameplay then flooded `MissingMethodException: AudioTimeSyncController/InitData .AudioTimeSyncController.get_state()`. TODO: add explicit logging around `GameplayInstaller`'s `IVRPlatformHelper.vrPlatformSDK == VRPlatformSDK.OpenXR` gate and verify whether that gate prevents `RemapBaseGameMenuButton`/`RestartHandler` from binding despite `UnityXRInputManager` being selected from `XRSettings.loadedDeviceName`. |
|
||||
| SquatToBegin | [github](https://github.com/kinsi55/BeatSaber_SquatToBegin) | <span style="color:#d29922; font-weight:600">installed; manual resmoke pending</span> | Local build from `/home/pleb/src/kinsi55/BeatSaber_SquatToBegin` at commit `8703b84` plus local 1.44 compatibility fixes; asset `SquatToBegin.dll`, SHA-256 `0bd6b5ee48cae370f255707afb9e452e08a68802f81493dd9a442b6a9f7b98bd`. BeatMods verified search on 2026-07-01 found no SquatToBegin entry for Beat Saber 1.44.1, 1.44.0, 1.43.0, 1.42.1, 1.42.0, 1.40.0, 1.39.1, or 1.20.0. | Replaces the prior GitHub `v0.0.7` DLL and the first local build. IPA loaded SquatToBegin 0.0.7, but the automated smoke did not reach the menu because Steam platform initialization failed. User manual smoke on 2026-07-01 found the squat gate did not arm and songs started immediately. Follow-up build keeps the 1.44 `IAudioTimeSource.State.Playing` fix and arms the gate from gameplay scene setup instead of relying on the stale level-selection prefix; verify VR behavior manually. |
|
||||
| JDFixer | [github](https://github.com/zeph-yr/JDFixer), [pr](https://github.com/zeph-yr/JDFixer/pull/26) | <span style="color:#d29922; font-weight:600">installed; smoke blocked</span> | Local build from PR 26 commit `3fce6ce465911bdd5e8e00411bc4672c54a317f7`, asset `JDFixer.dll`; SHA-256 `16b7dad9906d838dab40ce48a9b304be4847f18e700ddd31f2293d1065f4529d` | IPA loaded JDFixer 7.4.0 and the prior `OnEnable` Harmony failure did not recur; JDFixer logged config/donate activity. The smoke did not reach the main menu because Steam platform initialization failed (`SteamAPI Init failed`, Steam likely not running). Replaces failed GitHub `zeph-yr/JDFixer` tag `v.7.4.0`, asset `JDFixer.dll`, SHA-256 `a83ae3f68921a9698616ecd89d08b7397a550c2464a7871b6c65506ce0c7d360`; that release loaded JDFixer 7.4.0, but `OnEnable` failed with a Harmony patching exception because `StandardLevelScenesTransitionSetupDataSOPatch::TargetMethod()` returned null. |
|
||||
|
||||
### Batch 6: UI and Song Browser
|
||||
@@ -200,7 +200,7 @@ Purpose: restore song-list, menu, and visualization conveniences.
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 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. |
|
||||
| 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:#f85149; font-weight:600">failed compatibility trial</span> | GitHub `Auros/DiTails` tag `1.1.3`, asset `DiTails-v1.1.3-g1.42.0-271d394.zip`; GitHub release digest matched downloaded asset; BeatMods also verifies DiTails 1.1.3 for 1.44.1 as version id 2609, zipHash `437904f6db78a2ee928738d7d254a93f` | IPA loaded DiTails 1.1.3, but menu initialization failed in `DiTails.Managers.DetailContextManager.Initialize()` with `NullReferenceException`. Removed from the live instance after the failed smoke. |
|
||||
| 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. |
|
||||
|
||||
@@ -39,7 +39,7 @@ bs-manager installs these mostly as dependency closure. BeatMods records depende
|
||||
- Required by: most UI/config mods here, including SongCore, SiraUtil, PlaylistManager, BeatSaverDownloader, Chroma, Vivify, ScoreSaber, and BeatLeader.
|
||||
- BeatSaberPlaylistsLib https://github.com/Meivyn/BeatSaberPlaylistsLib
|
||||
- Required by: PlaylistManager.
|
||||
- BeatSaverSharp https://github.com/lolPants/BeatSaverSharp
|
||||
- BeatSaverSharp https://github.com/Auros/BeatSaverSharper
|
||||
- Required by: BeatSaverDownloader, BeatSaverUpdater, DiTails, PlaylistManager.
|
||||
- BS Utils https://github.com/Kylemc1413/Beat-Saber-Utils
|
||||
- Required by: BeatSaverDownloader, BeatSaverVoting, BeatLeader.
|
||||
|
||||
+18
-18
@@ -163,12 +163,12 @@ reason = "BeatMods verified BeatSaberPlaylistsLib 1.7.2 for Beat Saber 1.44.1 as
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsaversharp"
|
||||
repo = "lolPants/BeatSaverSharp"
|
||||
repo = "Auros/BeatSaverSharper"
|
||||
tag = "beatmods-3.4.5"
|
||||
asset = "BeatSaverSharp-3.4.5.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. BeatMods upstream URL returned inaccessible via the GitHub releases API, so this remains a BeatMods CDN fallback."
|
||||
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."
|
||||
|
||||
[[plugins]]
|
||||
id = "scoresabersharp"
|
||||
@@ -232,15 +232,6 @@ 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."
|
||||
|
||||
[[plugins]]
|
||||
id = "playlistmanager"
|
||||
repo = "rithik-b/PlaylistManager"
|
||||
tag = "pr-82-localbuild-da1ad17"
|
||||
asset = "PlaylistManager-1.7.4-bs1.44.0-da1ad17.zip"
|
||||
sha256 = "57b449e614db1d5214cd3a88000d52f5a989c8521390a9f4819b1c62f20f16fa"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "Local build from .state/build/playlistmanager-pr82-skilltest, artifact PlaylistManager/bin/Release/net48/zip/PlaylistManager-1.7.4-bs1.44.0-da1ad17.zip. Use this PR82 build instead of the failed BeatMods 1.7.3 compatibility trial for Beat Saber 1.44.1."
|
||||
|
||||
[[plugins]]
|
||||
id = "beatsaverupdater"
|
||||
repo = "ibillingsley/BeatSaverUpdater"
|
||||
@@ -289,20 +280,20 @@ reason = "User-provided GitHub repository URL https://github.com/qqrz997/KeepMyO
|
||||
[[plugins]]
|
||||
id = "keyremapper"
|
||||
repo = "lyyQwQ/KeyRemapper"
|
||||
tag = "0.3.0"
|
||||
asset = "KeyRemapper-0.3.0-bs1.39.1-8e4c11a.zip"
|
||||
sha256 = "962e3f6e18bebdf101e6575fa7b8b7e0d92179bab4f0825d122078f1842f7380"
|
||||
tag = "pr-6-23bb523-1.44-fix"
|
||||
asset = "KeyRemapper-0.3.0-bs1.44.0-8e4c11a.zip"
|
||||
sha256 = "b8aaaa72712ae10cec98f51f79b3b87fe7a588b0d3393dd087b7d84975246e6c"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "User-provided GitHub releases URL https://github.com/lyyQwQ/KeyRemapper/releases. Latest non-draft, non-prerelease release 0.3.0 exposes this asset; GitHub release digest matched the downloaded asset. Release notes label compatibility as Beat Saber 1.39.1 with BSIPA/BSML/SiraUtil dependencies."
|
||||
reason = "Local build from /home/pleb/src/lyyQwQ/KeyRemapper commit 23bb523, submitted upstream as PR https://github.com/lyyQwQ/KeyRemapper/pull/6. This updates the menu remap Affinity patch target from removed UnityXRHelper to DevicelessVRHelper and sets manifest gameVersion to 1.44.0 for Beat Saber 1.44 compatibility."
|
||||
|
||||
[[plugins]]
|
||||
id = "squattobegin"
|
||||
repo = "kinsi55/BeatSaber_SquatToBegin"
|
||||
tag = "v0.0.7"
|
||||
tag = "localbuild-8703b84-1.44-fix"
|
||||
asset = "SquatToBegin.dll"
|
||||
sha256 = "8426a64f6a3224b8cd79b9ee86347727a5a43c40a0fda116bf9c613f145fc18e"
|
||||
sha256 = "0bd6b5ee48cae370f255707afb9e452e08a68802f81493dd9a442b6a9f7b98bd"
|
||||
install_strategy = "dll-to-plugins"
|
||||
reason = "User-provided GitHub releases URL https://github.com/kinsi55/BeatSaber_SquatToBegin/releases. Latest non-draft, non-prerelease release v0.0.7 is labeled for Beat Saber 1.39.1+ and exposes this direct DLL asset; GitHub release digest matched the downloaded asset."
|
||||
reason = "Local build from /home/pleb/src/kinsi55/BeatSaber_SquatToBegin at commit 8703b84 plus local 1.44 compatibility fixes: change AudioTimeSyncController.State.Playing to IAudioTimeSource.State.Playing and arm the squat gate from gameplay scene setup instead of relying on the stale level-selection prefix. BeatMods verified search on 2026-07-01 found no SquatToBegin entry for Beat Saber 1.44.1, 1.44.0, 1.43.0, 1.42.1, 1.42.0, 1.40.0, 1.39.1, or 1.20.0."
|
||||
|
||||
[[plugins]]
|
||||
id = "introskip"
|
||||
@@ -340,6 +331,15 @@ 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."
|
||||
|
||||
[[plugins]]
|
||||
id = "ditails"
|
||||
repo = "Auros/DiTails"
|
||||
tag = "localbuild-601e3c4-1.44-fix"
|
||||
asset = "DiTails-1.1.3-bs1.44.1-601e3c4.zip"
|
||||
sha256 = "b8735f24545f1a50392865bf3013b930bee8e4ba7feac824a2482e2f1deeba1e"
|
||||
install_strategy = "bsipa-zip"
|
||||
reason = "Local build from /home/pleb/src/Auros/DiTails commit 601e3c4 on branch fix-1.44-artwork-initialization. Use this build instead of the failed upstream 1.1.3 / BeatMods DiTails trial, which loaded but failed menu initialization with a NullReferenceException on Beat Saber 1.44.1."
|
||||
|
||||
[[plugins]]
|
||||
id = "hidethelogo"
|
||||
repo = "TheBlackParrot/HideTheLogo"
|
||||
|
||||
@@ -407,7 +407,7 @@ required = true
|
||||
[[plugins]]
|
||||
id = "beatsaversharp"
|
||||
name = "BeatSaverSharp"
|
||||
repo = "lolPants/BeatSaverSharp"
|
||||
repo = "Auros/BeatSaverSharper"
|
||||
asset_patterns = ["BeatSaverSharp-*.zip"]
|
||||
install_strategy = "bsipa-zip"
|
||||
category = "library"
|
||||
|
||||
@@ -11,7 +11,7 @@ from .bootstrap import run_bootstrap
|
||||
from .bsipa import check_bsipa_health
|
||||
from .checker import check_lock
|
||||
from .github import fetch_releases
|
||||
from .installer import apply_plan, uninstall_plugin
|
||||
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
|
||||
@@ -34,7 +34,15 @@ def installed_plugins_report(
|
||||
) -> dict[str, Any]:
|
||||
locked_by_id = {plugin.id: plugin for plugin in lockfile.plugins}
|
||||
plugins: list[dict[str, Any]] = []
|
||||
for plugin_id, plugin_state in sorted(installed_state.get("plugins", {}).items()):
|
||||
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", [])
|
||||
@@ -48,6 +56,8 @@ def installed_plugins_report(
|
||||
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,
|
||||
}
|
||||
@@ -66,14 +76,15 @@ def print_installed_plugins(report: dict[str, Any]) -> None:
|
||||
print("No plugins have been installed by plugin-helper yet.")
|
||||
return
|
||||
|
||||
headers = ("Plugin", "Version", "Asset", "Files", "Installed")
|
||||
headers = ("Plugin", "Status", "Version", "Asset", "Files", "Installed")
|
||||
rows = [
|
||||
(
|
||||
f"{plugin['name']} ({plugin['id']})",
|
||||
plugin["status"],
|
||||
plugin["version"],
|
||||
plugin["asset"],
|
||||
str(plugin["fileCount"]),
|
||||
plugin["installedAt"],
|
||||
plugin["disabledAt"] or plugin["installedAt"],
|
||||
)
|
||||
for plugin in plugins
|
||||
]
|
||||
@@ -236,6 +247,25 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
uninstall.add_argument("plugin")
|
||||
uninstall.add_argument("--force", action="store_true", help="Delete even when current file hashes differ")
|
||||
|
||||
disable = subcommands.add_parser(
|
||||
"disable",
|
||||
help="Remove a managed plugin's files from the game but keep state/assets for re-enable",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
disable.add_argument("--instance", required=True)
|
||||
disable.add_argument("plugin")
|
||||
disable.add_argument("--force", action="store_true", help="Disable even when current file hashes differ")
|
||||
|
||||
enable = subcommands.add_parser(
|
||||
"enable",
|
||||
help="Reinstall a disabled 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("--lockfile")
|
||||
enable.add_argument("plugin")
|
||||
|
||||
backup = subcommands.add_parser(
|
||||
"backup-userdata",
|
||||
help="Copy UserData and Windows AppData into the adjacent backups repo",
|
||||
@@ -310,6 +340,8 @@ def _run_menu(inst_roots: list[Path], st_root: Path, input_func: Callable[[str],
|
||||
("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."),
|
||||
@@ -356,6 +388,12 @@ def _run_menu(inst_roots: list[Path], st_root: Path, input_func: Callable[[str],
|
||||
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])
|
||||
|
||||
@@ -566,6 +604,47 @@ def run(argv: list[str] | None = None) -> int:
|
||||
print(f" {item['path']}: {item['reason']}")
|
||||
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)
|
||||
print(f"Disabled: {args.plugin}")
|
||||
print(f"Removed: {len(result['removed'])}")
|
||||
if result["skipped"]:
|
||||
print("Skipped:")
|
||||
for item in result["skipped"]:
|
||||
print(f" {item['path']}: {item['reason']}")
|
||||
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=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},
|
||||
)
|
||||
result = apply_plan(plan, st_root)
|
||||
print(f"Enabled: {args.plugin}")
|
||||
print(f"Plan: {path}")
|
||||
print(f"Applied: {len(result['applied'])}")
|
||||
print(f"State: {result['statePath']}")
|
||||
return 0
|
||||
|
||||
if args.command == "backup-userdata":
|
||||
instance = get_instance(inst_roots, args.instance)
|
||||
root = repo_root()
|
||||
|
||||
@@ -77,6 +77,7 @@ def apply_plan(plan: dict[str, Any], state_root: Path) -> dict[str, Any]:
|
||||
"size": target.stat().st_size,
|
||||
}
|
||||
)
|
||||
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)
|
||||
@@ -110,3 +111,37 @@ def uninstall_plugin(instance: str, instance_path: Path, state_root: Path, plugi
|
||||
installed_state.get("plugins", {}).pop(plugin_id, None)
|
||||
save_installed_state(state_root, instance, installed_state)
|
||||
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)
|
||||
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}")
|
||||
|
||||
removed: list[str] = []
|
||||
skipped: list[dict[str, str]] = []
|
||||
for item in plugin_state.get("files", []):
|
||||
rel_path = ensure_relative(item["path"]).as_posix()
|
||||
target = ensure_inside(instance_path, instance_path / rel_path)
|
||||
if not target.exists():
|
||||
removed.append(rel_path)
|
||||
continue
|
||||
current_sha = sha256_file(target)
|
||||
if current_sha != item.get("sha256") and not force:
|
||||
skipped.append({"path": rel_path, "reason": "hash mismatch"})
|
||||
continue
|
||||
target.unlink()
|
||||
removed.append(rel_path)
|
||||
|
||||
if skipped and not force:
|
||||
return {"removed": removed, "skipped": skipped, "stateUpdated": False}
|
||||
|
||||
disabled_state = dict(plugin_state)
|
||||
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)
|
||||
return {"removed": removed, "skipped": skipped, "stateUpdated": True}
|
||||
|
||||
+122
-1
@@ -14,7 +14,7 @@ from plugin_helper.beatmods import by_version_id, normalize_mods
|
||||
from plugin_helper.checker import check_lock
|
||||
from plugin_helper.cli import installed_plugins_report, run
|
||||
from plugin_helper.fsutil import sha256_file
|
||||
from plugin_helper.installer import apply_plan, uninstall_plugin
|
||||
from plugin_helper.installer import apply_plan, disable_plugin, uninstall_plugin
|
||||
from plugin_helper.instances import get_instance, list_instances
|
||||
from plugin_helper.models import Lockfile, LockedPlugin, Registry, RegistryPlugin
|
||||
from plugin_helper.planner import create_plan
|
||||
@@ -242,6 +242,127 @@ class PluginHelperTests(unittest.TestCase):
|
||||
self.assertEqual(removed["removed"], ["Plugins/Example.dll"])
|
||||
self.assertFalse((instance / "Plugins" / "Example.dll").exists())
|
||||
|
||||
def test_disable_plugin_removes_files_but_keeps_disabled_state(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
instance = work / "instances" / "1.40.8"
|
||||
state = work / "state"
|
||||
instance.mkdir(parents=True)
|
||||
(instance / "Beat Saber_Data").mkdir()
|
||||
(instance / "Plugins").mkdir()
|
||||
|
||||
target = instance / "Plugins" / "Example.dll"
|
||||
target.write_bytes(b"managed dll")
|
||||
installed = {
|
||||
"instance": "1.40.8",
|
||||
"plugins": {
|
||||
"example": {
|
||||
"installedAt": "2026-06-14T17:18:40Z",
|
||||
"files": [
|
||||
{
|
||||
"path": "Plugins/Example.dll",
|
||||
"sha256": sha256_file(target),
|
||||
"size": target.stat().st_size,
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
from plugin_helper.state import save_installed_state
|
||||
|
||||
save_installed_state(state, "1.40.8", installed)
|
||||
|
||||
result = disable_plugin("1.40.8", instance, state, "example")
|
||||
|
||||
self.assertEqual(result["removed"], ["Plugins/Example.dll"])
|
||||
self.assertFalse(target.exists())
|
||||
updated = load_installed_state(state, "1.40.8")
|
||||
self.assertNotIn("example", updated["plugins"])
|
||||
self.assertIn("example", updated["disabledPlugins"])
|
||||
self.assertEqual(updated["disabledPlugins"]["example"]["files"][0]["path"], "Plugins/Example.dll")
|
||||
|
||||
def test_enable_command_reinstalls_disabled_plugin_from_asset(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
instance_root = work / "instances"
|
||||
instance = instance_root / "1.40.8"
|
||||
state = work / "state"
|
||||
registry_dir = work / "registry"
|
||||
locks_dir = work / "locks"
|
||||
registry_dir.mkdir()
|
||||
locks_dir.mkdir()
|
||||
instance.mkdir(parents=True)
|
||||
(instance / "Beat Saber_Data").mkdir()
|
||||
(instance / "Plugins").mkdir()
|
||||
|
||||
asset = plugin_downloads_dir(state, "1.40.8", "example") / "Example.dll"
|
||||
asset.write_bytes(b"managed dll")
|
||||
(registry_dir / "plugins.toml").write_text(
|
||||
"""
|
||||
[[plugins]]
|
||||
id = "example"
|
||||
name = "Example"
|
||||
repo = "owner/example"
|
||||
asset_patterns = ["*.dll"]
|
||||
install_strategy = "dll-to-plugins"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(locks_dir / "1.40.8.lock.toml").write_text(
|
||||
f"""
|
||||
beat_saber_version = "1.40.8"
|
||||
instance = "1.40.8"
|
||||
|
||||
[[plugins]]
|
||||
id = "example"
|
||||
repo = "owner/example"
|
||||
tag = "v1.0.0"
|
||||
asset = "Example.dll"
|
||||
sha256 = "{sha256_file(asset)}"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
disabled = {
|
||||
"instance": "1.40.8",
|
||||
"plugins": {},
|
||||
"disabledPlugins": {
|
||||
"example": {
|
||||
"installedAt": "2026-06-14T17:18:40Z",
|
||||
"disabledAt": "2026-06-14T17:20:00Z",
|
||||
"files": [
|
||||
{
|
||||
"path": "Plugins/Example.dll",
|
||||
"sha256": sha256_file(asset),
|
||||
"size": asset.stat().st_size,
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
from plugin_helper.state import save_installed_state
|
||||
|
||||
save_installed_state(state, "1.40.8", disabled)
|
||||
|
||||
with patch("plugin_helper.cli.repo_root", return_value=work):
|
||||
status = run(
|
||||
[
|
||||
"--instances-root",
|
||||
str(instance_root),
|
||||
"--state-dir",
|
||||
str(state),
|
||||
"enable",
|
||||
"--instance",
|
||||
"1.40.8",
|
||||
"example",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual((instance / "Plugins" / "Example.dll").read_bytes(), b"managed dll")
|
||||
updated = load_installed_state(state, "1.40.8")
|
||||
self.assertIn("example", updated["plugins"])
|
||||
self.assertNotIn("example", updated["disabledPlugins"])
|
||||
|
||||
def test_zip_to_pending_targets_ipa_pending(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
|
||||
Reference in New Issue
Block a user