Compare commits
272 Commits
v1.5.0-alpha.7
...
v1.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 80167a7f3f | |||
| 91cb80bbe2 | |||
| df1c7ca9d2 | |||
| 0f96db8d29 | |||
| 8042f1b9f6 | |||
| 757eb1f655 | |||
| 49fb694aaf | |||
| 508e290a24 | |||
| 413996f9f3 | |||
| cda4e000ee | |||
| 2e9f62d415 | |||
| 4ef821adc9 | |||
| 21f4eacefb | |||
| 72738a256b | |||
| a54c146772 | |||
| 43a4cdfe45 | |||
| 39d481467a | |||
| 3afcf1c12e | |||
| da140848ba | |||
| 288853a6cc | |||
| 365e251408 | |||
| eaa5eccb98 | |||
| eabeae00d6 | |||
| d209d6c336 | |||
| eff5d5728f | |||
| 2ee7a904d6 | |||
| f477870f74 | |||
| 2ccb3d469f | |||
| 37ad1d2d19 | |||
| 88f3467882 | |||
| c63b39ff01 | |||
| 4bb01146e6 | |||
| 35a476f6ab | |||
| 1744ae1dcc | |||
| 7a38bb22f1 | |||
| b325bf2a39 | |||
| 43ddfc652d | |||
| 52d1049c98 | |||
| 4c4b783aa7 | |||
| 2c1c94640f | |||
| 8476f869b4 | |||
| ad87874e09 | |||
| fd299e1267 | |||
| 374bfe6834 | |||
| 47158a264a | |||
| 812600a3ec | |||
| 8efba693b3 | |||
| 39afc09df9 | |||
| b94fe0a053 | |||
| 1c2321b438 | |||
| e6d96088fb | |||
| 5466ad61c3 | |||
| 0a2e2a2f47 | |||
| 950ca71a0a | |||
| 889e792b3c | |||
| c6ff848bfa | |||
| 5d4b79c041 | |||
| 28be0da96e | |||
| a74c6bba10 | |||
| ebd4d4115a | |||
| b91f0b06ab | |||
| 41af98dbfb | |||
| cf0e092705 | |||
| 93bcd358cb | |||
| 3cb744bf6c | |||
| 53c9f92344 | |||
| 5f6cf77f53 | |||
| 7caca61c1e | |||
| 3f5b5ca4a0 | |||
| ec0ae76d70 | |||
| 13984f886f | |||
| 29e5b1946d | |||
| 09ffdcae32 | |||
| 7fc6ffdda9 | |||
| 0085d1e52e | |||
| ccd36913de | |||
| 4dcc4e92a2 | |||
| a4dab93c36 | |||
| 39adbc42f0 | |||
| 71da1d9484 | |||
| 8020084e77 | |||
| 0b4331bef2 | |||
| b284b33669 | |||
| effe0608e1 | |||
| 773b54ca08 | |||
| 0c3a5b9e75 | |||
| dc3ae1971d | |||
| 350dff37cf | |||
| a47616086c | |||
| 96fb2ed8c7 | |||
| bacead72ae | |||
| 56450f73c8 | |||
| 68e260f831 | |||
| 1559224ff9 | |||
| 4b8004ea80 | |||
| e3fef23ef0 | |||
| 5f43a8fd4f | |||
| e4766f8a25 | |||
| 17deb2658e | |||
| b4d934f22a | |||
| 11e61b72b3 | |||
| 689a57e014 | |||
| a891ecb0db | |||
| 17bd2e8463 | |||
| 467ca6e300 | |||
| 66595dac06 | |||
| 1301bfe358 | |||
| 1732191be3 | |||
| fb7f68cd04 | |||
| e89333ea09 | |||
| f2ad6c1531 | |||
| 65d26dc819 | |||
| 664e28ad05 | |||
| 1d70c867f3 | |||
| 99ce770592 | |||
| afb4075408 | |||
| 9e20239328 | |||
| 989b045be7 | |||
| 5fb44d1adc | |||
| 2a99c76d09 | |||
| cc7237fef2 | |||
| e3d27e4b4f | |||
| 3ff2b729c9 | |||
| 35238685ce | |||
| 48e77920af | |||
| bbe708b767 | |||
| b6513fb096 | |||
| a4d9069a3d | |||
| be49f1e02b | |||
| 0417f3d4fa | |||
| 3feca3769a | |||
| c7d6bc54e4 | |||
| 80c555711f | |||
| a148ddaee2 | |||
| 023f8d31b0 | |||
| 14a7f5f00b | |||
| ec8b41187e | |||
| 774c3a2954 | |||
| 4381bb4696 | |||
| 6d1418dd7c | |||
| 22af4610eb | |||
| b4874eced4 | |||
| 61b728ed55 | |||
| 1310d16597 | |||
| 1985f5e6be | |||
| 30c6058edb | |||
| c69b110595 | |||
| 1f02551408 | |||
| cab1fb93de | |||
| 74ed168f98 | |||
| ea04073772 | |||
| 8ac1683fab | |||
| 38ef6191e4 | |||
| ed4b39598f | |||
| 4f58df6280 | |||
| ba26f3a01e | |||
| 661a90883c | |||
| 87a7e08e5f | |||
| 6c4acec8a2 | |||
| 6345e9148b | |||
| c76e585d71 | |||
| b20f6328e9 | |||
| bc220e6692 | |||
| 88f8dc7d5c | |||
| f0b7294ac5 | |||
| 7815e35506 | |||
| 32d604345f | |||
| 1f07c35cc6 | |||
| c8ec5db4d7 | |||
| 4d90102127 | |||
| b0c2a7c872 | |||
| 149e6bbc9d | |||
| b12e90a32a | |||
| 5a4df356bf | |||
| 97fbeb01a4 | |||
| 6beb675139 | |||
| 79693e2070 | |||
| 6708071b7a | |||
| 7ea75bd50a | |||
| 62abe32696 | |||
| b8fb1781aa | |||
| 379e7cfaf5 | |||
| 9cfcf28373 | |||
| 67f102738a | |||
| 025d6f8ccd | |||
| 69496221b2 | |||
| 7b1ae3772a | |||
| f92b621174 | |||
| 3f2147b796 | |||
| b61d540d0e | |||
| 9984780a99 | |||
| d7e1e0daad | |||
| 03cacc7fc8 | |||
| 4dff8f5765 | |||
| 3463a63e1d | |||
| 5926a72fab | |||
| 5824166a22 | |||
| 29eba3d572 | |||
| 2f87222084 | |||
| 52f3bbb3d7 | |||
| 513dba8393 | |||
| 250b41cd10 | |||
| 5654f650d5 | |||
| d615efb02c | |||
| faf8431e49 | |||
| 44b6979fee | |||
| c48c08dbf5 | |||
| 547054a76c | |||
| ad03a50cc8 | |||
| 941b10aa46 | |||
| 23ec61e2e6 | |||
| 99d3f6d237 | |||
| 989c3172e4 | |||
| 575d9c290e | |||
| cb4666ee8a | |||
| 4d42dfb2cb | |||
| 2c166e9fc5 | |||
| 5a87d91192 | |||
| 01c7e540d0 | |||
| 80863df85e | |||
| a2e2136196 | |||
| 98fba3289b | |||
| 2561a34200 | |||
| b128de6acc | |||
| fadf08771f | |||
| 488da1cdaf | |||
| 5771aed67a | |||
| 4dece596ef | |||
| 4636f46bab | |||
| c7722df0e4 | |||
| d44f9f9067 | |||
| d9318b6579 | |||
| 08be61928e | |||
| a2df9d3adf | |||
| 1e12f7eae2 | |||
| 0c87bfd11c | |||
| 71ab4b5fa8 | |||
| 93906f8ff2 | |||
| d0301088f1 | |||
| 362912b83b | |||
| 7d80cc5879 | |||
| f0f0512033 | |||
| 29689ce632 | |||
| e8118e8ae4 | |||
| 6d83096c2c | |||
| 0033b89942 | |||
| 6e4a796343 | |||
| 7f4fe63d58 | |||
| 6c868bf60d | |||
| 4d16b217ca | |||
| 556477a996 | |||
| 3a4672f25e | |||
| e6071c7395 | |||
| 95a95715b0 | |||
| d322178127 | |||
| 418fc7e864 | |||
| e4fdf8ffe9 | |||
| c91c1ca3ce | |||
| 276c5bfcab | |||
| 2232222d3b | |||
| f5d48ea946 | |||
| 7e85387ff4 | |||
| b0a4a27f78 | |||
| b29759365b | |||
| 3dd254a070 | |||
| 3d721d95a9 | |||
| b555318f2d | |||
| 7662f938da | |||
| 7f0f273652 | |||
| 9ed769dca0 | |||
| a4fef9f66c | |||
| 4b4bdbbff8 |
@@ -61,6 +61,7 @@ module.exports = {
|
||||
"react/button-has-type": "off",
|
||||
"max-classes-per-file": "off",
|
||||
"jest/no-standalone-expect": "off",
|
||||
"no-bitwise": "off",
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
|
||||
@@ -24,14 +24,15 @@ jobs:
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
cache: "npm"
|
||||
|
||||
# Install and setup rpm/Flatpak for Ubuntu
|
||||
- name: Install rpm/flatpak packages (Ubuntu only)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: sudo apt-get install -y flatpak flatpak-builder rpm libarchive-tools
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run package
|
||||
|
||||
# Install and setup Flatpak for Ubuntu
|
||||
- name: Install flatpak packages (Ubuntu only)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: sudo apt-get install -y flatpak flatpak-builder
|
||||
|
||||
- name: Setup flatpak repo (Ubuntu only)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
|
||||
@@ -18,3 +18,4 @@ jobs:
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
with:
|
||||
path: docs/wiki/
|
||||
strategy: init
|
||||
|
||||
@@ -20,8 +20,8 @@ jobs:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install flatpak packages
|
||||
run: sudo apt-get install -y flatpak flatpak-builder
|
||||
- name: Install rpm/flatpak packages
|
||||
run: sudo apt-get install -y flatpak flatpak-builder rpm libarchive-tools
|
||||
|
||||
- name: Setup flatpak repo
|
||||
run: |
|
||||
@@ -39,6 +39,9 @@ jobs:
|
||||
- name: Build deb
|
||||
run: npx electron-builder --config electron-builder.config.js --publish always --linux deb --x64
|
||||
|
||||
- name: Build rpm
|
||||
run: npx electron-builder --config electron-builder.config.js --publish always --linux rpm --x64
|
||||
|
||||
- name: Build flatpak
|
||||
run: env DEBUG="@malept/flatpak-bundler" npx electron-builder --config electron-builder.config.js --publish always --linux flatpak
|
||||
|
||||
|
||||
@@ -29,4 +29,4 @@ npm-debug.log.*
|
||||
*.scss.d.ts
|
||||
|
||||
# externals
|
||||
externals/**/target/*
|
||||
externals/**/target/**
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"MD007": { "indent": 4 },
|
||||
"MD013": false,
|
||||
"MD041": false,
|
||||
"MD024": false,
|
||||
"MD033": false,
|
||||
"MD023": false
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"]
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"davidanson.vscode-markdownlint"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,148 @@
|
||||
## Not released yet ~
|
||||
[PR Merged since the last release](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-01-18)\
|
||||
[PR Merged since the last alpha](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-01-11+)
|
||||
|
||||
## [1.4.18](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.18) (Jan 18, 2025)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where enabling Oculus sideloading was not working if BSManager installation path contained spaces #735
|
||||
- The IPA folder is now cleared each time BSIPA is installed to prevent potential crashes caused by conflicts with old BSIPA files #746
|
||||
- Added a check to ensure the file system supports symlinks before moving contents to the shared folder #739
|
||||
|
||||
### Other changes
|
||||
- Events occurring in `bs-admin-start.exe` and `oculus-allow-dev-sideloaded.exe` are now logged #735
|
||||
|
||||
## [1.4.17](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.17) (Jan 2, 2025)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where original Steam installations of Beat Saber did not have any mods available
|
||||
- Mod descriptions should no longer contain HTML tags
|
||||
|
||||
## [1.4.16](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.16) (Jan 2, 2025)
|
||||
|
||||
### Fixes
|
||||
- Fixed infinite mod loading when a mod file could not be found on BeatMods #725 (Thanks to @silentrald)
|
||||
- Fixed an issue where no mods were shown as installed when a mod dependency could not be found #726 (Thanks to @silentrald)
|
||||
- Fixed minor UI issues
|
||||
|
||||
## [1.4.15](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.15) (Jan 2, 2025)
|
||||
|
||||
### Fixes
|
||||
- Use the sideloading feature of Oculus to prevent errors when launching Oculus versions of Beat Saber #720
|
||||
- Oculus token starting with `OC` are now accepted
|
||||
|
||||
### Other changes
|
||||
- Mods now load faster #508
|
||||
- Switched to the new BeatMods API for loading mods
|
||||
|
||||
|
||||
## [1.4.14](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.14) (Dec 2, 2024)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where downloading private playlists through OneClick was always resulting in an error #679 (Thanks to @Top-Cat)
|
||||
- Beat Saber file verification was broken due to an error in DepotDownloader #677
|
||||
|
||||
|
||||
## [1.4.13](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.13) (Nov 26, 2024)
|
||||
|
||||
### Features
|
||||
- Added the possibility to unselect all mods from the mods panel #666
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where downloading maps with subfolders was not possible #649 (Thanks to @silentrald)
|
||||
- Corrected the mods search bar color in the light theme #660 (Thanks to @chk1)
|
||||
|
||||
### Other changes
|
||||
- Added the BeatLeader icon to the OneClick playlist settings #653
|
||||
- A warning now appears before downloading an outdated BeatSaber version #654
|
||||
- Oculus BeatSaber versions no longer launch if the Oculus library is not found #657 (Thanks to @LiamillionSS)
|
||||
- Maps from BeatSaver are now sorted by relevance by default #662 (Thanks to @Top-Cat)
|
||||
- Removed the `--no-yeet` argument when launching original copies of BeatSaber #667
|
||||
- Added warnings for outdated versions in the versions view #673
|
||||
|
||||
|
||||
## [1.4.12](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.12) (Nov 6, 2024)
|
||||
|
||||
### Fixes
|
||||
- Fixed OneClick playlist download that has been broken in the previous release #645
|
||||
- Fixed an issue where downloading playlists could fail if the playlist's file contained special characters #645
|
||||
|
||||
## [1.4.11](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.11) (Nov 4, 2024)
|
||||
|
||||
### Fixes
|
||||
- BSManager was not working if the path to BSM's installation folder contained URL special characters #636
|
||||
- Linking folders was no longer working if the folders contained files
|
||||
- Under certain conditions, loading maps could result in a black screen
|
||||
|
||||
### Other changes
|
||||
- The "Broken Models" notification has been removed #640
|
||||
|
||||
|
||||
## [1.4.10](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.10) (Oct 27, 2024)
|
||||
|
||||
### Fixes
|
||||
- Mods using a manifest file were sometimes not detected as installed #564
|
||||
- BSIPA installation was always considered as successfull even if an error occured #511
|
||||
- Some map filter tags were no longer working #619 (Thanks to @silentrald)
|
||||
- Fixed an issue where, under certain conditions, linking a folder could result in the loss of its contents #568
|
||||
|
||||
### Other changes
|
||||
- Default mods are no longer reselected when mods are already installed #535
|
||||
- The `DLC` folder has been removed from the default list of folders that can be linked #628
|
||||
- The naming scheme for downloaded maps and playlists has been updated to prevent duplicates when downloading maps or playlists from Beat Saber or other tools
|
||||
|
||||
|
||||
## [1.4.9](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.9) (Oct 15, 2024)
|
||||
|
||||
### Fixes
|
||||
- Updated DepotDownloader dependency to fix connection errors with Steam
|
||||
|
||||
### Other changes
|
||||
- Added support for `info.dat` v4
|
||||
|
||||
|
||||
## [1.4.8](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.8) (Aug 1, 2024)
|
||||
|
||||
### Other changes
|
||||
- Added checks to avoid logging sensitive data in log files
|
||||
- The oldest log files are deleted to retain a maximum of 5 log files
|
||||
|
||||
|
||||
## [1.4.7](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.7) (Mar 13, 2024)
|
||||
|
||||
### Fixes
|
||||
- Fixed being unable to download models with BSManager #436
|
||||
|
||||
### Other changes
|
||||
- Fixed a typo in german translation (Thanks to @fllppi)
|
||||
|
||||
## [1.4.6](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.6) (Mar 6, 2024)
|
||||
|
||||
### Features
|
||||
- It's now possible to exclude already installed maps when downloading maps from BSManager #426 (Thanks to @Liborsaf)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where BSManager couldn't get path to user's documents under certain conditions, preventing any action from the user #431
|
||||
- To prevent the last launched Beat Saber version getting wiped by Oculus auto-updates, the symlink created to launch the version is now deleted after the game stops #432
|
||||
|
||||
## [1.4.5](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.5) (Feb 12, 2024)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue preventing launching downgraded Steam versions under certain conditions #415
|
||||
- Fixed an issue preventing launching downgraded Oculus versions under certain conditions #421
|
||||
- Fixed an issue where SteamVR was not properly restored after launching Beat Saber from a shortcut with FPFC mode #417 (Thanks to @slinkstr)
|
||||
|
||||
## [1.4.4](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.4) (Feb 1, 2024)
|
||||
|
||||
### Fixes
|
||||
- When launching an Oculus Beat Saber version, the Oculus app is also started if needed to avoid crashes #405
|
||||
- If Steam is running as admin, Beat Saber is now also started as admin to avoid crashes #404
|
||||
- Fixed the issue where BSManager could fail to start due to a missing DLL #400
|
||||
- Fixed the issue where BSManager could not locate the Oculus library #398
|
||||
|
||||
### Other changes
|
||||
- Added some tooltips to the maps UIs #397
|
||||
- Updated the maps panel UI for consistency with the rest of BSManager UIs #399
|
||||
|
||||
## [1.4.3](https://github.com/Zagrios/bs-manager/releases/tag/v1.4.3) (Dec 27, 2023)
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
----------------------------------------
|
||||
BADGES
|
||||
@@ -58,7 +57,7 @@
|
||||
src="https://img.shields.io/badge/-DISCORD-5865f2?style=for-the-badge&logo=discord&logoColor=ffffff"
|
||||
alt="discord" /></a>
|
||||
<a href="https://twitter.com/BSManager_"><img
|
||||
src="https://img.shields.io/badge/-Twitter-black?style=for-the-badge&logo=X" alt="Twitter" /></a>
|
||||
src="https://img.shields.io/badge/-Twitter-black?style=for-the-badge&logo=X" alt="Twitter" /></a>
|
||||
<a href="https://www.bsmanager.io">
|
||||
<img src="https://img.shields.io/badge/-WebSite-00649c?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPgogIDxnIGZpbGw9IiMzYjgyZmYiPgogICAgPHBhdGggZD0ibTYyNi43NSA0ODUuOTEgMjAuODIgMzUxLjk0YTM1LjIxIDM1LjIxIDAgMCAxLTE1LjQ5IDMxLjM2bC0xNDQuNzEgOTYuMTRoLS4wNmEzNS41NiAzNS41NiAwIDAgMS0xMS4zMSA0Ljc3IDM0Ljg1IDM0Ljg1IDAgMCAxLTIzLjUxLTIuODdMMTM2LjU4IDgwOS45YTExNS40NSAxMTUuNDUgMCAwIDEtNjMuNzUtOTYuNDlMNTIgMzYxLjQ2YTM1LjI2IDM1LjI2IDAgMCAxIDE1LjIzLTMxLjIxbDE0NS4wNS05Ni4zNmEzNS43MiAzNS43MiAwIDAgMSAxMS4yNy00LjcgMzQuODcgMzQuODcgMCAwIDEgMjMuNTEgMi44N0w1NjMgMzg5LjQyYTExNS40MSAxMTUuNDEgMCAwIDEgNjMuNzUgOTYuNDlaIi8+CiAgICA8cGF0aCBmaWx0ZXI9ImJyaWdodG5lc3MoMzAlKSIgZD0ibTYxMy43OCA0ODYuNjcgMjAuODIgMzUyYTIyLjM0IDIyLjM0IDAgMCAxLTMyLjI2IDIxLjMxTDI4Ni40IDcwMi41N0ExMDIuNDMgMTAyLjQzIDAgMCAxIDIyOS44MyA2MTdMMjA5IDI2NWEyMi4zNSAyMi4zNSAwIDAgMSAzMi4yNi0yMS4zMkw1NTcuMiA0MDEuMDVhMTAyLjQyIDEwMi40MiAwIDAgMSA1Ni41OCA4NS42MloiLz4KICA8L2c+CiAgPHBhdGggZD0iTTcxOC4yOSA3NzYuNDggNzAwIDgwNC4xOGMtNDAuNjMgMzkuNS04Ny4wNiA2Ny4yNS0xMzMuODEgOTAuNzUtODcuMzIgNDMuOS0yMDMuNjcgNzcuMTktMjQ5IDkwLjM0YTQ4IDQ4IDAgMCAxLTI2LjE3LjE2Yy02NC0xNy43Mi0xODguNjItNzItMjM2LjI4LTEyNS40OWE0LjIzIDQuMjMgMCAwIDEgNS4zLTYuNDVjMzUuNjQgMjAuODcgMTExLjIyIDU3LjY0IDE1Ni4zNyA3NC4yMiA0OS4xOCAxOC4wNiA4MS4yOSA4Ljc0IDkyLjI4LTMuMjZhNC43NyA0Ljc3IDAgMCAxIDcuMjQuMjFjMTkuNTEgMjQgMTA3LjI2IDQuNiAxNjYuNjQtMTMgNjQuMzQtMTkgMTg1Ljg1LTc2LjU2IDIzNS43Mi0xMzUuMThabS0xOTYuMiAxNTcuMzMtMTQ3LjQ1IDU1LjY4YTMuOTIgMy45MiAwIDAgMCAxLjQ0IDcuNThsNTQuNjItLjc3YTM1LjYzIDM1LjYzIDAgMCAwIDE5LTUuNzlsNzYtNDkuNTVhNCA0IDAgMCAwLTMuNjEtNy4xNVpNMjE4LjkyIDE5NS41NmMtMjEuMTQuMDctMzMuNTUgMzUuODYtMzMuMTYgODkuMDguMiAyOCAyLjU4IDkyLjc0IDQuNzkgMTQ3LjczLjIzIDUuNzEtOC4xOCA2LjQ4LTkgLjgxbC0zOC40Mi0yNzAuNzFhNC4zOCA0LjM4IDAgMCAxIDcuMTItNCA2My42MyA2My42MyAwIDAgMCAyMS45MyAxMS44NSA0LjYxIDQuNjEgMCAwIDAgNS45LTMuNzJjNC4yLTI3LjcgMTYuNjMtNTguNzQgMzcuMzUtODguNzRhNC4zNiA0LjM2IDAgMCAxIDcuNTYuNjljMTAuODYgMjMuODcgNDkgMTAzLjggOTMuMzQgMTUyLjc5YTQuNjUgNC42NSAwIDAgMS01LjYyIDcuMjJjLTM2LjU3LTE5LjM4LTgzLjAxLTQzLjAyLTkxLjc5LTQzWm0tMTUuNDcgMzg0LjhhNC41MSA0LjUxIDAgMCAxLTQuNDYtMy45MmwtMTMuODItMTA3LjI5YTQuNSA0LjUgMCAwIDEgOC45My0xLjE1bDEzLjgyIDEwNy4yOWE0LjUgNC41IDAgMCAxLTMuODkgNSAzLjg2IDMuODYgMCAwIDEtLjU4LjA3Wk01NzcuMDggNTQuNThjLTQ2Ljc5IDE1Ljg1LTExNS40NSA0MC41Ni0xNTguNzMgNjIuNzlhMzUuNSAzNS41IDAgMCAwLTE4LjU3IDI0LjUzYy0zLjk1IDE5LjQ5LTExIDU2LjA5LTE5IDEwNy4yNi0uNyA0LjQ4LTcuMzYgMy43OC03LjExLS43NWwxMC42Ni0xOTQuNzNhMy43OCAzLjc4IDAgMCAxIDcuNDYtLjY2bDMuNDkgMTQuODVhMi41NyAyLjU3IDAgMCAwIDQuNjkuNzdjNi4xOC0xMCAyMi4zNy0zMi43OSA1Ny4zMi02NWE0LjEzIDQuMTMgMCAwIDEgNi41NiA0LjcybC0zMi41NCA3Mi44OXM3OC43OC0xOS4zNyAxNDQtMzMuMDljNC4xMS0uODcgNS42OSA1LjA4IDEuNzcgNi40MloiIGZpbGw9IiNmZmYiIHN0cm9rZT0iYmxhY2siLz4KICA8ZyBmaWxsPSIjZjQ0Ij4KICAgIDxwYXRoIGZpbHRlcj0iYnJpZ2h0bmVzcyg2MCUpIHNhdHVyYXRlKDEyMCUpIiBkPSJtOTQ3LjA3IDQxMC4zNS01Ny43NSAzNDcuNzlhMzUuMTggMzUuMTggMCAwIDEtMjIuMDYgMjcuMTVsLTE2Mi40MyA2MS42NWgtLjA2YTM1LjUyIDM1LjUyIDAgMCAxLTEyLjA2IDIuMTEgMzQuODMgMzQuODMgMCAwIDEtMjIuMjktOGwtMjczLjE3LTIyMy41YTExNS40MyAxMTUuNDMgMCAwIDEtNDAuNzYtMTA4LjIybDU3Ljc1LTM0Ny44QTM1LjI1IDM1LjI1IDAgMCAxIDQzNiAxMzQuNDdsMTYyLjgzLTYxLjc5YTM1LjUxIDM1LjUxIDAgMCAxIDEyLTIuMDggMzQuODMgMzQuODMgMCAwIDEgMjIuMjkgOGwyNzMuMTkgMjIzLjUyYTExNS4zOCAxMTUuMzggMCAwIDEgNDAuNzYgMTA4LjIzWiIvPgogICAgPHBhdGggZD0iTTkzNC4yNSA0MDguMjIgODc2LjUgNzU2YTIyLjM0IDIyLjM0IDAgMCAxLTM2LjE4IDEzLjYzbC0yNzMuMTctMjIzLjVhMTAyLjQyIDEwMi40MiAwIDAgMS0zNi4xNy05Nmw1Ny43NS0zNDcuODNhMjIuMzQgMjIuMzQgMCAwIDEgMzYuMTgtMTMuNjNsMjczLjE3IDIyMy41MWExMDIuNDIgMTAyLjQyIDAgMCAxIDM2LjE3IDk2LjA0WiIvPgogIDwvZz4KICA8ZWxsaXBzZSBmaWxsPSIjZmZmIiBzdHJva2U9ImJsYWNrIiBjeD0iNzMyLjYxIiBjeT0iNDI5LjE2IiByeD0iNjIuODMiIHJ5PSIxMTAuNzMiIHRyYW5zZm9ybT0icm90YXRlKC0xNS40NSA3MzIuNzAzIDQyOS4xOTkpIi8+Cjwvc3ZnPg==" alt="Website" /></a>
|
||||
<a href="https://www.patreon.com/bsmanager"><img
|
||||
@@ -72,76 +71,64 @@
|
||||
-->
|
||||
<div>
|
||||
<hr>
|
||||
<details open>
|
||||
<details open>
|
||||
<summary><b>Table of Contents</b></summary>
|
||||
<ul>
|
||||
<li>
|
||||
<li>
|
||||
<details>
|
||||
<summary><a href="#bsmanager-your-beat-saber-assistant">BSManager: Your Beat Saber Assistant</a></summary>
|
||||
<ul>
|
||||
<summary><a href="#bsmanager-your-beat-saber-assistant">BSManager: Your Beat Saber Assistant</a></summary>
|
||||
<ul>
|
||||
<li><a href="#what-is-it">What is it?</a></li>
|
||||
<li><a href="#why-use-it">Why use it?</a></li>
|
||||
<li><a href="#built-with">Built with</a></li>
|
||||
<li><a href="#how-to-install">How to install</a></li>
|
||||
</ul>
|
||||
<li><a href="#how-to-install">How to install?</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary><a href="#version-management">Version Management</a></summary>
|
||||
<ul>
|
||||
<li>
|
||||
<details>
|
||||
<summary><a href="#downloading-multiple-versions">Downloading multiple versions</a></summary>
|
||||
<ul>
|
||||
<li>via Steam account</li>
|
||||
<li>via Oculus (PCVR) account</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li><a href="#multi-version-sharing">Multi-version Sharing</a></li>
|
||||
<li><a href="#importing-an-existing-version-of-beat-saber">Importing an existing version of Beat Saber</a>
|
||||
</li>
|
||||
<li><a href="#launch-arguments">Launch arguments</a></li>
|
||||
<li><a href="#shortcut-creation">Shortcut creation</a></li>
|
||||
</ul>
|
||||
<summary><a href="#version-management">Version Management</a></summary>
|
||||
<ul>
|
||||
<li><a href="#downloading-multiple-versions">Downloading Multiple Versions</a></li>
|
||||
<li><a href="#multi-version-sharing">Multi-Version Sharing</a></li>
|
||||
<li><a href="#importing-an-existing-version-of-beat-saber">Importing an Existing Version of Beat Saber</a></li>
|
||||
<li><a href="#launch-arguments">Launch Arguments</a></li>
|
||||
<li><a href="#shortcut-creation">Shortcut Creation</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary><a href="#download-and-installation">Download and Installation</a></summary>
|
||||
<ul>
|
||||
<li><a href="#maps">Maps</a></li>
|
||||
<li><a href="#playlists">Playlists</a></li>
|
||||
<li><a href="#mods">Mods</a></li>
|
||||
<li><a href="#models">Models</a></li>
|
||||
</ul>
|
||||
<summary><a href="#download-and-installation">Download and Installation</a></summary>
|
||||
<ul>
|
||||
<li><a href="#features">Features</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary><a href="#customization">Customization</a></summary>
|
||||
<ul>
|
||||
<summary><a href="#customization">Customization</a></summary>
|
||||
<ul>
|
||||
<li><a href="#tool">Tool</a></li>
|
||||
<li><a href="#beat-saber-instance">Beat Saber Instance</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary><a href="#more-information">More Information</a></summary>
|
||||
<ul>
|
||||
<summary><a href="#more-information">More Information</a></summary>
|
||||
<ul>
|
||||
<li><a href="#support">Support</a></li>
|
||||
<li><a href="#credits">Credits</a></li>
|
||||
<li><a href="#Contributing">Contributing</a></li>
|
||||
</ul>
|
||||
<li><a href="#contributing">Contributing</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<div align="right">
|
||||
</details>
|
||||
<div align="right">
|
||||
[<a href="#readme-bot">Go to bottom</a>]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -155,7 +142,6 @@
|
||||
<h1><b>BSManager: Your Beat Saber Assistant</b></h1>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
WHAT IS IT?
|
||||
----------------------------------------
|
||||
@@ -166,7 +152,6 @@
|
||||
Saber</a> experience by centralizing the management of mods, maps, and game versions in one place.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
WHY USE IT?
|
||||
----------------------------------------
|
||||
@@ -189,7 +174,6 @@
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/012b0196-74ef-4ee5-86a4-49e835612f96"/>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
BUILT WITH
|
||||
----------------------------------------
|
||||
@@ -219,7 +203,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
HOW TO INSTALL
|
||||
----------------------------------------
|
||||
@@ -231,8 +214,12 @@
|
||||
<div>
|
||||
<h3><b>Windows Installation</b></h3>
|
||||
<ul>
|
||||
<li>Download the <a href="https://github.com/Zagrios/bs-manager/releases/latest">latest release</a> from <a
|
||||
href="https://github.com/Zagrios/bs-manager/releases">Releases</a>.</li>
|
||||
<li>Download and install <a href="https://www.bsmanager.io">BSManager</a> from one of the following sources:</li>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Zagrios/bs-manager/releases/latest">GitHub</a></li>
|
||||
<li><a href="https://www.bsmanager.io">Official Website</a></li>
|
||||
<li><a href="https://www.nexusmods.com/beatsaber/mods/18?tab=files">Nexus Mods</a></li>
|
||||
</ul>
|
||||
<li>Execute the installer and <a href="https://github.com/Zagrios/bs-manager">BSManager</a> will start itself.</li>
|
||||
<li>Once it's done, if you want to download a version, select a version and download it! </li>
|
||||
<li>From the version page you can choose options, launch the version, and more!</li>
|
||||
@@ -244,8 +231,8 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3><b>Linux installation</b></h3>
|
||||
<p>Refer to <a href="https://github.com/Zagrios/bs-manager/wiki/Linux">Linux wiki</a></p>
|
||||
<h3><b>Linux Installation</b></h3>
|
||||
<p>BSManager is also available on Linux! Follow the step-by-step guide in our <a href="https://github.com/Zagrios/bs-manager/wiki/Linux">Linux installation wiki</a> to set it up properly.</p>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@@ -264,48 +251,27 @@
|
||||
existing ones, provided that you own the game on a Steam or Oculus (PC) account.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
DOWNLOADING MULTIPLE VERSIONS
|
||||
----------------------------------------
|
||||
-->
|
||||
<div>
|
||||
<h2>Downloading Multiple Versions</h2>
|
||||
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> provides two authentication methods to access your
|
||||
Steam account, ensuring safe and flexible access to
|
||||
different game versions:</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://github.com/Zagrios/bs-manager/assets/40648115/09d579de-8b8e-45d3-8829-21a25cc7daf8"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> allows you to safely and easily access different versions of Beat Saber, ensuring flexibility for modding or version-specific preferences.</p>
|
||||
<p>To download a version, you can authenticate using two methods:</p>
|
||||
<ul>
|
||||
<li><strong>via Steam</strong>: Use either your credentials, which will not be stored by <a
|
||||
href="https://github.com/Zagrios/bs-manager">BSManager</a> or
|
||||
<a href="https://github.com/SteamRE/DepotDownloader">DepotDownloader</a>, or opt for a quick and secure
|
||||
authentication via QR with the Steam Guard app.</li>
|
||||
<li><strong>via Steam</strong>: Authenticate securely using your Steam credentials or by scanning a QR code with the Steam Guard app.</li>
|
||||
<li><strong>via Oculus</strong>: BSManager redirects you to the official Oculus website for authentication to retrieve your token securely. Alternatively, you can follow <a href="https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token">this guide</a> to obtain your token manually and paste it into BSManager.</li>
|
||||
</ul>
|
||||
<p>For detailed instructions on installing versions, follow this tutorial:
|
||||
👉 <a href="https://github.com/Zagrios/bs-manager/wiki/install-or-import-a-version">How to install or import a version</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://github.com/Zagrios/bs-manager/assets/40648115/26da3a19-a936-4e6b-b79b-a139d7bd55de" />
|
||||
|
||||
<img src="https://github.com/Zagrios/bs-manager/assets/40648115/09d579de-8b8e-45d3-8829-21a25cc7daf8" alt=""/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul>
|
||||
<li><strong>via Oculus</strong>: For Oculus users, <a href="https://github.com/Zagrios/bs-manager">BSManager</a>
|
||||
requires you to retrieve a connection token by following the instructions in this guide: <a href="https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token">How to obtain your Oculus Token</a>. Once obtained, please insert it into the form.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img height=450 src="https://github.com/Zagrios/bs-manager/assets/40648115/3674e629-542d-4de3-b8db-b248f25126d7" />
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
MULTI-VERSION SHARING
|
||||
----------------------------------------
|
||||
@@ -320,10 +286,9 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/ba39aceb-f55f-4e58-8386-51fcfaad4957"/>
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/ba39aceb-f55f-4e58-8386-51fcfaad4957"/>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
IMPORTING AN EXISTING VERSION OF BEAT SABER
|
||||
----------------------------------------
|
||||
@@ -340,49 +305,53 @@
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/cdedb951-d97a-474d-a217-2422be3919f7"/>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
LAUNCH ARGUMENTS
|
||||
----------------------------------------
|
||||
-->
|
||||
<div>
|
||||
<h2>Launch Arguments</h2>
|
||||
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> provides efficient management of <a
|
||||
href="https://beatsaber.com/">Beat Saber</a> launch arguments, including three predefined ones to
|
||||
enhance your gaming experience:</p>
|
||||
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> provides advanced management of launch arguments for <a href="https://beatsaber.com/">Beat Saber</a>, allowing you to customize your gaming experience.</p>
|
||||
|
||||
<h3>Predefined Arguments</h3>
|
||||
<ul>
|
||||
<li><strong>Oculus Mode</strong>: Launches the game directly in Oculus mode, avoiding the use of STEAM VR.</li>
|
||||
<li><strong>FPFC Mode</strong>: Enables "FPFC" mode, allowing you to control the game without using a VR
|
||||
headset.</li>
|
||||
<li><strong>Debug Mode</strong>: Activates debugging mode, generating Windows log outputs for IPA.</li>
|
||||
<li><strong>Oculus Mode</strong>: Allows you to use Oculus's VR composer while bypassing SteamVR, which may improve performance. (Only useful if you launch the game via Steam).</li>
|
||||
<li><strong>FPFC Mode</strong>: Enables "First Person Flying Controller" mode, allowing you to navigate menus using the keyboard (WASD) and mouse without a VR headset.</li>
|
||||
<li><strong>Debug Mode</strong>: Activates the IPA debug console, displaying logs used by mods.</li>
|
||||
<li><strong>Skip Steam</strong>: Prevents Steam from opening automatically with Beat Saber, useful if you use an alternative VR runtime like WIVRn or Monado.</li>
|
||||
<li><strong>Map Editor</strong>: Launches the official Beat Saber map editor instead of the game.</li>
|
||||
</ul>
|
||||
<p>You also have the freedom to add your own custom launch arguments by inserting them into "Advanced Launch" and
|
||||
separating them with a semicolon (;).</p>
|
||||
|
||||
<h3>Custom Arguments</h3>
|
||||
<p>You can also add your own custom launch arguments by inserting them into "Advanced Launch" and separating them with a semicolon (;).</p>
|
||||
|
||||
<h3>Pinning Arguments</h3>
|
||||
<p>You can now pin your favorite launch arguments for quick access. By default, only <strong>Oculus Mode</strong> and <strong>Debug Mode</strong> are pinned.</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://github.com/Zagrios/bs-manager/assets/40648115/1244d49f-20e1-400c-9818-528f05cfadf5" />
|
||||
<img src="https://github.com/user-attachments/assets/c247b237-9c49-4e7b-9366-aeb1dd3b7b9a" alt="" />
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
SHORTCUT CREATION
|
||||
----------------------------------------
|
||||
-->
|
||||
<div>
|
||||
<h2>Shortcut Creation</h2>
|
||||
<p>Create shortcuts to easily access your different versions of <a href="https://beatsaber.com/">Beat Saber</a> with
|
||||
<a href="https://github.com/Zagrios/bs-manager">BSManager</a>. After selecting your
|
||||
version, go to the version settings, click on "Create a Shortcut", and then select your launch options. Thus,
|
||||
you can quickly and easily launch your preferred version of <a href="https://beatsaber.com/">Beat Saber</a> with the
|
||||
desired settings.</p>
|
||||
<p>Create shortcuts to easily access your different versions of <a href="https://beatsaber.com/">Beat Saber</a> with <a href="https://github.com/Zagrios/bs-manager">BSManager</a>.
|
||||
</p>
|
||||
<p>To learn how to create a shortcut, follow the tutorial available here:
|
||||
👉 <a href="https://github.com/Zagrios/bs-manager/wiki/create-a-shortcut">Shortcut Creation Guide</a>
|
||||
</p>
|
||||
<p>This allows you to quickly and easily launch your preferred version of <a href="https://beatsaber.com/">Beat Saber</a> with the desired settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/bfa1e97d-6392-4dec-a1fc-bb05048cf6d2" />
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
----------------------------------------
|
||||
DOWNLOAD AND INSTALLATION
|
||||
@@ -392,79 +361,30 @@
|
||||
<hr>
|
||||
<h1><b>Download and Installation</b></h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> provides centralized and organized management of
|
||||
beatmaps, mods, and models, integrating download features and sharing between <a href="https://beatsaber.com/">Beat
|
||||
Saber</a> versions.</p>
|
||||
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> centralizes and simplifies the management of beatmaps, playlists, mods, and models, offering seamless downloads and sharing between different <a href="https://beatsaber.com/">Beat Saber</a> versions.</p>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
MAPS
|
||||
----------------------------------------
|
||||
-->
|
||||
<div>
|
||||
<h2>Maps</h2>
|
||||
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> integrates the <a href="https://beatsaver.com">Beat
|
||||
Saver</a> API to provide you with a comprehensive list of maps downloadable directly from its interface. Moreover,
|
||||
the OneClick feature allows you to download maps with one click from various sources such as <a
|
||||
href="https://beatsaver.com">Beat Saver</a>, <a href="https://bsaber.com">BeastSaber</a>, <a
|
||||
href="https://scoresaber.com">ScoreSaber</a>, and <a href="https://www.beatleader.xyz">Beat Leader</a>. To add
|
||||
maps, simply select the desired version or the "shared" tab, then click "ADD".</p>
|
||||
<h2>Features</h2>
|
||||
<p>With <a href="https://github.com/Zagrios/bs-manager">BSManager</a>, you can manage your:
|
||||
</p>
|
||||
<ul>
|
||||
<li><b>Maps:</b> Browse and download maps directly from sources like <a href="https://beatsaver.com">Beat Saver</a>, <a href="https://bsaber.com">BeastSaber</a>, <a href="https://scoresaber.com">ScoreSaber</a>, and <a href="https://www.beatleader.xyz">Beat Leader</a>.</li>
|
||||
<li><b>Playlists:</b> Search for and download playlists directly from BSManager, just like maps. 😉
|
||||
<li><b>Mods:</b> Manage and install mods via the "Mods" tab, offering the same features as Mod Assistant.</li>
|
||||
<li><b>Models:</b> Add and manage models easily with the integration of the <a href="https://modelsaber.com">Model Saber</a> API.</li>
|
||||
</ul>
|
||||
<p>For detailed instructions on each feature, check the following guides:</p>
|
||||
<ul>
|
||||
<li>👉 <a href="https://github.com/Zagrios/bs-manager/wiki/install-any-map">Install Any Map</a></li>
|
||||
<li>👉 <a href="https://github.com/Zagrios/bs-manager/wiki/install-all-playlists-you-want">Install Playlists</a></li>
|
||||
<li>👉 <a href="https://github.com/Zagrios/bs-manager/wiki/install-mods-on-your-version">Install Mods</a></li>
|
||||
<li>👉 <a href="https://github.com/Zagrios/bs-manager/wiki/modify-models-in-your-beat-saber">Manage Models</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/3f9f9968-ac55-465e-bc28-17aebfc787ea"/>
|
||||
|
||||
</div>
|
||||
|
||||
<!--
|
||||
PLAYLISTS
|
||||
----------------------------------------
|
||||
-->
|
||||
<div>
|
||||
<h2>Playlists</h2>
|
||||
<p>Although the playlist management interface is not yet developed, <a
|
||||
href="https://github.com/Zagrios/bs-manager">BSManager</a> offers a OneClick feature for playlists from <a
|
||||
href="https://beatsaver.com">Beat Saver</a>.</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/733fe6bb-c157-4c47-8a79-f8062380bcc8" />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
MODS
|
||||
----------------------------------------
|
||||
-->
|
||||
<div>
|
||||
<h2>Mods</h2>
|
||||
<p>Manage your mods easily with <a href="https://github.com/Zagrios/bs-manager">BSManager</a>. After downloading a
|
||||
version, navigate to the "Mods" tab of the selected version to manage and install mods. <a
|
||||
href="https://github.com/Zagrios/bs-manager">BSManager</a> operates similarly to Mod Assistant and offers the same
|
||||
mods as this tool.</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/ca8f0727-b2dd-41f3-bbea-9bcaf61a89f1"/>
|
||||
|
||||
</div>
|
||||
|
||||
<!--
|
||||
MODELS
|
||||
----------------------------------------
|
||||
-->
|
||||
<div>
|
||||
<h2>Models</h2>
|
||||
<p>Add and manage models simply via the corresponding tab on the desired version or by going to the "shared" tab. The
|
||||
<a href="https://modelsaber.com">Model Saber</a> API is integrated to provide easy and direct access to models
|
||||
available for <a href="https://beatsaber.com/">Beat Saber</a>.</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/d983b34b-9f4c-4274-aee4-4c1c03dd90d0"/>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
----------------------------------------
|
||||
CUSTOMIZATION
|
||||
@@ -495,7 +415,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<div align="center">
|
||||
<video src = "https://github.com/Zagrios/bs-manager/assets/40648115/d2c1b0ec-a2f5-41b5-99d5-c4c2cd34cbd4" />
|
||||
</div>
|
||||
|
||||
@@ -520,7 +440,6 @@
|
||||
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/f66a2c99-770e-42ca-9a71-b1806946ad0c" />
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
----------------------------------------
|
||||
MORE INFORMATION
|
||||
@@ -546,7 +465,6 @@
|
||||
GitHub</a>.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
CREDITS
|
||||
----------------------------------------
|
||||
@@ -559,7 +477,7 @@
|
||||
<li><a href="https://github.com/GaetanGrd">GaetanGrd</a> - Co-Developer & Co-Founder, Documentation Lead.</li>
|
||||
<li><a href="https://github.com/cheddZy">cheddZy</a> - Icon Creator.</li>
|
||||
<li><a href="https://github.com/Insprill">Insprill</a> - Co-Developer, Linux Developer, AUR Maintainer.</li>
|
||||
<li><a href="https://github.com/silentrald">silentrald</a> - Co-Developer, Linux Developer, deb and flatpak Maintainer.</li>
|
||||
<li><a href="https://github.com/silentrald">silentrald</a> - Co-Developer, Linux Developer, deb/rpm/flatpak Maintainer.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -575,12 +493,11 @@
|
||||
href="https://github.com/Zagrios/bs-manager">BSManager</a>!</p>
|
||||
<div align="center">
|
||||
<a href="https://github.com/Zagrios/bs-manager/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Zagrios/bs-manager" />
|
||||
<img src="https://contrib.rocks/image?repo=Zagrios/bs-manager" alt="Contributor list" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
----------------------------------------
|
||||
Others
|
||||
|
||||
@@ -813,5 +813,14 @@
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/65df6b24c3b8a7f415080ac01394917cedabf9fc.png",
|
||||
"ReleaseDate": "1733846128",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.1",
|
||||
"BSManifest": "8655702238937650396",
|
||||
"OculusBinaryId": "8526525354113835",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510696972023234581",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/5cc1ca353151ed31da2764716cdcbe89442aeeeb.png",
|
||||
"ReleaseDate": "1738144954",
|
||||
"year": "2025"
|
||||
}
|
||||
]
|
||||
@@ -127,5 +127,20 @@
|
||||
},
|
||||
{
|
||||
"username": "Paul Smith"
|
||||
},
|
||||
{
|
||||
"username": "MunityVR",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Sk4venger"
|
||||
},
|
||||
{
|
||||
"username": "Alexander Herman",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Arts Rimuro Suraimu",
|
||||
"type": "gold"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "Anheften",
|
||||
"unpin": "Lösen"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "Konnte den Umgebungsvariablen-String nicht richtig parsen."
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "veraltet"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "Diese Version ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht wie erwartet. Es wird empfohlen, die empfohlene Version ({recommendedVersion}) von Beat Saber zu verwenden, um die neuesten Funktionen und Fehlerbehebungen zu genießen.",
|
||||
"advanced-launch": {
|
||||
"button": "Startoptionen",
|
||||
"placeholder": "Weitere Argumente, bspw: --revert; --nowait"
|
||||
"placeholder": "Startoptionen, bspw: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "Steam überspringen",
|
||||
"skipsteam-description": "Verhindert, dass Steam automatisch mit Beat Saber geöffnet wird. Aktivieren Sie dies, wenn Sie eine andere VR-Laufzeit wie WiVRn oder Monado verwenden, mit der SteamVR interferieren könnte.",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Deine Karten importieren",
|
||||
"subtext": "Lasse deine ZIP-Dateien hier fallen, um deine Karten zu importieren"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Name",
|
||||
"song-author": "Liedautor",
|
||||
"map-author": "Kartenautor",
|
||||
"bpm": "BPM",
|
||||
"duration": "Dauer",
|
||||
"likes": "Likes",
|
||||
"date-uploaded": "Hochladedatum"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Importiere deine Wiedergabelisten",
|
||||
"subtext": "Ziehe deine \".bplist\" oder \".json\" Dateien hierhin, um sie zu importieren"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Titel",
|
||||
"author": "Autor",
|
||||
"number-of-maps": "Anzahl der Karten",
|
||||
"duration": "Dauer",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mods bereits installiert",
|
||||
"description": "Alle ausgewählten Mods sind bereits installiert"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "Veraltetes Mod",
|
||||
"title-plural": "Veraltete Mods",
|
||||
"description": "Das Mod {name} ist veraltet. Möchten Sie es aktualisieren?",
|
||||
"description-plural": "Diese Version enthält {nb} veraltete Mods. Möchten Sie sie aktualisieren?",
|
||||
"dont-remind-me": "Nicht mehr daran erinnern",
|
||||
"update": "Aktualisieren"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -211,6 +240,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -221,6 +251,7 @@
|
||||
"en-US": "Englisch, USA",
|
||||
"fr-FR": "Französisch",
|
||||
"es-ES": "Spanisch",
|
||||
"pt-BR": "Portugiesisch",
|
||||
"it-IT": "Italienisch",
|
||||
"de-DE": "Deutsch",
|
||||
"ru-RU": "Russisch",
|
||||
@@ -625,6 +656,7 @@
|
||||
"EPERM": "BSManager hat nicht die erforderlichen Berechtigungen, um den Ordner zu verknüpfen.",
|
||||
"EACCES": "BSManager hat nicht die erforderlichen Berechtigungen, um den Ordner zu verknüpfen.",
|
||||
"ENOSPC": "Die Festplatte ist voll, machen Sie Platz und versuchen Sie es erneut.",
|
||||
"LinkingNotSupported": "Ordnerverknüpfung wird auf diesem Dateisystem nicht unterstützt.",
|
||||
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist beim Verknüpfen des Ordners aufgetreten."
|
||||
}
|
||||
},
|
||||
@@ -636,7 +668,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Verknüpfung erstellt",
|
||||
"msg": "Die Verknüpfung wurde auf dem Desktop erstellt."
|
||||
"msg": "Die Verknüpfung wurde auf dem Desktop erstellt.",
|
||||
"msg-steam": "Die Verknüpfung wurde in der Steam-Bibliothek erstellt."
|
||||
},
|
||||
"error": {
|
||||
"msg": "Beim Erstellen der Verknüpfung ist ein Fehler aufgetreten."
|
||||
@@ -843,7 +876,9 @@
|
||||
"desc": "Das Erstellen einer Verknüpfung ermöglicht es dir, Beat Saber mit den ausgewählten Optionen zu starten, ohne durch BSManager zu gehen.",
|
||||
"launch-options": "Startoptionen",
|
||||
"advanced-launch": "Erweiterter Start",
|
||||
"valid-btn": "Verknüpfung erstellen"
|
||||
"valid-btn": "Verknüpfung erstellen",
|
||||
"create-steam-shortcut": "Steam-Verknüpfung erstellen",
|
||||
"steam-shortcut-tippy": "Wenn aktiviert, wird anstelle einer Verknüpfung auf dem Desktop eine Verknüpfung in Steam erstellt."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Mit Meta verbinden",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "Could not properly parse the env var string."
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "outdated"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "This version is outdated, and some mods or features may no longer work as expected. Prefer using the recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes.",
|
||||
"advanced-launch": {
|
||||
"button": "Launch options",
|
||||
"placeholder": "Additional arguments ex: --revert; --nowait"
|
||||
"placeholder": "Launch options ex: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "Skip Steam",
|
||||
"skipsteam-description": "Stops Steam from opening automatically with Beat Saber, enable if you are using a different VR runtime like WiVRn or Monado that SteamVR may interfere with.",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Import your maps",
|
||||
"subtext": "Drop your zip files here to import your maps"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Name",
|
||||
"song-author": "Song Author",
|
||||
"map-author": "Map Author",
|
||||
"bpm": "BPM",
|
||||
"duration": "Duration",
|
||||
"likes": "Likes",
|
||||
"date-uploaded": "Date Uploaded"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Import your playlists",
|
||||
"subtext": "Drop your \".bplist\" or \".json\" files here to import them"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Title",
|
||||
"author": "Author",
|
||||
"number-of-maps": "Number of Maps",
|
||||
"duration": "Duration",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mods already installed",
|
||||
"description": "All selected mods are already installed"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "Outdated Mod",
|
||||
"title-plural": "Outdated Mods",
|
||||
"description": "{name} mod is outdated. Do you want to update it?",
|
||||
"description-plural": "This version has {nb} outdated mods. Do you want to update them?",
|
||||
"dont-remind-me": "Don't remind me",
|
||||
"update": "Update"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -211,6 +240,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -223,6 +253,7 @@
|
||||
"es-ES": "Spanish",
|
||||
"it-IT": "Italian",
|
||||
"de-DE": "German",
|
||||
"pt-BR": "Portuguese (Brazil)",
|
||||
"ru-RU": "Russian",
|
||||
"zh-CN": "Chinese (Simplified)",
|
||||
"zh-TW": "Chinese (Traditional)",
|
||||
@@ -625,6 +656,7 @@
|
||||
"EPERM": "BSManager does not have the necessary permissions to link the folder.",
|
||||
"EACCES": "BSManager does not have the necessary permissions to link the folder.",
|
||||
"ENOSPC": "The disk is full, make some space and try again.",
|
||||
"LinkingNotSupported": "Folder linking is not supported on this file system.",
|
||||
"UNKNOWN_ERROR": "An unknown error has occurred while linking the folder."
|
||||
}
|
||||
},
|
||||
@@ -636,7 +668,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Shortcut created",
|
||||
"msg": "The shortcut has been created on the desktop."
|
||||
"msg": "The shortcut has been created on the desktop.",
|
||||
"msg-steam": "The shortcut has been created in the Steam library."
|
||||
},
|
||||
"error": {
|
||||
"msg": "An error occurred while creating the shortcut."
|
||||
@@ -785,6 +818,11 @@
|
||||
"multiple": "These maps will also be removed from versions using shared maps"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Delete maps?",
|
||||
"desc": "Only the map \"{map}\" is a duplicate. Are you sure you want to delete it?",
|
||||
"desc-plural": "{nb} duplicate maps have been found. Are you sure you want to delete them?"
|
||||
}
|
||||
},
|
||||
"link-contents":{
|
||||
@@ -838,7 +876,9 @@
|
||||
"desc": "Creating a shortcut will allow you to start Beat Saber with the chosen options without going through BSManager.",
|
||||
"launch-options": "Launch options",
|
||||
"advanced-launch": "Advanced launch",
|
||||
"valid-btn": "Create the shortcut"
|
||||
"valid-btn": "Create the shortcut",
|
||||
"create-steam-shortcut": "Create a Steam shortcut",
|
||||
"steam-shortcut-tippy": "If enabled, instead of creating a shortcut on the desktop, it will be created in Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Connect to Meta",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "Fijar",
|
||||
"unpin": "Desfijar"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "No se pudo analizar correctamente la cadena de la variable de entorno."
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "obsoleto"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "Esta versión está desactualizada, y algunos mods o funciones pueden no funcionar como se espera. Es mejor usar la versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas características y correcciones.",
|
||||
"advanced-launch": {
|
||||
"button": "Opciones de lanzamiento",
|
||||
"placeholder": "Argumentos adicionales ej: --revert; --no-wait"
|
||||
"placeholder": "Opciones de lanzamiento ej: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "Saltar Steam",
|
||||
"skipsteam-description": "Evita que Steam se abra automáticamente con Beat Saber, habilita esto si estás usando un runtime de VR diferente como WiVRn o Monado que SteamVR podría interferir.",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Importar tus mapas",
|
||||
"subtext": "Suelta tus archivos zip aquí para importar tus mapas"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Nombre",
|
||||
"song-author": "Autor de la canción",
|
||||
"map-author": "Autor del mapa",
|
||||
"bpm": "BPM",
|
||||
"duration": "Duración",
|
||||
"likes": "Me gusta",
|
||||
"date-uploaded": "Fecha de subida"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Importa tus listas de reproducción",
|
||||
"subtext": "Suelta tus archivos \".bplist\" o \".json\" aquí para importarlos"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Título",
|
||||
"author": "Autor",
|
||||
"number-of-maps": "Número de mapas",
|
||||
"duration": "Duración",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mods ya instalados",
|
||||
"description": "Todos los mods seleccionados ya están instalados"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "Mod desactualizado",
|
||||
"title-plural": "Mods desactualizados",
|
||||
"description": "El mod {name} está desactualizado. ¿Quieres actualizarlo?",
|
||||
"description-plural": "Esta versión tiene {nb} mods desactualizados. ¿Quieres actualizarlos?",
|
||||
"dont-remind-me": "No me lo recuerdes",
|
||||
"update": "Actualizar"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -211,6 +240,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -223,6 +253,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Alemán",
|
||||
"pt-BR": "Portugués (Brasil)",
|
||||
"ru-RU": "Ruso",
|
||||
"zh-CN": "Chino simplificado",
|
||||
"zh-TW": "Chino tradicional",
|
||||
@@ -625,6 +656,7 @@
|
||||
"EPERM": "BSManager no tiene los permisos necesarios para enlazar la carpeta.",
|
||||
"EACCES": "BSManager no tiene los permisos necesarios para enlazar la carpeta.",
|
||||
"ENOSPC": "El disco está lleno, libera espacio e inténtalo de nuevo.",
|
||||
"LinkingNotSupported": "El enlace de carpetas no es compatible con este sistema de archivos.",
|
||||
"UNKNOWN_ERROR": "Se ha producido un error desconocido al enlazar la carpeta."
|
||||
}
|
||||
},
|
||||
@@ -636,7 +668,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Acceso directo creado",
|
||||
"msg": "El acceso directo se ha creado en el escritorio."
|
||||
"msg": "El acceso directo se ha creado en el escritorio.",
|
||||
"msg-steam": "El acceso directo se ha creado en la biblioteca de Steam."
|
||||
},
|
||||
"error": {
|
||||
"msg": "Se produjo un error al crear el acceso directo."
|
||||
@@ -843,7 +876,9 @@
|
||||
"desc": "Crear un atajo te permitirá iniciar Beat Saber con las opciones seleccionadas sin pasar por BSManager.",
|
||||
"launch-options": "Opciones de lanzamiento",
|
||||
"advanced-launch": "Lanzamiento avanzado",
|
||||
"valid-btn": "Crear el atajo"
|
||||
"valid-btn": "Crear el atajo",
|
||||
"create-steam-shortcut": "Crear un atajo directo de Steam",
|
||||
"steam-shortcut-tippy": "Si está activado, en lugar de crear un atajo directo en el escritorio, se creará en Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Conexión a Meta",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "Épingler",
|
||||
"unpin": "Désépingler"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "Impossible d'analyser correctement la chaîne de variable d'environnement."
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "obsolète"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "Cette version est obsolète et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Préférez utiliser la version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et correctifs.",
|
||||
"advanced-launch": {
|
||||
"button": "Options de lancement",
|
||||
"placeholder": "Arguments supplémentaires ex: --revert; --nowait"
|
||||
"placeholder": "Options de lancement ex: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "Ignorer Steam",
|
||||
"skipsteam-description": "Empêche Steam de s'ouvrir automatiquement avec Beat Saber, activez-le si vous utilisez un autre runtime VR comme WiVRn ou Monado avec lequel SteamVR pourrait interférer.",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Importer vos maps",
|
||||
"subtext": "Déposez vos fichiers zip ici pour importer vos maps"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Nom",
|
||||
"song-author": "Auteur de la chanson",
|
||||
"map-author": "Auteur de la carte",
|
||||
"bpm": "BPM",
|
||||
"duration": "Durée",
|
||||
"likes": "J'aime",
|
||||
"date-uploaded": "Date de téléchargement"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Importer vos playlists",
|
||||
"subtext": "Déposez vos fichiers \".bplist\" ou \".json\" ici pour les importer"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Titre",
|
||||
"author": "Auteur",
|
||||
"number-of-maps": "Nombre de cartes",
|
||||
"duration": "Durée",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mods déjà installées",
|
||||
"description": "Tous les mods séléctionnées sont déjà installées"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "Mod obsolète",
|
||||
"title-plural": "Mods obsolètes",
|
||||
"description": "Le mod {name} est obsolète. Voulez-vous le mettre à jour ?",
|
||||
"description-plural": "Cette version contient {nb} mods obsolètes. Voulez-vous les mettre à jour ?",
|
||||
"dont-remind-me": "Ne plus me rappeler",
|
||||
"update": "Mettre à jour"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -211,6 +240,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -221,6 +251,7 @@
|
||||
"en-US": "Anglais, États-Unis",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Espagnol",
|
||||
"pt-BR": "Portugais (Brésil)",
|
||||
"it-IT": "Italien",
|
||||
"de-DE": "Allemand",
|
||||
"ru-RU": "Russe",
|
||||
@@ -625,6 +656,7 @@
|
||||
"EPERM": "BSManager n'a pas les autorisations nécessaires pour lier le dossier.",
|
||||
"EACCES": "BSManager n'a pas les autorisations nécessaires pour lier le dossier.",
|
||||
"ENOSPC": "Le disque est plein, faites de la place et réessayez.",
|
||||
"LinkingNotSupported": "La liaison de dossier n'est pas supportée sur ce système de fichiers.",
|
||||
"UNKNOWN_ERROR": "Une erreur inconnue est survenue lors de la liaison du dossier."
|
||||
}
|
||||
},
|
||||
@@ -637,7 +669,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Raccourci créé",
|
||||
"msg": "Le raccourci a été créé sur le bureau."
|
||||
"msg": "Le raccourci a été créé sur le bureau.",
|
||||
"msg-steam": "Le raccourci a été créé dans la bibliothèque Steam."
|
||||
},
|
||||
"error": {
|
||||
"msg": "Une erreur s'est produite lors de la création du raccourci."
|
||||
@@ -844,7 +877,9 @@
|
||||
"desc": "Créer un raccourci te permettra de démarrer Beat Saber avec les options choisies sans passer par BSManager.",
|
||||
"launch-options": "Options de lancement",
|
||||
"advanced-launch": "Lancement avancé",
|
||||
"valid-btn": "Créer le raccourci"
|
||||
"valid-btn": "Créer le raccourci",
|
||||
"create-steam-shortcut": "Créer un raccourci Steam",
|
||||
"steam-shortcut-tippy": "Si activé, au lieu de créer un raccourci sur le bureau, celui-ci sera créé dans Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Connexion à Meta",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "Fissa",
|
||||
"unpin": "Rimuovi Fissaggio"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "Impossibile analizzare correttamente la stringa della variabile d'ambiente."
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "obsoleta"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "Questa versione è obsoleta, e alcune mod o funzioni non potrebbero più funzionare correttamente. Consigliamo di usare la versione raccomandata ({recommendedVersion}) di Beat Saber per godere delle ultime funzioni e bugfix.",
|
||||
"advanced-launch": {
|
||||
"button": "Opzioni di Lancio",
|
||||
"placeholder": "Argomenti aggiuntivi es: --revert; --nowait"
|
||||
"placeholder": "Opzioni di Lancio es: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "Salta Steam",
|
||||
"skipsteam-description": "Ferma Steam da aprirsi automaticamente con Beat Saber, abilitalo se stai usando un VR runtime differente come WiVRn o Monado con cui SteamVR potrebbe interferire.",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Importa le tue mappe",
|
||||
"subtext": "Rilascia qui i tuoi file zip per importare le tue mappe"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Nome",
|
||||
"song-author": "Autore della canzone",
|
||||
"map-author": "Autore della mappa",
|
||||
"bpm": "BPM",
|
||||
"duration": "Durata",
|
||||
"likes": "Mi piace",
|
||||
"date-uploaded": "Data di caricamento"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Importa le tue playlist",
|
||||
"subtext": "Rilascia qui i file \".bplist\" o \".json\" per importarli"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Titolo",
|
||||
"author": "Autore",
|
||||
"number-of-maps": "Numero di mappe",
|
||||
"duration": "Durata",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mod già installata",
|
||||
"description": "Tutte le mod selezionate sono già installate"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "Mod obsoleto",
|
||||
"title-plural": "Mod obsoleti",
|
||||
"description": "Il mod {name} è obsoleto. Vuoi aggiornarlo?",
|
||||
"description-plural": "Questa versione ha {nb} mod obsoleti. Vuoi aggiornarli?",
|
||||
"dont-remind-me": "Non ricordarmelo",
|
||||
"update": "Aggiorna"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -211,6 +240,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -223,6 +253,7 @@
|
||||
"es-ES": "Spagnolo",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Tedesco",
|
||||
"pt-BR": "Portoghese (Brasile)",
|
||||
"ru-RU": "Russo",
|
||||
"zh-CN": "Cinese (Semplificato)",
|
||||
"zh-TW": "Cinese (Tradizionale)",
|
||||
@@ -625,6 +656,7 @@
|
||||
"EPERM": "BSManager non ha i permessi necessari per condividere la cartella.",
|
||||
"EACCES": "BSManager non ha i permessi necessari per condividere la cartella.",
|
||||
"ENOSPC": "Il disco è pieno, libera un pò di spazio e riprova.",
|
||||
"LinkingNotSupported": "Il collegamento delle cartelle non è supportato su questo file system.",
|
||||
"UNKNOWN_ERROR": "C'è stato un errore sconosciuto durante la condivisione della cartella."
|
||||
}
|
||||
},
|
||||
@@ -636,7 +668,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Scorciatoia creata",
|
||||
"msg": "La scorciatoia è già stat creata sul desktop."
|
||||
"msg": "La scorciatoia è già stat creata sul desktop.",
|
||||
"msg-steam": "Il collegamento è stato creato nella libreria di Steam."
|
||||
},
|
||||
"error": {
|
||||
"msg": "C'è stato un errore durante la creazione della scorciatoia."
|
||||
@@ -785,6 +818,11 @@
|
||||
"multiple": "Queste mappe saranno rimosse anche dalle versioni che usano mappe condivise"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Eliminare le mappe?",
|
||||
"desc": "Solo la mappa \"{map}\" è un duplicato. Sei sicuro di volerla eliminare?",
|
||||
"desc-plural": "Sono state trovate {nb} mappe duplicate. Sei sicuro di volerle eliminare?"
|
||||
}
|
||||
},
|
||||
"link-contents":{
|
||||
@@ -838,7 +876,9 @@
|
||||
"desc": "Creare una scorciatoia ti permetterà di avviare Beat Saber con le opzioni scelte senza passare per BSManager.",
|
||||
"launch-options": "Opzioni di lancio",
|
||||
"advanced-launch": "Lancio avanzato",
|
||||
"valid-btn": "Crea la scorciatoia"
|
||||
"valid-btn": "Crea la scorciatoia",
|
||||
"create-steam-shortcut": "Crea un scorciatoia Steam",
|
||||
"steam-shortcut-tippy": "Se abilitato, invece di creare un scorciatoia sul desktop, verrà creato in Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Connettiti a Meta",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "固定",
|
||||
"unpin": "固定解除"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "環境変数の文字列を正しく解析できませんでした。"
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "時代遅れ"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "このバージョンは古いため、一部のMODや機能が期待通りに動作しない可能性があります。最新の機能やバグ修正を楽しむには、推奨バージョン ({recommendedVersion}) のBeat Saberを使用することをお勧めします。",
|
||||
"advanced-launch": {
|
||||
"button": "起動オプション",
|
||||
"placeholder": "追加引数 例:--revert; --nowait"
|
||||
"placeholder": "起動オプション 例:KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "Steamをスキップ",
|
||||
"skipsteam-description": "Beat Saberと一緒にSteamが自動的に開くのを防ぎます。SteamVRが干渉する可能性のあるWiVRnやMonadoなど、別のVRランタイムを使用している場合は有効にしてください。",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "マップをインポート",
|
||||
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
|
||||
},
|
||||
"sort": {
|
||||
"name": "名前",
|
||||
"song-author": "曲の作者",
|
||||
"map-author": "地図の作者",
|
||||
"bpm": "BPM",
|
||||
"duration": "持続時間",
|
||||
"likes": "いいね",
|
||||
"date-uploaded": "アップロード日"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "プレイリストをインポート",
|
||||
"subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート"
|
||||
},
|
||||
"sort": {
|
||||
"title": "タイトル",
|
||||
"author": "著者",
|
||||
"number-of-maps": "地図の数",
|
||||
"duration": "持続時間",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "すでにインストール済みのMOD",
|
||||
"description": "選択したすべてのMODはすでにインストールされています"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "古いMod",
|
||||
"title-plural": "古いMod",
|
||||
"description": "{name} Modが古くなっています。更新しますか?",
|
||||
"description-plural": "このバージョンには古いModが{nb}個あります。更新しますか?",
|
||||
"dont-remind-me": "もう知らせない",
|
||||
"update": "更新する"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -211,6 +240,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -223,6 +253,7 @@
|
||||
"es-ES": "スペイン語",
|
||||
"it-IT": "イタリア語",
|
||||
"de-DE": "ドイツ語",
|
||||
"pt-BR": "ポルトガル語 (ブラジル)",
|
||||
"ru-RU": "ロシア語",
|
||||
"zh-CN": "簡体字中国",
|
||||
"zh-TW": "繁体字中国",
|
||||
@@ -625,6 +656,7 @@
|
||||
"EPERM": "BSManagerにはフォルダをリンクするための必要な権限がありません。",
|
||||
"EACCES": "BSManagerにはフォルダをリンクするための必要な権限がありません。",
|
||||
"ENOSPC": "ディスクがいっぱいです。空き容量を作ってもう一度試してください。",
|
||||
"LinkingNotSupported": "このファイルシステムではフォルダのリンクはサポートされていません。",
|
||||
"UNKNOWN_ERROR": "フォルダをリンク中に不明なエラーが発生しました。"
|
||||
}
|
||||
},
|
||||
@@ -636,7 +668,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "ショートカットを作成しました",
|
||||
"msg": "デスクトップにショートカットを作成されました。"
|
||||
"msg": "デスクトップにショートカットを作成されました。",
|
||||
"msg-steam": "ショートカットがSteamライブラリに作成されました。"
|
||||
},
|
||||
"error": {
|
||||
"msg": "ショートカットの作成中にエラーが発生しました。"
|
||||
@@ -843,7 +876,9 @@
|
||||
"desc": "ショートカットを作成すると、BSManagerを起動せずに、直接Beat Saberを起動できます。",
|
||||
"launch-options": "起動オプション",
|
||||
"advanced-launch": "高度な起動",
|
||||
"valid-btn": "ショートカットを作成"
|
||||
"valid-btn": "ショートカットを作成",
|
||||
"create-steam-shortcut": "Steamショートカットを作成",
|
||||
"steam-shortcut-tippy": "有効にすると、デスクトップにショートカットを作成する代わりに、Steam内に作成されます。"
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Metaに接続する",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "고정",
|
||||
"unpin": "고정 해제"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "환경 변수 문자열을 제대로 파싱할 수 없습니다."
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "구식"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "이 버전은 오래되어 일부 모드나 기능이 예상대로 작동하지 않을 수 있습니다. 최신 기능과 버그 수정을 사용하려면 권장 버전({recommendedVersion})의 Beat Saber를 사용하는 것이 좋습니다.",
|
||||
"advanced-launch": {
|
||||
"button": "실행 옵션",
|
||||
"placeholder": "추가 인수 예: --revert; --nowait"
|
||||
"placeholder": "실행 옵션 예: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "Steam 실행 건너뛰기",
|
||||
"skipsteam-description": "Beat Saber와 함께 Steam이 자동으로 열리는 것을 방지합니다. SteamVR이 간섭할 수 있는 WiVRn 또는 Monado와 같은 다른 VR 런타임을 사용하는 경우 활성화하세요.",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "맵 가져오기",
|
||||
"subtext": "ZIP 파일을 여기에 드롭하여 맵 가져오기"
|
||||
},
|
||||
"sort": {
|
||||
"name": "이름",
|
||||
"song-author": "작곡가",
|
||||
"map-author": "지도 제작자",
|
||||
"bpm": "BPM",
|
||||
"duration": "지속 시간",
|
||||
"likes": "좋아요",
|
||||
"date-uploaded": "업로드 날짜"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "재생 목록 가져오기",
|
||||
"subtext": "\".bplist\" 또는 \".json\" 파일을 여기에 드롭하여 가져오기"
|
||||
},
|
||||
"sort": {
|
||||
"title": "제목",
|
||||
"author": "저자",
|
||||
"number-of-maps": "지도 수",
|
||||
"duration": "지속 시간",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +153,16 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "이미 설치된 모드",
|
||||
"description": "선택한 모든 모드가 이미 설치되었습니다."
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "구식 모드",
|
||||
"title-plural": "구식 모드",
|
||||
"description": "{name} 모드가 오래되었습니다. 업데이트하시겠습니까?",
|
||||
"description-plural": "이 버전에는 {nb}개의 구식 모드가 있습니다. 업데이트하시겠습니까?",
|
||||
"dont-remind-me": "다시는 알리지 않기",
|
||||
"update": "업데이트"
|
||||
}
|
||||
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "모드를 가져오기",
|
||||
@@ -211,6 +241,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -223,6 +254,7 @@
|
||||
"es-ES": "스페인어",
|
||||
"it-IT": "이탈리아어",
|
||||
"de-DE": "독일어",
|
||||
"pt-BR": "포르투갈 인 (브라질)",
|
||||
"ru-RU": "러시아어",
|
||||
"zh-CN": "중국어 (간체)",
|
||||
"zh-TW": "중국어 (번체)",
|
||||
@@ -625,6 +657,7 @@
|
||||
"EPERM": "BSManager에 폴더를 연결하기 위한 권한이 부족합니다.",
|
||||
"EACCES": "BSManager에 폴더를 연결하기 위한 권한이 부족합니다.",
|
||||
"ENOSPC": "디스크가 가득 찼습니다. 공간을 확보하고 다시 시도하세요.",
|
||||
"LinkingNotSupported": "이 파일 시스템에서는 폴더 연결이 지원되지 않습니다.",
|
||||
"UNKNOWN_ERROR": "폴더를 연결하는 중 알 수 없는 오류가 발생했습니다."
|
||||
}
|
||||
},
|
||||
@@ -636,7 +669,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "바로 가기 생성 완료",
|
||||
"msg": "바탕 화면에 바로 가기가 생성되었습니다."
|
||||
"msg": "바탕 화면에 바로 가기가 생성되었습니다.",
|
||||
"msg-steam": "바로 가기가 Steam 라이브러리에 생성되었습니다."
|
||||
},
|
||||
"error": {
|
||||
"msg": "바로 가기 생성 중 오류가 발생했습니다."
|
||||
@@ -838,12 +872,14 @@
|
||||
"title": "공유 폴더 추가",
|
||||
"description": "「{folder}」폴더를 연결할 때 문제가 발생할 수 있습니다. 정말 추가하시겠습니까?"
|
||||
},
|
||||
"create-launcher-shortcut": {
|
||||
"title": "런처 바로가기 만들기",
|
||||
"description": "바탕화면에 실행 파일의 바로가기를 만들어 BSManager를 손쉽게 실행하세요.",
|
||||
"buttons": {
|
||||
"submit": "바로가기 만들기"
|
||||
}
|
||||
"create-launch-shortcut": {
|
||||
"title": "바로가기를 생성",
|
||||
"desc": "바로가기를 생성하면 BSManager를 거치지 않고 선택한 옵션으로 Beat Saber를 시작할 수 있습니다.",
|
||||
"launch-options": "실행 옵션",
|
||||
"advanced-launch": "고급 실행",
|
||||
"valid-btn": "바로가기 생성",
|
||||
"create-steam-shortcut": "Steam 바로가기를 생성",
|
||||
"steam-shortcut-tippy": "활성화하면 바탕 화면에 바로가기를 생성하는 대신 Steam에 생성됩니다."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Meta에 연결하기",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "Закрепить",
|
||||
"unpin": "Открепить"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "Не удалось правильно разобрать строку переменной окружения."
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "устаревший"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "Эта версия устарела, и некоторые моды или функции могут работать не так, как ожидалось. Рекомендуется использовать рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться последними функциями и исправлениями ошибок.",
|
||||
"advanced-launch": {
|
||||
"button": "Параметры запуска",
|
||||
"placeholder": "Параметры запуска, например: --revert; --nowait"
|
||||
"placeholder": "Параметры запуска, например: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "Пропустить Steam",
|
||||
"skipsteam-description": "Предотвращает автоматическое открытие Steam с Beat Saber, включите, если вы используете другую VR-среду, такую как WiVRn или Monado, с которой SteamVR может мешать.",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте свои карты",
|
||||
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
|
||||
},
|
||||
"sort": {
|
||||
"name": "Имя",
|
||||
"song-author": "Автор песни",
|
||||
"map-author": "Автор карты",
|
||||
"bpm": "BPM",
|
||||
"duration": "Длительность",
|
||||
"likes": "Лайки",
|
||||
"date-uploaded": "Дата загрузки"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте ваши плейлисты",
|
||||
"subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Заголовок",
|
||||
"author": "Автор",
|
||||
"number-of-maps": "Количество карт",
|
||||
"duration": "Длительность",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "Моды уже установлены",
|
||||
"description": "Все выбранные моды уже установлены"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "Устаревший мод",
|
||||
"title-plural": "Устаревшие моды",
|
||||
"description": "Мод {name} устарел. Хотите его обновить?",
|
||||
"description-plural": "В этой версии есть {nb} устаревших модов. Хотите их обновить?",
|
||||
"dont-remind-me": "Не напоминать мне",
|
||||
"update": "Обновить"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -211,6 +240,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -223,6 +253,7 @@
|
||||
"es-ES": "Испанский",
|
||||
"it-IT": "Итальянец",
|
||||
"de-DE": "Немецкий",
|
||||
"pt-BR": "португальский (Бразилия)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "Китайский (упрощенный)",
|
||||
"zh-TW": "Китайский (традиционный)",
|
||||
@@ -625,6 +656,7 @@
|
||||
"EPERM": "BSManager не имеет необходимых прав для связывания папки.",
|
||||
"EACCES": "BSManager не имеет необходимых прав для связывания папки.",
|
||||
"ENOSPC": "Диск заполнен, освободите место и попробуйте снова.",
|
||||
"LinkingNotSupported": "Ссылки на папки не поддерживаются в этой файловой системе.",
|
||||
"UNKNOWN_ERROR": "Произошла неизвестная ошибка при связывании папки."
|
||||
}
|
||||
},
|
||||
@@ -636,7 +668,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Ярлык создан",
|
||||
"msg": "Ярлык находится на рабочем столе."
|
||||
"msg": "Ярлык находится на рабочем столе.",
|
||||
"msg-steam": "Ярлык был создан в библиотеке Steam."
|
||||
},
|
||||
"error": {
|
||||
"msg": "Ошибка создания ярлыка."
|
||||
@@ -843,7 +876,9 @@
|
||||
"desc": "Ярлык позволит вам запустить Beat Saber без помощи BSManager.",
|
||||
"launch-options": "Параметры запуска",
|
||||
"advanced-launch": "Расширенный запуск",
|
||||
"valid-btn": "Создать ярлык"
|
||||
"valid-btn": "Создать ярлык",
|
||||
"create-steam-shortcut": "Создать ярлык для Steam",
|
||||
"steam-shortcut-tippy": "Если включено, вместо создания ярлыка на рабочем столе, он будет создан в Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Подключение к Meta",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "固定",
|
||||
"unpin": "取消固定"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "無法正確解析環境變數字串。"
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "過時"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "此版本已過時,某些模組或功能可能無法按預期運作。建議使用推薦版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修復。",
|
||||
"advanced-launch": {
|
||||
"button": "啟動選項",
|
||||
"placeholder": "額外啟動參數,例如: --revert; --nowait"
|
||||
"placeholder": "啟動選項,例如: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "跳過 Steam",
|
||||
"skipsteam-description": "防止 Steam 與 Beat Saber 自動打開,如果您使用的是其他 VR 執行時,如 WiVRn 或 Monado,SteamVR 可能會干擾,請啟用此選項。",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "導入您的地圖",
|
||||
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
|
||||
},
|
||||
"sort": {
|
||||
"name": "名字",
|
||||
"song-author": "歌曲作者",
|
||||
"map-author": "地圖作者",
|
||||
"bpm": "BPM",
|
||||
"duration": "持續時間",
|
||||
"likes": "喜歡",
|
||||
"date-uploaded": "上傳日期"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "匯入您的播放列表",
|
||||
"subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入"
|
||||
},
|
||||
"sort": {
|
||||
"title": "標題",
|
||||
"author": "作者",
|
||||
"number-of-maps": "地圖數量",
|
||||
"duration": "持續時間",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "模組已安裝",
|
||||
"description": "所有選中的模組已經安裝"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "過時的模組",
|
||||
"title-plural": "過時的模組",
|
||||
"description": "{name} 模組已過時。您想更新嗎?",
|
||||
"description-plural": "此版本有 {nb} 個過時的模組。您想更新它們嗎?",
|
||||
"dont-remind-me": "不要提醒我",
|
||||
"update": "更新"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -211,6 +240,7 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "簡體中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -223,6 +253,7 @@
|
||||
"es-ES": "西班牙語",
|
||||
"it-IT": "義大利語",
|
||||
"de-DE": "德語",
|
||||
"pt-BR": "葡萄牙語 (巴西)",
|
||||
"ru-RU": "俄語",
|
||||
"zh-CN": "簡體中文",
|
||||
"zh-TW": "正體中文",
|
||||
@@ -625,6 +656,7 @@
|
||||
"EPERM": "BSManager沒有必要的權限來連結文件夾。",
|
||||
"EACCES": "BSManager沒有必要的權限來連結文件夾。",
|
||||
"ENOSPC": "磁碟已滿,請騰出空間後再試。",
|
||||
"LinkingNotSupported": "該檔案系統不支援資料夾連結。",
|
||||
"UNKNOWN_ERROR": "連結文件夾時發生未知錯誤。"
|
||||
}
|
||||
},
|
||||
@@ -636,7 +668,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "捷徑已創建",
|
||||
"msg": "捷徑已在桌面上創建"
|
||||
"msg": "捷徑已在桌面上創建",
|
||||
"msg-steam": "捷徑已在 Steam 庫中建立。"
|
||||
},
|
||||
"error": {
|
||||
"msg": "創建捷徑時發生錯誤"
|
||||
@@ -843,7 +876,9 @@
|
||||
"desc": "創建捷徑使得你可以在不通過 BSManager 的情況下以特定選項啟動 BeatSaber",
|
||||
"launch-options": "啟動選項",
|
||||
"advanced-launch": "高級啟動",
|
||||
"valid-btn": "創建捷徑"
|
||||
"valid-btn": "創建捷徑",
|
||||
"create-steam-shortcut": "建立 Steam 快捷方式",
|
||||
"steam-shortcut-tippy": "如果啟用,將不會在桌面建立捷徑,而是在 Steam 中建立。"
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "連接到 Meta",
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"pin": "固定",
|
||||
"unpin": "取消固定"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "无法正确解析环境变量字符串。"
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "过时"
|
||||
},
|
||||
@@ -51,7 +56,7 @@
|
||||
"outdated-tippy": "此版本已过时,某些模组或功能可能无法按预期运行。建议使用推荐版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修复。",
|
||||
"advanced-launch": {
|
||||
"button": "启动选项",
|
||||
"placeholder": "额外启动参数,例如: --revert; --nowait"
|
||||
"placeholder": "启动选项,例如: KEY=VALUE %command% fpfc"
|
||||
},
|
||||
"skipsteam": "跳过 Steam",
|
||||
"skipsteam-description": "防止 Steam 与 Beat Saber 自动打开,如果您使用的是其他 VR 运行时,如 WiVRn 或 Monado,SteamVR 可能会干扰,请启用此选项。",
|
||||
@@ -91,6 +96,15 @@
|
||||
"drop-zone": {
|
||||
"text": "导入您的地图",
|
||||
"subtext": "将ZIP文件拖放到此处以导入您的地图"
|
||||
},
|
||||
"sort": {
|
||||
"name": "名字",
|
||||
"song-author": "歌曲作者",
|
||||
"map-author": "地图作者",
|
||||
"bpm": "BPM",
|
||||
"duration": "持续时间",
|
||||
"likes": "喜欢",
|
||||
"date-uploaded": "上传日期"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -102,6 +116,13 @@
|
||||
"drop-zone": {
|
||||
"text": "导入您的播放列表",
|
||||
"subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入"
|
||||
},
|
||||
"sort": {
|
||||
"title": "标题",
|
||||
"author": "作者",
|
||||
"number-of-maps": "地图数量",
|
||||
"duration": "持续时间",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +153,14 @@
|
||||
"all-mods-already-installed": {
|
||||
"title": "模组已安装",
|
||||
"description": "所有选中的模组已经安装"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "过时的模组",
|
||||
"title-plural": "过时的模组",
|
||||
"description": "{name} 模组已过时。您想更新吗?",
|
||||
"description-plural": "此版本有 {nb} 个过时的模组。您想更新它们吗?",
|
||||
"dont-remind-me": "不要提醒我",
|
||||
"update": "更新"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
@@ -223,6 +252,7 @@
|
||||
"es-ES": "西班牙语",
|
||||
"it-IT": "意大利语",
|
||||
"de-DE": "德语",
|
||||
"pt-BR": "葡萄牙语 (巴西)",
|
||||
"ru-RU": "俄语",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正体中文",
|
||||
@@ -625,6 +655,7 @@
|
||||
"EPERM": "BSManager没有必要的权限来链接文件夹。",
|
||||
"EACCES": "BSManager没有必要的权限来链接文件夹。",
|
||||
"ENOSPC": "磁盘已满,请腾出空间后再试。",
|
||||
"LinkingNotSupported": "该文件系统不支持文件夹链接。",
|
||||
"UNKNOWN_ERROR": "链接文件夹时发生未知错误。"
|
||||
}
|
||||
},
|
||||
@@ -636,7 +667,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "快捷方式已创建",
|
||||
"msg": "快捷方式已在桌面上创建"
|
||||
"msg": "快捷方式已在桌面上创建",
|
||||
"msg-steam": "快捷方式已在 Steam 库中创建。"
|
||||
},
|
||||
"error": {
|
||||
"msg": "创建快捷方式时发生错误"
|
||||
@@ -843,7 +875,9 @@
|
||||
"desc": "创建快捷方式使得你可以在不通过 BSManager 的情况下以特定选项启动 BeatSaber",
|
||||
"launch-options": "启动选项",
|
||||
"advanced-launch": "高级启动",
|
||||
"valid-btn": "创建快捷方式"
|
||||
"valid-btn": "创建快捷方式",
|
||||
"create-steam-shortcut": "创建 Steam 快捷方式",
|
||||
"steam-shortcut-tippy": "如果启用,将不会在桌面创建快捷方式,而是在 Steam 中创建。"
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "连接到 Meta",
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
# Add write permissions to anybody so that this can be sync with the
|
||||
# github's bs-versions.json when starting bsmanager
|
||||
/usr/bin/chmod +002 /opt/BSManager/resources/assets/jsons/bs-versions.json
|
||||
|
||||
# https://github.com/electron/electron/issues/42510
|
||||
/usr/bin/chmod 4755 /opt/BSManager/chrome-sandbox
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
If you encounter a keyboard shortcut conflict between AMD Software and Oculus Rift, you can either disable or modify the conflicting shortcut.
|
||||
|
||||
_Special thanks to_ `𝔽𝕪𝕟𝕟 (fynn07)` _for its contribution with information and screenshots_
|
||||
### __Follow these steps to disable the shortcut in AMD Software:__
|
||||
# Step 1 - Access Settings:
|
||||
- Open AMD Software and click on the gear icon in the top right corner to go to settings.
|
||||
|
||||

|
||||
# Step 2 - Modify Keyboard Shortcuts:
|
||||
- In the settings menu, select the "Hotkey" tab.
|
||||
|
||||

|
||||
# Step 3 -Disabling Shortcuts:
|
||||
- Turn off the "Use Hotkeys" option to prevent any conflicts with Oculus Rift.
|
||||
|
||||

|
||||
# Step 3 alt - Modify the Shortcut:
|
||||
- If you prefer to keep using keyboard shortcuts, consider changing the specific conflicting shortcut (e.g., "Ctrl + Shift + I") to another combination that is less likely to interfere.
|
||||
|
||||
ℹ️ _**Keep in mind you can't change the full keybind!**_
|
||||
_**You can only Change the "i" to an different Button!**_ ℹ️
|
||||
@@ -0,0 +1,60 @@
|
||||
## Table of Contents
|
||||
|
||||
- [With BSManager](#with-bsmanager)
|
||||
- [Install playlists](#how-to-install-playlists-with-bsmanager)
|
||||
- [Import playlists](#how-to-import-playlist-with-bsmanager)
|
||||
- [Manual Installation](#manual-installation)
|
||||
- [One-Click Installation](#one-click-installation)
|
||||
|
||||
## With BSManager
|
||||
|
||||
### How to Install Playlists with BSManager
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
2. Launch BSManager and select the version of *Beat Saber* where you want to install playlists from the list on the left side of the interface.
|
||||
3. Go to the `Playlist` section by clicking on the __`Maps`__ tab.
|
||||
4. Click the __`Add`__ button, then select __`Browse playlist`__ to open the list of online playlists.
|
||||
5. Use the search bar at the top of the interface to filter playlists or sort the list.
|
||||
6. Download to install it.
|
||||
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) and enjoy your new playlists!
|
||||
|
||||
> [!NOTE]
|
||||
> You can click on a playlist's image to view the maps it contains and download only the maps you want.
|
||||
|
||||
### How to Import Playlist with BSManager
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
2. Launch BSManager and select the version of *Beat Saber* where you want to install playlists from the list on the left side of the interface.
|
||||
3. Go to the `playlist` section by clicking on the __`Maps`__ tab.
|
||||
4. From here, there are two possible methods:
|
||||
1. Click the __`Add`__ button, then select __`Import a playlist`__ and choose the file to import.
|
||||
2. Drag and drop your file directly onto the playlist interface in BSManager.
|
||||
5. Wait for the import to complete. Once finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) and enjoy your new playlists!
|
||||
|
||||
> [!Important]
|
||||
> The file must be in `.bplist` or `.json` format to be imported.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
1. Download your preferred custom playlists from trusted sources. Ensure that they are in __`.bplist`__ format.
|
||||
2. Open [BSManager](https://www.bsmanager.io) and select the version of Beat Saber in which you want to install the playlist.
|
||||
3. Click on the version options at the top-right corner of the BSM interface, then select __`Open Folder`__.
|
||||
4. In the folder for the Beat Saber version, locate the __`Playlists`__ folder. If this folder doesn't exist, create it.
|
||||
5. Copy and paste the downloaded __`.bplist`__ files of your custom playlists into this folder.
|
||||
6. Launch the game with BSM, navigate to your playlist, click on it, and download it.
|
||||
|
||||
> [!WARNING]
|
||||
> Exercise caution when downloading custom playlists for Beat Saber. Only use reliable sources to avoid any risks to your computer.
|
||||
|
||||
## One-Click Installation
|
||||
|
||||
1. Launch [BSManager](https://www.bsmanager.io) and make sure that the One-Click installation feature is enabled for playlists.
|
||||
2. Go to one of the supported websites and search for the playlist you want to install.
|
||||
3. Once you've found the playlist, click on the __`One-Click`__ or __`Install`__ button on the website.
|
||||
4. A pop-up window will appear, asking for confirmation. Click on __`Yes`__ to proceed with the installation.
|
||||
@@ -0,0 +1,55 @@
|
||||
## Table of Contents
|
||||
|
||||
- [With BSManager](#with-bsmanager)
|
||||
- [Install maps](#how-to-install-maps-with-bsmanager)
|
||||
- [Import maps](#how-to-import-maps-with-bsmanager)
|
||||
- [Manual Installation](#manual-installation)
|
||||
- [One-Click Installation](#one-click-installation)
|
||||
|
||||
## With BSManager
|
||||
|
||||
### How to Install Maps with BSManager
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
2. Launch BSManager and select the version of *Beat Saber* where you want to install maps from the list on the left side of the interface.
|
||||
3. Go to the `Maps` section by clicking on the __`Maps`__ tab.
|
||||
4. Click the __`Add`__ button to access the list of online maps.
|
||||
5. Use the search bar at the top of the interface to filter maps by __`name`__, __`type`__, __`mods`__, __`tags`__, __`notes per second`__, or __`duration`__, and order the list by __`latest`__, __`relevance`__, __`rating`__, or __`curated`__.
|
||||
6. Download to install it.
|
||||
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new maps!
|
||||
|
||||
### How to Import Maps with BSManager
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
2. Launch BSManager and select the version of *Beat Saber* where you want to install maps from the list on the left side of the interface.
|
||||
3. Go to the `Maps` section by clicking on the __`Maps`__ tab.
|
||||
4. From here, there are two possible methods:
|
||||
1. Click the __`Add`__ button, then select __`Import maps`__ and choose the .zip file to import.
|
||||
2. Drag and drop your .zip file directly onto the maps interface in BSManager.
|
||||
5. Wait for the import to complete. Once finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new maps!
|
||||
|
||||
> [!Important]
|
||||
> The file must be in `.zip` format to be imported.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
1. Find the map you want to install on a site like [BeatSaver](https://beatsaver.com).
|
||||
2. Download the map to your computer.
|
||||
3. Open [BSManager](https://www.bsmanager.io) and select the version of Beat Saber to which you want to install the map.
|
||||
4. Click on the version options at the top-right corner of the interface, then select __`Open Folder`__.
|
||||
5. Move the downloaded map `.zip` file into the __`CustomLevels`__ folder, which is located at `Beat Saber_Data\CustomLevels`.
|
||||
6. Right-click the `.zip` file, select __`Extract All`__, then press __`Extract`__ in the popup. You can then delete the `.zip` file.
|
||||
7. Launch Beat Saber, and the map should now be available to play.
|
||||
|
||||
## One-Click Installation
|
||||
|
||||
1. Launch [BSManager](https://www.bsmanager.io) and make sure that the One-Click installation feature is enabled for maps.
|
||||
2. Go to one of the supported websites and search for the map you want to install.
|
||||
3. Once you've found the map, click on the __`One-Click`__ or __`Install`__ button on the website.
|
||||
4. A pop-up window will appear, asking for confirmation. Click on __`Yes`__ to proceed with the installation.
|
||||
@@ -0,0 +1,33 @@
|
||||
> [!WARNING]
|
||||
>
|
||||
> __The models require mods to work properly! Don’t forget to download them!__
|
||||
>
|
||||
> - For custom sabers, use [CustomSabersLite](https://github.com/qqrz997/CustomSabersLite) or an equivalent mod.
|
||||
> - For custom avatars, use [Custom Avatars](https://github.com/nicoco007/BeatSaberCustomAvatars) or an equivalent mod.
|
||||
> - For custom blocks, use [Custom Notes](https://github.com/legoandmars/BeatSaberCustomNotes) or an equivalent mod.
|
||||
> - For custom platforms, use [Custom Platforms](https://github.com/affederaffe/CustomPlatforms) or an equivalent mod.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [With BSManager](#with-bsmanager)
|
||||
- [One-Click Installation](#one-click-installation)
|
||||
|
||||
## With BSManager
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
2. Launch [BSManager](https://www.bsmanager.io) and select the Beat Saber version where you want to install models from the list on the left of the interface.
|
||||
3. Go to models by clicking on the __`Models`__ tab.
|
||||
4. Click on the __`Add`__ button to access the list of online models.
|
||||
5. Find the models that you want to install from the provided list. You can use the search bar at the top of the interface to filter models by __`name`__, __`type`__, and order the list by __`date`__, __`name`__, or __`author`__.
|
||||
6. Download to install the model.
|
||||
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new models!
|
||||
|
||||
## One-Click Installation
|
||||
|
||||
1. Launch [BSManager](https://www.bsmanager.io) and make sure that the One-Click installation feature is enabled for models.
|
||||
2. Go to one of the supported websites and search for the models you want to install.
|
||||
3. Once you've found the models, click on the __`One-Click`__ or __`Install`__ button on the website.
|
||||
4. A pop-up window will appear, asking for confirmation. Click on __`Yes`__ to proceed with the installation.
|
||||
@@ -0,0 +1,61 @@
|
||||
## Table of Contents
|
||||
|
||||
- [With BSManager](#with-bsmanager)
|
||||
- [Install mods](#how-to-install-mods-with-bsmanager)
|
||||
- [Import mods](#how-to-import-mods-with-bsmanager)
|
||||
- [Manual Installation](#manual-installation)
|
||||
|
||||
## With BSManager
|
||||
|
||||
### How to Install Mods with BSManager
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
2. Launch [BSManager](https://www.bsmanager.io) and select the version of Beat Saber that you want to use from the list on the left side of the interface.
|
||||
3. Go to mods by clicking on the __`Mods`__ tab.
|
||||
4. Find the mods that you want to install from the provided list. You can use the search bar at the top of the interface to filter mods by name.
|
||||
5. Check the box next to the name of each mod that you want to install.
|
||||
6. Click __`Install or Update`__ at the bottom-right corner of the interface.
|
||||
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new mods!
|
||||
|
||||
### How to Import Mods with BSManager
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
2. Launch BSManager and select the version of *Beat Saber* where you want to install mods from the list on the left side of the interface.
|
||||
3. Go to the `Mods` tab.
|
||||
4. From here, there are two possible methods:
|
||||
1. Click on __`⁝`__ button, then select __`Import mods`__ and choose the .zip or .dll file to import.
|
||||
2. Drag and drop your .zip or .dll file directly onto the mods interface in BSManager.
|
||||
5. Wait for the import to complete. Once finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new mods!
|
||||
|
||||
> [!Important]
|
||||
> The file must be in `.zip` or `.dll` format to be imported.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This guide assumes your game already has core mods installed
|
||||
>
|
||||
> If it doesn't: open [BSManager](https://www.bsmanager.io), select the game version you want to mod, go to the Mods tab, and click __`Install or Update`__. Then, run your game once, go to the main menu, and close it. ℹ️*
|
||||
|
||||
1. Open [BSManager](https://www.bsmanager.io) and select the version of Beat Saber you want to install the mod to.
|
||||
2. Click on the version options button at the top-right corner, then select __`Open Folder`__.
|
||||
3. Download the desired mod from a trusted source, often found on community GitHub repositories. Search for the mod's name followed by "GitHub" in your search engine.
|
||||
4. Installing Different Types of Mods:
|
||||
- __For mods that come as a single `.dll` file:__ If the mod consists of a single __`.dll`__ file, copy this file into the __`Plugins`__ folder in the directory you opened in step 2.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Do __not__ place files in __`Beat Saber_Data\Plugins`__. If you do not see a __`Plugins`__ folder directly in the folder you opened in step 2, make sure you have core mods installed and have run your game at least once after installing mods.
|
||||
|
||||
- __For mods that come as a `.zip` file:__ If the mod includes multiple files or folders, you need to maintain the provided folder structure. Double-click the zip file to open it, press __`CTRL+A`__ and then __`CTRL+C`__ to copy all files/folders inside the zip, go to the directory you opened in step 2, and press __`CTRL+V`__ to paste them there. Do __not__ paste them in any subfolder, such as __`Plugins`__ or __`Beat Saber_Data`__. If you are prompted to merge folders or overwrite files, click "Yes."
|
||||
|
||||
5. The mod should now be installed. Run Beat Saber to check if the mod functions correctly. If it doesn't, double-check that you:
|
||||
- Have all required dependencies installed. These are typically listed somewhere on the mod's release page.
|
||||
- Placed the mod in the correct install folder.
|
||||
- Downloaded a mod intended for your game version.
|
||||
@@ -0,0 +1,58 @@
|
||||
## Table of Contents
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Steps to Contribute](#steps-to-contribute)
|
||||
- [1. Create a Fork on GitHub](#1-create-a-fork-on-github)
|
||||
- [2. Clone Your Fork](#2-clone-your-fork)
|
||||
- [3. Make Your Changes](#3-make-your-changes)
|
||||
- [4. Submit a Pull Request](#4-submit-a-pull-request)
|
||||
|
||||
## Introduction
|
||||
|
||||
Thank you for your interest in improving the wiki! This guide will help you through the process of contributing, whether you’re adding new pages, fixing errors, or enhancing existing content.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before contributing, make sure you have:
|
||||
|
||||
1. A GitHub account.
|
||||
2. Basic knowledge of Markdown.
|
||||
|
||||
## Steps to Contribute
|
||||
|
||||
### 1. Create a Fork on GitHub
|
||||
|
||||
To contribute, start by creating a fork of the repository:
|
||||
|
||||
1. Go to the [GitHub page of the repository](https://github.com/Zagrios/bs-manager).
|
||||
2. Click on the __`Fork`__ button in the top-right corner of the page.
|
||||
3. Once the fork is created, it will appear under your GitHub account.
|
||||
|
||||
### 2. Clone Your Fork
|
||||
|
||||
Now, clone your fork to your local machine for editing:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/[your-username]/[repository-name].git
|
||||
cd [repository-name]
|
||||
```
|
||||
|
||||
### 3. Make Your Changes
|
||||
|
||||
Make edits, add new pages, or update content as needed. Then commit and push your changes to your fork:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Describe your changes here"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
### 4. Submit a Pull Request
|
||||
|
||||
Once your changes are ready:
|
||||
|
||||
1. Go to the __[original repository](https://github.com/Zagrios/bs-manager)__ on GitHub.
|
||||
2. Click the __`Pull Requests`__ tab, then __`New Pull Request`__.
|
||||
3. Compare changes between your fork and the original repository.
|
||||
4. Add a clear description of your changes and submit the pull request.
|
||||
|
After Width: | Height: | Size: 112 KiB |
@@ -0,0 +1,46 @@
|
||||
## Table of Contents
|
||||
|
||||
- [How to create a shortcut on Windows](#how-to-create-a-shortcut-on-windows)
|
||||
- [On desktop](#on-desktop)
|
||||
- [On taskbar](#on-taskbar)
|
||||
- [How to create a shortcut on Steam](#how-to-create-a-shortcut-on-steam)
|
||||
|
||||
## How to create a shortcut on Windows
|
||||
|
||||
### On Desktop
|
||||
|
||||
1. Launch [BSManager](https://www.bsmanager.io) and select your version.
|
||||
2. Click on the gear icon at the top-right corner of the version.
|
||||
3. Click on the __`Create a Shortcut`__ button.
|
||||
4. Choose your launch options.
|
||||
5. Click on __`Create a Shortcut`__. It should now appear on your desktop.
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Guides/Installation-and-updates/Create-a-shortcut/BsmShortcutInterface.png" alt="SeeTxtFile.png" />
|
||||
</div>
|
||||
|
||||
### On Taskbar
|
||||
|
||||
1. Open Notepad and drag & drop the version shortcut you need.
|
||||
2. Grab the version link (e.g., `bsmanager://launch/?version=1.34.2&versionIno=19140298416807704`) - More info [here](#how-to-create-a-shortcut-on-steam).
|
||||
3. Open a new Notepad window and type the following:
|
||||
|
||||
```bat
|
||||
@echo off
|
||||
start <version link>
|
||||
```
|
||||
|
||||
4. Save the file with a `.bat` extension.
|
||||
5. Create a shortcut to this `.bat` file. Edit the shortcut path to: `cmd.exe /C <shortcut path>`.
|
||||
|
||||
## How to create a shortcut on Steam
|
||||
|
||||
1. Launch [BSManager](https://www.bsmanager.io) and select your version.
|
||||
2. Click on the gear icon at the top-right corner of the version.
|
||||
3. Click on the __`Create a Shortcut`__ button.
|
||||
4. Choose your launch options.
|
||||
5. Click on __`Create a Shortcut`__. It should now appear on your desktop.
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Guides/Installation-and-updates/Create-a-shortcut/SteamShortcutCheck.png" alt="SeeTxtFile.png" />
|
||||
</div>
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,70 @@
|
||||
> [!WARNING]
|
||||
>
|
||||
> [BSManager](https://www.bsmanager.io) does not store your login information. It uses the open-source tool [`DepotDownloader`](https://github.com/SteamRE/DepotDownloader) to download versions of Beat Saber.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Install a Version with Steam](#install-a-version-with-steam)
|
||||
- [Install a Version with Oculus Store (PCVR)](#install-a-version-with-oculus-store-pcvr)
|
||||
- [Import a Version](#how-to-import-a-version)
|
||||
|
||||
## Install a Version with Steam
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
2. Open the __`Add Versions`__ interface and select the version you wish to download.
|
||||
3. Choose __`Steam`__ as your platform.
|
||||
4. Enter your Steam credentials (ID and password) or scan the QR code using the Steam app on your phone.
|
||||
5. Once logged into Steam, the download will start automatically. Please wait for it to complete.
|
||||
6. When the download is finished, select the downloaded version and click the __`Launch`__ button to verify that the game launches correctly.
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Guides/Installation-and-updates/Install-or-import-a-version/SteamLogin.png" alt="SteamLogin.png" />
|
||||
</div>
|
||||
|
||||
## Install a Version with Oculus Store (PCVR)
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - Ensure that Beat Saber is in your __Oculus PCVR__ library to enable downgrading.
|
||||
> - You must run Beat Saber at least once on your computer before attempting to launch retrograde Oculus versions from [BSManager](https://www.bsmanager.io).
|
||||
|
||||
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
|
||||
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
|
||||
- [Official Website](https://www.bsmanager.io)
|
||||
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
|
||||
2. Open the __`Add Versions`__ interface and select the version you wish to download.
|
||||
3. Choose __`Oculus Store`__ as your platform.
|
||||
4. Click on __`Connect to Meta`__ or __`Enter my login token manually`__ based on your preferred login method.
|
||||
|
||||
### If you selected __`Connect to Meta`__
|
||||
|
||||
1. You will be redirected to the Oculus website. Enter your email and follow the instructions to log in.
|
||||
2. Once logged in, the download will start automatically. Please wait for it to complete.
|
||||
3. When the download finishes, select the downloaded version and click the __`Launch`__ button to verify that the game launches correctly.
|
||||
|
||||
### If you selected __`Enter my login token manually`__
|
||||
|
||||
1. Follow [this guide](How-to-obtain-your-Oculus-Token) to obtain your Oculus token.
|
||||
2. Once the token is entered and validated, the download will start automatically. Please wait for it to complete.
|
||||
3. When the download finishes, select the downloaded version and click the __`Launch`__ button to verify that the game launches correctly.
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Guides/Installation-and-updates/Install-or-import-a-version/OculusLogin.png" alt="OculusLogin.png" />
|
||||
<img src="../wiki/Guides/Installation-and-updates/Install-or-import-a-version/OculusLoginToken.png" alt="OculusLoginToken.png" />
|
||||
</div>
|
||||
|
||||
## How to Import a Version
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - You will need to download a version of Beat Saber yourself before you can import it.
|
||||
> - The [BSManager](https://www.bsmanager.io) team provides tutorials like [this one](https://steamcommunity.com/sharedfiles/filedetails/?id=1805934840) to help you download official versions of Beat Saber. However, no team member will offer assistance for cracked versions.
|
||||
|
||||
1. Launch [BSManager](https://www.bsmanager.io).
|
||||
2. Click the gear icon in the top-right corner of the interface.
|
||||
3. Select the __`Import a Version`__ option.
|
||||
4. Choose the file containing the version to be imported.
|
||||
5. Wait a few moments. The imported version should appear alongside the others in the version list.
|
||||
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -1,4 +1,12 @@
|
||||
# Linux Guide
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Ubuntu, Debian (deb)](#ubuntu-debian-deb)
|
||||
- [PPA Repository](#ppa-repository)
|
||||
- [dpkg Install](#dpkg-install)
|
||||
- [Arch (AUR)](#arch-aur)
|
||||
- [Universal (flatpak)](#universal-flatpak)
|
||||
- [Proton Setup](#proton-setup)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -19,6 +27,7 @@ sudo apt update
|
||||
```
|
||||
|
||||
Install the `bs-manager` package using `apt`.
|
||||
|
||||
```bash
|
||||
sudo apt install bs-manager
|
||||
```
|
||||
@@ -26,13 +35,15 @@ sudo apt install bs-manager
|
||||
#### dpkg install
|
||||
|
||||
Download the `.deb` file in the releases and run the following command:
|
||||
|
||||
```bash
|
||||
dpkg -i ./bsmanager.deb
|
||||
```
|
||||
|
||||
**NOTE:** When installed using dpkg, BSManager will not automatically update to the latest version. You have to either:
|
||||
* Download the latest `.deb` file on the Releases page; or
|
||||
* Install thru PPA repository to automatically update with `sudo apt update & sudo apt upgrade`.
|
||||
|
||||
- Download the latest `.deb` file on the Releases page; or
|
||||
- Install thru PPA repository to automatically update with `sudo apt update & sudo apt upgrade`.
|
||||
|
||||
### Arch (AUR)
|
||||
|
||||
@@ -67,55 +78,3 @@ Flatpak also supports sandboxing which gives the minimal access to your machine.
|
||||
[Proton](https://github.com/ValveSoftware/Proton) is needed to run the Beat Saber executable under Linux. You need to download this from either from Steam or building it from their GitHub repo.
|
||||
|
||||
Once Proton is installed, when you open your BSManager application for the first time, it will ask you to link the _Proton Folder_. The _Proton Folder_ also verifies if the `proton` and `files/bin/wine64` binaries exists. Once set, you should be able to launch the Beat Saber (using `proton`) and install mods (using `files/bin/wine64`). You can still change the _Proton Folder_ in the **settings page** if any new version of Steam Proton is downloaded.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Missing Icons in game
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/linux-missing-icons.png" alt="Linux Missing Icons" width="300"/>
|
||||
<br>
|
||||
NOTE: "?" are missing unicode icons
|
||||
</p>
|
||||
|
||||
This is due to [BSML](https://github.com/monkeymanboy/BeatSaberMarkupLanguage) using a missing font (`Segoe UI Symbols`) for unicode emojis.
|
||||
|
||||
To fix the issue, you need to get a copy of `seguisym.ttf` from the fonts folder an official copy Windows. Once you have a copy, add the font file to `~/.steam/steam/steamapps/compatdata/620980/pfx/drive_c/windows/Fonts`.
|
||||
|
||||
## Permission denied on "bs-versions.json"
|
||||
|
||||
<pre>
|
||||
Unhandled Exception UnhandledRejection Error: EACCES: permission denied, open '/opt/BSManager/resources/assets/jsons/bs-versions.json'
|
||||
</pre>
|
||||
|
||||
To fix this issue, the current user must have write permissions to the "bs-versions.json". To correct the permissions do command below:
|
||||
|
||||
```bash
|
||||
chmod +002 /opt/BSManager/resources/assets/jsons/bs-versions.json
|
||||
|
||||
# or
|
||||
|
||||
chown $(whoami) /opt/BSManager/resources/assets/jsons/bs-versions.json
|
||||
```
|
||||
|
||||
## [deb] The SUID sandbox helper binary was found.
|
||||
|
||||
This is encountered when running the app file or executing the app in the terminal. This is due to a change to Ubuntu 24.04. In order to fix the issue, take a look into the path of "chrome-sandbox" described in the error log and give the correct permissions within the terminal, for example:
|
||||
|
||||
```bash
|
||||
chmod 4755 /opt/BSManager/chrome-sandbox
|
||||
```
|
||||
|
||||
ref: https://github.com/electron/electron/issues/42510
|
||||
|
||||
## [Flatpak] Steam Beat Saber version not showing / Proton not detected
|
||||
|
||||
Flatpak should have permissions with the steam games folder. By default, the minimum permissions are `~/.steam/steam/steamapps/common:ro` and `~/.steam/steam/steamapps/common:ro`. If you changed the steam installation path, add that path instead into the permissions.
|
||||
|
||||
## [Flatpak] Changing installation folder
|
||||
|
||||
To change the installation path of the **BSManager** folder, you have to edit the flatpak permissions to destination folder.
|
||||
- In flatpak or Flatseal, add the destination folder with `:create` permissions.
|
||||
- In BSM, move the folder to the destination folder.
|
||||
- [Optional] In flatpak or Flatseal, remove the original folder permissions.
|
||||
|
||||
|
Before Width: | Height: | Size: 687 KiB After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 321 KiB |
@@ -0,0 +1,61 @@
|
||||
> [!WARNING]
|
||||
>
|
||||
> Your token is a confidential piece of information. Possession of this token allows individuals to download applications, send messages, among other actions, under your identity.
|
||||
>
|
||||
>However, you might wonder why it is necessary to provide this token to BSManager. The reason is that BSManager requires the token to continue the download with Oculus. Once you've input the token, it is used exclusively to communicate with Oculus servers to verify that you are the rightful owner of the game.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Step 1 - Install and log into the Oculus Rift app](#step-1---install-and-log-into-the-oculus-rift-app)
|
||||
- [Step 2 - Open developer tools](#step-2---open-developer-tools)
|
||||
- [Step 3 - Copy your Token](#step-3---copy-your-token)
|
||||
|
||||
# Solution 1 - Using the Oculus app
|
||||
|
||||
## Step 1 - Install and log into the Oculus Rift app
|
||||
|
||||
1. Download the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/).
|
||||
2. Install the Oculus Rift app on your computer.
|
||||
|
||||
> [!NOTE]
|
||||
>__If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with [BSManager](https://www.bsmanager.io), first claim it from its store page.__
|
||||
|
||||
## Step 2 - Open developer tools
|
||||
|
||||
1. Open the Oculus app.
|
||||
2. Open the developer tools by pressing `Ctrl` + `Shift` + `i`.
|
||||
|
||||
## Step 3 - Copy your Token
|
||||
|
||||
In the developer tools:
|
||||
|
||||
1. Open the __`Network`__ tab.
|
||||
2. Filter for __`graph`__.
|
||||
3. Click on the first request that appears.
|
||||
4. Open the __`Payload`__ tab.
|
||||
5. Scroll to the bottom to locate your token. It should start with __`FRL`__.
|
||||
6. Select the token using your mouse and press `Ctrl` + `C` to copy it.
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Guides/Login/How-to-obtain-your-Oculus-Token/GetYourOculusToken.png" alt="GetYourOculusToken.png" />
|
||||
</div>
|
||||
|
||||
# Solution 2 - Using the Oculus website
|
||||
|
||||
## Steps
|
||||
|
||||
1. Log in to your Meta account at https://secure.oculus.com
|
||||
2. Open the developer tools in your web browser:
|
||||
- Chrome/Edge/Firefox: Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> or <kbd>F12</kbd>
|
||||
- Safari: Press <kbd>Option</kbd>+<kbd>Command</kbd>+<kbd>I</kbd>
|
||||
3. Go to the "Application" tab
|
||||
4. Expand "Cookies" in the left sidebar
|
||||
5. Click on `https://secure.oculus.com`
|
||||
6. Find the cookie named `oc_ac_at` (value starts with `OC`)
|
||||
7. Copy the entire `oc_ac_at` cookie value - this is your token
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Guides/Login/How-to-obtain-your-Oculus-Token/GetYourOculusTokenBrowser.png" alt="GetYourOculusTokenBrowser.png" />
|
||||
</div>
|
||||
|
||||
Paste the token into BSManager or other trusted apps to manage your Beat Saber game and mods. Keep it confidential.
|
||||
@@ -1,14 +1,72 @@
|
||||
Welcome to the bs-manager wiki! (wip c:)
|
||||
# Welcome to the BSManager Wiki
|
||||
|
||||
to get you oculus token check here : https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token
|
||||
BSManager is your all-in-one tool designed to simplify and enhance your Beat Saber experience. With BSManager, you can effortlessly manage game versions, mods, maps, and more, ensuring a seamless and enjoyable gameplay journey.
|
||||
|
||||
---
|
||||
## 📚 Guides
|
||||
|
||||
This Wiki accepts contributions!
|
||||
Our comprehensive guides are crafted to assist you in maximizing BSManager's capabilities:
|
||||
|
||||
To contribute to the wiki:
|
||||
### 🛠️ Installation and Updates
|
||||
|
||||
- Fork the project
|
||||
- Add/Edit `.md` files in the `docs/wiki/` folder
|
||||
- Create a Pull Request
|
||||
- Once the PR is merged (after review), it will be present in the repository's wiki
|
||||
Step-by-step instructions to install BSManager on your system and keep it up to date
|
||||
|
||||
- [Install BSManager on Linux](install-bsmanager-on-linux)
|
||||
- [Install or import a version](install-or-import-a-version)
|
||||
- [Create a shortcut](create-a-shortcut)
|
||||
|
||||
### 🔑 Login
|
||||
|
||||
Guidance on logging into BSManager to access all features
|
||||
|
||||
- [How to obtain your Oculus Token](How-to-obtain-your-Oculus-Token)
|
||||
|
||||
### 🎮 Content Management
|
||||
|
||||
- __🗺️ Maps__: Learn how to download and organize custom maps for endless entertainment.
|
||||
- [Install any map](install-any-map)
|
||||
- [Install all playlists you want](install-all-playlists-you-want)
|
||||
- __🧩 Mods__: Discover how to install, update, and manage mods to enhance your gameplay.
|
||||
- [Install mods on your version](install-mods-on-your-version)
|
||||
- __🖼️ Models__: Find out how to integrate custom models into your Beat Saber experience.
|
||||
- [Modify models in your Beat Saber](modify-models-in-your-beat-saber)
|
||||
|
||||
### 🤝 Contribution
|
||||
|
||||
Information on how to contribute to the development and improvement of BSManager
|
||||
|
||||
- [Help us update the wiki](help-us-update-the-wiki)
|
||||
- [Contribute to the project](https://github.com/Zagrios/bs-manager/blob/master/CONTRIBUTING.md)
|
||||
|
||||
## 🐞 Troubleshooting
|
||||
|
||||
Our troubleshooting section is here to help you resolve common issues quickly and efficiently:
|
||||
|
||||
<!-- ### ⚙️ Connection Issues: Solutions for problems related to connecting BSManager to required services. -->
|
||||
|
||||
### 💾 Installation Problems
|
||||
|
||||
Guidance on fixing errors during setup or version updates
|
||||
|
||||
- __🐧Linux__:
|
||||
- [Missing Icons in Game]([Linux]-Missing-Icons-in-Game)
|
||||
- [[Deb] The SUID Sandbox Helper Binary Was Found]([Linux]-[deb]-The-SUID-Sandbox-Helper-Binary-Was-Found)
|
||||
- [[Flatpak] Steam Beat Saber Version Not Showing / Proton Not Detected]([Linux]-[Flatpak]-Steam-Beat-Saber-Version-Not-Showing-Proton-Not-Detected)
|
||||
- [[Flatpak] Changing Installation Folder]([Linux]-[Flatpak]-Changing-Installation-Folder)
|
||||
|
||||
<!-- ### 🎮 Gameplay Issues: Fixes for issues impacting Beat Saber performance. -->
|
||||
|
||||
### 🔍 Debugging Tips
|
||||
|
||||
Steps to gather logs and identify specific issues
|
||||
|
||||
- [Activate Oculus Sideloading](Activate-Oculus-sideloading)
|
||||
- [How to find your logs](how-to-find-your-logs)
|
||||
- [Submitting a bug report](https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&projects=&template=1-bug-report.yaml&title=%5BBUG%5D+%3A+)
|
||||
|
||||
***
|
||||
|
||||
We are committed to keeping this wiki up to date with the latest information and best practices. Your contributions are welcome; please refer to our [Help us update the wiki](help-us-update-the-wiki) to get started.
|
||||
|
||||
For additional support and community interaction, join our [Discord server](https://discord.gg/uSqbHVpKdV).
|
||||
|
||||
Thank you for using BSManager!
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
### Important
|
||||
|
||||
Your token is a confidential piece of information. Possession of this token allows individuals to download applications, send messages, among other actions, under your identity.
|
||||
|
||||
However, you might wonder why it is necessary to provide this token to BSManager. The reason is that BSManager requires the token to continue the download with Oculus. Once you've input the token, it is used exclusively to communicate with Oculus servers to verify that you are the rightful owner of the game.
|
||||
|
||||
# Solution 1 - Using the Oculus app
|
||||
|
||||
## Step 1 - Install and log into the Oculus Rift app
|
||||
- Get the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/)
|
||||
- Install the Oculus Rift app
|
||||
|
||||
ℹ️ **If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with BSManager, first claim it from its store page** ℹ️
|
||||
|
||||
## Step 2 - Open developer tools
|
||||
|
||||
- Open Oculus app
|
||||
- Open the developer tools by pressing <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>i</kbd>.
|
||||
|
||||
## Step 3 - Copy your Token
|
||||
In the developer tools :
|
||||
- Open the `Network` tab
|
||||
- Filter for `graph`
|
||||
- Click on the first request
|
||||
- Open the `Payload` tab
|
||||
- Locate your token, it should start with `FRL`
|
||||
- Select the token using your mouse and press <kbd>Ctrl</kbd> + <kbd>c</kbd> to copy it
|
||||
|
||||

|
||||
|
||||
# Solution 2 - Using the Oculus website
|
||||
|
||||
## Steps
|
||||
|
||||
1. Log in to your Meta account at https://secure.oculus.com
|
||||
2. Open the developer tools in your web browser:
|
||||
- Chrome/Edge/Firefox: Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> or <kbd>F12</kbd>
|
||||
- Safari: Press <kbd>Option</kbd>+<kbd>Command</kbd>+<kbd>I</kbd>
|
||||
3. Go to the "Application" tab
|
||||
4. Expand "Cookies" in the left sidebar
|
||||
5. Click on `https://secure.oculus.com`
|
||||
6. Find the cookie named `oc_ac_at` (value starts with `OC`)
|
||||
7. Copy the entire `oc_ac_at` cookie value - this is your token
|
||||
|
||||
<img width="500" src="https://github.com/user-attachments/assets/8499035c-4e7c-423d-914d-f35b6a3d77af">
|
||||
|
||||
Paste the token into BSManager or other trusted apps to manage your Beat Saber game and mods. Keep it confidential.
|
||||
|
||||
# Known bugs
|
||||
- [Nothing opens when I press `Ctrl`+`Shift`+`i`.](https://github.com/Zagrios/bs-manager/wiki/Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60i%60.)
|
||||
@@ -1,4 +0,0 @@
|
||||
If nothing opens when you press `Ctrl` + `Shift` + `I`, it's possible that the keyboard shortcut to open the development tools is being used by another application.
|
||||
|
||||
Here's a non-exhaustive list of applications that have caused a problem and their suggested solutions:
|
||||
- [AMD software](https://github.com/Zagrios/bs-manager/wiki/AMD-Sofware-%E2%80%90-Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60I%60)
|
||||
@@ -6,6 +6,9 @@ Enabling Oculus sideloading allows games located outside if your Oculus library
|
||||
- **Step 4:** Name the new value `AllowDevSideloaded` and set the value to `1`.
|
||||
|
||||
You should end up with something like this:
|
||||

|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Troubleshoots/Debugging-tips/Activate-Oculus-sideloading/enable-oculus-sideloading.png" alt="enable-oculus-sideloading.png" />
|
||||
</div>
|
||||
|
||||
After completing these steps, you should be able to start Beat Saber from BSManager and play it on your Oculus Quest. If you are still having issues, please join our [Discord](https://discord.gg/uSqbHVpKdV) server for further assistance.
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,37 @@
|
||||
## Summary
|
||||
|
||||
- [BSManager Log Files](#bsmanager-log-files)
|
||||
- [Beat Saber (BSIPA) Log Files](#beat-saber-bsipa-log-files)
|
||||
- [Player.log (Unity) Log File](#playerlog-unity-log-file)
|
||||
|
||||
## BSManager Log Files
|
||||
|
||||
1. Launch BSManager and go to __Settings__.
|
||||
2. Scroll to the bottom of the page.
|
||||
3. Click on the __`Open Logs`__ button.
|
||||
4. Right-click on the log file to be sent and select __`Copy`__.
|
||||
> [!NOTE]
|
||||
> The requested file is usually the most recent log file, located at the bottom of the list.
|
||||
5. __`Paste`__ your file in the channel where log files are requested.
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Troubleshoots/Debugging-tips/How-to-find-your-logs/BSMLogs.png" alt="BSMLogs.png" />
|
||||
</div>
|
||||
|
||||
## Beat Saber (BSIPA) Log Files
|
||||
|
||||
1. Launch BSManager and select the version with which you are experiencing the problem.
|
||||
2. Click the version options button at the top-right corner of the interface, then select __`Open Folder`__.
|
||||
3. Double-click the __`Logs`__ folder to open it.
|
||||
4. Right-click the log file named __`_latest.log`__ (or just __`_latest`__) and select __`Copy`__.
|
||||
> [!NOTE]
|
||||
> This file will usually be at the top of the list.
|
||||
5. __`Paste`__ the file in the channel where log files are requested using __`Ctrl+V`__.
|
||||
|
||||
## Player.log (Unity) Log File
|
||||
|
||||
1. Press __`Win+R`__ simultaneously on your keyboard.
|
||||
2. Type or copy-paste the following path into the window that pops up, then press __Enter__:
|
||||
`%appdata%\..\LocalLow\Hyperbolic Magnetism\Beat Saber`
|
||||
3. Right-click the log file named __`Player.log`__ (or just __`Player`__) and select __`Copy`__.
|
||||
4. __`Paste`__ the file in the channel where log files are requested using __`Ctrl+V`__.
|
||||
@@ -0,0 +1,4 @@
|
||||
If nothing opens when you press `Ctrl` + `Shift` + `I`, it’s possible that the keyboard shortcut to open the developer tools is being used by another application.
|
||||
|
||||
Here’s a non-exhaustive list of applications that may cause this problem and their suggested solutions:
|
||||
- [AMD Software](https://github.com/Zagrios/bs-manager/wiki/AMD-Software-%E2%80%90-Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60I%60)
|
||||
@@ -0,0 +1,30 @@
|
||||
If you encounter a keyboard shortcut conflict between AMD Software and Oculus Rift, you can either disable or modify the conflicting shortcut.
|
||||
|
||||
_Special thanks to_ `𝔽𝕪𝕟𝕟 (fynn07)` _for their contribution with information and screenshots._
|
||||
|
||||
### __Follow these steps to disable or modify the shortcut in AMD Software:__
|
||||
|
||||
## Step 1 - Access Settings
|
||||
|
||||
1. Open AMD Software and click on the gear icon in the top-right corner to access the settings.
|
||||
|
||||

|
||||
|
||||
## Step 2 - Modify Keyboard Shortcuts
|
||||
|
||||
1. In the settings menu, select the __"Hotkey"__ tab.
|
||||
|
||||

|
||||
|
||||
## Step 3 - Disable Shortcuts
|
||||
|
||||
1. Turn off the __"Use Hotkeys"__ option to prevent any conflicts with Oculus Rift.
|
||||
|
||||

|
||||
|
||||
## Step 3 (Alternative) - Modify the Shortcut
|
||||
|
||||
1. If you prefer to keep using keyboard shortcuts, consider changing the specific conflicting shortcut (e.g., __"Ctrl + Shift + I"__) to another combination that is less likely to interfere.
|
||||
|
||||
> ℹ️ __Note:__ You can't change the full keybind!
|
||||
> You can only modify the __"I"__ to a different key. ℹ️
|
||||
@@ -0,0 +1,9 @@
|
||||
<p align="center">
|
||||
<img src="../wiki/Troubleshoots/Installation-Problems/Linux/%5BLinux%5D-Missing-Icons-in-Game/linux-missing-icons.png" alt="Linux Missing Icons" width="300"/>
|
||||
<br>
|
||||
NOTE: "?" are missing unicode icons
|
||||
</p>
|
||||
|
||||
This is due to [BSML](https://github.com/monkeymanboy/BeatSaberMarkupLanguage) using a missing font (`Segoe UI Symbols`) for unicode emojis.
|
||||
|
||||
To fix the issue, you need to get a copy of `seguisym.ttf` from the fonts folder an official copy Windows. Once you have a copy, add the font file to `~/.steam/steam/steamapps/compatdata/620980/pfx/drive_c/windows/Fonts`.
|
||||
|
Before Width: | Height: | Size: 782 KiB After Width: | Height: | Size: 782 KiB |
@@ -0,0 +1,5 @@
|
||||
To change the installation path of the **BSManager** folder, you have to edit the flatpak permissions to destination folder.
|
||||
|
||||
- In flatpak or Flatseal, add the destination folder with `:create` permissions.
|
||||
- In BSM, move the folder to the destination folder.
|
||||
- [Optional] In flatpak or Flatseal, remove the original folder permissions.
|
||||
@@ -0,0 +1 @@
|
||||
Flatpak should have permissions with the steam games folder. By default, the minimum permissions are `~/.steam/steam/steamapps/common:ro` and `~/.steam/steam/steamapps/common:ro`. If you changed the steam installation path, add that path instead into the permissions.
|
||||
@@ -0,0 +1,7 @@
|
||||
This is encountered when running the app file or executing the app in the terminal. This is due to a change to Ubuntu 24.04. In order to fix the issue, take a look into the path of "chrome-sandbox" described in the error log and give the correct permissions within the terminal, for example:
|
||||
|
||||
```bash
|
||||
chmod 4755 /opt/BSManager/chrome-sandbox
|
||||
```
|
||||
|
||||
ref: https://github.com/electron/electron/issues/42510
|
||||
@@ -0,0 +1,90 @@
|
||||
# 🌐 Navigation
|
||||
|
||||
<ul>
|
||||
<li><a href="Home">Home</a></li>
|
||||
</ul>
|
||||
|
||||
## 📚 Guides
|
||||
|
||||
<details>
|
||||
<summary>🛠️ Installation and Updates</summary>
|
||||
<ul>
|
||||
<li><a href="install-bsmanager-on-linux">Install BSManager on Linux</a></li>
|
||||
<li><a href="install-or-import-a-version">Install or Import a Version</a></li>
|
||||
<li><a href="create-a-shortcut">Create a Shortcut</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🔑 Login</summary>
|
||||
<ul>
|
||||
<li><a href="How-to-obtain-your-Oculus-Token">How to obtain your Oculus Token</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🎮 Content Management</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<details>
|
||||
<summary>🗺️ Maps</summary>
|
||||
<ul>
|
||||
<li><a href="install-any-map">Install Any Map</a></li>
|
||||
<li><a href="install-all-playlists-you-want">Install All Playlists You Want</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary>🧩 Mods</summary>
|
||||
<ul>
|
||||
<li><a href="install-mods-on-your-version">Install Mods on Your Version</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary>🖼️ Models</summary>
|
||||
<ul>
|
||||
<li><a href="modify-models-in-your-beat-saber">Modify Models in Your Beat Saber</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🤝 Contribution</summary>
|
||||
<ul>
|
||||
<li><a href="help-us-update-the-wiki">Help Us Update the Wiki</a></li>
|
||||
<li><a href="https://github.com/Zagrios/bs-manager/blob/master/CONTRIBUTING.md">Contribute to the Project</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
## 🐞 Troubleshooting
|
||||
|
||||
<details>
|
||||
<summary>💾 Installation Problems</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<details>
|
||||
<summary>🐧 Linux</summary>
|
||||
<ul>
|
||||
<li><a href="%5BLinux%5D-Missing-Icons-in-Game">Missing Icons in Game</a></li>
|
||||
<li><a href="%5BLinux%5D-%5Bdeb%5D-The-SUID-Sandbox-Helper-Binary-Was-Found">[Deb] The SUID Sandbox Helper Binary Was Found</a></li>
|
||||
<li><a href="%5BLinux%5D-%5BFlatpak%5D-Steam-Beat-Saber-Version-Not-Showing-Proton-Not-Detected">[Flatpak] Steam Beat Saber Version Not Showing / Proton Not Detected</a></li>
|
||||
<li><a href="%5BLinux%5D-%5BFlatpak%5D-Changing-Installation-Folder">[Flatpak] Changing Installation Folder</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🔍 Debugging Tips</summary>
|
||||
<ul>
|
||||
<li><a href="Activate-Oculus-sideloading">Activate Oculus Sideloading</a></li>
|
||||
<li><a href="how-to-find-your-logs">How to Find Your Logs</a></li>
|
||||
<li><a href="https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&projects=&template=1-bug-report.yaml&title=%5BBUG%5D+%3A+">Submitting a Bug Report</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
@@ -29,6 +29,7 @@ const config = {
|
||||
linux: {
|
||||
target: [
|
||||
"deb",
|
||||
"rpm",
|
||||
],
|
||||
icon: "./build/icons/png",
|
||||
category: "Utility;Game;",
|
||||
@@ -69,6 +70,10 @@ const config = {
|
||||
"--filesystem=~/.steam/steam/steamapps:ro", // for the libraryfolders.vdf
|
||||
"--filesystem=~/.steam/steam/steamapps/common:create", // Steam game folder
|
||||
"--filesystem=~/.steam/steam/steamapps/common/Beat Saber:create", // For installing mods/maps to original Beat Saber version
|
||||
"--filesystem=~/Desktop", // allow writing shortcuts to desktop
|
||||
"--filesystem=~/.steam/steam/userdata", // allow writing steam shortcuts
|
||||
// Allow BSManager to create the compat folder if it does not exist
|
||||
"--filesystem=~/.steam/steam/steamapps/compatdata/620980:create",
|
||||
// Allow communication with network
|
||||
"--share=network",
|
||||
// System notifications with libnotify
|
||||
|
||||
@@ -2,6 +2,52 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d"
|
||||
dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.76"
|
||||
@@ -20,6 +66,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.195"
|
||||
@@ -40,13 +92,112 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_fmt"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "start_beat_saber_admin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"simplelog",
|
||||
"winres",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8"
|
||||
|
||||
[[package]]
|
||||
name = "sval_buffer"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f"
|
||||
dependencies = [
|
||||
"sval",
|
||||
"sval_ref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_dynamic"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4"
|
||||
dependencies = [
|
||||
"sval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_fmt"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"sval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_json"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"sval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_nested"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f"
|
||||
dependencies = [
|
||||
"sval",
|
||||
"sval_buffer",
|
||||
"sval_ref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_ref"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa"
|
||||
dependencies = [
|
||||
"sval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_serde"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"sval",
|
||||
"sval_nested",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.48"
|
||||
@@ -58,6 +209,37 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
@@ -67,12 +249,54 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
||||
dependencies = [
|
||||
"value-bag-serde1",
|
||||
"value-bag-sval2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "value-bag-serde1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"serde",
|
||||
"serde_fmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "value-bag-sval2"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a"
|
||||
dependencies = [
|
||||
"sval",
|
||||
"sval_buffer",
|
||||
"sval_dynamic",
|
||||
"sval_fmt",
|
||||
"sval_json",
|
||||
"sval_ref",
|
||||
"sval_serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winres"
|
||||
version = "0.1.12"
|
||||
|
||||
@@ -4,6 +4,10 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
simplelog = { version = "0.12.2", default-features = false }
|
||||
log = "0.4.24"
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
||||
@@ -11,6 +15,7 @@ winres = "0.1.12"
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.winres]
|
||||
FileDescription = "Start Beat Saber as Admin"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.77.0"
|
||||
@@ -1,5 +1,10 @@
|
||||
|
||||
use std::{process::Command, path::Path};
|
||||
use std::env::current_exe;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use log::{error, info, LevelFilter};
|
||||
use simplelog::{CombinedLogger, Config, WriteLogger};
|
||||
|
||||
const EXECUTABLE_NAME: &str = "Beat Saber.exe";
|
||||
const AUTORIZED_ARGS: [&str; 8] = [
|
||||
@@ -14,18 +19,37 @@ const AUTORIZED_ARGS: [&str; 8] = [
|
||||
];
|
||||
|
||||
fn main() {
|
||||
|
||||
let default_log_file_path = current_exe().unwrap().with_file_name("bs-admin-start.log");
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let log_file_path: PathBuf = if let Some(pos) = args.iter().position(|arg| arg == "--log-path") {
|
||||
if let Some(log_path) = args.get(pos + 1) {
|
||||
PathBuf::from(log_path)
|
||||
} else {
|
||||
default_log_file_path
|
||||
}
|
||||
} else {
|
||||
default_log_file_path
|
||||
};
|
||||
|
||||
CombinedLogger::init(
|
||||
vec![
|
||||
WriteLogger::new(LevelFilter::Info, Config::default(), File::create(log_file_path).unwrap()),
|
||||
]
|
||||
).unwrap();
|
||||
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let executable_path = Path::new(&args[1]);
|
||||
|
||||
if executable_path.file_name().unwrap() != EXECUTABLE_NAME {
|
||||
eprintln!("Executable path is not Beat Saber.exe.");
|
||||
return;
|
||||
return error!("Executable path is not Beat Saber.exe.");
|
||||
}
|
||||
|
||||
if !executable_path.exists() || !executable_path.is_file() {
|
||||
eprintln!("Executable path is not valid.");
|
||||
return;
|
||||
return error!("Executable path is not valid.");
|
||||
}
|
||||
|
||||
let mut command = Command::new(executable_path);
|
||||
@@ -43,5 +67,11 @@ fn main() {
|
||||
|
||||
command.env("SteamAppId", "620980");
|
||||
|
||||
let _ = command.spawn();
|
||||
let res = command.spawn();
|
||||
|
||||
if let Err(e) = res {
|
||||
return error!("Failed to start Beat Saber ({}): {}", executable_path.display(), e);
|
||||
}
|
||||
|
||||
info!("Beat Saber started ({})", executable_path.display());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -8,19 +8,67 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d"
|
||||
dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "oculus-allow-dev-sideloaded"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"simplelog",
|
||||
"winreg",
|
||||
"winres",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.92"
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -34,6 +82,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
@@ -55,16 +109,144 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.93"
|
||||
name = "serde_fmt"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
|
||||
checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8"
|
||||
|
||||
[[package]]
|
||||
name = "sval_buffer"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f"
|
||||
dependencies = [
|
||||
"sval",
|
||||
"sval_ref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_dynamic"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4"
|
||||
dependencies = [
|
||||
"sval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_fmt"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"sval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_json"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"sval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_nested"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f"
|
||||
dependencies = [
|
||||
"sval",
|
||||
"sval_buffer",
|
||||
"sval_ref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_ref"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa"
|
||||
dependencies = [
|
||||
"sval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval_serde"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"sval",
|
||||
"sval_nested",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
@@ -74,12 +256,54 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
||||
dependencies = [
|
||||
"value-bag-serde1",
|
||||
"value-bag-sval2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "value-bag-serde1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"serde",
|
||||
"serde_fmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "value-bag-sval2"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a"
|
||||
dependencies = [
|
||||
"sval",
|
||||
"sval_buffer",
|
||||
"sval_dynamic",
|
||||
"sval_fmt",
|
||||
"sval_json",
|
||||
"sval_ref",
|
||||
"sval_serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
||||
@@ -6,8 +6,11 @@ edition = "2021"
|
||||
[build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
||||
|
||||
[dependencies]
|
||||
winreg = "0.52.0"
|
||||
simplelog = { version = "0.12.2", default-features = false }
|
||||
log = "0.4.24"
|
||||
|
||||
[package.metadata.winres]
|
||||
FileDescription = "Enable Sideloaded Apps"
|
||||
@@ -18,3 +21,4 @@ CompanyName = "Zagrios"
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.77.0"
|
||||
@@ -1,22 +1,48 @@
|
||||
use std::env::current_exe;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use simplelog::{CombinedLogger, Config, LevelFilter, WriteLogger};
|
||||
use winreg::enums::HKEY_LOCAL_MACHINE;
|
||||
use winreg::RegKey;
|
||||
use log::{error, info};
|
||||
|
||||
const PATH: &str = "SOFTWARE\\Wow6432Node\\Oculus VR, LLC\\Oculus";
|
||||
const REG_PATH: &str = "SOFTWARE\\Wow6432Node\\Oculus VR, LLC\\Oculus";
|
||||
|
||||
fn main() {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
|
||||
// Create (or open if it already exists) the subkey
|
||||
let (key, _) = match hklm.create_subkey(PATH) {
|
||||
Ok(res) => res,
|
||||
Err(err) => return println!("{}", err.to_string()),
|
||||
let default_log_file_path = current_exe().unwrap().with_file_name("oculus-allow-dev-sideloaded.log");
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let log_file_path: PathBuf = if let Some(pos) = args.iter().position(|arg| arg == "--log-path") {
|
||||
if let Some(log_path) = args.get(pos + 1) {
|
||||
PathBuf::from(log_path)
|
||||
} else {
|
||||
default_log_file_path
|
||||
}
|
||||
} else {
|
||||
default_log_file_path
|
||||
};
|
||||
|
||||
CombinedLogger::init(
|
||||
vec![
|
||||
WriteLogger::new(LevelFilter::Info, Config::default(), File::create(log_file_path).unwrap()),
|
||||
]
|
||||
).unwrap();
|
||||
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
|
||||
info!("Opening or creating subkey ({})...", REG_PATH);
|
||||
let (key, _) = match hklm.create_subkey(REG_PATH) {
|
||||
Ok(res) => res,
|
||||
Err(err) => return error!("Unable to create subkey ({}) : {}", REG_PATH, err.to_string())
|
||||
};
|
||||
|
||||
info!("Setting value AllowDevSideloaded = 1 in {REG_PATH}...");
|
||||
let res = key.set_value("AllowDevSideloaded", &1u32);
|
||||
|
||||
if let Err(err) = res {
|
||||
return println!("{}", err.to_string());
|
||||
return error!("Unable to set value AllowDevSideloaded = 1 in {REG_PATH} : {}", err.to_string());
|
||||
}
|
||||
|
||||
println!("AllowDevSideloaded = 1 has been successfully set in {PATH}");
|
||||
info!("AllowDevSideloaded = 1 has been successfully set in {REG_PATH}");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "bs-manager",
|
||||
"description": "Manage maps, mods and more for Beat Saber",
|
||||
"main": "./.erb/dll/main.bundle.dev.js",
|
||||
"version": "1.5.0-alpha.7",
|
||||
"version": "1.5.1",
|
||||
"scripts": {
|
||||
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
|
||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||
@@ -55,7 +55,7 @@
|
||||
"url": "https://github.com/Zagrios"
|
||||
},
|
||||
"contributors": [],
|
||||
"license": "MIT",
|
||||
"license": "GPL-3.0-only",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Zagrios/bs-manager/issues"
|
||||
},
|
||||
@@ -108,9 +108,9 @@
|
||||
"css-loader": "^6.10.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"detect-port": "^1.5.1",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron": "^33.3.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electronmon": "^2.0.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
@@ -167,13 +167,13 @@
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.3.4",
|
||||
"electron-updater": "^6.3.9",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^14.4.4",
|
||||
"got": "^14.4.5",
|
||||
"history": "^5.3.0",
|
||||
"is-elevated": "^4.0.0",
|
||||
"md5-file": "^5.0.0",
|
||||
@@ -181,7 +181,7 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"pako": "^2.1.0",
|
||||
"protobufjs": "^7.4.0",
|
||||
"qrcode.react": "^4.0.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"query-process": "^0.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
@@ -189,16 +189,16 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-range": "^1.10.0",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.24",
|
||||
"react-window": "^1.8.10",
|
||||
"react-virtualized-auto-sizer": "^1.0.25",
|
||||
"react-window": "^1.8.11",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"rfdc": "^1.4.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.6.3",
|
||||
"serialize-error": "^11.0.3",
|
||||
"serialize-error": "^12.0.0",
|
||||
"striptags": "^4.0.0-alpha.4",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-scoped-groups": "^2.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"to-ico": "^1.1.5",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0-alpha.7",
|
||||
"version": "1.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0-alpha.7",
|
||||
"version": "1.5.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0-alpha.7",
|
||||
"version": "1.5.1",
|
||||
"description": "BSManager",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { parseEnvString } from "main/helpers/env.helpers";
|
||||
|
||||
describe("Test parseEnvString", () => {
|
||||
|
||||
it("Empty", () => {
|
||||
const envVars = parseEnvString("");
|
||||
expect(envVars).toEqual({});
|
||||
});
|
||||
|
||||
it("Single test; no quotes", () => {
|
||||
const envString = "HELLO=World!";
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
HELLO: "World!",
|
||||
});
|
||||
});
|
||||
|
||||
it("Single test; single quotes", () => {
|
||||
const envString = "SINGLE_QOUTE='Single quote with spaces'";
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
SINGLE_QOUTE: "Single quote with spaces",
|
||||
});
|
||||
});
|
||||
|
||||
it("Single test; double quotes", () => {
|
||||
const envString = 'DOUBLE_QOUTE="Some random quote."';
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
DOUBLE_QOUTE: "Some random quote.",
|
||||
});
|
||||
});
|
||||
|
||||
it("Single test; empty value", () => {
|
||||
const envString = "EMPTY=";
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
EMPTY: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("Multiple test; combined", () => {
|
||||
const envString = `HELLO=World! DOUBLE_QUOTE="Two Words" SINGLE_QUOTE='' EMPTY=`
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual(expect.objectContaining({
|
||||
HELLO: "World!",
|
||||
DOUBLE_QUOTE: "Two Words",
|
||||
SINGLE_QUOTE: "",
|
||||
EMPTY: ""
|
||||
}));
|
||||
});
|
||||
|
||||
it("Key with numbers and lower case", () => {
|
||||
const envString = "H3ll0=world";
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
H3ll0: "world",
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { mkdir, pathExistsSync, rm, writeFile } from "fs-extra";
|
||||
import { getSize } from "main/helpers/fs.helpers";
|
||||
import path from "path";
|
||||
|
||||
jest.mock("electron", () => ({ app: {
|
||||
getPath: () => "",
|
||||
getName: () => "",
|
||||
}}));
|
||||
jest.mock("electron-log", () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}));
|
||||
|
||||
const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "fs");
|
||||
|
||||
describe("Test fs.helpers getSize", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
if (pathExistsSync(TEST_FOLDER)) {
|
||||
await rm(TEST_FOLDER, { recursive: true, force: true });
|
||||
}
|
||||
await mkdir(TEST_FOLDER);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(TEST_FOLDER, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("should return 0 for empty folder", async () => {
|
||||
const size = await getSize(TEST_FOLDER);
|
||||
expect(size).toBe(0);
|
||||
});
|
||||
|
||||
it("should throw error for non-existing folder", async () => {
|
||||
await expect(getSize(`${TEST_FOLDER}1`)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should return the total size of files in the directory", async () => {
|
||||
const filePath = path.join(TEST_FOLDER, "testFile.bin");
|
||||
const buffer = Buffer.alloc(10);
|
||||
|
||||
await writeFile(filePath, buffer);
|
||||
|
||||
const size = await getSize(TEST_FOLDER);
|
||||
expect(size).toBe(10);
|
||||
});
|
||||
|
||||
it("should include the sizes of all files in the directory", async () => {
|
||||
const filePath1 = path.join(TEST_FOLDER, "testFile1.bin");
|
||||
const filePath2 = path.join(TEST_FOLDER, "testFile2.bin");
|
||||
const buffer = Buffer.alloc(10);
|
||||
|
||||
await writeFile(filePath1, buffer);
|
||||
await writeFile(filePath2, buffer);
|
||||
|
||||
const size = await getSize(TEST_FOLDER);
|
||||
expect(size).toBe(20);
|
||||
});
|
||||
|
||||
it("should include the sizes of files in nested directories", async () => {
|
||||
const subFolder = path.join(TEST_FOLDER, "subFolder");
|
||||
const filePath1 = path.join(TEST_FOLDER, "testFile1.bin");
|
||||
const filePath2 = path.join(subFolder, "testFile2.bin");
|
||||
const buffer = Buffer.alloc(10);
|
||||
|
||||
await mkdir(subFolder);
|
||||
|
||||
await writeFile(filePath1, buffer);
|
||||
await writeFile(filePath2, buffer);
|
||||
|
||||
const size = await getSize(TEST_FOLDER);
|
||||
expect(size).toBe(20);
|
||||
});
|
||||
|
||||
it("should not include files beyond the default depth limit", async () => {
|
||||
const subFolder = path.join(TEST_FOLDER, "1", "2", "3", "4", "5");
|
||||
const filePath = path.join(subFolder, "testFile.bin");
|
||||
const buffer = Buffer.alloc(10);
|
||||
|
||||
await mkdir(subFolder, { recursive: true });
|
||||
await writeFile(filePath, buffer);
|
||||
|
||||
const size = await getSize(TEST_FOLDER);
|
||||
expect(size).toBe(0);
|
||||
});
|
||||
|
||||
it("should include files within the specified depth limit", async () => {
|
||||
const subFolder = path.join(TEST_FOLDER, "1", "2", "3", "4", "5");
|
||||
const filePath = path.join(subFolder, "testFile.bin");
|
||||
const filePath2 = path.join(TEST_FOLDER, "1", "2", "testFile2.bin");
|
||||
const buffer = Buffer.alloc(10);
|
||||
|
||||
await mkdir(subFolder, { recursive: true });
|
||||
await writeFile(filePath, buffer);
|
||||
await writeFile(filePath2, buffer);
|
||||
|
||||
const size = await getSize(TEST_FOLDER, 6);
|
||||
expect(size).toBe(20);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
BsmShellLog,
|
||||
bsmSpawn,
|
||||
// bsmExec,
|
||||
isProcessRunning,
|
||||
@@ -23,7 +24,8 @@ jest.mock("electron-log", () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}));
|
||||
jest.mock("ps-list", () => () => []);
|
||||
|
||||
jest.mock("ps-list", () => (): unknown[] => []);
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const IS_LINUX = process.platform === "linux";
|
||||
@@ -76,7 +78,7 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
it("Simple spawn command with logging", () => {
|
||||
bsmSpawn("mkdir", {
|
||||
args: ["new_folder"],
|
||||
log: true,
|
||||
log: BsmShellLog.Command,
|
||||
});
|
||||
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(spawnSpy).toHaveBeenCalledWith("mkdir new_folder", expect.anything());
|
||||
@@ -86,7 +88,7 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
|
||||
it("Complex spawn command call (Mods install)", () => {
|
||||
bsmSpawn(`"./BSIPA.exe" "./Beat Saber.exe" -n`, {
|
||||
log: true,
|
||||
log: BsmShellLog.Command,
|
||||
linux: { prefix: `"./wine64"` },
|
||||
});
|
||||
|
||||
@@ -109,7 +111,7 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
detached: true,
|
||||
env: BS_ENV,
|
||||
},
|
||||
log: true,
|
||||
log: BsmShellLog.Command,
|
||||
linux: { prefix: `"./proton" run` },
|
||||
});
|
||||
|
||||
@@ -152,7 +154,7 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
detached: true,
|
||||
env: newEnv,
|
||||
},
|
||||
log: true,
|
||||
log: BsmShellLog.Command,
|
||||
linux: { prefix: `"./proton" run` },
|
||||
flatpak: {
|
||||
host: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { ProviderPlatform } from "shared/models/provider-platform.enum";
|
||||
|
||||
export function execOnOs<T>(executions: { [key in ProviderPlatform]?: () => T }, noError = false): T {
|
||||
@@ -10,4 +11,118 @@ export function execOnOs<T>(executions: { [key in ProviderPlatform]?: () => T },
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
enum EnvParserState {
|
||||
NAME_START,
|
||||
NAME,
|
||||
VALUE_START,
|
||||
VALUE,
|
||||
QUOTE_VALUE,
|
||||
DQUOTE_VALUE,
|
||||
SPACE,
|
||||
ERROR,
|
||||
};
|
||||
|
||||
const isAlphaCharacter = (c: string) =>
|
||||
(c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
|
||||
const isNumber = (c: string) => c >= "0" && c <= "9";
|
||||
|
||||
export function parseEnvString(envString: string): Record<string, string> {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
let state: EnvParserState = EnvParserState.NAME_START;
|
||||
let index = 0;
|
||||
let newName = "";
|
||||
for (let pos = 0; pos < envString.length; ++pos) {
|
||||
const c = envString[pos];
|
||||
|
||||
switch (state) {
|
||||
case EnvParserState.NAME_START:
|
||||
if (isAlphaCharacter(c) || c === "_") {
|
||||
state = EnvParserState.NAME;
|
||||
index = pos;
|
||||
} else if (c !== " ") {
|
||||
state = EnvParserState.ERROR;
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.NAME:
|
||||
if (c === "=") {
|
||||
state = EnvParserState.VALUE_START;
|
||||
newName = envString.substring(index, pos);
|
||||
index = pos + 1;
|
||||
} else if (!isAlphaCharacter(c) && !isNumber(c) && c !== "_") {
|
||||
state = EnvParserState.ERROR;
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.VALUE_START:
|
||||
if (c === "'") {
|
||||
++index;
|
||||
state = EnvParserState.QUOTE_VALUE;
|
||||
} else if (c === '"') {
|
||||
++index;
|
||||
state = EnvParserState.DQUOTE_VALUE;
|
||||
} else if (c === " ") {
|
||||
state = EnvParserState.NAME_START;
|
||||
envVars[newName] = "";
|
||||
} else {
|
||||
state = EnvParserState.VALUE;
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.VALUE:
|
||||
if (c === " ") {
|
||||
state = EnvParserState.NAME_START;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.QUOTE_VALUE:
|
||||
if (c === "'") {
|
||||
state = EnvParserState.SPACE;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.DQUOTE_VALUE:
|
||||
if (c === '"') {
|
||||
state = EnvParserState.SPACE;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.SPACE:
|
||||
if (c === " ") {
|
||||
state = EnvParserState.NAME_START;
|
||||
} else {
|
||||
state = EnvParserState.ERROR;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
if (state === EnvParserState.ERROR) {
|
||||
throw new CustomError(
|
||||
`parseEnvString failed: invalid character at position ${pos}`,
|
||||
"generic.env.parse"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (state === EnvParserState.VALUE_START || state === EnvParserState.VALUE) {
|
||||
envVars[newName] = envString.substring(index);
|
||||
return envVars;
|
||||
}
|
||||
|
||||
if (state === EnvParserState.NAME_START || state === EnvParserState.SPACE) {
|
||||
return envVars;
|
||||
}
|
||||
|
||||
throw new CustomError(
|
||||
"parseEnvString failed: invalid ending state",
|
||||
"generic.env.parse"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CopyOptions, MoveOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, stat, symlink } from "fs-extra";
|
||||
import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises";
|
||||
import { CopyOptions, MoveOptions, RmOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, rm, stat, symlink, unlink, unlinkSync } from "fs-extra";
|
||||
import { access, mkdir, readdir, lstat, readlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { Observable, concatMap, from } from "rxjs";
|
||||
import log from "electron-log";
|
||||
@@ -28,18 +28,53 @@ export async function ensureFolderExist(path: string): Promise<void> {
|
||||
.then(() => {});
|
||||
}
|
||||
|
||||
export async function deleteFolder(folderPath: string): Promise<void> {
|
||||
export async function deleteFile(filepath: string) {
|
||||
try {
|
||||
log.info("Deleting file", `"${filepath}"`);
|
||||
await unlink(filepath);
|
||||
} catch (error: any) {
|
||||
log.error("Could not delete file", `"${filepath}"`);
|
||||
throw CustomError.fromError(error, "generic.fs.delete-file");
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteFileSync(filepath: string) {
|
||||
try {
|
||||
log.info("Deleting file", `"${filepath}"`);
|
||||
unlinkSync(filepath);
|
||||
} catch (error: any) {
|
||||
log.error("Could not delete file", `"${filepath}"`);
|
||||
throw CustomError.fromError(error, "generic.fs.delete-file");
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFolder(folderPath: string, options?: RmOptions) {
|
||||
if (!(await pathExist(folderPath))) {
|
||||
return;
|
||||
}
|
||||
return rm(folderPath, { recursive: true, force: true });
|
||||
|
||||
try {
|
||||
options = options || { recursive: true, force: true };
|
||||
log.info("Deleting folder", `"${folderPath}"`, options);
|
||||
await rm(folderPath, options);
|
||||
} catch (error: any) {
|
||||
log.error("Could not delete folder", `"${folderPath}"`);
|
||||
throw CustomError.fromError(error, "generic.fs.delete-folder");
|
||||
}
|
||||
}
|
||||
|
||||
export async function unlinkPath(path: string): Promise<void> {
|
||||
if (!(await pathExist(path))) {
|
||||
export function deleteFolderSync(folderPath: string, options?: RmOptions) {
|
||||
if (!pathExistsSync(folderPath)) {
|
||||
return;
|
||||
}
|
||||
return unlink(path);
|
||||
|
||||
try {
|
||||
options = options || { recursive: true, force: true };
|
||||
log.info("Deleting folder", `"${folderPath}"`, options);
|
||||
} catch (error: any) {
|
||||
log.error("Could not delete folder", `"${folderPath}"`);
|
||||
throw CustomError.fromError(error, "generic.fs.delete-folder");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFoldersInFolder(folderPath: string, opts?: { ignoreSymlinkTargetError?: boolean }): Promise<string[]> {
|
||||
@@ -81,6 +116,7 @@ export async function getFilesInFolder(folderPath: string): Promise<string[]> {
|
||||
}
|
||||
|
||||
export function moveFolderContent(src: string, dest: string, option?: MoveOptions): Observable<Progression> {
|
||||
log.info(`(moveFolderContent) Moving ${src} to ${dest}`);
|
||||
const progress: Progression = { current: 0, total: 0 };
|
||||
return new Observable<Progression>(subscriber => {
|
||||
subscriber.next(progress);
|
||||
@@ -96,21 +132,32 @@ export function moveFolderContent(src: string, dest: string, option?: MoveOption
|
||||
const files = await readdir(src, { encoding: "utf-8", withFileTypes: true });
|
||||
progress.total = files.length;
|
||||
|
||||
for(const file of files){
|
||||
for (const file of files) {
|
||||
const srcFullPath = path.join(src, file.name);
|
||||
const destFullPath = path.join(dest, file.name);
|
||||
|
||||
const srcChilds = file.isDirectory() ? await readdir(srcFullPath, { encoding: "utf-8", recursive: true }) : [];
|
||||
const allChildsAlreadyExist = srcChilds.every(child => pathExistsSync(path.join(destFullPath, child)));
|
||||
|
||||
if(file.isFile() || !allChildsAlreadyExist){
|
||||
if (file.isFile() || !allChildsAlreadyExist) {
|
||||
const prevSize = await getSize(srcFullPath);
|
||||
await move(srcFullPath, destFullPath, option);
|
||||
const afterSize = await getSize(destFullPath);
|
||||
|
||||
// The size after moving should be the same or greater than the size before moving but never less
|
||||
if (afterSize < prevSize) {
|
||||
throw new CustomError(`File size mismath. before: ${prevSize}, after: ${afterSize} (${srcFullPath})`, "FILE_SIZE_MISMATCH");
|
||||
}
|
||||
} else {
|
||||
log.info(`Skipping ${srcFullPath} to ${destFullPath}, all child already exist in destination`);
|
||||
}
|
||||
|
||||
progress.current++;
|
||||
subscriber.next(progress);
|
||||
}
|
||||
})().catch(err => subscriber.error(CustomError.fromError(err, err?.code))).finally(() => subscriber.complete());
|
||||
})()
|
||||
.catch(err => subscriber.error(CustomError.fromError(err, err?.code)))
|
||||
.finally(() => subscriber.complete());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,7 +196,7 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti
|
||||
await copy(sourcePath, destinationPath, options);
|
||||
} else if (item.isSymbolicLink()) {
|
||||
if (options?.overwrite) {
|
||||
await unlinkPath(destinationPath);
|
||||
await deleteFile(destinationPath);
|
||||
}
|
||||
const symlinkTarget = await readlink(sourcePath);
|
||||
const relativePath = path.relative(src, symlinkTarget);
|
||||
@@ -169,8 +216,7 @@ export function hashFile(filePath: string, algorithm = "sha256"): Promise<string
|
||||
});
|
||||
}
|
||||
|
||||
export async function dirSize(dirPath: string): Promise<number>{
|
||||
|
||||
export async function dirSize(dirPath: string): Promise<number> {
|
||||
const entries = await readdir(dirPath);
|
||||
|
||||
const paths = entries.map(async entry => {
|
||||
@@ -189,11 +235,10 @@ export async function dirSize(dirPath: string): Promise<number>{
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (await Promise.all(paths)).flat(Infinity).reduce((acc, size ) => acc + size, 0);
|
||||
return (await Promise.all(paths)).flat(Infinity).reduce((acc, size) => acc + size, 0);
|
||||
}
|
||||
|
||||
export function rxCopy(src: string, dest: string, option?: CopyOptions): Observable<Progression> {
|
||||
|
||||
const dirSizePromise = dirSize(src).catch(err => {
|
||||
log.error("dirSizePromise", err);
|
||||
return 0;
|
||||
@@ -205,15 +250,19 @@ export function rxCopy(src: string, dest: string, option?: CopyOptions): Observa
|
||||
|
||||
return new Observable<Progression>(sub => {
|
||||
sub.next(progress);
|
||||
copy(src, dest, {...option, filter: (src) => {
|
||||
stat(src).then(stats => {
|
||||
progress.current += stats.size;
|
||||
sub.next(progress);
|
||||
});
|
||||
return true;
|
||||
}})
|
||||
.then(() => sub.complete()).catch(err => sub.error(err))
|
||||
})
|
||||
copy(src, dest, {
|
||||
...option,
|
||||
filter: src => {
|
||||
stat(src).then(stats => {
|
||||
progress.current += stats.size;
|
||||
sub.next(progress);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
})
|
||||
.then(() => sub.complete())
|
||||
.catch(err => sub.error(err));
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -246,7 +295,7 @@ export function ensurePathNotAlreadyExistSync(path: string): string {
|
||||
return destPath;
|
||||
}
|
||||
|
||||
export async function isJunction(path: string): Promise<boolean>{
|
||||
export async function isJunction(path: string): Promise<boolean> {
|
||||
const [stats, lstats] = await Promise.all([stat(path), lstat(path)]);
|
||||
return lstats.isSymbolicLink() && stats.isDirectory();
|
||||
}
|
||||
@@ -254,7 +303,7 @@ export async function isJunction(path: string): Promise<boolean>{
|
||||
export function resolveGUIDPath(guidPath: string): string {
|
||||
const guidVolume = path.parse(guidPath).root;
|
||||
const command = `powershell -command "(Get-WmiObject -Class Win32_Volume | Where-Object { $_.DeviceID -like '${guidVolume}' }).DriveLetter"`;
|
||||
const {result: driveLetter, error} = tryit(() => execSync(command).toString().trim());
|
||||
const { result: driveLetter, error } = tryit(() => execSync(command).toString().trim());
|
||||
if (!driveLetter || error) {
|
||||
throw new Error("Unable to resolve GUID path", error);
|
||||
}
|
||||
@@ -274,6 +323,37 @@ export function getUniqueFileNamePath(filePath: string): string {
|
||||
return path.join(dir, newFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws {Error} Can throw file system errors
|
||||
*/
|
||||
export async function getSize(targetPath: string, maxDepth = 5): Promise<number> {
|
||||
const visited = new Set<string>();
|
||||
|
||||
const computeSize = async (currentPath: string, depth: number): Promise<number> => {
|
||||
if (visited.has(currentPath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
visited.add(currentPath);
|
||||
|
||||
const stats = await stat(currentPath);
|
||||
|
||||
if (stats.isFile()) {
|
||||
return stats.size;
|
||||
}
|
||||
|
||||
if (!stats.isDirectory() || depth >= maxDepth) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const entries = await readdir(currentPath);
|
||||
const sizes = await Promise.all(entries.map(entry => computeSize(path.join(currentPath, entry), depth + 1)));
|
||||
return sizes.reduce((acc, cur) => acc + cur, 0);
|
||||
};
|
||||
|
||||
return computeSize(targetPath, 0);
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown, D = unknown> {
|
||||
total: number;
|
||||
current: number;
|
||||
|
||||
@@ -18,21 +18,22 @@ type FlatpakOptions = {
|
||||
env?: string[];
|
||||
};
|
||||
|
||||
export type BsmSpawnOptions = {
|
||||
export enum BsmShellLog {
|
||||
Command = 0x0000_0001,
|
||||
EnvVariables = 0x0000_0002,
|
||||
};
|
||||
|
||||
interface BsmShellOptions<OptionsType> {
|
||||
args?: string[];
|
||||
options?: cp.SpawnOptions;
|
||||
log?: boolean;
|
||||
options?: OptionsType;
|
||||
// Look into BsmShellLog values
|
||||
log?: number;
|
||||
linux?: LinuxOptions;
|
||||
flatpak?: FlatpakOptions;
|
||||
};
|
||||
|
||||
export type BsmExecOptions = {
|
||||
args?: string[];
|
||||
options?: cp.ExecOptions;
|
||||
log?: boolean;
|
||||
linux?: LinuxOptions;
|
||||
flatpak?: FlatpakOptions;
|
||||
};
|
||||
export type BsmSpawnOptions = BsmShellOptions<cp.SpawnOptions>;
|
||||
export type BsmExecOptions = BsmShellOptions<cp.ExecOptions>;
|
||||
|
||||
function updateCommand(command: string, options: BsmSpawnOptions) {
|
||||
if (options?.args) {
|
||||
@@ -63,14 +64,25 @@ function updateCommand(command: string, options: BsmSpawnOptions) {
|
||||
return command;
|
||||
}
|
||||
|
||||
function logValues(shell: "spawn" | "exec", command: string, options?: BsmShellOptions<cp.SpawnOptions | cp.ExecOptions>) {
|
||||
const platform = process.platform === "win32" ? "Windows" : "Linux";
|
||||
const optionsLog = options?.log || 0;
|
||||
|
||||
if ((optionsLog & BsmShellLog.EnvVariables) > 0) {
|
||||
log.info(platform, shell, "env", options?.options?.env);
|
||||
}
|
||||
|
||||
if ((optionsLog & BsmShellLog.Command) > 0) {
|
||||
log.info(platform, shell, "command\n>", command);
|
||||
}
|
||||
}
|
||||
|
||||
export function bsmSpawn(command: string, options?: BsmSpawnOptions) {
|
||||
options = options || {};
|
||||
options.options = options.options || {};
|
||||
command = updateCommand(command, options);
|
||||
|
||||
if (options?.log) {
|
||||
log.info(process.platform === "win32" ? "Windows" : "Linux", "spawn command\n>", command);
|
||||
}
|
||||
logValues("spawn", command, options);
|
||||
|
||||
return cp.spawn(command, options.options);
|
||||
}
|
||||
@@ -83,12 +95,7 @@ export function bsmExec(command: string, options?: BsmExecOptions): Promise<{
|
||||
options.options = options.options || {};
|
||||
command = updateCommand(command, options);
|
||||
|
||||
if (options?.log) {
|
||||
log.info(
|
||||
process.platform === "win32" ? "Windows" : "Linux",
|
||||
"exec command\n>", command
|
||||
);
|
||||
}
|
||||
logValues("exec", command, options);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
cp.exec(command, options?.options || {}, (error: Error, stdout: string, stderr: string) => {
|
||||
@@ -111,7 +118,7 @@ async function isProcessRunningLinux(name: string): Promise<boolean> {
|
||||
try {
|
||||
const processName = transformProcessNameForPS(name);
|
||||
const { stdout: count } = await bsmExec(`ps awwxo args | grep -c "${processName}"`, {
|
||||
log: true,
|
||||
log: BsmShellLog.Command,
|
||||
flatpak: { host: IS_FLATPAK },
|
||||
});
|
||||
|
||||
@@ -157,7 +164,7 @@ async function getProcessIdLinux(name: string): Promise<number | null> {
|
||||
try {
|
||||
const processName = transformProcessNameForPS(name);
|
||||
const { stdout } = await bsmExec(`ps awwxo pid,args | grep "${processName}"`, {
|
||||
log: true,
|
||||
log: BsmShellLog.Command,
|
||||
flatpak: { host: IS_FLATPAK },
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ ipc.on("bs-launch.need-start-as-admin", (_, reply) => {
|
||||
|
||||
ipc.on("create-launch-shortcut", (args, reply) => {
|
||||
const bsLauncher = BSLauncherService.getInstance();
|
||||
reply(from(bsLauncher.createLaunchShortcut(args)));
|
||||
reply(from(bsLauncher.createLaunchShortcut(args.options, args.steamShortcut)));
|
||||
});
|
||||
|
||||
ipc.on("bs-launch.restore-steamvr", (_, reply) => {
|
||||
|
||||
@@ -23,9 +23,11 @@ import { LivShortcut } from "./services/liv/liv-shortcut.service";
|
||||
import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service";
|
||||
import { FileAssociationService } from "./services/file-association.service";
|
||||
import { SongDetailsCacheService } from "./services/additional-content/maps/song-details-cache.service";
|
||||
import { readdirSync, statSync, unlinkSync } from "fs-extra";
|
||||
import { Dirent, readdirSync } from "fs-extra";
|
||||
import { StaticConfigurationService } from "./services/static-configuration.service";
|
||||
import { configureProxy } from './helpers/proxy.helpers';
|
||||
import { deleteFileSync, deleteFolderSync } from "./helpers/fs.helpers";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
|
||||
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
|
||||
const staticConfig = StaticConfigurationService.getInstance();
|
||||
@@ -37,7 +39,7 @@ export const filterPatterns = new Set<RegExp>();
|
||||
filterPatterns.add(/(FRL|OC)\S{10,}/g);
|
||||
|
||||
initLogger();
|
||||
deleteOlestLogs();
|
||||
deleteOldestLogs();
|
||||
deleteOldLogs();
|
||||
|
||||
staticConfig.take("disable-hadware-acceleration", disabled => {
|
||||
@@ -156,11 +158,30 @@ if (!gotTheLock) {
|
||||
}).catch(log.error);
|
||||
}
|
||||
|
||||
function convertDateToDateString(date: Date): string {
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
return `${date.getFullYear()}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function initLogger(){
|
||||
log.transports.file.level = "info";
|
||||
|
||||
let filepath = "";
|
||||
let currentDateString = convertDateToDateString(new Date());
|
||||
log.transports.file.resolvePath = () => {
|
||||
const now = new Date();
|
||||
return path.join(app.getPath("logs"), `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-v${app.getVersion()}.log`);
|
||||
const nowString = convertDateToDateString(now);
|
||||
if (filepath && nowString === currentDateString) {
|
||||
return filepath;
|
||||
}
|
||||
|
||||
filepath = path.join(
|
||||
app.getPath("logs"), nowString,
|
||||
`${now.getTime()}-v${app.getVersion()}.log`
|
||||
);
|
||||
currentDateString = nowString;
|
||||
return filepath;
|
||||
};
|
||||
|
||||
log.hooks.push((message) => {
|
||||
@@ -203,70 +224,38 @@ function initLogger(){
|
||||
log.catchErrors();
|
||||
}
|
||||
|
||||
function getLogFilesEntries() {
|
||||
// Keep only the past week (7 days) of logs
|
||||
function deleteOldLogs(): void {
|
||||
let deleteLogFolders: Dirent[] = [];
|
||||
try {
|
||||
const logsFolder = app.getPath("logs");
|
||||
let logs = readdirSync(logsFolder, { withFileTypes: true });
|
||||
const filterDate = convertDateToDateString(
|
||||
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 7 days
|
||||
);
|
||||
deleteLogFolders = readdirSync(app.getPath("logs"), { withFileTypes: true })
|
||||
.filter(folder => folder.isDirectory() && folder.name <= filterDate);
|
||||
} catch (error) {
|
||||
log.error("Error while deleting old logs:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
logs = logs.filter(file => file.isFile() && path.extname(file.name) === ".log");
|
||||
|
||||
logs.sort((a, b) => {
|
||||
const aStat = statSync(path.join(logsFolder, a.name));
|
||||
const bStat = statSync(path.join(logsFolder, b.name));
|
||||
return bStat.mtime.getTime() - aStat.mtime.getTime();
|
||||
});
|
||||
|
||||
return logs.map(file => {
|
||||
const filePath = path.join(logsFolder, file.name);
|
||||
const stat = statSync(filePath);
|
||||
return {
|
||||
path: filePath,
|
||||
name: file.name,
|
||||
stats: stat
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
log.error('Error while retrieving log files entries:', err);
|
||||
return [];
|
||||
for (const folder of deleteLogFolders) {
|
||||
const folderPath = path.join(folder.parentPath, folder.name);
|
||||
tryit(() => deleteFolderSync(folderPath));
|
||||
}
|
||||
}
|
||||
|
||||
// keep only the last 5 logs
|
||||
function deleteOldLogs(): void{
|
||||
try {
|
||||
let logs = getLogFilesEntries();
|
||||
// Obsolete behavior, delete log files that are on the parent log folder
|
||||
function deleteOldestLogs(): void {
|
||||
const logsFolder = app.getPath("logs");
|
||||
const logs = readdirSync(logsFolder, { withFileTypes: true })
|
||||
.filter(file => file.isFile() && path.extname(file.name) === ".log");
|
||||
|
||||
logs = logs.slice(5);
|
||||
|
||||
logs.forEach(file => {
|
||||
try {
|
||||
unlinkSync(file.path);
|
||||
log.info(`Deleted log file: ${file.path}`);
|
||||
} catch (err) {
|
||||
log.error(`Error deleting file ${file.path}:`, err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
log.error("Error while deleting old logs:", err);
|
||||
for (const file of logs) {
|
||||
const filepath = path.join(file.parentPath, file.name);
|
||||
tryit(() => deleteFileSync(filepath));
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary function to delete logs before 2024-07-31
|
||||
function deleteOlestLogs(): void{
|
||||
// delete all logs before 2024-07-31
|
||||
const date = new Date(2024, 6, 31); // month is 0-based
|
||||
const logs = getLogFilesEntries().filter(file => file.stats.mtime.getTime() < date.getTime());
|
||||
|
||||
logs.forEach(file => {
|
||||
try {
|
||||
unlinkSync(file.path);
|
||||
log.info(`Deleted log file: ${file.path}`);
|
||||
} catch (err) {
|
||||
log.error(`Error deleting file ${file.path}:`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addFilterStringLog(filter: string): void {
|
||||
filterStrings.add(filter);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import path from "path";
|
||||
import { RequestService } from "../request.service";
|
||||
import { copyFileSync } from "fs-extra";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { Progression, ensureFolderExist, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { Progression, deleteFile, ensureFolderExist } from "../../helpers/fs.helpers";
|
||||
import { MODEL_FILE_EXTENSIONS, MODEL_TYPES, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { Observable, Subscription, lastValueFrom } from "rxjs";
|
||||
@@ -105,7 +105,9 @@ export class LocalModelsManagerService {
|
||||
}
|
||||
|
||||
public async oneClickDownloadModel(model: MSModel): Promise<void> {
|
||||
if (!model) { return; }
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const versions = await this.localVersion.getInstalledVersions();
|
||||
const downloaded = await lastValueFrom(this.downloadModel(model, versions.pop()));
|
||||
@@ -198,7 +200,7 @@ export class LocalModelsManagerService {
|
||||
};
|
||||
|
||||
for (const model of models) {
|
||||
await unlinkPath(model.path);
|
||||
await deleteFile(model.path);
|
||||
progression.data.push(model);
|
||||
progression.current = progression.data.length;
|
||||
subscriber.next(progression);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/mo
|
||||
import { readFileSync, Stats } from "fs";
|
||||
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
|
||||
import { copy, ensureDir, pathExists, pathExistsSync, realpath, writeFileSync } from "fs-extra";
|
||||
import { Progression, getUniqueFileNamePath, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { Progression, deleteFile, getUniqueFileNamePath } from "../../helpers/fs.helpers";
|
||||
import { FileAssociationService } from "../file-association.service";
|
||||
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
@@ -242,7 +242,9 @@ export class LocalPlaylistsManagerService {
|
||||
...localBPList,
|
||||
duration: 0,
|
||||
nbMaps: localBPList.songs?.length ?? 0,
|
||||
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined
|
||||
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined,
|
||||
minNps: Infinity,
|
||||
maxNps: -Infinity,
|
||||
}
|
||||
|
||||
const mappers = new Set<number>();
|
||||
@@ -254,12 +256,25 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
bpListDetails.duration += songDetails?.duration ? +songDetails.duration : 0;
|
||||
mappers.add(songDetails.uploader?.id);
|
||||
bpListDetails.minNps = Math.min(bpListDetails?.minNps ?? 0, Math.min(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
|
||||
bpListDetails.maxNps = Math.max(bpListDetails?.maxNps ?? 0, Math.max(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
|
||||
|
||||
const mapNps = songDetails.difficulties?.map(d => d?.nps).filter(nps => typeof nps === "number" && !Number.isNaN(nps));
|
||||
|
||||
if(mapNps?.length){
|
||||
bpListDetails.minNps = Math.min(bpListDetails.minNps, ...mapNps);
|
||||
bpListDetails.maxNps = Math.max(bpListDetails.maxNps, ...mapNps);
|
||||
}
|
||||
|
||||
song.songDetails = songDetails;
|
||||
}
|
||||
|
||||
if(!Number.isFinite(bpListDetails.minNps)){
|
||||
bpListDetails.minNps = 0;
|
||||
}
|
||||
|
||||
if(!Number.isFinite(bpListDetails.maxNps)){
|
||||
bpListDetails.maxNps = 0;
|
||||
}
|
||||
|
||||
bpListDetails.nbMappers = mappers.size;
|
||||
|
||||
return bpListDetails;
|
||||
@@ -394,7 +409,7 @@ export class LocalPlaylistsManagerService {
|
||||
}
|
||||
|
||||
public deletePlaylistFile(bpList: LocalBPList): Observable<void>{
|
||||
return from(unlinkPath(bpList.path));
|
||||
return from(deleteFile(bpList.path));
|
||||
}
|
||||
|
||||
public exportPlaylists(opt: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}): Observable<Progression<string>> {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { InstallationLocationService } from "../../installation-location.service
|
||||
import { UtilsService } from "../../utils.service";
|
||||
import crypto, { BinaryLike } from "crypto";
|
||||
import { lstatSync } from "fs";
|
||||
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra";
|
||||
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath } from "fs-extra";
|
||||
import { RequestService } from "../../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { DeepLinkService } from "../../deep-link.service";
|
||||
@@ -15,7 +15,7 @@ import log from "electron-log";
|
||||
import { WindowManagerService } from "../../window-manager.service";
|
||||
import { Observable, Subject, lastValueFrom } from "rxjs";
|
||||
import { Archive } from "../../../models/archive.class";
|
||||
import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
|
||||
import { Progression, deleteFile, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
|
||||
import { readFile } from "fs/promises";
|
||||
import { FolderLinkerService } from "../../folder-linker.service";
|
||||
import { allSettled } from "../../../../shared/helpers/promise.helpers";
|
||||
@@ -280,6 +280,8 @@ export class LocalMapsManagerService {
|
||||
return new Observable<Progression>(observer => {
|
||||
const progress: Progression = { total: hashs.length, current: 0 };
|
||||
|
||||
observer.next(progress);
|
||||
|
||||
(async () => {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
@@ -447,7 +449,7 @@ export class LocalMapsManagerService {
|
||||
const zip = await BsmZipExtractor.fromPath(zipPath);
|
||||
await zip.extract(mapPath);
|
||||
zip.close();
|
||||
await unlink(zipPath);
|
||||
await deleteFile(zipPath);
|
||||
|
||||
const localMap = await this.loadMapInfoFromPath(mapPath);
|
||||
localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash);
|
||||
|
||||
@@ -5,9 +5,37 @@ import path from "path";
|
||||
import log from "electron-log";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { LinuxService } from "../linux.service";
|
||||
import { bsmSpawn } from "main/helpers/os.helpers";
|
||||
import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers";
|
||||
import { IS_FLATPAK } from "main/constants";
|
||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { parseEnvString } from "main/helpers/env.helpers";
|
||||
|
||||
export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] {
|
||||
const launchArgs = [];
|
||||
|
||||
if(!launchOptions.version.steam && !launchOptions.version.oculus){
|
||||
launchArgs.push("--no-yeet")
|
||||
}
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)) {
|
||||
launchArgs.push("-vrmode");
|
||||
launchArgs.push("oculus");
|
||||
}
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.FPFC)) {
|
||||
launchArgs.push("fpfc");
|
||||
}
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)) {
|
||||
launchArgs.push("--verbose");
|
||||
}
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.EDITOR)) {
|
||||
launchArgs.push("editor");
|
||||
}
|
||||
|
||||
if (launchOptions.command) {
|
||||
launchArgs.push(launchOptions.command);
|
||||
}
|
||||
|
||||
return Array.from(new Set(launchArgs).values());
|
||||
}
|
||||
|
||||
export abstract class AbstractLauncherService {
|
||||
|
||||
@@ -19,34 +47,9 @@ export abstract class AbstractLauncherService {
|
||||
this.localVersions = BSLocalVersionService.getInstance();
|
||||
}
|
||||
|
||||
protected buildBsLaunchArgs(launchOptions: LaunchOption): string[]{
|
||||
const launchArgs = [];
|
||||
private readonly COMMAND_FORMAT = "%command%";
|
||||
|
||||
if(!launchOptions.version.steam && !launchOptions.version.oculus){
|
||||
launchArgs.push("--no-yeet")
|
||||
}
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)) {
|
||||
launchArgs.push("-vrmode");
|
||||
launchArgs.push("oculus");
|
||||
}
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.FPFC)) {
|
||||
launchArgs.push("fpfc");
|
||||
}
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)) {
|
||||
launchArgs.push("--verbose");
|
||||
}
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.EDITOR)) {
|
||||
launchArgs.push("editor");
|
||||
}
|
||||
|
||||
if (launchOptions.additionalArgs) {
|
||||
launchArgs.push(...launchOptions.additionalArgs);
|
||||
}
|
||||
|
||||
return Array.from(new Set(launchArgs).values());
|
||||
}
|
||||
|
||||
protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams {
|
||||
protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): ChildProcessWithoutNullStreams {
|
||||
|
||||
const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), ...(options || {}) };
|
||||
|
||||
@@ -56,8 +59,8 @@ export abstract class AbstractLauncherService {
|
||||
|
||||
spawnOptions.shell = true; // For windows to spawn properly
|
||||
return bsmSpawn(`"${bsExePath}"`, {
|
||||
args, options: spawnOptions, log: true,
|
||||
linux: { prefix: this.linux.getProtonPrefix() },
|
||||
args, options: spawnOptions, log: BsmShellLog.Command,
|
||||
linux: { prefix: options?.protonPrefix || "" },
|
||||
flatpak: {
|
||||
host: IS_FLATPAK,
|
||||
env: [
|
||||
@@ -70,6 +73,8 @@ export abstract class AbstractLauncherService {
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
||||
"STEAM_COMPAT_APP_ID",
|
||||
"SteamEnv",
|
||||
"PROTON_LOG",
|
||||
"PROTON_LOG_DIR",
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -78,7 +83,7 @@ export abstract class AbstractLauncherService {
|
||||
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||
const process = this.launchBSProcess(bsExePath, args, options);
|
||||
|
||||
let timoutId: NodeJS.Timeout;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const exit = new Promise<number>((resolve, reject) => {
|
||||
// Don't remove, useful for debugging!
|
||||
@@ -101,7 +106,7 @@ export abstract class AbstractLauncherService {
|
||||
|
||||
const unrefAfter = options?.unrefAfter ?? sToMs(10);
|
||||
|
||||
timoutId = setTimeout(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
log.error("BS process unref after timeout", unrefAfter);
|
||||
process.unref();
|
||||
process.removeAllListeners();
|
||||
@@ -109,13 +114,44 @@ export abstract class AbstractLauncherService {
|
||||
}, unrefAfter);
|
||||
|
||||
}).finally(() => {
|
||||
clearTimeout(timoutId);
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
return { process, exit };
|
||||
}
|
||||
|
||||
protected injectAdditionalArgsEnvs(
|
||||
launchOptions: LaunchOption,
|
||||
env: Record<string, string>
|
||||
) {
|
||||
if (!launchOptions.command) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { command } = launchOptions;
|
||||
const index = command.indexOf(this.COMMAND_FORMAT);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const envString = command.substring(0, index);
|
||||
log.info("Parsing env string ", `"${envString}"`)
|
||||
for (const [ key, value ] of Object.entries(parseEnvString(envString))) {
|
||||
if (key in env) {
|
||||
log.warn("Ignoring", `${key}=${value}`, "already set env launch command");
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info("Injecting", `${key}="${value}"`, "to the env launch command");
|
||||
env[key] = value;
|
||||
}
|
||||
|
||||
launchOptions.command = command.substring(index + this.COMMAND_FORMAT.length);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type SpawnBsProcessOptions = {
|
||||
protonPrefix?: string;
|
||||
unrefAfter?: number;
|
||||
} & SpawnOptionsWithoutStdio;
|
||||
|
||||
@@ -22,6 +22,9 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsStore } from "../../../shared/models/bs-store.enum";
|
||||
import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { StaticConfigurationService } from "../static-configuration.service";
|
||||
import { SteamService } from "../steam.service";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { LinuxService } from "../linux.service";
|
||||
|
||||
export class BSLauncherService {
|
||||
private static instance: BSLauncherService;
|
||||
@@ -32,8 +35,10 @@ export class BSLauncherService {
|
||||
private readonly ipc: IpcService;
|
||||
private readonly remoteVersion: BSVersionLibService;
|
||||
private readonly steamLauncher: SteamLauncherService;
|
||||
private readonly steam: SteamService;
|
||||
private readonly oculusLauncher: OculusLauncherService;
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
private readonly linux: LinuxService;
|
||||
|
||||
public static getInstance(): BSLauncherService {
|
||||
if (!BSLauncherService.instance) {
|
||||
@@ -51,6 +56,8 @@ export class BSLauncherService {
|
||||
this.steamLauncher = SteamLauncherService.getInstance();
|
||||
this.oculusLauncher = OculusLauncherService.getInstance();
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
this.steam = SteamService.getInstance();
|
||||
this.linux = LinuxService.getInstance();
|
||||
|
||||
this.bsmProtocolService.on("launch", link => {
|
||||
log.info("Launch from bsm protocol", link.toString());
|
||||
@@ -88,10 +95,6 @@ export class BSLauncherService {
|
||||
|
||||
const params = objectFromEntries(shortcutLink.searchParams.entries()) as ShortcutParams;
|
||||
|
||||
if(typeof params.additionalArgs === "string"){
|
||||
params.additionalArgs = [params.additionalArgs];
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -116,7 +119,7 @@ export class BSLauncherService {
|
||||
oculus: params.versionOculus === "true",
|
||||
ino: +params.versionIno
|
||||
},
|
||||
additionalArgs: params.additionalArgs,
|
||||
command: params.command,
|
||||
launchMods,
|
||||
};
|
||||
|
||||
@@ -134,7 +137,7 @@ export class BSLauncherService {
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)){ res.oculusMode = "true"; }
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.FPFC)){ res.desktopMode = "true"; }
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)){ res.debug = "true"; }
|
||||
if(launchOptions.additionalArgs){ res.additionalArgs = launchOptions.additionalArgs; }
|
||||
if(launchOptions.command){ res.command = launchOptions.command; }
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.SKIP_STEAM)){ res.skipSteam = "true"; }
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)){ res.protonLogs = "true"; }
|
||||
|
||||
@@ -178,7 +181,7 @@ export class BSLauncherService {
|
||||
* @returns {Promise<string>} Path of the icon
|
||||
*/
|
||||
private async createShortcutIco(color: Color): Promise<string>{
|
||||
const pngBuffer = await this.createShortcutPngBuffer(color);
|
||||
const pngBuffer = this.createShortcutPngBuffer(color);
|
||||
const icoBuffer = await toIco([pngBuffer]);
|
||||
|
||||
await ensureDir(IMAGE_CACHE_PATH);
|
||||
@@ -195,12 +198,39 @@ export class BSLauncherService {
|
||||
return this.bsmProtocolService.buildLink("launch", shortcutParams).toString();
|
||||
}
|
||||
|
||||
public async createLaunchShortcut(launchOptions: LaunchOption): Promise<boolean>{
|
||||
public async createLaunchShortcut(launchOptions: LaunchOption, steamShortcut?: boolean): Promise<boolean>{
|
||||
const shortcutUrl = this.createLaunchLink(launchOptions);
|
||||
|
||||
const shortcutName = ["Beat Saber", launchOptions.version.BSVersion, launchOptions.version.name].join(" ");
|
||||
const shortcutIconColor = new Color(launchOptions.version.color, "hex");
|
||||
|
||||
if(steamShortcut){
|
||||
const userId = await tryit(() => this.steam.getActiveUser());
|
||||
const exePath = app.getPath("exe");
|
||||
const icon = await this.createShortcutPng(shortcutIconColor)
|
||||
return this.steam.createShortcut(
|
||||
process.platform === "win32"
|
||||
? {
|
||||
AppName: shortcutName,
|
||||
Exe: exePath,
|
||||
StartDir: path.dirname(exePath),
|
||||
LaunchOptions: this.createLaunchLink(launchOptions),
|
||||
OpenVR: "\u0001", icon,
|
||||
}
|
||||
: await this.linux.getSteamShortcutData(
|
||||
shortcutName, icon, launchOptions,
|
||||
await this.steam.getSteamPath(),
|
||||
await this.localVersionService.getVersionPath(launchOptions.version)
|
||||
),
|
||||
userId.result
|
||||
)
|
||||
.then(() => true)
|
||||
.catch(e => {
|
||||
log.error(e);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return execOnOs({
|
||||
win32: async () => (
|
||||
shell.writeShortcutLink(path.join(app.getPath("desktop"), `${shortcutName}.lnk`), {
|
||||
@@ -210,12 +240,13 @@ export class BSLauncherService {
|
||||
description: [shortcutName, launchOptions.version.color].join(" "), // <= Need color in description to help windows know that the shortcut is different
|
||||
})
|
||||
),
|
||||
linux: async () => (
|
||||
createDesktopUrlShortcut(path.join(app.getPath("desktop"), `${shortcutName}.desktop`), {
|
||||
name: shortcutName,
|
||||
url: shortcutUrl,
|
||||
icon: await this.createShortcutPng(shortcutIconColor),
|
||||
})
|
||||
linux: async () => this.linux.createDesktopShortcut(
|
||||
path.join(app.getPath("desktop"), `${shortcutName}.desktop`),
|
||||
shortcutName,
|
||||
await this.createShortcutPng(shortcutIconColor),
|
||||
launchOptions,
|
||||
await this.steam.getSteamPath(),
|
||||
await this.localVersionService.getVersionPath(launchOptions.version)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -227,7 +258,7 @@ export class BSLauncherService {
|
||||
const bsPath: string = await (async () => {
|
||||
const bsPath = await this.localVersionService.getInstalledVersionPath(launchOption.version);
|
||||
return bsPath ?? this.localVersionService.getVersionPath(launchOption.version);
|
||||
})().catch(e => {
|
||||
})().catch((e): null => {
|
||||
log.error(e);
|
||||
return null;
|
||||
});
|
||||
@@ -252,7 +283,7 @@ type ShortcutParams = {
|
||||
oculusMode?: string;
|
||||
desktopMode?: string;
|
||||
debug?: string;
|
||||
additionalArgs?: string[];
|
||||
command?: string;
|
||||
skipSteam?: string;
|
||||
protonLogs?: string;
|
||||
version: string;
|
||||
@@ -262,26 +293,3 @@ type ShortcutParams = {
|
||||
versionOculus?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create .desktop file for url shortcut (only for linux)
|
||||
* @param {string} shortcutPath
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
function createDesktopUrlShortcut(shortcutPath: string, options?: {
|
||||
url: string
|
||||
name: string,
|
||||
icon: string
|
||||
}): Promise<boolean> {
|
||||
const { url, name, icon } = options || {};
|
||||
|
||||
const data = [
|
||||
"[Desktop Entry]",
|
||||
"Type=Link",
|
||||
`Name=${name}`,
|
||||
`Icon=${icon}`,
|
||||
`URL=${url}`
|
||||
].join("\n");
|
||||
|
||||
return writeFile(shortcutPath, data).then(() => true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Observable, ReplaySubject } from "rxjs";
|
||||
import { Observable } from "rxjs";
|
||||
import { StoreLauncherInterface } from "./store-launcher.interface";
|
||||
import { BSLaunchError, BSLaunchEvent, BSLaunchEventData, LaunchOption } from "../../../shared/models/bs-launch";
|
||||
import { OculusService } from "../oculus.service";
|
||||
@@ -6,10 +6,9 @@ import { BS_EXECUTABLE } from "../../constants";
|
||||
import path from "path";
|
||||
import log from "electron-log";
|
||||
import { pathExists } from "fs-extra";
|
||||
import { AbstractLauncherService } from "./abstract-launcher.service";
|
||||
import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service";
|
||||
import { isProcessRunning } from "../../helpers/os.helpers";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { UtilsService } from "../utils.service";
|
||||
|
||||
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
|
||||
|
||||
@@ -23,14 +22,10 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
}
|
||||
|
||||
private readonly oculus: OculusService;
|
||||
private readonly util: UtilsService;
|
||||
|
||||
private readonly oculusLib$ = new ReplaySubject<string>();
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
this.oculus = OculusService.getInstance();
|
||||
this.util = UtilsService.getInstance();
|
||||
}
|
||||
|
||||
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData> {
|
||||
@@ -54,10 +49,17 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
// Make sure Oculus is running
|
||||
await this.oculus.startOculus().catch(err => log.error("Error while starting Oculus", err));
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
this.injectAdditionalArgsEnvs(launchOptions, env);
|
||||
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
|
||||
// Launch Beat Saber
|
||||
const process = this.launchBs(exePath, this.buildBsLaunchArgs(launchOptions));
|
||||
const process = this.launchBs(
|
||||
exePath,
|
||||
buildBsLaunchArgs(launchOptions),
|
||||
{ env }
|
||||
);
|
||||
|
||||
return process.exit.catch(err => {
|
||||
throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR);
|
||||
|
||||
@@ -6,11 +6,12 @@ import { SteamService } from "../steam.service";
|
||||
import path from "path";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { AbstractLauncherService } from "./abstract-launcher.service";
|
||||
import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { exec } from "child_process";
|
||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { app } from "electron";
|
||||
|
||||
export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{
|
||||
|
||||
@@ -88,7 +89,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
obs.next({type: BSLaunchWarning.UNABLE_TO_LAUNCH_STEAM});
|
||||
});
|
||||
}
|
||||
else {
|
||||
else if(skipSteam) {
|
||||
obs.next({ type: BSLaunchEvent.SKIPPING_STEAM_LAUNCH});
|
||||
}
|
||||
|
||||
@@ -101,7 +102,6 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
await this.restoreSteamVR().catch(log.error);
|
||||
}
|
||||
|
||||
const launchArgs = this.buildBsLaunchArgs(launchOptions);
|
||||
const steamPath = await this.steam.getSteamPath();
|
||||
|
||||
const env = {
|
||||
@@ -111,20 +111,31 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
"SteamGameId": BS_APP_ID,
|
||||
};
|
||||
|
||||
let protonPrefix = "";
|
||||
// Linux setup
|
||||
if (process.platform === "linux") {
|
||||
await this.linux.setupLaunch(launchOptions, steamPath, bsFolderPath, env);
|
||||
const linuxSetup = await this.linux.setupLaunch(
|
||||
launchOptions, steamPath, bsFolderPath
|
||||
);
|
||||
protonPrefix = linuxSetup.protonPrefix;
|
||||
Object.assign(env, linuxSetup.env);
|
||||
}
|
||||
|
||||
this.injectAdditionalArgsEnvs(launchOptions, env);
|
||||
const launchArgs = buildBsLaunchArgs(launchOptions);
|
||||
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
|
||||
const spawnOpts = { env, cwd: bsFolderPath };
|
||||
|
||||
const launchPromise = !launchOptions.admin ? (
|
||||
this.launchBs(bsExePath, launchArgs, spawnOpts).exit
|
||||
this.launchBs(bsExePath, launchArgs, {
|
||||
...spawnOpts,
|
||||
protonPrefix
|
||||
}).exit
|
||||
) : (
|
||||
new Promise<number>(resolve => {
|
||||
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${bsExePath}" ${launchArgs.join(" ")}`, spawnOpts);
|
||||
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${bsExePath}" ${launchArgs.join(" ")} --log-path "${path.join(app.getPath("logs"), "bs-admin-start.log")}"`, spawnOpts);
|
||||
adminProcess.on("error", err => {
|
||||
log.error("Error while starting BS as Admin", err);
|
||||
resolve(-1)
|
||||
|
||||
@@ -5,8 +5,6 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { RequestService } from "./request.service";
|
||||
import { readJSON } from "fs-extra";
|
||||
import { allSettled } from "../../shared/helpers/promise.helpers";
|
||||
import { LinuxService } from "./linux.service";
|
||||
import { IS_FLATPAK } from "main/constants";
|
||||
import { StaticConfigurationService } from "./static-configuration.service";
|
||||
|
||||
export class BSVersionLibService {
|
||||
@@ -15,7 +13,6 @@ export class BSVersionLibService {
|
||||
|
||||
private static instance: BSVersionLibService;
|
||||
|
||||
private readonly linuxService: LinuxService;
|
||||
private readonly utilsService: UtilsService;
|
||||
private readonly requestService: RequestService;
|
||||
private readonly configService: StaticConfigurationService;
|
||||
@@ -23,7 +20,6 @@ export class BSVersionLibService {
|
||||
private bsVersions: BSVersion[];
|
||||
|
||||
private constructor() {
|
||||
this.linuxService = LinuxService.getInstance();
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
this.configService = StaticConfigurationService.getInstance();
|
||||
@@ -40,15 +36,10 @@ export class BSVersionLibService {
|
||||
return this.requestService.getJSON<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL).then(res => res.data);
|
||||
}
|
||||
|
||||
private async shouldLoadFromConfig(): Promise<boolean> {
|
||||
// Some special cases of readonly memory installations
|
||||
return process.platform === "linux" && (IS_FLATPAK || this.linuxService.isNixOS());
|
||||
}
|
||||
|
||||
private async getLocalVersions(): Promise<BSVersion[]> {
|
||||
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
|
||||
|
||||
if (!(await this.shouldLoadFromConfig())) {
|
||||
if (process.platform !== "linux") {
|
||||
return readJSON(localVersionsPath);
|
||||
}
|
||||
|
||||
@@ -60,7 +51,7 @@ export class BSVersionLibService {
|
||||
}
|
||||
|
||||
private async updateLocalVersions(versions: BSVersion[]): Promise<void> {
|
||||
if (await this.shouldLoadFromConfig()) {
|
||||
if (process.platform === "linux") {
|
||||
this.configService.set("versions", versions);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { InstallationLocationService } from "./installation-location.service";
|
||||
import log from "electron-log";
|
||||
import { deleteFolder, ensureFolderExist, moveFolderContent, pathExist, unlinkPath } from "../helpers/fs.helpers";
|
||||
import { deleteFile, deleteFileSync, deleteFolder, deleteFolderSync, ensureFolderExist, moveFolderContent, pathExist } from "../helpers/fs.helpers";
|
||||
import { lstat, symlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { copy, readlink } from "fs-extra";
|
||||
import { copy, mkdirSync, readlink, symlinkSync } from "fs-extra";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { StaticConfigurationService } from "./static-configuration.service";
|
||||
import { randomUUID } from "crypto";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
|
||||
export class FolderLinkerService {
|
||||
private static instance: FolderLinkerService;
|
||||
@@ -41,6 +44,29 @@ export class FolderLinkerService {
|
||||
}
|
||||
}
|
||||
|
||||
public isLinkingSupported(): boolean {
|
||||
const uuid = randomUUID();
|
||||
const installationPath = this.installLocationService.installationDirectory();
|
||||
const testFolder = path.join(installationPath, uuid);
|
||||
const testLink = path.join(installationPath, `${uuid}_link`);
|
||||
|
||||
const resLink = tryit(() => {
|
||||
mkdirSync(testFolder);
|
||||
symlinkSync(testFolder, testLink, this.getLinkingType());
|
||||
});
|
||||
|
||||
tryit(() => {
|
||||
deleteFolderSync(testFolder);
|
||||
deleteFileSync(testLink);
|
||||
});
|
||||
|
||||
if(resLink.error){
|
||||
log.error("Unable to create symlink", resLink.error);
|
||||
}
|
||||
|
||||
return !resLink.error;
|
||||
}
|
||||
|
||||
public sharedFolder(): string {
|
||||
return this.installLocationService.sharedContentPath();
|
||||
}
|
||||
@@ -74,6 +100,10 @@ export class FolderLinkerService {
|
||||
}
|
||||
|
||||
public async linkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
|
||||
if(!this.isLinkingSupported()){
|
||||
throw new CustomError("Linking is not supported on this platform", "LinkingNotSupported");
|
||||
}
|
||||
|
||||
const sharedPath = await this.getSharedFolder(folderPath, options?.intermediateFolder);
|
||||
|
||||
if (await this.isFolderSymlink(folderPath)) {
|
||||
@@ -83,7 +113,7 @@ export class FolderLinkerService {
|
||||
if (isTargetedToSharedPath) {
|
||||
return;
|
||||
}
|
||||
await unlinkPath(folderPath);
|
||||
await deleteFile(folderPath);
|
||||
|
||||
log.info(`Linking ${folderPath} to ${sharedPath}; type: ${this.linkingType}`);
|
||||
return symlink(sharedPath, folderPath, this.getLinkingType());
|
||||
@@ -111,7 +141,7 @@ export class FolderLinkerService {
|
||||
if (!(await this.isFolderSymlink(folderPath))) {
|
||||
return;
|
||||
}
|
||||
await unlinkPath(folderPath);
|
||||
await deleteFile(folderPath);
|
||||
|
||||
const sharedPath = await this.getSharedFolder(folderPath, options?.intermediateFolder);
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import fs from "fs-extra";
|
||||
import log from "electron-log";
|
||||
import path from "path";
|
||||
import { BS_APP_ID, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||
import { InstallationLocationService } from "./installation-location.service";
|
||||
import { StaticConfigurationService } from "./static-configuration.service";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { BSLaunchError, LaunchOption } from "shared/models/bs-launch";
|
||||
import { bsmExec } from "main/helpers/os.helpers";
|
||||
import { BsmShellLog, bsmExec } from "main/helpers/os.helpers";
|
||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { SteamShortcutData } from "shared/models/steam/shortcut.model";
|
||||
import { buildBsLaunchArgs } from "./bs-launcher/abstract-launcher.service";
|
||||
|
||||
export class LinuxService {
|
||||
private static instance: LinuxService;
|
||||
@@ -18,38 +21,46 @@ export class LinuxService {
|
||||
return LinuxService.instance;
|
||||
}
|
||||
|
||||
private readonly installLocationService: InstallationLocationService;
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
private protonPrefix = "";
|
||||
|
||||
private nixOS: boolean | undefined;
|
||||
|
||||
private constructor() {
|
||||
this.installLocationService = InstallationLocationService.getInstance();
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
}
|
||||
|
||||
// === Launching === //
|
||||
|
||||
private getCompatDataPath() {
|
||||
const sharedFolder = this.installLocationService.sharedContentPath();
|
||||
return path.resolve(sharedFolder, "compatdata");
|
||||
}
|
||||
|
||||
public async setupLaunch(
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: string,
|
||||
env: Record<string, string>
|
||||
) {
|
||||
bsFolderPath: string
|
||||
): Promise<{
|
||||
protonPrefix: string;
|
||||
env: Record<string, string>;
|
||||
}> {
|
||||
if (launchOptions.admin) {
|
||||
log.warn("Launching as admin is not supported on Linux! Starting the game as a normal user.");
|
||||
launchOptions.admin = false;
|
||||
}
|
||||
|
||||
// Create the compat data path if it doesn't exist.
|
||||
// If the user never ran Beat Saber through steam before
|
||||
// using bsmanager, it won't exist, and proton will fail
|
||||
// to launch the game.
|
||||
const compatDataPath = `${steamPath}/steamapps/compatdata/${BS_APP_ID}`;
|
||||
if (!fs.existsSync(compatDataPath)) {
|
||||
log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`);
|
||||
fs.mkdirSync(compatDataPath);
|
||||
}
|
||||
const protonPath = await this.getProtonPath();
|
||||
return {
|
||||
protonPrefix: await this.isNixOS()
|
||||
? `steam-run "${protonPath}" run`
|
||||
: `"${protonPath}" run`,
|
||||
env: await this.buildEnvVariables(launchOptions, steamPath, bsFolderPath)
|
||||
};
|
||||
}
|
||||
|
||||
private async getProtonPath(): Promise<string> {
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
throw CustomError.fromError(
|
||||
new Error("Proton folder not set"),
|
||||
@@ -67,27 +78,41 @@ export class LinuxService {
|
||||
);
|
||||
}
|
||||
|
||||
this.protonPrefix = await this.isNixOS()
|
||||
? `steam-run "${protonPath}" run`
|
||||
: `"${protonPath}" run`;
|
||||
return protonPath;
|
||||
}
|
||||
|
||||
private async buildEnvVariables(
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: string
|
||||
): Promise<Record<string, string>> {
|
||||
// Create the compat data path if it doesn't exist.
|
||||
// If the user never ran Beat Saber through steam before
|
||||
// using bsmanager, it won't exist, and proton will fail
|
||||
// to launch the game.
|
||||
const compatDataPath = this.getCompatDataPath();
|
||||
if (!fs.existsSync(compatDataPath)) {
|
||||
log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`);
|
||||
await fs.ensureDir(compatDataPath);
|
||||
}
|
||||
|
||||
// Setup Proton environment variables
|
||||
Object.assign(env, {
|
||||
const envVars: Record<string, string> = {
|
||||
"WINEDLLOVERRIDES": "winhttp=n,b", // Required for mods to work
|
||||
"STEAM_COMPAT_DATA_PATH": compatDataPath,
|
||||
"STEAM_COMPAT_INSTALL_PATH": bsFolderPath,
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH": steamPath,
|
||||
"STEAM_COMPAT_APP_ID": BS_APP_ID,
|
||||
// Run game in steam environment; fixes #585 for unicode song titles
|
||||
"SteamEnv": 1,
|
||||
});
|
||||
"SteamEnv": "1",
|
||||
};
|
||||
|
||||
if (launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)) {
|
||||
Object.assign(env, {
|
||||
"PROTON_LOG": 1,
|
||||
"PROTON_LOG_DIR": path.join(bsFolderPath, "Logs"),
|
||||
});
|
||||
envVars.PROTON_LOG = "1";
|
||||
envVars.PROTON_LOG_DIR = path.join(bsFolderPath, "Logs");
|
||||
}
|
||||
|
||||
return envVars;
|
||||
}
|
||||
|
||||
public verifyProtonPath(protonFolder: string = ""): boolean {
|
||||
@@ -120,9 +145,10 @@ export class LinuxService {
|
||||
return winePath;
|
||||
}
|
||||
|
||||
public getProtonPrefix(): string {
|
||||
// Set in setupLaunch
|
||||
return this.protonPrefix;
|
||||
public getWinePrefixPath(): string {
|
||||
const compatDataPath = this.getCompatDataPath();
|
||||
return fs.existsSync(compatDataPath)
|
||||
? path.join(compatDataPath, "pfx") : "";
|
||||
}
|
||||
|
||||
// === NixOS Specific === //
|
||||
@@ -134,7 +160,7 @@ export class LinuxService {
|
||||
|
||||
try {
|
||||
await bsmExec("nixos-version", {
|
||||
log: true,
|
||||
log: BsmShellLog.Command,
|
||||
flatpak: { host: IS_FLATPAK },
|
||||
});
|
||||
this.nixOS = true;
|
||||
@@ -145,4 +171,96 @@ export class LinuxService {
|
||||
|
||||
return this.nixOS;
|
||||
}
|
||||
|
||||
// === Shortcuts === //
|
||||
|
||||
private getCommand(
|
||||
protonPrefix: string,
|
||||
bsFolderPath: string,
|
||||
env: Record<string, string>,
|
||||
launchOptions: LaunchOption
|
||||
): string {
|
||||
const envString = Object.entries(env)
|
||||
.map(([ key, value ]) => `${key}="${value}"`)
|
||||
.join(" ");
|
||||
const bsExe = path.join(bsFolderPath, BS_EXECUTABLE);
|
||||
const args = buildBsLaunchArgs(launchOptions).join(" ");
|
||||
return `${envString} ${protonPrefix} "${bsExe}" ${args}`;
|
||||
}
|
||||
|
||||
public async createDesktopShortcut(
|
||||
shortcutPath: string,
|
||||
name: string,
|
||||
icon: string,
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const {
|
||||
protonPrefix, env
|
||||
} = await this.setupLaunch(launchOptions, steamPath, bsFolderPath);
|
||||
|
||||
Object.assign(env, {
|
||||
"SteamAppId": BS_APP_ID,
|
||||
"SteamOverlayGameId": BS_APP_ID,
|
||||
"SteamGameId": BS_APP_ID,
|
||||
});
|
||||
|
||||
const command = this.getCommand(
|
||||
protonPrefix, bsFolderPath,
|
||||
env, launchOptions
|
||||
);
|
||||
|
||||
const desktopEntry = [
|
||||
"[Desktop Entry]",
|
||||
"Type=Application",
|
||||
`Name=${name}`,
|
||||
`Icon=${icon}`,
|
||||
`Path=${bsFolderPath}`,
|
||||
`Exec=${command}`
|
||||
].join("\n");
|
||||
|
||||
await fs.writeFile(shortcutPath, desktopEntry);
|
||||
log.info("Created shorcut at ", `"${shortcutPath}/${name}"`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("Could not create shortcut", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getSteamShortcutData(
|
||||
shortcutName: string,
|
||||
icon: string,
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: string
|
||||
): Promise<SteamShortcutData> {
|
||||
const env = await this.buildEnvVariables(
|
||||
launchOptions, steamPath, bsFolderPath
|
||||
);
|
||||
Object.assign(env, {
|
||||
"SteamAppId": BS_APP_ID,
|
||||
"SteamOverlayGameId": BS_APP_ID,
|
||||
"SteamGameId": BS_APP_ID,
|
||||
});
|
||||
|
||||
const protonPrefix = await this.isNixOS()
|
||||
? "steam-run %command% run"
|
||||
: "%command% run";
|
||||
|
||||
return {
|
||||
AppName: shortcutName,
|
||||
Exe: await this.getProtonPath(),
|
||||
StartDir: bsFolderPath,
|
||||
icon,
|
||||
OpenVR: "\x01",
|
||||
LaunchOptions: this.getCommand(
|
||||
protonPrefix, bsFolderPath,
|
||||
env, launchOptions
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ export class BeatModsApiService {
|
||||
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`).then(({ data }) => {
|
||||
this.updateModsHashCache(data?.modVersions ?? []);
|
||||
return data?.modVersions?.at(0);
|
||||
}).catch((e): undefined => {
|
||||
log.error(`Failed to get mod by hash: ${hash}`, e);
|
||||
}).catch((): undefined => {
|
||||
log.error(`Failed to get mod by hash: ${hash}`);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,18 +6,18 @@ import md5File from "md5-file";
|
||||
import { RequestService } from "../request.service";
|
||||
import { BS_EXECUTABLE } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { deleteFile, deleteFolder, pathExist, Progression } from "../../helpers/fs.helpers";
|
||||
import { lastValueFrom, Observable } from "rxjs";
|
||||
import recursiveReadDir from "recursive-readdir";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { copyFile, ensureDir, pathExistsSync } from "fs-extra";
|
||||
import { copyFile, ensureDir, pathExistsSync, readdirSync } from "fs-extra";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { popElement } from "shared/helpers/array.helpers";
|
||||
import { LinuxService } from "../linux.service";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import crypto from "crypto";
|
||||
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
|
||||
import { bsmSpawn } from "main/helpers/os.helpers";
|
||||
import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers";
|
||||
import { BbmFullMod, BbmModVersion, ExternalMod } from "../../../shared/models/mods/mod.interface";
|
||||
|
||||
export class BsModsManagerService {
|
||||
@@ -96,8 +96,14 @@ export class BsModsManagerService {
|
||||
return mod;
|
||||
});
|
||||
|
||||
const mods = await Promise.all(promises);
|
||||
return mods.filter(Boolean);
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
return results.reduce((mods, mod) => {
|
||||
if (mod?.status === "fulfilled" && mod?.value) {
|
||||
mods.push(mod.value);
|
||||
}
|
||||
return mods;
|
||||
}, [] as BbmModVersion[]);
|
||||
}
|
||||
|
||||
private async getBsipaInstalled(version: BSVersion): Promise<BbmModVersion> {
|
||||
@@ -139,25 +145,34 @@ export class BsModsManagerService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
const cmd = `"${ipaPath}" "${bsExePath}" ${args.join(" ")}`;
|
||||
let winePath: string = "";
|
||||
if (process.platform === "linux") {
|
||||
const { error, result } = tryit(() => this.linuxService.getWinePath());
|
||||
if (error) {
|
||||
log.error(error);
|
||||
const { error: winePathError, result: winePathResult } =
|
||||
tryit(() => this.linuxService.getWinePath());
|
||||
if (winePathError) {
|
||||
log.error(winePathError);
|
||||
return false;
|
||||
}
|
||||
winePath = `"${result}"`;
|
||||
winePath = `"${winePathResult}"`;
|
||||
|
||||
const winePrefix = this.linuxService.getWinePrefixPath();
|
||||
if (winePrefix) {
|
||||
env.WINEPREFIX = winePrefix;
|
||||
} else {
|
||||
log.warn("Could not find BSManager WINEPREFIX path, using system's default value instead");
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
log.info("START IPA PROCESS", cmd);
|
||||
const processIPA = bsmSpawn(cmd, {
|
||||
log: true,
|
||||
log: BsmShellLog.Command | BsmShellLog.EnvVariables,
|
||||
options: {
|
||||
cwd: versionPath,
|
||||
detached: true,
|
||||
shell: true
|
||||
shell: true,
|
||||
env
|
||||
},
|
||||
linux: { prefix: winePath },
|
||||
});
|
||||
@@ -194,6 +209,12 @@ export class BsModsManagerService {
|
||||
private async installMod(mod: BbmFullMod, version: BSVersion): Promise<boolean> {
|
||||
log.info("INSTALL MOD", mod.mod.name, "for version", `${version.BSVersion} - ${version.name}`);
|
||||
|
||||
const isBSIPA = mod.mod.name.toLowerCase() === "bsipa";
|
||||
|
||||
if(isBSIPA){
|
||||
await this.clearIpaFolder(version).catch(e => log.error("Error while clearing IPA folder", e));
|
||||
}
|
||||
|
||||
const downloadUrl = this.getModDownload(mod.version);
|
||||
|
||||
if (!downloadUrl) {
|
||||
@@ -222,7 +243,6 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const isBSIPA = mod.mod.name.toLowerCase() === "bsipa";
|
||||
const destDir = isBSIPA ? versionPath : path.join(versionPath, ModsInstallFolder.PENDING);
|
||||
|
||||
log.info("Start extracting mod zip", mod.mod.name, "to", destDir);
|
||||
@@ -249,6 +269,41 @@ export class BsModsManagerService {
|
||||
return res;
|
||||
}
|
||||
|
||||
private async clearIpaFolder(version: BSVersion): Promise<void> {
|
||||
log.info("Clearing IPA folder");
|
||||
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const ipaPath = path.join(versionPath, ModsInstallFolder.IPA);
|
||||
|
||||
if(!pathExistsSync(ipaPath)){
|
||||
log.info("IPA folder does not exist, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
const contents = readdirSync(ipaPath, { withFileTypes: true });
|
||||
|
||||
for(const content of contents){
|
||||
|
||||
if (content.name === 'Backups' || content.name === 'Pending') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentPath = path.join(ipaPath, content.name);
|
||||
|
||||
const res = await tryit(() => content.isDirectory()
|
||||
? deleteFolder(contentPath)
|
||||
: deleteFile(contentPath)
|
||||
);
|
||||
|
||||
if(res.error){
|
||||
log.error("Error while clearing IPA folder content", content.name, res.error);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("IPA folder cleared successfully");
|
||||
|
||||
}
|
||||
|
||||
private async uninstallBSIPA(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||
const verionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const hasIPAExe = await pathExist(path.join(verionPath, "IPA.exe"));
|
||||
@@ -262,7 +317,7 @@ export class BsModsManagerService {
|
||||
|
||||
const promises = mod.version.contentHashes.map(content => {
|
||||
const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
|
||||
return unlinkPath(path.join(verionPath, file));
|
||||
return deleteFile(path.join(verionPath, file));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -275,7 +330,10 @@ export class BsModsManagerService {
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
|
||||
const promises = mod.version.contentHashes.map(async content => {
|
||||
return Promise.all([unlinkPath(path.join(versionPath, content.path)), unlinkPath(path.join(versionPath, "IPA", "Pending", content.path))]);
|
||||
return Promise.all([
|
||||
deleteFile(path.join(versionPath, content.path)),
|
||||
deleteFile(path.join(versionPath, "IPA", "Pending", content.path))
|
||||
]);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -292,8 +350,8 @@ export class BsModsManagerService {
|
||||
|
||||
const bsipa = await this.getBsipaInstalled(version);
|
||||
|
||||
const pluginsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.PLUGINS), this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING)]);
|
||||
const libsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.LIBS), this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING)]);
|
||||
const pluginsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING), this.getModsInDir(version, ModsInstallFolder.PLUGINS)]);
|
||||
const libsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING), this.getModsInDir(version, ModsInstallFolder.LIBS)]);
|
||||
|
||||
const dirMods = pluginsMods.flat().concat(libsMods.flat());
|
||||
|
||||
@@ -481,9 +539,11 @@ export class BsModsManagerService {
|
||||
const versionMods = await this.getAvailableMods(version);
|
||||
const installedMods = await this.getInstalledMods(version);
|
||||
|
||||
const fullInstalledMods: BbmFullMod[] = installedMods?.map(version => {
|
||||
return { version, mod: versionMods.find(mod => version.modId === mod.mod.id)?.mod };
|
||||
}) ?? [];
|
||||
const fullInstalledMods = (installedMods || []).reduce((mods, version) => {
|
||||
const mod = versionMods.find(mod => version.modId === mod.mod.id)?.mod;
|
||||
if (mod) { mods.push({ version, mod }); }
|
||||
return mods;
|
||||
}, [] as BbmFullMod[]);
|
||||
|
||||
const progress = { current: 0, total: fullInstalledMods.length };
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { pathExist, resolveGUIDPath } from "../helpers/fs.helpers";
|
||||
import log from "electron-log";
|
||||
import { lstat } from "fs-extra";
|
||||
import { tryit } from "../../shared/helpers/error.helpers";
|
||||
import { shell } from "electron";
|
||||
import { app, shell } from "electron";
|
||||
import { isProcessRunning } from "../helpers/os.helpers";
|
||||
import { sToMs } from "../../shared/helpers/time.helpers";
|
||||
import { execOnOs } from "../helpers/env.helpers";
|
||||
@@ -169,7 +169,7 @@ export class OculusService {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
log.info("Enabling sideloaded apps");
|
||||
const process = exec(exePath);
|
||||
const process = exec(`"${exePath}" --log-path "${path.join(app.getPath("logs"), "oculus-allow-dev-sideloaded.log")}"`);
|
||||
process.on("exit", code => {
|
||||
if(code === 0){
|
||||
resolve();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { createWriteStream, WriteStream } from 'fs';
|
||||
import { Progression } from 'main/helpers/fs.helpers';
|
||||
import { deleteFileSync, Progression } from 'main/helpers/fs.helpers';
|
||||
import { Observable } from 'rxjs';
|
||||
import { shareReplay, tap } from 'rxjs/operators';
|
||||
import log from 'electron-log';
|
||||
import got from 'got';
|
||||
import { IncomingHttpHeaders, IncomingMessage } from 'http';
|
||||
import { unlinkSync } from 'fs-extra';
|
||||
import { tryit } from 'shared/helpers/error.helpers';
|
||||
import path from 'path';
|
||||
import { pipeline } from 'stream/promises';
|
||||
@@ -84,7 +83,7 @@ export class RequestService {
|
||||
|
||||
pipeline(stream, file).catch(err => {
|
||||
file?.destroy();
|
||||
tryit(() => unlinkSync(dest));
|
||||
tryit(() => deleteFileSync(dest));
|
||||
subscriber.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,49 +2,55 @@ import { RegDwordValue } from "regedit-rs"
|
||||
import path from "path";
|
||||
import { parse } from "@node-steam/vdf";
|
||||
import { readFile } from "fs/promises";
|
||||
import { pathExist } from "../helpers/fs.helpers";
|
||||
import log from "electron-log";
|
||||
import { app, shell } from "electron";
|
||||
import { getProcessId, isProcessRunning } from "main/helpers/os.helpers";
|
||||
import { isElevated } from "query-process";
|
||||
import { execOnOs } from "../helpers/env.helpers";
|
||||
import { pathExists, pathExistsSync, readdir, writeFile } from "fs-extra";
|
||||
import { SteamShortcut, SteamShortcutData } from "../../shared/models/steam/shortcut.model";
|
||||
|
||||
const { list } = (execOnOs({ win32: () => require("regedit-rs") }, true) ?? {}) as typeof import("regedit-rs");
|
||||
|
||||
export class SteamService {
|
||||
|
||||
private static readonly PROCESS_NAME: string = process.platform === "linux"
|
||||
? "steam-runtime-launcher-service"
|
||||
: "steam.exe";
|
||||
private static readonly PROCESS_NAME: string = process.platform === "linux" ? "steam-runtime-launcher-service" : "steam.exe";
|
||||
|
||||
private static instance: SteamService;
|
||||
|
||||
private steamPath: string = '';
|
||||
private steamPath: string = "";
|
||||
|
||||
private constructor(){}
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(){
|
||||
if(!SteamService.instance){ SteamService.instance = new SteamService(); }
|
||||
public static getInstance() {
|
||||
if (!SteamService.instance) {
|
||||
SteamService.instance = new SteamService();
|
||||
}
|
||||
return SteamService.instance;
|
||||
}
|
||||
|
||||
public async getActiveUser(): Promise<number>{
|
||||
public async getActiveUser(): Promise<number> {
|
||||
const res = await list("HKCU\\Software\\Valve\\Steam\\ActiveProcess");
|
||||
const key = res["HKCU\\Software\\Valve\\Steam\\ActiveProcess"];
|
||||
if(!key.exists){ throw new Error("Key \"HKCU\\Software\\Valve\\Steam\\ActiveProcess\" not exist"); }
|
||||
if (!key.exists) {
|
||||
throw new Error('Key "HKCU\\Software\\Valve\\Steam\\ActiveProcess" not exist');
|
||||
}
|
||||
const registryValue = key.values.ActiveUser as RegDwordValue;
|
||||
if(!registryValue){ throw new Error("Value \"ActiveUser\" not exist"); }
|
||||
if (!registryValue) {
|
||||
throw new Error('Value "ActiveUser" not exist');
|
||||
}
|
||||
return registryValue.value;
|
||||
}
|
||||
|
||||
public async isSteamRunning(): Promise<boolean>{
|
||||
public async isSteamRunning(): Promise<boolean> {
|
||||
const steamProcessRunning = await isProcessRunning(SteamService.PROCESS_NAME);
|
||||
if(process.platform === "linux") { return steamProcessRunning; }
|
||||
if (process.platform === "linux") {
|
||||
return steamProcessRunning;
|
||||
}
|
||||
const activeUser = await this.getActiveUser().catch(err => log.error(err));
|
||||
return steamProcessRunning && !!activeUser;
|
||||
}
|
||||
|
||||
public async getSteamPid(): Promise<number>{
|
||||
public async getSteamPid(): Promise<number> {
|
||||
return getProcessId(SteamService.PROCESS_NAME);
|
||||
}
|
||||
|
||||
@@ -53,26 +59,29 @@ export class SteamService {
|
||||
* @throws Can throw an error if the Steam process is running as admin
|
||||
* @returns true if the Steam process is running as administrator
|
||||
*/
|
||||
public async isElevated(): Promise<boolean>{
|
||||
public async isElevated(): Promise<boolean> {
|
||||
const steamPid = await this.getSteamPid();
|
||||
|
||||
if(!steamPid){ return false; }
|
||||
if (!steamPid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isElevated(steamPid);
|
||||
}
|
||||
|
||||
public async getSteamPath(): Promise<string>{
|
||||
|
||||
if(this.steamPath){ return this.steamPath; }
|
||||
public async getSteamPath(): Promise<string> {
|
||||
if (this.steamPath) {
|
||||
return this.steamPath;
|
||||
}
|
||||
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
this.steamPath = path.join(app.getPath('home'), '.steam', "steam");
|
||||
this.steamPath = path.join(app.getPath("home"), ".steam", "steam");
|
||||
return this.steamPath;
|
||||
case "win32": {
|
||||
const res = await list(['HKLM\\SOFTWARE\\Valve\\Steam', 'HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam']);
|
||||
const win64 = res['HKLM\\SOFTWARE\\Valve\\Steam'];
|
||||
const win32 = res['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam'];
|
||||
const res = await list(["HKLM\\SOFTWARE\\Valve\\Steam", "HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]);
|
||||
const win64 = res["HKLM\\SOFTWARE\\Valve\\Steam"];
|
||||
const win32 = res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"];
|
||||
|
||||
if (win64.exists && win64?.values?.InstallPath?.value) {
|
||||
this.steamPath = win64.values.InstallPath.value as string;
|
||||
@@ -93,23 +102,29 @@ export class SteamService {
|
||||
|
||||
let libraryFolders: any = path.join(steamPath, "steamapps", "libraryfolders.vdf");
|
||||
|
||||
if (!(await pathExist(libraryFolders))) { return null; }
|
||||
if (!(await pathExists(libraryFolders))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
libraryFolders = parse(await readFile(libraryFolders, { encoding: "utf-8" }));
|
||||
|
||||
if (!libraryFolders.libraryfolders) { return null; }
|
||||
if (!libraryFolders.libraryfolders) {
|
||||
return null;
|
||||
}
|
||||
libraryFolders = libraryFolders.libraryfolders;
|
||||
|
||||
for (const libKey in Object.keys(libraryFolders)) {
|
||||
if (!libraryFolders?.[libKey]?.apps) { continue; }
|
||||
if (!libraryFolders?.[libKey]?.apps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (libraryFolders[libKey].apps[gameId] != null) {
|
||||
return path.join(libraryFolders[libKey].path, "steamapps", "common", gameFolder);
|
||||
const commonFolder = path.join(libraryFolders[libKey].path, "steamapps", "common");
|
||||
return gameFolder ? path.join(commonFolder, gameFolder) : commonFolder;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
return null;
|
||||
@@ -117,7 +132,6 @@ export class SteamService {
|
||||
}
|
||||
|
||||
public async openSteam(): Promise<void> {
|
||||
|
||||
await shell.openExternal("steam://open/games");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -125,7 +139,9 @@ export class SteamService {
|
||||
const interval = setInterval(() => {
|
||||
const steamRunning = this.isSteamRunning().catch(() => false);
|
||||
steamRunning.then(running => {
|
||||
if(!running){ return; }
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
});
|
||||
@@ -138,4 +154,70 @@ export class SteamService {
|
||||
}, 60_000);
|
||||
});
|
||||
}
|
||||
|
||||
private async getUserDataFolders(): Promise<string[]> {
|
||||
const steamPath = await this.getSteamPath();
|
||||
|
||||
if (!steamPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const configPath = path.join(steamPath, "userdata");
|
||||
|
||||
if (!pathExistsSync(configPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const folders = readdir(configPath, { withFileTypes: true })
|
||||
.then(entries => {
|
||||
return entries.reduce((acc, entry) => {
|
||||
if (entry.isDirectory() && Number.isInteger(parseInt(entry.name))) {
|
||||
acc.push(path.join(configPath, entry.name));
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
})
|
||||
.catch((err): string[] => {
|
||||
log.error("Error while reading steam user data folders", err);
|
||||
return [];
|
||||
});
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
private async getShortcutsPath(userId: number): Promise<string> {
|
||||
return path.join(await this.getSteamPath(), "userdata", userId.toString(), "config", "shortcuts.vdf");
|
||||
}
|
||||
|
||||
private async getShortcuts(userId: number): Promise<SteamShortcut[]> {
|
||||
const shortcutsPath = await this.getShortcutsPath(userId);
|
||||
const shortcutsString = await readFile(shortcutsPath, { encoding: "utf-8" });
|
||||
|
||||
return SteamShortcut.parseShortcutsRawData(shortcutsString);
|
||||
}
|
||||
|
||||
private async writeShortcuts(shortcuts: SteamShortcut[], userId: number): Promise<void> {
|
||||
const shortcutsPath = await this.getShortcutsPath(userId);
|
||||
const stringData = SteamShortcut.getShortcutsString(shortcuts);
|
||||
await writeFile(shortcutsPath, stringData, { encoding: "utf-8" });
|
||||
}
|
||||
|
||||
public async createShortcut(shortcutData: SteamShortcutData, userId?: number): Promise<void> {
|
||||
const userIds = userId ? [userId] : await (async (): Promise<number[]> => {
|
||||
const folders = await this.getUserDataFolders();
|
||||
return folders.map(folder => parseInt(path.basename(folder)));
|
||||
})();
|
||||
|
||||
for (const userId of userIds) {
|
||||
const shortcuts = await this.getShortcuts(userId).catch(e => {
|
||||
log.warn("Error while reading shortcuts", e);
|
||||
return [] as SteamShortcut[];
|
||||
});
|
||||
shortcuts.push(new SteamShortcut(shortcutData));
|
||||
|
||||
await this.writeShortcuts(shortcuts, userId);
|
||||
}
|
||||
|
||||
log.info("Shortcut created", shortcutData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalMapsListPanel, LocalMapsListPanelRef } from "./maps/local-maps-list-panel.component";
|
||||
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
|
||||
import { FilterPanel } from "./maps/filter-panel.component";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
import { MapFilter, MapSort } from "shared/models/maps/beat-saver.model";
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component";
|
||||
@@ -20,10 +20,14 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
|
||||
import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local-playlist-filter-panel.component";
|
||||
import { LocalPlaylistFilter, LocalPlaylistFilterPanel, LocalPlaylistSort } from "./playlists/local-playlist-filter-panel.component";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { Dropzone } from "../shared/dropzone.component";
|
||||
import { logRenderError } from "renderer";
|
||||
import { BsmSelect, BsmSelectOption } from "../shared/bsm-select.component";
|
||||
import { BsmButton } from "../shared/bsm-button.component";
|
||||
import { mapSorter } from "shared/models/maps/sorter";
|
||||
import { playlistSorter } from "shared/models/playlists/sorter";
|
||||
|
||||
type Props = {
|
||||
readonly version?: BSVersion;
|
||||
@@ -39,12 +43,15 @@ export const InstalledMapsContext = createContext<{
|
||||
|
||||
export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
|
||||
const MAP_TAB = 0;
|
||||
const PLAYLIST_TAB = 1;
|
||||
|
||||
const mapsManager = useService(MapsManagerService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
const playlistsManager = useService(PlaylistsManagerService);
|
||||
const playlistsDownloader = useService(PlaylistDownloaderService);
|
||||
|
||||
const t = useTranslation();
|
||||
const { text: t } = useTranslationV2();
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
const [mapsDropZoneOpen, setMapsDropZoneOpen] = useState(false);
|
||||
@@ -64,7 +71,23 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
const playlistsRef = useRef<LocalPlaylistsListRef>();
|
||||
|
||||
const [mapFilter, setMapFilter] = useState<MapFilter>({});
|
||||
const [mapSort, setMapSort] = useState<MapSort>({
|
||||
compare: mapSorter.getDefaultComparator(),
|
||||
ascending: true,
|
||||
});
|
||||
const [playlistFilter, setPlaylistFilter] = useState<LocalPlaylistFilter>({});
|
||||
const [playlistSort, setPlaylistSort] = useState<LocalPlaylistSort>({
|
||||
compare: playlistSorter.getDefaultComparator(),
|
||||
ascending: true,
|
||||
});
|
||||
const [selectedSort, setSelectedSort] = useState<string>("name");
|
||||
|
||||
const [sortOptions, setSortOptions] = useState<BsmSelectOption<string>[]>(
|
||||
() => mapSorter.getComparatorKeys().map(key => ({
|
||||
text: `pages.version-viewer.maps.tabs.maps.sort.${key}`,
|
||||
value: key,
|
||||
}))
|
||||
);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const mapsLinkedState = useObservable(() => {
|
||||
@@ -100,6 +123,42 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return mapsManager.unlinkVersion(version);
|
||||
};
|
||||
|
||||
const handleTabChange = (index: number) => {
|
||||
if (tabIndex === index) {
|
||||
return;
|
||||
}
|
||||
setTabIndex(index);
|
||||
|
||||
switch (index) {
|
||||
case MAP_TAB:
|
||||
setSortOptions(() => mapSorter.getComparatorKeys().map(key => ({
|
||||
text: `pages.version-viewer.maps.tabs.maps.sort.${key}`,
|
||||
value: key,
|
||||
})));
|
||||
setSelectedSort(mapSorter.defaultKey);
|
||||
setMapSort({
|
||||
compare: mapSorter.getDefaultComparator(),
|
||||
ascending: playlistSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
case PLAYLIST_TAB:
|
||||
setSortOptions(() => playlistSorter.getComparatorKeys().map(key => ({
|
||||
text: `pages.version-viewer.maps.tabs.playlists.sort.${key}`,
|
||||
value: key,
|
||||
})));
|
||||
setSelectedSort(playlistSorter.defaultKey);
|
||||
setPlaylistSort({
|
||||
compare: playlistSorter.getDefaultComparator(),
|
||||
ascending: mapSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlaylistLinkClick = () => {
|
||||
if(playlistLinkedState === FolderLinkState.Pending || playlistLinkedState === FolderLinkState.Processing){ return Promise.resolve(false); }
|
||||
|
||||
@@ -135,8 +194,50 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
];
|
||||
})();
|
||||
|
||||
const handleSortChange = (key: string) => {
|
||||
switch (tabIndex) {
|
||||
case MAP_TAB:
|
||||
setMapSort({
|
||||
compare: mapSorter.getComparator(key),
|
||||
ascending: mapSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
case PLAYLIST_TAB:
|
||||
setPlaylistSort({
|
||||
compare: playlistSorter.getComparator(key),
|
||||
ascending: playlistSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const handleAscendingClick = () => {
|
||||
switch (tabIndex) {
|
||||
case MAP_TAB:
|
||||
setMapSort({
|
||||
compare: mapSort.compare,
|
||||
ascending: !mapSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
case PLAYLIST_TAB:
|
||||
setPlaylistSort({
|
||||
compare: playlistSort.compare,
|
||||
ascending: !playlistSort.ascending,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const dropDownItems = ((): DropDownItem[] => {
|
||||
if (tabIndex === 0) {
|
||||
if (tabIndex === MAP_TAB) {
|
||||
return [
|
||||
{ icon: "export", text: "pages.version-viewer.maps.search-bar.dropdown.export-maps", onClick: () => mapsRef.current.exportMaps?.() },
|
||||
{ icon: "trash", text: "pages.version-viewer.maps.search-bar.dropdown.delete-maps", onClick: () => mapsRef.current.deleteMaps?.() },
|
||||
@@ -153,7 +254,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white mb-4">
|
||||
<nav className="w-full shrink-0 flex h-9 justify-center gap-2 text-main-color-1 dark:text-white mb-4">
|
||||
<BsmDropdownButton
|
||||
classNames={{
|
||||
mainContainer: "h-full relative z-[1] flex justify-center w-fit",
|
||||
@@ -164,7 +265,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
icon="add"
|
||||
buttonColor="primary"
|
||||
text="misc.add"
|
||||
align="center"
|
||||
align="left"
|
||||
menuTranslationY="6px"
|
||||
withBar={false}
|
||||
items={addDropDownItems}
|
||||
@@ -173,7 +274,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
<input
|
||||
type="text"
|
||||
className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2"
|
||||
placeholder={tabIndex === 0 ? t("pages.version-viewer.maps.search-bar.search-placeholder") : t("playlist.search-playlist")}
|
||||
placeholder={tabIndex === MAP_TAB ? t("pages.version-viewer.maps.search-bar.search-placeholder") : t("playlist.search-playlist")}
|
||||
value={search}
|
||||
onChange={e => setSearch(() => e.target.value)}
|
||||
tabIndex={-1}
|
||||
@@ -188,16 +289,37 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
)
|
||||
)}
|
||||
</BsmDropdownButton>
|
||||
<BsmDropdownButton className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3" buttonClassName="rounded-full h-full w-full p-[6px]" icon="three-dots" withBar={false} items={dropDownItems} menuTranslationY="6px" align="center" />
|
||||
|
||||
<div className="h-full flex bg-theme-2 rounded-full">
|
||||
<BsmButton
|
||||
className="h-full aspect-square rounded-l-full z-[1]"
|
||||
iconClassName="transition-transform size-full ease-in-out duration-200 bg-theme-2"
|
||||
iconStyle={{ transform: (tabIndex === MAP_TAB && mapSort.ascending)
|
||||
|| (tabIndex === PLAYLIST_TAB && playlistSort.ascending)
|
||||
? "rotate(0deg)" : "rotate(180deg)"}}
|
||||
icon="arrow-upward"
|
||||
withBar={false}
|
||||
onClick={handleAscendingClick}
|
||||
/>
|
||||
|
||||
<BsmSelect
|
||||
className="h-full bg-theme-2 rounded-r-full cursor-pointer outline-none pb-1 text-center"
|
||||
options={sortOptions}
|
||||
selected={selectedSort}
|
||||
onChange={handleSortChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BsmDropdownButton className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3" buttonClassName="rounded-full h-full w-full p-[6px]" icon="three-dots" withBar={false} items={dropDownItems} menuTranslationY="6px" align="right" />
|
||||
</nav>
|
||||
<BsContentTabPanel
|
||||
tabIndex={tabIndex}
|
||||
onTabChange={index => setTabIndex(index)}
|
||||
onTabChange={handleTabChange}
|
||||
tabs={[
|
||||
{
|
||||
text: "misc.maps",
|
||||
icon: MapIcon,
|
||||
onClick: () => setTabIndex(0),
|
||||
onClick: () => handleTabChange(MAP_TAB),
|
||||
linkProps: version ? {
|
||||
state: mapsLinkedState,
|
||||
onClick: handleMapsLinkClick,
|
||||
@@ -206,7 +328,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
{
|
||||
text: "misc.playlists",
|
||||
icon: PlaylistIcon,
|
||||
onClick: () => setTabIndex(1),
|
||||
onClick: () => handleTabChange(PLAYLIST_TAB),
|
||||
linkProps: version ? {
|
||||
state: playlistLinkedState,
|
||||
onClick: handlePlaylistLinkClick,
|
||||
@@ -228,7 +350,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
properties: ["openFile", "multiSelections"],
|
||||
}}}
|
||||
>
|
||||
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
|
||||
<LocalMapsListPanel
|
||||
className="w-full h-full shrink-0"
|
||||
isActive={isActive && tabIndex === 0}
|
||||
ref={mapsRef}
|
||||
version={version}
|
||||
filter={mapFilter}
|
||||
search={search}
|
||||
linkedState={mapsLinkedState}
|
||||
sort={mapSort}
|
||||
/>
|
||||
</Dropzone>
|
||||
|
||||
<Dropzone
|
||||
@@ -243,7 +374,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
properties: ["openFile", "multiSelections"],
|
||||
}}}
|
||||
>
|
||||
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
|
||||
<LocalPlaylistsListPanel
|
||||
className="w-full h-full shrink-0"
|
||||
isActive={isActive && tabIndex === 1}
|
||||
ref={playlistsRef}
|
||||
version={version}
|
||||
linkedState={playlistLinkedState}
|
||||
filter={playlistFilter}
|
||||
search={search}
|
||||
sort={playlistSort}
|
||||
/>
|
||||
</Dropzone>
|
||||
|
||||
</InstalledMapsContext.Provider>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { Subscription, BehaviorSubject, lastValueFrom } from "rxjs";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
import { MapFilter, MapSort } from "shared/models/maps/beat-saver.model";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { bufferTime, last, tap } from "rxjs/operators";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
@@ -32,6 +32,7 @@ type Props = {
|
||||
search?: string;
|
||||
linkedState?: FolderLinkState;
|
||||
isActive?: boolean;
|
||||
sort: MapSort;
|
||||
};
|
||||
|
||||
export type LocalMapsListPanelRef = {
|
||||
@@ -40,7 +41,15 @@ export type LocalMapsListPanelRef = {
|
||||
removeDuplicates: () => void;
|
||||
}
|
||||
|
||||
export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
|
||||
export const LocalMapsListPanel = forwardRef<unknown, Props>(({
|
||||
version,
|
||||
className,
|
||||
filter,
|
||||
search,
|
||||
linkedState,
|
||||
isActive,
|
||||
sort,
|
||||
}, forwardRef) => {
|
||||
const mapsManager = useService(MapsManagerService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
|
||||
@@ -204,7 +213,9 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ ve
|
||||
}, [version]);
|
||||
|
||||
const preppedMaps: RenderableMap[] = (() => {
|
||||
return renderableMaps?.filter(renderableMap => isLocalMapFitMapFilter({ map: renderableMap.map, filter, search })) ?? [];
|
||||
return (renderableMaps?.filter(renderableMap => isLocalMapFitMapFilter({ map: renderableMap.map, filter, search })) ?? [])
|
||||
// NOTE: might be good to cache sorted maps, to reduce loading
|
||||
.sort((map1, map2) => sort.compare(map1.map, map2.map) * (sort.ascending === false ? -1 : 1));
|
||||
})();
|
||||
|
||||
if (!maps) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { hourToS, sToMs } from "shared/helpers/time.helpers";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import formatDuration from "format-duration";
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { Comparator } from "shared/models/comparator.type";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -120,3 +122,8 @@ export type LocalPlaylistFilter = Partial<{
|
||||
minDuration: number;
|
||||
maxDuration: number;
|
||||
}>
|
||||
|
||||
export interface LocalPlaylistSort {
|
||||
compare: Comparator<LocalBPListsDetails>;
|
||||
ascending: boolean;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playl
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper";
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
import { LocalPlaylistFilter } from "./local-playlist-filter-panel.component";
|
||||
import { LocalPlaylistFilter, LocalPlaylistSort } from "./local-playlist-filter-panel.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
|
||||
@@ -48,6 +48,7 @@ type Props = {
|
||||
search?: string;
|
||||
linkedState?: FolderLinkState;
|
||||
isActive?: boolean;
|
||||
sort: LocalPlaylistSort;
|
||||
};
|
||||
|
||||
export type LocalPlaylistsListRef = {
|
||||
@@ -57,7 +58,15 @@ export type LocalPlaylistsListRef = {
|
||||
exportPlaylists: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(({ version, className, filter: playlistFiler, search, isActive, linkedState }, forwardedRef) => {
|
||||
export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(({
|
||||
version,
|
||||
className,
|
||||
filter: playlistFilter,
|
||||
search,
|
||||
isActive,
|
||||
linkedState,
|
||||
sort: playlistSort,
|
||||
}, forwardedRef) => {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
@@ -397,27 +406,31 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
if(!p.playlistTitle?.toLowerCase().includes(search.toLowerCase())){ return false; }
|
||||
if(!p.playlistAuthor?.toLowerCase().includes(search.toLowerCase())){ return false; }
|
||||
|
||||
if(typeof p.nbMaps === "number" && (typeof playlistFiler?.minNbMaps === "number" || typeof playlistFiler?.maxNbMaps === "number")){
|
||||
if(playlistFiler?.minNbMaps && p.nbMaps < playlistFiler.minNbMaps){ return false; }
|
||||
if(playlistFiler?.maxNbMaps && p.nbMaps > playlistFiler.maxNbMaps){ return false; }
|
||||
if(typeof p.nbMaps === "number" && (typeof playlistFilter?.minNbMaps === "number" || typeof playlistFilter?.maxNbMaps === "number")){
|
||||
if(playlistFilter?.minNbMaps && p.nbMaps < playlistFilter.minNbMaps){ return false; }
|
||||
if(playlistFilter?.maxNbMaps && p.nbMaps > playlistFilter.maxNbMaps){ return false; }
|
||||
}
|
||||
|
||||
if(typeof p.nbMappers === "number" && (typeof playlistFiler?.minNbMappers === "number" || typeof playlistFiler?.maxNbMappers === "number")){
|
||||
if(playlistFiler?.minNbMappers && p.nbMappers < playlistFiler.minNbMappers){ return false; }
|
||||
if(playlistFiler?.maxNbMappers && p.nbMappers > playlistFiler.maxNbMappers){ return false; }
|
||||
if(typeof p.nbMappers === "number" && (typeof playlistFilter?.minNbMappers === "number" || typeof playlistFilter?.maxNbMappers === "number")){
|
||||
if(playlistFilter?.minNbMappers && p.nbMappers < playlistFilter.minNbMappers){ return false; }
|
||||
if(playlistFilter?.maxNbMappers && p.nbMappers > playlistFilter.maxNbMappers){ return false; }
|
||||
}
|
||||
|
||||
if(typeof p.duration === "number" && (typeof playlistFiler?.minDuration === "number" || typeof playlistFiler?.maxDuration === "number")){
|
||||
if(playlistFiler?.minDuration && p.duration < playlistFiler.minDuration){ return false; }
|
||||
if(playlistFiler?.maxDuration && p.duration > playlistFiler.maxDuration){ return false; }
|
||||
if(typeof p.duration === "number" && (typeof playlistFilter?.minDuration === "number" || typeof playlistFilter?.maxDuration === "number")){
|
||||
if(playlistFilter?.minDuration && p.duration < playlistFilter.minDuration){ return false; }
|
||||
if(playlistFilter?.maxDuration && p.duration > playlistFilter.maxDuration){ return false; }
|
||||
}
|
||||
|
||||
if(typeof p.minNps === "number" && typeof playlistFiler.minNps === "number" && p.minNps < playlistFiler.minNps){ return false; }
|
||||
if(typeof p.maxNps === "number" && typeof playlistFiler.maxNps === "number" && p.maxNps > playlistFiler.maxNps){ return false; }
|
||||
if(typeof p.minNps === "number" && typeof playlistFilter.minNps === "number" && p.minNps < playlistFilter.minNps){ return false; }
|
||||
if(typeof p.maxNps === "number" && typeof playlistFilter.maxNps === "number" && p.maxNps > playlistFilter.maxNps){ return false; }
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [playlists, search, playlistFiler]);
|
||||
})
|
||||
.sort((playlist1, playlist2) =>
|
||||
playlistSort.compare(playlist1, playlist2)
|
||||
* (playlistSort.ascending ? 1 : -1)
|
||||
);
|
||||
}, [playlists, search, playlistFilter, playlistSort]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
|
||||
@@ -1,37 +1,46 @@
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { LaunchOption } from "shared/models/bs-launch";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BSLauncherService } from "renderer/services/bs-launcher.service";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.component";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { LaunchMod } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { BsStore } from "shared/models/bs-store.enum";
|
||||
|
||||
export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion> = ({resolver, options: {data}}) => {
|
||||
export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, launchOption: LaunchOption }, BSVersion> = ({resolver, options: {data}}) => {
|
||||
|
||||
const bsLauncher = useService(BSLauncherService);
|
||||
|
||||
const t = useTranslation();
|
||||
const { text: t } = useTranslationV2();
|
||||
const color = useThemeColor("second-color");
|
||||
|
||||
const [launchOptions, setLaunchOptions] = useState(bsLauncher.getLaunchOptions(data));
|
||||
const [advanced, setAdvanced] = useState(!!launchOptions.additionalArgs?.length);
|
||||
const [additionalArgsString, setAdditionalArgsString] = useState(launchOptions.additionalArgs?.join("; ") ?? "");
|
||||
const [launchOption, setLaunchOptions] = useState(bsLauncher.getLaunchOptions(data));
|
||||
const [advanced, setAdvanced] = useState(!!launchOption.command?.length);
|
||||
const [command, setCommand] = useState(launchOption.command || "");
|
||||
const [steamShortcut, setSteamShortcut] = useState(false);
|
||||
|
||||
const isSteamVersion = useMemo(() => {
|
||||
return data.steam || data.metadata?.store === BsStore.STEAM;
|
||||
}, [data]);
|
||||
|
||||
const completeModal = () => {
|
||||
launchOption.command = command.trim();
|
||||
resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }});
|
||||
}
|
||||
|
||||
if(advanced) {
|
||||
launchOptions.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length);
|
||||
const toogleLaunchMod = (mod: LaunchMod, enabled: boolean) => {
|
||||
if(enabled) {
|
||||
setLaunchOptions(prev => ({...prev, launchMods: [...prev.launchMods, mod]}));
|
||||
} else {
|
||||
launchOptions.additionalArgs = undefined;
|
||||
setLaunchOptions(prev => ({...prev, launchMods: prev.launchMods.filter(m => m !== mod)}));
|
||||
}
|
||||
|
||||
resolver({exitCode: ModalExitCode.COMPLETED, data: launchOptions});
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -49,20 +58,20 @@ export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion>
|
||||
<div className="mb-1 grid grid-flow-col gap-3 w-full rounded-md py-2 bg-light-main-color-1 dark:bg-main-color-1">
|
||||
{data.oculus !== true && (
|
||||
<div className="h-full flex justify-center items-center gap-2">
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOptions.oculus} onChange={e => setLaunchOptions({...launchOptions, oculus: e})} />
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOption.launchMods.includes("oculus")} onChange={e => toogleLaunchMod("oculus", e)} />
|
||||
<Tippy className="!bg-main-color-1" content={t("pages.version-viewer.launch-mods.oculus-description")} delay={[300, 0]} arrow={false}>
|
||||
<span className="font-bold cursor-help">{t("pages.version-viewer.launch-mods.oculus")}</span>
|
||||
</Tippy>
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full flex justify-center items-center gap-2">
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOptions.desktop} onChange={e => setLaunchOptions({...launchOptions, desktop: e})} />
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOption.launchMods.includes("fpfc")} onChange={e => toogleLaunchMod("fpfc", e)} />
|
||||
<Tippy className="!bg-main-color-1" content={t("pages.version-viewer.launch-mods.desktop-description")} delay={[300, 0]} arrow={false}>
|
||||
<span className="font-bold cursor-help">{t("pages.version-viewer.launch-mods.desktop")}</span>
|
||||
</Tippy>
|
||||
</div>
|
||||
<div className="h-full flex justify-center items-center gap-2">
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOptions.debug} onChange={e => setLaunchOptions({...launchOptions, debug: e})} />
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOption.launchMods.includes("debug")} onChange={e => toogleLaunchMod("debug", e)} />
|
||||
<Tippy className="!bg-main-color-1" content={t("pages.version-viewer.launch-mods.debug-description")} delay={[300, 0]} arrow={false}>
|
||||
<span className="font-bold cursor-help">{t("pages.version-viewer.launch-mods.debug")}</span>
|
||||
</Tippy>
|
||||
@@ -80,13 +89,21 @@ export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion>
|
||||
type="text"
|
||||
className="w-full rounded-md text-center outline-none bg-light-main-color-3 dark:bg-main-color-3"
|
||||
placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")}
|
||||
value={additionalArgsString}
|
||||
onChange={e => setAdditionalArgsString(e.target.value)}
|
||||
value={command}
|
||||
onChange={e => setCommand(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSteamVersion && (
|
||||
<Tippy placement="right" theme="default" content={t("modals.create-launch-shortcut.steam-shortcut-tippy")}>
|
||||
<div className="h-full flex items-center gap-1.5 mt-3 mb-4 w-fit pr-1">
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={steamShortcut} onChange={e => setSteamShortcut(() => e)} />
|
||||
<span>{t("modals.create-launch-shortcut.create-steam-shortcut")}</span>
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-2">
|
||||
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={completeModal} withBar={false} text="modals.create-launch-shortcut.valid-btn" />
|
||||
|
||||
@@ -221,7 +221,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
handleSearch();
|
||||
}}
|
||||
/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} selected={searchParams.sortOrder} onChange={sort => handleSortChange(sort)} />
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center w-min" options={sortOptions} selected={searchParams.sortOrder} onChange={sort => handleSortChange(sort)} />
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PlaylistsManagerService } from "renderer/services/playlists-manager.ser
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { map } from "rxjs";
|
||||
|
||||
export const LinkContentModal: ModalComponent<boolean, {version: BSVersion, contentType: "maps"|"playlists"|"avatars"|"sabers"|"platforms"|"blocks"}> = ({options: { data: { version, contentType } }, resolver }) => {
|
||||
export const LinkContentModal: ModalComponent<void, {version: BSVersion, contentType: "maps"|"playlists"|"avatars"|"sabers"|"platforms"|"blocks"}> = ({options: { data: { version, contentType } }, resolver }) => {
|
||||
const { text: t, element: te } = useTranslationV2();
|
||||
|
||||
const versionLinker = useService(VersionFolderLinkerService);
|
||||
|
||||