Compare commits

...

188 Commits

Author SHA1 Message Date
MathieuG-P 689a57e014 bumb to v1.5.0-alpha.8 2025-01-11 20:47:48 +01:00
MathieuG-P a891ecb0db Merge pull request #735 from Zagrios/bugfix/reduce-dll-deps-and-add-logs-file-to-external-scripts
Reduce DLL dependencies for "oculus-allow-dev-sideloaded.exe"  and add logging for external scripts
2025-01-11 18:27:41 +01:00
MathieuG-P 17bd2e8463 [bugfix] reduce DLL deps of "oculus-allow-dev-sideload.exe" and add log file to all external scripts 2025-01-11 18:14:41 +01:00
MathieuG-P 467ca6e300 Merge pull request #734 from silentrald/bugfix/733
[bugfix-733] manually set WINEPREFIX when executing IPA.exe
2025-01-11 14:34:01 +01:00
MathieuG-P 66595dac06 Merge branch 'master' into bugfix/733 2025-01-11 13:09:13 +01:00
MathieuG-P 1301bfe358 Merge pull request #601 from silentrald/feat/216
[feat-216] support for sorting maps/songs and playlists
2025-01-11 12:59:15 +01:00
MathieuG-P 1732191be3 try fix unit test type 2025-01-11 12:55:05 +01:00
silentrald fb7f68cd04 [bugfix-733] point to the correct compat folder 2025-01-10 10:02:49 +08:00
silentrald e89333ea09 [feat-216] refactored sorting services to classes and const 2025-01-10 01:02:39 +08:00
silentrald f2ad6c1531 [bugfix-733] fix issue with unit tests 2025-01-09 23:55:17 +08:00
silentrald 65d26dc819 [bugfix-733] manually set WINEPREFIX when executing IPA.exe 2025-01-09 23:42:56 +08:00
MathieuG-P 664e28ad05 update deps 2025-01-08 21:03:28 +01:00
silentrald 1d70c867f3 [feat-216] fix ui issues + change some dropdown options 2025-01-08 21:31:39 +08:00
MathieuG-P 99ce770592 little css adjustment 2025-01-07 19:37:19 +01:00
MathieuG-P afb4075408 Merge pull request #732 from Zagrios/Chore/Patch-Wiki-sidebar
Update _Sidebar.md
2025-01-06 20:18:01 +01:00
MathieuG-P 9e20239328 Merge pull request #731 from Zagrios/feature/create-steam-shortcut
[feature] create steam shortcut
2025-01-06 20:17:22 +01:00
GaetanGrd 5fb44d1adc Update _Sidebar.md 2025-01-06 18:28:43 +01:00
MathieuG-P 2a99c76d09 [feature] we can now create launch shortcut in steam library 2025-01-06 18:17:47 +01:00
MathieuG-P cc7237fef2 Merge pull request #730 from silentrald/bugfix/allow-compat-folder-create-perms
[bugfix] allow compat folder create permission for linux flatpak
2025-01-06 15:56:16 +01:00
silentrald e3d27e4b4f [feat-216] applied ui for filter/sorting map/playlist
* also applied change to model filter ui
2025-01-06 19:25:51 +08:00
MathieuG-P 3ff2b729c9 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2025-01-06 11:15:09 +01:00
MathieuG-P 35238685ce add patreon 2025-01-06 11:15:06 +01:00
silentrald 48e77920af [bugfix] allow compat folder create permission for linux flatpak 2025-01-06 12:57:05 +08:00
MathieuG-P c7d6bc54e4 Merge pull request #729 from GaetanGrd/master
Update BSManager wiki for actual release
2025-01-05 21:52:38 +01:00
Shidorien 80c555711f fix forget element to restore orignal name for oculus token page 2025-01-05 21:47:18 +01:00
MathieuG-P a148ddaee2 add patreon 2025-01-05 21:45:33 +01:00
Shidorien 023f8d31b0 Add sidebar navigation and troubleshooting sections to the wiki 2025-01-05 21:37:22 +01:00
Shidorien 14a7f5f00b restore orignal name and original link for How-to-obtain-your-Oculus-Token 2025-01-05 21:36:48 +01:00
Shidorien ec8b41187e Correct capitalization in Linux installation instructions and error guidance 2025-01-05 21:33:52 +01:00
MathieuG-P 774c3a2954 [feature] advancement on steam shortcut 2025-01-05 21:33:23 +01:00
Shidorien 4381bb4696 Add markdownlint extension to VSCode recommendations 2025-01-05 20:45:51 +01:00
silentrald 6d1418dd7c [feat-216] aligned with change with v4 schema maps
* linted/auto-formatted sorter.service.ts files
* changed generics.type to comparator.type
2025-01-05 14:43:37 +08:00
silentrald 22af4610eb [feat-216] added translations for playlists sorting 2025-01-05 14:43:37 +08:00
silentrald b4874eced4 [feat-216] support for sorting playlists 2025-01-05 14:43:37 +08:00
silentrald 61b728ed55 [feat-216] added translations for maps/songs sorting 2025-01-05 14:43:37 +08:00
silentrald 1310d16597 [feat-216] support for sorting maps/songs 2025-01-05 14:43:37 +08:00
GaetanGrd 1985f5e6be Merge pull request #2 from Zagrios/master
add patreon
2025-01-05 00:26:25 +01:00
MathieuG-P 30c6058edb add patreon 2025-01-05 00:21:54 +01:00
Shidorien c69b110595 Fix image alt text in Oculus token guide for clarity 2025-01-05 00:21:36 +01:00
Shidorien 1f02551408 Add browser methode to get oculus token 2025-01-05 00:20:12 +01:00
MathieuG-P cab1fb93de [feature] add checkbox to create a steam shortcut + fix create shortcut modal 2025-01-05 00:06:04 +01:00
Shidorien 74ed168f98 Reorder "Activate Oculus Sideloading" link in Home.md for better visibility 2025-01-04 23:19:07 +01:00
Shidorien ea04073772 fix nexusmods links 2025-01-04 23:17:23 +01:00
Shidorien 8ac1683fab delete useless file 2025-01-04 19:50:53 +01:00
Shidorien 38ef6191e4 Remove video elements from shortcut creation and version import guides 2025-01-04 19:39:28 +01:00
Shidorien ed4b39598f Update video source path in installation guide for importing versions 2025-01-04 19:31:38 +01:00
Shidorien 4f58df6280 Fix video source path in installation guide for importing versions 2025-01-04 19:07:26 +01:00
Shidorien ba26f3a01e Recreate and restore the installation guide for importing versions of Beat Saber 2025-01-04 19:01:50 +01:00
Shidorien 661a90883c Fix image path for Linux missing icons troubleshooting guide 2025-01-04 19:00:13 +01:00
Shidorien 87a7e08e5f Update image paths in Linux missing icons and Oculus sideloading troubleshooting guides 2025-01-04 18:55:05 +01:00
Shidorien 6c4acec8a2 Remove troubleshooting section for Linux installation guide 2025-01-04 17:31:41 +01:00
Shidorien 6345e9148b Capitalize heading for Linux installation guide in Home.md 2025-01-04 17:30:54 +01:00
Shidorien c76e585d71 Refactor installation problems section to improve heading formatting and clarity 2025-01-04 17:30:31 +01:00
Shidorien b20f6328e9 Update installation problems section to enhance Linux heading formatting 2025-01-04 17:29:49 +01:00
Shidorien bc220e6692 Add troubleshooting guides for Linux installation issues 2025-01-04 17:27:06 +01:00
Shidorien 88f8dc7d5c Remove outdated installation guide for importing versions 2025-01-04 16:43:09 +01:00
Shidorien f0b7294ac5 Update installation guide to improve heading level for Arch (AUR) section 2025-01-04 16:37:43 +01:00
Shidorien 7815e35506 Update installation guide to improve heading hierarchy and consistency 2025-01-04 16:36:48 +01:00
Shidorien 32d604345f Add missing line breaks in installation guide for improved readability 2025-01-04 16:35:06 +01:00
Shidorien 1f07c35cc6 Remove unnecessary line breaks in installation guide for improved readability 2025-01-04 16:34:35 +01:00
Shidorien c8ec5db4d7 Add warning about login information and clarify use of DepotDownloader in installation guide 2025-01-04 16:33:53 +01:00
Shidorien 4d90102127 Update installation guide to improve heading structure and consistency 2025-01-04 16:33:26 +01:00
Shidorien b0c2a7c872 Add table of contents and restructure installation guide for clarity 2025-01-04 16:31:03 +01:00
Shidorien 149e6bbc9d Refactor installation guide to improve heading structure and consistency 2025-01-04 16:25:55 +01:00
Shidorien b12e90a32a Enhance installation guide by updating section headings for improved readability 2025-01-04 16:25:11 +01:00
Shidorien 5a4df356bf Update installation guide headings for consistency and clarity 2025-01-04 16:22:31 +01:00
Shidorien 97fbeb01a4 test 2025-01-04 16:21:36 +01:00
Shidorien 6beb675139 Add Oculus sideloading activation guide and restructure documentation 2025-01-04 16:10:23 +01:00
Shidorien 79693e2070 Merge branch 'Zagrios-master' 2025-01-04 15:34:24 +01:00
Shidorien 6708071b7a Merge branch 'master' of https://github.com/Zagrios/bs-manager into Zagrios-master 2025-01-04 15:34:06 +01:00
Shidorien 7ea75bd50a Update links in README and modal component for consistency in token retrieval instructions 2025-01-04 15:32:20 +01:00
Shidorien 62abe32696 Remove unnecessary detail tags and improve the structure of the installation guide 2025-01-04 15:06:49 +01:00
Shidorien b8fb1781aa test 2025-01-04 15:06:07 +01:00
Shidorien 379e7cfaf5 test 2025-01-04 15:05:58 +01:00
Shidorien 9cfcf28373 Replace caution and important emojis with warning and note callouts for consistency in documentation 2025-01-04 15:01:41 +01:00
Shidorien 67f102738a Fix formatting and capitalization in multiple documentation files for consistency 2025-01-04 14:49:15 +01:00
Shidorien 025d6f8ccd Add troubleshooting guide for keyboard shortcut conflicts with AMD Software 2025-01-04 14:43:41 +01:00
Shidorien 69496221b2 Fix formatting and capitalization in How-to-find-your-logs.md for consistency 2025-01-04 14:41:56 +01:00
Shidorien 7b1ae3772a Fix formatting and punctuation in Get-your-Oculus-token.md for consistency and clarity 2025-01-04 14:40:25 +01:00
Shidorien f92b621174 Fix formatting and capitalization in Install-or-import-a-version.md for consistency 2025-01-04 14:40:07 +01:00
Shidorien 3f2147b796 Fix formatting and capitalization in Create-a-shortcut.md for consistency 2025-01-04 14:39:03 +01:00
Shidorien b61d540d0e Fix formatting in help-us-update-the-wiki.md for consistency 2025-01-04 14:36:55 +01:00
Shidorien 9984780a99 Fix formatting in Modify-models-in-your-beat-saber.md for consistency 2025-01-04 14:35:05 +01:00
Shidorien d7e1e0daad Fix punctuation in installation guide for clarity 2025-01-04 14:34:47 +01:00
Shidorien 03cacc7fc8 Fix formatting and capitalization in installation guide for consistency 2025-01-04 14:34:28 +01:00
Shidorien 4dff8f5765 Improve formatting and consistency in Modify-models-in-your-beat-saber.md 2025-01-04 14:31:13 +01:00
Shidorien 3463a63e1d test 2025-01-04 14:30:17 +01:00
Shidorien 5926a72fab Fix formatting and capitalization in installation guides for consistency 2025-01-04 14:28:51 +01:00
Shidorien 5824166a22 Grammar and spelling fixes 2025-01-04 14:25:51 +01:00
MathieuG-P 29eba3d572 Capitalize mods categories 2025-01-04 01:52:11 +01:00
MathieuG-P 29426f21b2 bumb to v1.5.0alpha-7 2025-01-03 16:55:53 +01:00
MathieuG-P db1e51b90a Refresh mods after import 2025-01-03 16:52:00 +01:00
MathieuG-P eba7e55539 [docs] put the "Known bugs" section at the bottom 2025-01-03 16:21:01 +01:00
MathieuG-P ef5f6ee5f6 Merge pull request #674 from basti564/patch-1
wiki: easier method to obtain oculus token
2025-01-03 16:15:57 +01:00
MathieuG-P 7c185b9d2c Merge branch 'master' into patch-1 2025-01-03 16:14:52 +01:00
MathieuG-P 8766809fb8 [docs] Update Oculus Token retrieval instructions and add known bugs section 2025-01-03 16:08:55 +01:00
MathieuG-P a424651f9d Merge pull request #728 from Zagrios/apply-changes-from-1.4.15-1.4.16-1.4.17
Apply changes from [1.4.15-1.4.17]
2025-01-03 15:52:42 +01:00
MathieuG-P 5113d72db2 fix lint issue 2025-01-03 15:46:51 +01:00
MathieuG-P 4968022b7e remove old mods models from the index 2025-01-03 15:43:03 +01:00
MathieuG-P 0c65b7b537 Add "OC" token pattern to log filter 2025-01-03 15:36:05 +01:00
MathieuG-P 8270ec800e Apply mods changes from [1.4.15-1.4.17] + some clean 2025-01-03 15:27:51 +01:00
MathieuG-P a70229c80b add patreon 2025-01-03 10:21:26 +01:00
MathieuG-P bf4e5aefa4 Merge pull request #721 from Davitekk/master
[Translation] Add Italian translation
2025-01-03 01:21:25 +01:00
Davitekk 3c49570f76 Update it.json 2025-01-02 17:17:27 +01:00
Davitekk 0f80f6ae28 Update it.json 2025-01-02 17:10:05 +01:00
Davitekk 066e740a62 Update it.json 2025-01-02 13:15:19 +01:00
Davitekk f296f34e62 Merge branch 'Zagrios:master' into master 2025-01-02 12:46:29 +01:00
Davitekk fb174b4451 Update it.json 2025-01-02 10:50:37 +01:00
Davitekk dfad8a09cc Update italy-icon.component.tsx 2025-01-02 10:47:01 +01:00
MathieuG-P 69b246949f Merge pull request #712 from silentrald/wiki/deb
[wiki] added deb install thru ppa repo in github
2025-01-02 02:33:57 +01:00
MathieuG-P 93ea2e1233 Merge pull request #724 from eaglesemanation/master
Update flatpak dependencies to non-EOL versions
2025-01-02 02:31:16 +01:00
Vladimir Romashchenko 23d9a605df Update flatpak dependencies to non-EOL versions 2025-01-01 18:11:39 -05:00
silentrald 03d4f007a3 [wiki] added deb install thru ppa repo in github 2024-12-25 16:15:34 +08:00
shidorien 2f87222084 Add detailed instructions for finding logs in new How-to-find-your-logs.md file 2024-12-01 22:24:15 +01:00
shidorien 52f3bbb3d7 Fix link in Home.md for Help us to update the wiki section 2024-12-01 22:21:58 +01:00
shidorien 513dba8393 Add detailed instructions for finding logs in new How-to-find-your-logs.md file and update link in Home.md 2024-12-01 22:21:30 +01:00
shidorien 250b41cd10 Update Discord server link in Home.md for direct access 2024-12-01 22:07:19 +01:00
shidorien 5654f650d5 Update Home.md with active links and add Help us to update the wiki guide 2024-12-01 22:06:56 +01:00
shidorien d615efb02c Disable MD023 rule in markdownlint configuration for improved linting flexibility 2024-12-01 21:53:20 +01:00
shidorien faf8431e49 Fix code formatting in Create-a-shortcut.md for improved readability 2024-12-01 21:52:37 +01:00
shidorien 44b6979fee Update section headings from "Summary" to "Table of content" for consistency across guides 2024-12-01 21:51:55 +01:00
shidorien c48c08dbf5 Add detailed guide for modifying models in Beat Saber and restore link in Home.md 2024-12-01 21:41:31 +01:00
shidorien 547054a76c Update version link reference in Create-a-shortcut.md for improved navigation 2024-12-01 21:34:18 +01:00
shidorien ad03a50cc8 Update links in installation guide for mods to use internal anchors for improved navigation 2024-12-01 21:11:29 +01:00
shidorien 941b10aa46 Fix formatting in Home.md by removing unnecessary line break in Login section 2024-12-01 21:06:03 +01:00
shidorien 23ec61e2e6 Refactor section headings in Home.md for consistency and clarity 2024-12-01 21:03:09 +01:00
shidorien 99d3f6d237 Refactor section headings in Home.md for improved readability and structure 2024-12-01 21:02:31 +01:00
shidorien 989c3172e4 Refactor headings in Home.md for improved clarity and consistency 2024-12-01 21:00:15 +01:00
shidorien 575d9c290e Update note heading in installation guide for mods to improve clarity 2024-12-01 20:57:14 +01:00
shidorien cb4666ee8a Fix formatting in Install-mods-on-your-version.md by updating step numbering for clarity 2024-12-01 20:56:48 +01:00
shidorien 4d42dfb2cb Add installation guide for mods and update links in Home.md 2024-12-01 20:56:08 +01:00
shidorien 2c166e9fc5 Add installation guide for custom playlists and restore link in Home.md 2024-12-01 20:32:06 +01:00
shidorien 5a87d91192 Update headings in installation guides from 'Summary' to '## Summary' for consistency 2024-11-30 13:37:46 +01:00
shidorien 01c7e540d0 Enhance Install-any-maps.md with detailed installation steps for BSManager; fix formatting issues in Create-a-shortcut.md and Home.md 2024-11-30 13:31:12 +01:00
shidorien 80863df85e Enhance Get-your-Oculus-token.md with links to BSManager and clarify download instructions; add new Install-any-maps.md guide 2024-11-30 12:32:59 +01:00
shidorien a2e2136196 Normalize link casing for uncreated links in Home.md 2024-11-30 12:19:51 +01:00
shidorien 98fba3289b Correct 'Playload' tab reference to 'Payload' in Get-your-Oculus-token.md 2024-11-30 12:04:51 +01:00
shidorien 2561a34200 Mark uncreated links as strikethrough in Home.md for clarity 2024-11-30 12:00:50 +01:00
shidorien b128de6acc Update instructions in Get-your-Oculus-token.md: correct 'Playload' tab reference and remove numbering for clarity 2024-11-30 11:55:27 +01:00
shidorien fadf08771f Update img to get token 2024-11-30 11:53:36 +01:00
shidorien 488da1cdaf Add blank line for improved readability in Get-your-Oculus-token.md 2024-11-30 11:51:53 +01:00
shidorien 5771aed67a Refactor Oculus token guidance: update link format and consolidate instructions into a new document 2024-11-30 11:51:40 +01:00
shidorien 4dece596ef Fix formatting of version link in Create-a-shortcut.md for clarity 2024-11-30 11:37:18 +01:00
shidorien 4636f46bab Recreate Create-a-shortcut.md with detailed instructions for Windows and Steam shortcuts 2024-11-30 10:57:10 +01:00
shidorien c7722df0e4 Refactor Home.md to remove unnecessary links and improve readability 2024-11-30 10:56:05 +01:00
shidorien d44f9f9067 Remove placeholder text from troubleshooting section in Home.md 2024-11-30 10:54:07 +01:00
shidorien d9318b6579 Add placeholder text to troubleshooting section in Home.md 2024-11-30 10:53:50 +01:00
shidorien 08be61928e Add installation guide for creating shortcuts on Windows and Steam 2024-11-30 10:48:28 +01:00
shidorien a2df9d3adf Update Home.md to include links to BSManager for improved navigation 2024-11-30 10:46:51 +01:00
shidorien 1e12f7eae2 Update installation guide to replace video placeholder with actual video link for import instructions 2024-11-30 10:02:43 +01:00
shidorien 0c87bfd11c remove useless video 2024-11-30 10:00:33 +01:00
shidorien 71ab4b5fa8 Update Home.md to add links for shortcuts, tokens, maps, mods, and contributions 2024-11-30 10:00:06 +01:00
shidorien 93906f8ff2 Update installation guide to include links for BSManager and improve clarity of instructions 2024-11-30 09:48:48 +01:00
shidorien d0301088f1 Disable MD033 rule in markdownlint configuration 2024-11-30 09:46:02 +01:00
shidorien 362912b83b Update installation guide to replace video embed with a placeholder for import instructions 2024-11-30 09:29:45 +01:00
shidorien 7d80cc5879 Fix image paths and update video embed in installation guide 2024-11-30 09:23:56 +01:00
shidorien f0f0512033 Update installation guide to use HTML tags for images and videos 2024-11-29 22:37:01 +01:00
shidorien 29689ce632 Update installation guide and restructure documentation 2024-11-29 22:10:43 +01:00
shidorien e8118e8ae4 hft 2024-11-29 22:03:06 +01:00
shidorien 6d83096c2c grd 2024-11-29 22:00:42 +01:00
shidorien 0033b89942 teest 2024-11-29 22:00:00 +01:00
shidorien 6e4a796343 prout 2024-11-29 21:58:30 +01:00
shidorien 7f4fe63d58 test 2024-11-29 21:57:25 +01:00
shidorien 6c868bf60d test 2024-11-29 21:57:05 +01:00
shidorien 4d16b217ca Fix image path in installation guide for correct display 2024-11-29 21:46:11 +01:00
shidorien 556477a996 Fix image path in installation guide for correct display 2024-11-29 21:44:15 +01:00
shidorien 3a4672f25e Merge branch 'master' of https://github.com/GaetanGrd/bs-manager 2024-11-29 21:41:56 +01:00
shidorien e6071c7395 test path 2024-11-29 21:41:54 +01:00
GaetanGrd 95a95715b0 add strategy: init 2024-11-29 20:37:15 +01:00
shidorien d322178127 Add links for wiki development and contribution in Home.md 2024-11-29 20:29:08 +01:00
shidorien 418fc7e864 try path 2024-11-29 20:29:02 +01:00
Ellie 
:3 5a46a67e1e wiki: easier method to obtain oculus token 2024-11-26 23:01:04 +01:00
shidorien e4fdf8ffe9 Add images for Steam and Oculus Store installation steps in the guide 2024-11-20 21:45:58 +01:00
shidorien c91c1ca3ce Reorganize and enhance installation guide with detailed instructions for Steam and Oculus Store 2024-11-20 21:44:55 +01:00
shidorien 276c5bfcab Fix link format for "Install or import a version" in Home.md 2024-11-20 21:33:58 +01:00
shidorien 2232222d3b Fix link format for "Install or import a version" in Home.md 2024-11-20 21:33:44 +01:00
shidorien f5d48ea946 Fix link format for "Install or import a version" in Home.md 2024-11-20 21:32:51 +01:00
shidorien 7e85387ff4 Update markdownlint configuration and enhance installation guide with important notes 2024-11-20 21:30:02 +01:00
shidorien b0a4a27f78 Fix link format for "Install or import a version" in Home.md 2024-11-20 21:21:16 +01:00
shidorien b29759365b Recreate the "How to install or import a version" guide with updated content and structure 2024-11-20 21:19:15 +01:00
shidorien 3dd254a070 Update installation guide and home page links; add video placeholder 2024-11-20 21:18:31 +01:00
shidorien 3d721d95a9 try anything 2024-11-20 21:16:33 +01:00
shidorien b555318f2d Add new pages 2024-11-20 21:14:09 +01:00
shidorien 7662f938da add Troubleshooting section 2024-11-20 21:00:55 +01:00
shidorien 7f0f273652 update home.md and add mardownlint.json 2024-11-20 20:37:50 +01:00
shidorien 9ed769dca0 move files 2024-11-20 19:50:18 +01:00
GaetanGrd a4fef9f66c Update Home.md 2024-11-20 19:36:52 +01:00
GaetanGrd 4b4bdbbff8 Update Home.md 2024-11-20 19:30:28 +01:00
103 changed files with 3075 additions and 1001 deletions
+1
View File
@@ -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,
+1
View File
@@ -18,3 +18,4 @@ jobs:
- uses: Andrew-Chen-Wang/github-wiki-action@v4
with:
path: docs/wiki/
strategy: init
+1 -1
View File
@@ -29,4 +29,4 @@ npm-debug.log.*
*.scss.d.ts
# externals
externals/**/target/*
externals/**/target/**
+8
View File
@@ -0,0 +1,8 @@
{
"MD007": { "indent": 4 },
"MD013": false,
"MD041": false,
"MD024": false,
"MD033": false,
"MD023": false
}
+5 -1
View File
@@ -1,3 +1,7 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"]
"recommendations": [
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"davidanson.vscode-markdownlint"
]
}
+14
View File
@@ -124,5 +124,19 @@
},
{
"username": "Maximilian"
},
{
"username": "Paul Smith"
},
{
"username": "MunityVR",
"type": "gold"
},
{
"username": "Sk4venger"
},
{
"username": "Alexander Herman",
"type": "gold"
}
]
+21 -2
View File
@@ -91,6 +91,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 +111,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"
}
}
}
@@ -636,7 +652,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 +860,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",
+21 -2
View File
@@ -91,6 +91,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 +111,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"
}
}
}
@@ -636,7 +652,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."
@@ -838,7 +855,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",
+21 -2
View File
@@ -91,6 +91,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 +111,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"
}
}
}
@@ -636,7 +652,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 +860,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",
+21 -2
View File
@@ -91,6 +91,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 +111,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"
}
}
}
@@ -637,7 +653,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 +861,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",
+37 -9
View File
@@ -91,6 +91,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 +111,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"
}
}
}
@@ -209,6 +225,7 @@
"en-US": "English, US",
"fr-FR": "Français",
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"ru-RU": "Русский",
"zh-CN": "简体中文",
@@ -217,9 +234,10 @@
"ko-KR": "한국어",
"translated": {
"en-EN": "Inglese, Regno Unito",
"en-US": "English, Stati Uniti d'America",
"en-US": "Inglese, Stati Uniti d'America",
"fr-FR": "Francese",
"es-ES": "Spagnolo",
"it-IT": "Italiano",
"de-DE": "Tedesco",
"ru-RU": "Russo",
"zh-CN": "Cinese (Semplificato)",
@@ -634,7 +652,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."
@@ -836,7 +855,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",
@@ -909,7 +930,8 @@
"tags": "tag",
"specificities": "generale",
"requirements": "requisiti",
"exclude": "escludi"
"exclude": "escludi",
"leaderboard": "classifica"
},
"map-types": {
"accuracy": "precisione",
@@ -965,15 +987,21 @@
"verified": "verificata",
"fullSpread": "completa"
},
"map-leaderboard": {
"All": "Tutte",
"Ranked": "Classificate",
"BeatLeader": "BeatLeader",
"ScoreSaber": "ScoreSaber"
},
"map-excludes": {
"installed": "installata"
},
"difficulties": {
"Easy": "Easy",
"Normal": "Normal",
"Hard": "Hard",
"Expert": "Expert",
"ExpertPlus": "Expert+"
"Easy": "Facile",
"Normal": "Normale",
"Hard": "Difficile",
"Expert": "Esperto",
"ExpertPlus": "Esperto+"
},
"map-item": {
"by": "Di {songAutor}",
+21 -2
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "マップをインポート",
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
},
"sort": {
"name": "名前",
"song-author": "曲の作者",
"map-author": "地図の作者",
"bpm": "BPM",
"duration": "持続時間",
"likes": "いいね",
"date-uploaded": "アップロード日"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "プレイリストをインポート",
"subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート"
},
"sort": {
"title": "タイトル",
"author": "著者",
"number-of-maps": "地図の数",
"duration": "持続時間",
"notes-per-second": "NPS"
}
}
}
@@ -636,7 +652,8 @@
"create-launch-shortcut": {
"success": {
"title": "ショートカットを作成しました",
"msg": "デスクトップにショートカットを作成されました。"
"msg": "デスクトップにショートカットを作成されました。",
"msg-steam": "ショートカットがSteamライブラリに作成されました。"
},
"error": {
"msg": "ショートカットの作成中にエラーが発生しました。"
@@ -843,7 +860,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に接続する",
+26 -7
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "맵 가져오기",
"subtext": "ZIP 파일을 여기에 드롭하여 맵 가져오기"
},
"sort": {
"name": "이름",
"song-author": "작곡가",
"map-author": "지도 제작자",
"bpm": "BPM",
"duration": "지속 시간",
"likes": "좋아요",
"date-uploaded": "업로드 날짜"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "재생 목록 가져오기",
"subtext": "\".bplist\" 또는 \".json\" 파일을 여기에 드롭하여 가져오기"
},
"sort": {
"title": "제목",
"author": "저자",
"number-of-maps": "지도 수",
"duration": "지속 시간",
"notes-per-second": "NPS"
}
}
}
@@ -636,7 +652,8 @@
"create-launch-shortcut": {
"success": {
"title": "바로 가기 생성 완료",
"msg": "바탕 화면에 바로 가기가 생성되었습니다."
"msg": "바탕 화면에 바로 가기가 생성되었습니다.",
"msg-steam": "바로 가기가 Steam 라이브러리에 생성되었습니다."
},
"error": {
"msg": "바로 가기 생성 중 오류가 발생했습니다."
@@ -838,12 +855,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에 연결하기",
+21 -2
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "Импортируйте свои карты",
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
},
"sort": {
"name": "Имя",
"song-author": "Автор песни",
"map-author": "Автор карты",
"bpm": "BPM",
"duration": "Длительность",
"likes": "Лайки",
"date-uploaded": "Дата загрузки"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "Импортируйте ваши плейлисты",
"subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта"
},
"sort": {
"title": "Заголовок",
"author": "Автор",
"number-of-maps": "Количество карт",
"duration": "Длительность",
"notes-per-second": "NPS"
}
}
}
@@ -636,7 +652,8 @@
"create-launch-shortcut": {
"success": {
"title": "Ярлык создан",
"msg": "Ярлык находится на рабочем столе."
"msg": "Ярлык находится на рабочем столе.",
"msg-steam": "Ярлык был создан в библиотеке Steam."
},
"error": {
"msg": "Ошибка создания ярлыка."
@@ -843,7 +860,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",
+21 -2
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "導入您的地圖",
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
},
"sort": {
"name": "名字",
"song-author": "歌曲作者",
"map-author": "地圖作者",
"bpm": "BPM",
"duration": "持續時間",
"likes": "喜歡",
"date-uploaded": "上傳日期"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "匯入您的播放列表",
"subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入"
},
"sort": {
"title": "標題",
"author": "作者",
"number-of-maps": "地圖數量",
"duration": "持續時間",
"notes-per-second": "NPS"
}
}
}
@@ -636,7 +652,8 @@
"create-launch-shortcut": {
"success": {
"title": "捷徑已創建",
"msg": "捷徑已在桌面上創建"
"msg": "捷徑已在桌面上創建",
"msg-steam": "捷徑已在 Steam 庫中建立。"
},
"error": {
"msg": "創建捷徑時發生錯誤"
@@ -843,7 +860,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",
+21 -2
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "导入您的地图",
"subtext": "将ZIP文件拖放到此处以导入您的地图"
},
"sort": {
"name": "名字",
"song-author": "歌曲作者",
"map-author": "地图作者",
"bpm": "BPM",
"duration": "持续时间",
"likes": "喜欢",
"date-uploaded": "上传日期"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "导入您的播放列表",
"subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入"
},
"sort": {
"title": "标题",
"author": "作者",
"number-of-maps": "地图数量",
"duration": "持续时间",
"notes-per-second": "NPS"
}
}
}
@@ -636,7 +652,8 @@
"create-launch-shortcut": {
"success": {
"title": "快捷方式已创建",
"msg": "快捷方式已在桌面上创建"
"msg": "快捷方式已在桌面上创建",
"msg-steam": "快捷方式已在 Steam 库中创建。"
},
"error": {
"msg": "创建快捷方式时发生错误"
@@ -843,7 +860,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",
Binary file not shown.
Binary file not shown.
@@ -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.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/2762f59c-f12a-46ba-80fa-9bf79beb64a9)
# Step 2 - Modify Keyboard Shortcuts:
- In the settings menu, select the "Hotkey" tab.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/262d4fcc-529f-4c4c-9b87-e2da21df1b95)
# Step 3 -Disabling Shortcuts:
- Turn off the "Use Hotkeys" option to prevent any conflicts with Oculus Rift.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/756018ad-7de3-469c-9f40-0082be007805)
# 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,28 @@
## Table of Contents
- [With BSManager](#with-bsmanager)
- [Manual Installation](#manual-installation)
- [One-Click Installation](#one-click-installation)
## With BSManager
(Coming with version 1.5.0)
## 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,32 @@
## Table of Contents
- [With BSManager](#with-bsmanager)
- [Manual Installation](#manual-installation)
- [One-Click Installation](#one-click-installation)
## With BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from [GitHub](https://github.com/Zagrios/bs-manager/releases/latest).
2. Launch [BSManager](https://www.bsmanager.io) and select the Beat Saber version where you want to install maps from the list on the left of the interface.
3. Go to maps by clicking on the __`Maps`__ tab.
4. Click on the __`Add`__ button to access the list of online maps.
5. Find the maps that you want to install from the provided list. You can use the search bar at the top of the interface to filter maps by __`name`__, __`type`__, __`mods`__, __`tags`__, __`notes per second`__, __`duration`__, and order the list by __`latest`__, __`relevance`__, __`rating`__, or __`curated`__.
6. Double-click on the map or simply click on the download button to install the map.
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!
## 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,30 @@
> [!WARNING]
>
> __The models require mods to work properly! Dont 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 [GitHub](https://github.com/Zagrios/bs-manager/releases/latest), [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files), or [Website](https://www.bsmanager.io).
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. Double-click on the model or simply click on the download button 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,38 @@
## Table of Contents
- [With BSManager](#with-bsmanager)
- [Manual Installation](#manual-installation)
## With BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from [GitHub](https://github.com/Zagrios/bs-manager/releases/latest), [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files), or the [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!
## 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 youre 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.
@@ -0,0 +1,55 @@
## 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.
### 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. In [BSManager](https://www.bsmanager.io), select the version you want to open in Steam from the list on the left side.
2. Click the gear icon at the top-right corner and select __`Create a Shortcut`__. This will create a shortcut on your desktop for launching the game.
3. Open Notepad (or any other text editor) and drag & drop the shortcut file into the text editor. You will see a string of text similar to this:
```yaml
bsmanager://launch/?version=1.29.1&versionIno=3940649674381032
```
4. Copy this text and remove any spaces.
<div align="center">
<img src="../wiki/Guides/Installation-and-updates/Create-a-shortcut/SeeTxtFile.png" alt="SeeTxtFile.png" />
</div>
5. Open Steam and add [BSManager](https://www.bsmanager.io) as a non-Steam game.
6. In your Steam library, right-click on [BSManager](https://www.bsmanager.io), select __`Properties`__, and add the link from step 4 as a launch parameter.
7. Disable "Desktop Game Theatre" and scroll down to enable "Include in VR Library" so you can launch it from your VR headset.
8. Rename this entry to whatever you want and customize the name and the icon of the shortcut if desired.
<div align="center">
<img src="../wiki/Guides/Installation-and-updates/Create-a-shortcut/SteamShortcut.png" alt="SteamShortcut.png" />
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

@@ -0,0 +1,53 @@
> [!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 [GitHub](https://github.com/Zagrios/bs-manager/releases/latest), [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files), or the [official website](https://www.bsmanager.io).
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 [GitHub](https://github.com/Zagrios/bs-manager/releases/latest), [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files), or the [official website](https://www.bsmanager.io).
2. Open the __`Add Versions`__ interface and select the version you wish to download.
3. Choose __`Oculus Store`__ as your platform.
4. Follow [this guide](How-to-obtain-your-Oculus-Token) to obtain your Oculus token.
5. Once logged into Oculus, 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/OculusLogin.png" alt="OculusLogin.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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

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
@@ -6,11 +14,37 @@ Go to [Releases](https://github.com/Zagrios/bs-manager/releases) page and go to
### Ubuntu, Debian (deb)
#### PPA Repository
Refer to [bs-manager-deb](https://github.com/silentrald/bs-manager-deb).
Add the BSManager PPA repository into your system with the following commands.
```bash
curl -fsSL https://raw.githubusercontent.com/silentrald/bs-manager-deb/refs/heads/main/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/bs-manager.gpg
echo "deb [signed-by=/usr/share/keyrings/bs-manager.gpg] https://raw.githubusercontent.com/silentrald/bs-manager-deb/refs/heads/main ./" | sudo tee /etc/apt/sources.list.d/bs-manager.list
sudo apt update
```
Install the `bs-manager` package using `apt`.
```bash
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`.
### Arch (AUR)
Refer to [bs-manager-git](https://aur.archlinux.org/packages/bs-manager-git).
@@ -44,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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 KiB

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

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.
+68 -9
View File
@@ -1,14 +1,73 @@
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)
- [Permission Denied on "bs-versions.json"]([Linux]-Permission-Denied-on-bs-version.json)
- [[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,30 +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.
## 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
![image](https://raw.githubusercontent.com/Zagrios/bs-manager/refs/heads/master/docs/assets/oculus-token.png)
# 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:
![image](https://raw.githubusercontent.com/Zagrios/bs-manager/refs/heads/master/docs/assets/enable-oculus-sideloading.png)
<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

Binary file not shown.

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`, its possible that the keyboard shortcut to open the developer tools is being used by another application.
Heres 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.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/2762f59c-f12a-46ba-80fa-9bf79beb64a9)
## Step 2 - Modify Keyboard Shortcuts
1. In the settings menu, select the __"Hotkey"__ tab.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/262d4fcc-529f-4c4c-9b87-e2da21df1b95)
## Step 3 - Disable Shortcuts
1. Turn off the __"Use Hotkeys"__ option to prevent any conflicts with Oculus Rift.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/756018ad-7de3-469c-9f40-0082be007805)
## 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,13 @@
<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
```
@@ -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
+91
View File
@@ -0,0 +1,91 @@
# 🌐 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-Permission-Denied-on-bs-version.json">Permission Denied on "bs-versions.json"</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>
+6
View File
@@ -51,6 +51,10 @@ const config = {
fpm: ["--after-install=build/after-install.sh"],
},
flatpak: {
// Version of org.electronjs.Electron2.BaseApp
baseVersion: "24.08",
// Version of org.freedesktop.Platform
runtimeVersion: "24.08",
finishArgs: [
// Wayland/X11 Rendering
"--socket=wayland",
@@ -65,6 +69,8 @@ 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
// 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
+224
View File
@@ -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"
+5
View File
@@ -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"
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "1.77.0"
+35 -5
View File
@@ -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());
}
+231 -7
View File
@@ -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"
+4
View File
@@ -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"
+34 -8
View File
@@ -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}");
}
+541 -321
View File
File diff suppressed because it is too large Load Diff
+13 -12
View File
@@ -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.6",
"version": "1.5.0-alpha.8",
"scripts": {
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
@@ -21,7 +21,8 @@
"test": "jest",
"test:unit": "jest ./src/__tests__/unit",
"publish": "npm run build && electron-builder -c.win.certificateSha1=206941d969c4fa8a0e04d9427def361e13b02fd0 --config electron-builder.config.js --publish always --win --x64",
"publish:linux": "npm run build && electron-builder --config electron-builder.config.js --publish never --linux --x64"
"publish:linux": "npm run build && electron-builder --config electron-builder.config.js --publish never --linux --x64",
"publish:flatpak": "npm run build && env DEBUG='@malept/flatpak-bundler' npx electron-builder --config electron-builder.config.js --publish never --linux flatpak"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
@@ -107,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",
@@ -166,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",
@@ -180,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",
@@ -188,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",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.6",
"version": "1.5.0-alpha.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.0-alpha.6",
"version": "1.5.0-alpha.8",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.6",
"version": "1.5.0-alpha.8",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
+7 -5
View File
@@ -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,
+28 -21
View File
@@ -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 },
});
+1 -1
View File
@@ -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) => {
+1 -1
View File
@@ -34,7 +34,7 @@ export const filterStrings = new Set<string>();
export const filterPatterns = new Set<RegExp>();
// Filter all occulus tokens
filterPatterns.add(/FRL\S{10,}/g);
filterPatterns.add(/(FRL|OC)\S{10,}/g);
initLogger();
deleteOlestLogs();
@@ -5,7 +5,7 @@ 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";
@@ -56,7 +56,7 @@ export abstract class AbstractLauncherService {
spawnOptions.shell = true; // For windows to spawn properly
return bsmSpawn(`"${bsExePath}"`, {
args, options: spawnOptions, log: true,
args, options: spawnOptions, log: BsmShellLog.Command,
linux: { prefix: this.linux.getProtonPrefix() },
flatpak: {
host: IS_FLATPAK,
@@ -22,6 +22,8 @@ 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";
export class BSLauncherService {
private static instance: BSLauncherService;
@@ -32,6 +34,7 @@ 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;
@@ -51,6 +54,7 @@ export class BSLauncherService {
this.steamLauncher = SteamLauncherService.getInstance();
this.oculusLauncher = OculusLauncherService.getInstance();
this.staticConfig = StaticConfigurationService.getInstance();
this.steam = SteamService.getInstance();
this.bsmProtocolService.on("launch", link => {
log.info("Launch from bsm protocol", link.toString());
@@ -195,12 +199,28 @@ 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");
return this.steam.createShortcut({
AppName: shortcutName,
Exe: exePath,
StartDir: path.dirname(exePath),
LaunchOptions: this.createLaunchLink(launchOptions),
icon: await this.createShortcutPng(shortcutIconColor),
OpenVR: "\u0001"
}, userId.result).then(() => true).catch(e => {
log.error(e);
return false;
});
}
return execOnOs({
win32: async () => (
shell.writeShortcutLink(path.join(app.getPath("desktop"), `${shortcutName}.lnk`), {
@@ -227,7 +247,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;
});
@@ -11,6 +11,7 @@ import { CustomError } from "../../../shared/models/exceptions/custom-error.clas
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{
@@ -124,7 +125,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
this.launchBs(bsExePath, launchArgs, spawnOpts).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)
@@ -8,7 +8,6 @@ import { ConfigurationService } from "./configuration.service";
import { lstat, rename } from "fs/promises";
import log from "electron-log";
import { OculusService } from "./oculus.service";
import { DownloadLinkType } from "shared/models/mods";
import sanitize from "sanitize-filename";
import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, rxCopy } from "../helpers/fs.helpers";
import { FolderLinkerService } from "./folder-linker.service";
@@ -242,16 +241,6 @@ export class BSLocalVersionService {
return version.name ?? version.BSVersion;
}
public getVersionType(version: BSVersion): DownloadLinkType {
if (version.steam) {
return "steam";
}
if (version.oculus) {
return "oculus";
}
return "universal";
}
private async getSteamVersion(): Promise<BSVersion> {
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
+24 -11
View File
@@ -3,9 +3,10 @@ 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 { StaticConfigurationService } from "./static-configuration.service";
import { SteamService } from "./steam.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";
export class LinuxService {
@@ -19,16 +20,32 @@ export class LinuxService {
}
private readonly staticConfig: StaticConfigurationService;
private readonly steamService: SteamService;
private protonPrefix = "";
private nixOS: boolean | undefined;
private constructor() {
this.staticConfig = StaticConfigurationService.getInstance();
this.steamService = SteamService.getInstance();
}
// === Launching === //
private async getCompatDataPath() {
// 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 commonFolder = await this.steamService.getGameFolder(BS_APP_ID);
const compatDataPath = path.resolve(commonFolder, "..", "compatdata", BS_APP_ID);
if (!fs.existsSync(compatDataPath)) {
log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`);
fs.mkdirSync(compatDataPath);
}
return compatDataPath;
}
public async setupLaunch(
launchOptions: LaunchOption,
steamPath: string,
@@ -40,15 +57,7 @@ export class LinuxService {
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 compatDataPath = this.getCompatDataPath();
if (!this.staticConfig.has("proton-folder")) {
throw CustomError.fromError(
@@ -120,6 +129,10 @@ export class LinuxService {
return winePath;
}
public async getWinePrefixPath(): Promise<string> {
return path.join(await this.getCompatDataPath(), "pfx");
}
public getProtonPrefix(): string {
// Set in setupLaunch
return this.protonPrefix;
@@ -134,7 +147,7 @@ export class LinuxService {
try {
await bsmExec("nixos-version", {
log: true,
log: BsmShellLog.Command,
flatpak: { host: IS_FLATPAK },
});
this.nixOS = true;
+25 -80
View File
@@ -1,22 +1,19 @@
import { BSVersion } from "shared/bs-version.interface";
import { Mod } from "shared/models/mods/mod.interface";
import { BbmFullMod, BbmMod, BbmModVersion, BbmPlatform } from "../../../shared/models/mods/mod.interface";
import { RequestService } from "../request.service";
import { BsStore } from "../../../shared/models/bs-store.enum";
import log from "electron-log"
export class BeatModsApiService {
private static instance: BeatModsApiService;
private readonly requestService: RequestService;
private readonly BEAT_MODS_ALIAS = "https://alias.beatmods.com/aliases.json";
public readonly MODS_REPO_URL = "https://beatmods.com";
private readonly MODS_REPO_API_URL = `${this.MODS_REPO_URL}/api`;
private readonly BEAT_MODS_API_URL = "https://beatmods.com/api/v1/";
public readonly BEAT_MODS_URL = "https://beatmods.com";
private readonly aliasesCache = new Map<string, BSVersion[]>();
private readonly versionModsCache = new Map<string, Mod[]>();
private readonly modsHashCache = new Map<string, Mod>();
private allModsCache: Mod[];
private readonly versionModsCache = new Map<string, BbmFullMod[]>();
private readonly modsHashCache = new Map<string, BbmModVersion>();
public static getInstance(): BeatModsApiService {
if (!BeatModsApiService.instance) {
@@ -30,101 +27,49 @@ export class BeatModsApiService {
}
private getVersionModsUrl(version: BSVersion): string {
return `${this.BEAT_MODS_API_URL}mod?status=approved&gameVersion=${version.BSVersion}&sort=&sortDirection=1`;
const platform: BbmPlatform = version.oculus || version.metadata?.store === BsStore.OCULUS ? BbmPlatform.OculusPC : BbmPlatform.SteamPC;
return `${this.MODS_REPO_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}&gameName=BeatSaber&platform=${platform}`;
}
private getAllModsUrl(): string {
return `${this.BEAT_MODS_API_URL}mod`;
}
private async getVersionAlias(): Promise<Map<string, BSVersion[]>> {
if (this.aliasesCache.size) {
return this.aliasesCache;
}
return this.requestService.getJSON<Record<string, string[]>>(this.BEAT_MODS_ALIAS).then(({ data: rawAliases }) => {
Object.entries(rawAliases).forEach(([key, value]) => {
this.aliasesCache.set(
key,
value.map(s => ({ BSVersion: s } as BSVersion))
);
});
return this.aliasesCache;
});
}
private async getAliasOfVersion(version: BSVersion): Promise<BSVersion> {
return this.getVersionAlias().then(aliases => {
if (Array.from(aliases.keys()).some(k => k === version.BSVersion)) {
return version;
}
const alias = Array.from(aliases.entries()).find(([, value]) => value.find(v => v.BSVersion === version.BSVersion))?.[0];
return { BSVersion: alias } as BSVersion;
});
}
private asignDependencies(mod: Mod, mods: Mod[]): Mod {
mod.dependencies = mod.dependencies.map(dep => mods.find(mod => mod.name === dep.name));
return mod;
}
private updateModsHashCache(mods: Mod[]): void {
private updateModsHashCache(mods: BbmModVersion[]): void {
if(!Array.isArray(mods)){
return;
}
for (const mod of mods) {
for (const downloads of (mod.downloads ?? [])) {
for (const hashMd5 of (downloads.hashMd5 ?? [])) {
this.modsHashCache.set(hashMd5.hash, mod);
}
}
for (const dep of (mod.dependencies ?? [])) {
for (const downloads of (dep.downloads ?? [])) {
for (const hashMd5 of (downloads.hashMd5 ?? [])) {
this.modsHashCache.set(hashMd5.hash, dep);
}
}
for (const content of (mod.contentHashes ?? [])) {
this.modsHashCache.set(content.hash, mod);
}
}
}
public async getVersionMods(version: BSVersion): Promise<Mod[]> {
public async getVersionMods(version: BSVersion): Promise<BbmFullMod[]> {
if (this.versionModsCache.has(version.BSVersion)) {
return this.versionModsCache.get(version.BSVersion);
}
const alias = await this.getAliasOfVersion(version);
return this.requestService.getJSON<{ mods: {mod: BbmMod, latest: BbmModVersion}[] }>(this.getVersionModsUrl(version)).then(({ data }) => {
const fullMods: BbmFullMod[] = data?.mods?.map(mod => ({ mod: mod.mod, version: mod.latest })) ?? [];
this.versionModsCache.set(version.BSVersion, fullMods);
return this.requestService.getJSON<Mod[]>(this.getVersionModsUrl(alias)).then(({ data: mods }) => {
mods = mods.map(mod => this.asignDependencies(mod, mods));
this.versionModsCache.set(version.BSVersion, mods);
this.updateModsHashCache(fullMods.map(mod => mod.version));
this.updateModsHashCache(mods);
return mods;
return fullMods;
});
}
public async getAllMods(): Promise<Mod[]> {
if (this.allModsCache) {
return this.allModsCache;
}
return this.requestService.getJSON<Mod[]>(this.getAllModsUrl()).then(({ data: mods }) => {
this.allModsCache = mods;
return this.allModsCache;
});
}
public getModByHash(hash: string): Promise<Mod> {
public getModByHash(hash: string): Promise<BbmModVersion|undefined> {
if (this.modsHashCache.has(hash)) {
return Promise.resolve(this.modsHashCache.get(hash));
}
return this.requestService.getJSON<Mod[]>(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(({ data: mods }) => {
this.updateModsHashCache(mods);
return mods.at(0);
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);
return undefined;
});
}
}
@@ -1,5 +1,4 @@
import { BSVersion } from "shared/bs-version.interface";
import { DownloadLink, Mod } from "shared/models/mods";
import { BeatModsApiService } from "./beat-mods-api.service";
import { BSLocalVersionService } from "../bs-local-version.service";
import path from "path";
@@ -18,8 +17,8 @@ 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 { ExternalMod } from "shared/models/mods/mod.interface";
import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers";
import { BbmFullMod, BbmModVersion, ExternalMod } from "../../../shared/models/mods/mod.interface";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -29,7 +28,7 @@ export class BsModsManagerService {
private readonly linuxService: LinuxService;
private readonly requestService: RequestService;
private manifestMatches: Mod[];
private manifestMatches: BbmModVersion[];
public static getInstance(): BsModsManagerService {
if (!BsModsManagerService.instance) {
@@ -45,18 +44,18 @@ export class BsModsManagerService {
this.requestService = RequestService.getInstance();
}
private async getModFromHash(hash: string): Promise<Mod> {
private async getModFromHash(hash: string): Promise<BbmModVersion | undefined> {
const mod = await this.beatModsApi.getModByHash(hash);
if(mod?.name?.toLowerCase() === "bsipa"){
if(mod?.contentHashes?.some(content => content.path.includes("IPA.exe"))){
return undefined;
}
return mod;
}
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<Mod[]> {
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<BbmModVersion[]> {
const bsPath = await this.bsLocalService.getVersionPath(version);
const modsPath = path.join(bsPath, modsDir);
@@ -85,7 +84,7 @@ export class BsModsManagerService {
}
if (filePath.toLowerCase().includes("libs")) {
const manifestIndex = this.manifestMatches.findIndex(m => m.name === mod.name);
const manifestIndex = this.manifestMatches.findIndex(m => m.id === mod.id);
if (manifestIndex < 0) {
return undefined;
@@ -101,7 +100,7 @@ export class BsModsManagerService {
return mods.filter(Boolean);
}
private async getBsipaInstalled(version: BSVersion): Promise<Mod> {
private async getBsipaInstalled(version: BSVersion): Promise<BbmModVersion> {
const bsPath = await this.bsLocalService.getVersionPath(version);
const injectorPath = path.join(bsPath, "Beat Saber_Data", "Managed", "IPA.Injector.dll");
if (!(await pathExist(injectorPath))) {
@@ -112,15 +111,14 @@ export class BsModsManagerService {
}
private async downloadZip(zipUrl: string): Promise<BsmZipExtractor> {
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
zipUrl = path.join(this.beatModsApi.MODS_REPO_URL, zipUrl);
log.info("Download mod zip", zipUrl);
const buffer = await lastValueFrom(this.requestService.downloadBuffer(zipUrl))
.then(progress => progress.data)
.catch(e => {
.catch((e: Error) => {
log.error("ZIP", "Error while downloading zip", e);
return undefined;
});
if (!buffer) {
@@ -129,7 +127,6 @@ export class BsModsManagerService {
return BsmZipExtractor.fromBuffer(buffer);
}
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean> {
log.info("executeBSIPA", version?.BSVersion, args);
@@ -142,25 +139,35 @@ 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 { error: winePrefixError, result: winePrefixResult } =
await tryit(async () => this.linuxService.getWinePrefixPath());
if (winePrefixError) {
log.warn("Could not get WINEPREFIX value", winePrefixError);
} else {
env.WINEPREFIX = winePrefixResult;
}
}
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 },
});
@@ -190,25 +197,22 @@ export class BsModsManagerService {
});
}
private getModDownload(mod: Mod, version: BSVersion): DownloadLink {
return mod.downloads.find(download => {
const type = download.type.toLowerCase();
return type === "universal" || type === this.bsLocalService.getVersionType(version);
});
private getModDownload(modVersion: BbmModVersion): string {
return `/cdn/mod/${modVersion.zipHash}.zip`
}
private async installMod(mod: Mod, version: BSVersion): Promise<boolean> {
log.info("INSTALL MOD", mod.name, "for version", `${version.BSVersion} - ${version.name}`);
private async installMod(mod: BbmFullMod, version: BSVersion): Promise<boolean> {
log.info("INSTALL MOD", mod.mod.name, "for version", `${version.BSVersion} - ${version.name}`);
const download = this.getModDownload(mod, version);
const downloadUrl = this.getModDownload(mod.version);
if (!download) {
if (!downloadUrl) {
return false;
}
log.info("Start download mod zip", mod.name, download.url);
const zip = await this.downloadZip(download.url);
log.info("Mod zip download end", mod.name, download.url);
log.info("Start download mod zip", mod.mod.name, downloadUrl);
const zip = await this.downloadZip(downloadUrl);
log.info("Mod zip download end", mod.mod.name, downloadUrl);
if (!zip) {
return false;
@@ -220,18 +224,18 @@ export class BsModsManagerService {
const md5Hash = crypto.createHash("md5")
.update(buffer)
.digest("hex");
hashCount += +download.hashMd5.some(md5 => md5.hash === md5Hash);
hashCount += +mod.version.contentHashes.some(content => content.hash === md5Hash);
}
if (hashCount !== download.hashMd5.length) {
if (hashCount !== mod.version.contentHashes.length) {
return false;
}
const versionPath = await this.bsLocalService.getVersionPath(version);
const isBSIPA = mod.name.toLowerCase() === "bsipa";
const isBSIPA = mod.mod.name.toLowerCase() === "bsipa";
const destDir = isBSIPA ? versionPath : path.join(versionPath, ModsInstallFolder.PENDING);
log.info("Start extracting mod zip", mod.name, "to", destDir);
log.info("Start extracting mod zip", mod.mod.name, "to", destDir);
const extracted = await zip.extract(destDir)
.then(() => true)
.catch(e => {
@@ -242,7 +246,7 @@ export class BsModsManagerService {
zip.close();
});
log.info("Mod zip extraction end", mod.name, "to", destDir, "success:", extracted);
log.info("Mod zip extraction end", mod.mod.name, "to", destDir, "success:", extracted);
const res = isBSIPA
? extracted &&
@@ -255,9 +259,7 @@ export class BsModsManagerService {
return res;
}
private async uninstallBSIPA(mod: Mod, version: BSVersion): Promise<void> {
const download = this.getModDownload(mod, version);
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"));
const hasIPADir = await pathExist(path.join(verionPath, "IPA"));
@@ -268,34 +270,34 @@ export class BsModsManagerService {
await this.executeBSIPA(version, ["--revert", "-n"]);
const promises = download.hashMd5.map(files => {
const file = files.file.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
const promises = mod.version.contentHashes.map(content => {
const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
return unlinkPath(path.join(verionPath, file));
});
await Promise.all(promises);
}
private async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
if (mod.name.toLowerCase() === "bsipa") {
private async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise<void> {
if (mod.mod.name.toLowerCase() === "bsipa") {
return this.uninstallBSIPA(mod, version);
}
const download = this.getModDownload(mod, version);
const versionPath = await this.bsLocalService.getVersionPath(version);
const promises = download.hashMd5.map(async files => {
return Promise.all([unlinkPath(path.join(versionPath, files.file)), unlinkPath(path.join(versionPath, "IPA", "Pending", files.file))]);
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))]);
});
await Promise.all(promises);
}
public getAvailableMods(version: BSVersion): Promise<Mod[]> {
return this.beatModsApi.getVersionMods(version);
public async getAvailableMods(version: BSVersion): Promise<BbmFullMod[]> {
return this.beatModsApi.getVersionMods(version).catch(() => {
return [] as BbmFullMod[];
});
}
public async getInstalledMods(version: BSVersion): Promise<Mod[]> {
public async getInstalledMods(version: BSVersion): Promise<BbmModVersion[]> {
this.manifestMatches = [];
const bsipa = await this.getBsipaInstalled(version);
@@ -305,17 +307,17 @@ export class BsModsManagerService {
const dirMods = pluginsMods.flat().concat(libsMods.flat());
const modsDict = new Map<string, Mod>();
const modsDict = new Map<number, BbmModVersion>();
if (bsipa) {
modsDict.set(bsipa.name, bsipa);
modsDict.set(bsipa.id, bsipa);
}
for (const mod of dirMods.flat()) {
if (modsDict.has(mod.name)) {
if (modsDict.has(mod.id)) {
continue;
}
modsDict.set(mod.name, mod);
modsDict.set(mod.id, mod);
}
return Array.from(modsDict.values());
@@ -423,7 +425,7 @@ export class BsModsManagerService {
});
}
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
public installMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
const progress = { current: 0, total: mods.length };
return new Observable<Progression>(obs => {
@@ -434,7 +436,7 @@ export class BsModsManagerService {
obs.next(progress);
const bsipa = popElement(mod => mod.name.toLowerCase() === "bsipa", mods);
const bsipa = popElement(mod => mod.mod.name.toLowerCase() === "bsipa", mods);
if(bsipa){
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
@@ -460,7 +462,7 @@ export class BsModsManagerService {
});
}
public uninstallMods(mods: Mod[], version: BSVersion): Observable<Progression> {
public uninstallMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
const progress = { current: 0, total: mods.length };
return new Observable<Progression>(obs => {
@@ -485,16 +487,19 @@ export class BsModsManagerService {
public uninstallAllMods(version: BSVersion): Observable<Progression> {
return new Observable<Progression>(obs => {
(async () => {
const mods = await this.getInstalledMods(version).catch(err => {
log.error(err);
return [];
});
const progress = { current: 0, total: mods.length };
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 progress = { current: 0, total: fullInstalledMods.length };
obs.next(progress);
for (const mod of mods) {
for (const mod of fullInstalledMods) {
await this.uninstallMod(mod, version);
progress.current++;
obs.next(progress);
+2 -2
View File
@@ -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();
+113 -31
View File
@@ -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 [];
});
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,52 @@
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.additionalArgs?.length);
const [additionalArgsString, setAdditionalArgsString] = useState(launchOption.additionalArgs?.join("; ") ?? "");
const [steamShortcut, setSteamShortcut] = useState(false);
const isSteamVersion = useMemo(() => {
return data.steam || data.metadata?.store === BsStore.STEAM;
}, [data]);
const completeModal = () => {
if(advanced) {
launchOptions.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length);
launchOption.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length);
} else {
launchOptions.additionalArgs = undefined;
launchOption.additionalArgs = undefined;
}
resolver({exitCode: ModalExitCode.COMPLETED, data: launchOptions});
resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }});
}
const toogleLaunchMod = (mod: LaunchMod, enabled: boolean) => {
if(enabled) {
setLaunchOptions(prev => ({...prev, launchMods: [...prev.launchMods, mod]}));
} else {
setLaunchOptions(prev => ({...prev, launchMods: prev.launchMods.filter(m => m !== mod)}));
}
}
return (
@@ -49,20 +64,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>
@@ -87,6 +102,14 @@ export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion>
</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>
{(() => {
@@ -1,38 +0,0 @@
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { Mod } from "shared/models/mods/mod.interface";
export const UninstallModModal: ModalComponent<void, Mod> = ({ resolver, options: {data} }) => {
const mod = data;
const t = useTranslation();
const desc = mod.name.toLowerCase() === "bsipa" ? "modals.uninstall-mod.description-bsipa" : "modals.uninstall-mod.description";
return (
<form
onSubmit={e => {
e.preventDefault();
resolver({ exitCode: ModalExitCode.COMPLETED });
}}
>
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.uninstall-mod.title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t(desc, { mod: mod.name })}</p>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<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" type="submit" withBar={false} text="modals.bs-uninstall.buttons.submit" />
</div>
</form>
);
};
@@ -133,7 +133,7 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
return (
<div ref={ref} className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white">
<div className="w-full shrink-0 flex h-9 justify-center gap-2 text-main-color-1 dark:text-white">
<BsmButton
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold"
icon="add"
@@ -149,7 +149,7 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
<input type="text" className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2" placeholder={t("models.panel.actions.search")} onChange={e => setSearch(e.target.value)} tabIndex={-1} />
</div>
<BsmDropdownButton items={threeDotsItems} 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} menuTranslationY="6px" align="center" />
<BsmDropdownButton items={threeDotsItems} 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} menuTranslationY="6px" align="right" />
</div>
<BsContentTabPanel
tabIndex={currentTabIndex}
@@ -68,10 +68,11 @@ import { BrowseIcon } from "./icons/browse-icon.component";
import { AddFileIcon } from "./icons/add-file-icon.component";
import { CancelIcon } from "./icons/cancel-icon.component";
import { WarningIcon } from "./icons/warning-icon.component";
import { ArrowUpwardIcon } from "./icons/arrow-upward-icon.component";
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
// TODO : Very ugly very messy, need to find a better way to do this
@@ -282,6 +283,10 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
if(icon === "add-file") {
return <AddFileIcon className={className} style={style} />;
}
if(icon === "arrow-upward") {
return <ArrowUpwardIcon className={className} style={style} />;
}
if(icon === "null") {
return null;
}
@@ -2,11 +2,12 @@ import { createSvgIcon } from "../svg-icon.type";
export const ItalyIcon = createSvgIcon((props, ref) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3" ref={ref} {...props} width="71" height="48">
<rect width="5" height="3" fill="none" rx="6.64"/>
<rect width="1.67" height="3" x="0" fill="#008d46"/>
<rect width="1.67" height="3" x="1.67" fill="#ffffff"/>
<rect width="1.67" height="3" x="3.34" fill="#ce2b37"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" ref={ref} {...props} width="71" height="48">
<g fill="none">
<path d="M6.65.53A6.36 6.36 0 0 0 .29 6.89v34.27a6.36 6.36 0 0 0 6.36 6.36h17v-47z" fill="#57A863"/>
<path d="M23.63.53v47H47v-47z" fill="#FFF"/>
<path d="M63.92.53H47v47h16.92a6.36 6.36 0 0 0 6.36-6.36V6.89A6.36 6.36 0 0 0 63.92.53" fill="#ED5565"/>
</g>
</svg>
);
});
@@ -0,0 +1,15 @@
import { createSvgIcon } from "../svg-icon.type";
export const ArrowUpwardIcon = createSvgIcon((props, ref) => {
return (
<svg
ref={ref}
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
fill="currentColor"
>
<path d="M440-608 324-492q-11 11-28 11t-28-11q-11-11-11-28t11-28l184-184q12-12 28-12t28 12l184 184q11 11 11 28t-11 28q-11 11-28 11t-28-11L520-608v328q0 17-11.5 28.5T480-240q-17 0-28.5-11.5T440-280v-328Z" />
</svg>
);
});
@@ -1,21 +1,22 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { Mod } from "shared/models/mods/mod.interface";
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import useDoubleClick from "use-double-click";
import { gt } from "semver";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import striptags from "striptags";
import { safeGt } from "shared/helpers/semver.helpers";
type Props = {
className?: string;
mod: Mod;
mod: BbmFullMod;
installedVersion: string;
isDependency?: boolean;
isSelected?: boolean;
onChange?: (val: boolean) => void;
wantInfo?: boolean;
onWantInfo?: (mod: Mod) => void;
onWantInfo?: (mod: BbmFullMod) => void;
disabled?: boolean;
onUninstall?: () => void;
};
@@ -25,7 +26,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
const themeColor = useThemeColor("second-color");
const clickRef = useRef();
const isChecked = useMemo(() => isDependency || isSelected || mod.required, [isDependency, isSelected, mod.required]);
const isChecked = useMemo(() => isDependency || isSelected || mod.mod.category === BbmCategories.Core, [isDependency, isSelected, mod.mod.category]);
useDoubleClick({
onSingleClick: e => handleWantInfo(e),
@@ -39,7 +40,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
}, [isChecked]);
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
const isOutDated = installedVersion ? safeGt(mod.version.modVersion, installedVersion) : false;
const handleWantInfo = (e: MouseEvent) => {
e.preventDefault();
@@ -53,19 +54,19 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
return (
<li ref={clickRef} className={`${className} group`}>
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency || disabled} checked={isChecked} />
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.mod.category === BbmCategories.Core || isDependency || disabled} checked={isChecked} />
</div>
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>
{mod.name}
{mod.mod.name}
</span>
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90 ${installedVersion && isOutDated && "text-red-400 line-through"} ${installedVersion && !isOutDated && "text-green-400"}`} style={wantInfoStyle}>
{installedVersion || "-"}
</span>
<span className="min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
{mod.version}
{mod.version.modVersion}
</span>
<span title={mod.description} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
{mod.description}
<span title={striptags(mod.mod?.description ?? "", { tagReplacementText: " " })} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
{striptags(mod.mod?.summary ?? "", { tagReplacementText: " " })}
</span>
<div className="h-full bg-inherit flex items-center justify-center mr-3 rounded-r-md pr-2 border-t-2 border-b-2 border-r-2 group-hover:brightness-90" style={wantInfoStyle}>
{installedVersion && (
@@ -2,19 +2,19 @@ import { motion } from "framer-motion";
import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { Mod } from "shared/models/mods/mod.interface";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
import { ModItem } from "./mod-item.component";
type Props = {
modsMap: Map<string, Mod[]>;
installed: Map<string, Mod[]>;
modsSelected: Mod[];
onModChange: (selected: boolean, mod: Mod) => void;
moreInfoMod?: Mod;
onWantInfos: (mod: Mod) => void
modsMap: Map<BbmCategories, BbmFullMod[]>;
installed: Map<BbmCategories, BbmFullMod[]>;
modsSelected: BbmFullMod[];
onModChange: (selected: boolean, mod: BbmFullMod) => void;
moreInfoMod?: BbmFullMod;
onWantInfos: (mod: BbmFullMod) => void
disabled?: boolean;
uninstallMod?: (mods: Mod) => void;
uninstallMod?: (mods: BbmFullMod) => void;
uninstallAllMods?: () => void;
unselectAllMods?: () => void;
openModsDropZone?: () => void;
@@ -24,30 +24,31 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
const [filter, setFilter] = useState("");
const [filterEnabled, setFilterEnabled] = useState(false);
const t = useTranslation();
const { text: t } = useTranslationV2();
const installedModVersion = (key: string, mod: Mod): string => {
const installedModVersion = (key: BbmCategories, mod: BbmFullMod): string => {
if (!installed?.get(key)) {
return undefined;
}
const installedMod = installed.get(key).find(m => m.name === mod.name);
const installedMod = installed.get(key).find(m => m.mod.id === mod.mod.id);
if (!installedMod) {
return undefined;
}
return installedMod.version;
return installedMod.version.modVersion;
};
const isDependency = (mod: Mod): boolean => {
return modsSelected.some(m => {
const deps = m.dependencies?.map(dep => Array.from(modsMap.values()).flat().find(m => dep.name === m.name)) ?? [];
if (deps.some(depMod => depMod.name === mod.name)) {
return true;
}
return deps.some(depMod => depMod.dependencies?.some(depModDep => depModDep.name === mod.name));
});
const getAvailableMods = (): BbmFullMod[] => {
return Array.from(modsMap.values()).flat();
}
const isDependency = (mod: BbmFullMod): boolean => {
const selectedModsDepsIds = modsSelected.flatMap(m => m.version.dependencies);
const modsDeps = getAvailableMods().filter(m => selectedModsDepsIds.includes(m.version.id));
const modsDepsDepsIds = modsDeps.flatMap(m => m.version.dependencies);
return selectedModsDepsIds.includes(mod.version.id) || modsDepsDepsIds.includes(mod.version.id);
};
const isSelected = (mod: Mod): boolean => modsSelected.some(m => m.name === mod.name);
const isSelected = (mod: BbmFullMod): boolean => modsSelected.some(m => m.mod.id === mod.mod.id);
const handleInput = (val: string) => setFilter(val.toLowerCase());
@@ -75,21 +76,20 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
]} />
</span>
{Array.from(modsMap.keys()).map(
key =>
modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
{Array.from(modsMap.keys()).map(key => modsMap.get(key).some(mod => mod.mod.name.toLowerCase().includes(filter)) && (
<ul key={key} className="contents">
<h2 className="col-span-full py-1 font-bold pl-3">{key}</h2>
{modsMap.get(key).map(mod => mod.name?.toLowerCase().includes(filter) && (
<h2 className="col-span-full py-1 font-bold pl-3 capitalize">{key}</h2>
{modsMap.get(key).map(mod => mod.mod.name.toLowerCase().includes(filter) && (
<ModItem
key={mod.name}
key={mod.mod.id}
className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer"
mod={mod} installedVersion={installedModVersion(key, mod)}
mod={mod}
installedVersion={installedModVersion(key, mod)}
isDependency={isDependency(mod)}
isSelected={isSelected(mod)}
onChange={val => onModChange(val, mod)}
onWantInfo={onWantInfos}
wantInfo={mod.name === moreInfoMod?.name}
wantInfo={mod.mod.id === moreInfoMod?.mod.id}
disabled={disabled}
onUninstall={() => uninstallMod?.(mod)} />
))}
@@ -1,16 +1,15 @@
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { BSVersion } from "shared/bs-version.interface";
import { Mod } from "shared/models/mods/mod.interface";
import { BbmCategories, BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
import { ModsGrid } from "./mods-grid.component";
import { ConfigurationService } from "renderer/services/configuration.service";
import { DefaultConfigKey } from "renderer/config/default-configuration.config";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png";
import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { lastValueFrom } from "rxjs";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { useTranslation, useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import { useInView } from "framer-motion";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
@@ -26,7 +25,7 @@ import { Dropzone } from "renderer/components/shared/dropzone.component";
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
const t = useTranslation();
const { text: t } = useTranslationV2();
const modsManager = useService(BsModsManagerService);
const configService = useService(ConfigurationService);
@@ -37,10 +36,10 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const ref = useRef(null);
const isVisible = useInView(ref, { amount: 0.5 });
const [modsAvailable, setModsAvailable] = useState(null as Map<string, Mod[]>);
const [modsInstalled, setModsInstalled] = useState(null as Map<string, Mod[]>);
const [modsSelected, setModsSelected] = useState([] as Mod[]);
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
const [modsAvailable, setModsAvailable] = useState(null as Map<BbmCategories, BbmFullMod[]>);
const [modsInstalled, setModsInstalled] = useState(null as Map<BbmCategories, BbmFullMod[]>);
const [modsSelected, setModsSelected] = useState([] as BbmFullMod[]);
const [moreInfoMod, setMoreInfoMod] = useState(null as BbmFullMod);
const [reinstallAllMods, setReinstallAllMods] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
const [installing, setInstalling] = useState(false);
@@ -50,86 +49,53 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const downloadRef = useRef(null);
const [downloadWith, setDownloadWidth] = useState(0);
const modsToCategoryMap = (mods: Mod[]): Map<string, Mod[]> => {
const modsToCategoryMap = (mods: BbmFullMod[]): Map<BbmCategories, BbmFullMod[]> => {
if (!mods) {
return new Map<string, Mod[]>();
return new Map<BbmCategories, BbmFullMod[]>();
}
const map = new Map<string, Mod[]>();
mods.forEach(mod => map.set(mod.category, [...(map.get(mod.category) ?? []), mod]));
const map = new Map<BbmCategories, BbmFullMod[]>();
mods.forEach(mod => map.set(mod.mod.category, [...(map.get(mod.mod.category) ?? []), mod]));
return map;
};
const handleModChange = (selected: boolean, mod: Mod) => {
const handleModChange = (selected: boolean, mod: BbmFullMod) => {
if (selected) {
return setModsSelected(mods => {
if (mods.some(m => m.name === mod.name)) {
if (mods.some(m => m.mod.id === mod.mod.id)) {
return mods;
}
return [...mods, mod];
});
}
setModsSelected(mods => mods.filter(m => m.name !== mod.name));
setModsSelected(mods => mods.filter(m => m.mod.id !== mod.mod.id));
};
const handleMoreInfo = (mod: Mod) => {
if (mod.name === moreInfoMod?.name) {
const handleMoreInfo = (mod: BbmFullMod) => {
if (mod.mod.id === moreInfoMod?.mod.id) {
return setMoreInfoMod(null);
}
setMoreInfoMod(mod);
};
const handleOpenMoreInfo = () => {
if (!moreInfoMod?.link) {
return;
if (moreInfoMod?.mod?.gitUrl) {
linkOpener.open(moreInfoMod.mod.gitUrl);
}
linkOpener.open(moreInfoMod.link);
};
// private isDependency(mod: Mod, selectedMods: Mod[], availableMods: Mod[]) {
// return selectedMods.some(m => {
// const deps = m.dependencies.map(dep => Array.from(availableMods.values()).find(m => dep.name === m.name));
// if (deps.some(depMod => depMod.name === mod.name)) {
// return true;
// }
// return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
// });
// }
// private async resolveDependencies(mods: Mod[], version: BSVersion): Promise<Mod[]> {
// const availableMods = await this.beatModsApi.getVersionMods(version);
// return Array.from(
// new Map<string, Mod>(
// availableMods.reduce((res, mod) => {
// if (mod.required || this.isDependency(mod, mods, availableMods)) {
// res.push([mod.name, mod]);
// }
// return res;
// }, [])
// ).values()
// );
// }
const getAllDependencies = (mods: Mod[], availableMods: Mod[]): Mod[] => {
const collectedDependencies = new Set<Mod>();
const getAllDependencies = (mods: BbmFullMod[], availableMods: BbmFullMod[]): BbmFullMod[] => {
const collectedDependencies = new Set<BbmFullMod>();
const modIdsToProcess = new Set(mods.flatMap(m => m.version.dependencies));
const collectDependencies = (mod: Mod) => {
if (!mod.dependencies) { return; }
for (const dependency of mod.dependencies) {
const dependencyMod = availableMods.find(avMod => avMod.name === dependency.name);
if (dependencyMod && !collectedDependencies.has(dependencyMod)) {
collectedDependencies.add(dependencyMod);
collectDependencies(dependencyMod);
}
for (const currentId of modIdsToProcess) {
const dependency = availableMods.find(m => m.version.id === currentId);
if (dependency && !collectedDependencies.has(dependency)) {
collectedDependencies.add(dependency);
dependency.version.dependencies?.forEach(depId => modIdsToProcess.add(depId));
}
};
mods.forEach(collectDependencies);
availableMods.forEach(mod => {
if(mod.required){
collectedDependencies.add(mod);
}
});
}
return Array.from(collectedDependencies);
};
@@ -145,18 +111,23 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
let modsToInstall = [
...modsSelected,
...getAllDependencies(modsSelected, Array.from(modsAvailable.values()).flat())
]
];
modsToInstall = reinstallAll ? (
modsToInstall // If reinstalling all, we install all selected mods
) : (
modsToInstall.filter(mod => { // Else we only install the mods that are not installed or have a newer version
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name);
return !installedMod || lt(installedMod.version, mod.version);
const installedMod = modsInstalled.get(mod.mod.category)?.find(installedMod => installedMod.mod.id === mod.mod.id);
return !installedMod || lt(installedMod.version.modVersion, mod.version.modVersion);
})
);
modsToInstall = Array.from(new Set(modsToInstall)); // Remove duplicates
// Remove duplicates, null and undefined
const set = new Set(modsToInstall);
set.delete(null);
set.delete(undefined);
modsToInstall = Array.from(set); // Remove duplicates
if (!modsToInstall.length) {
notification.notifyInfo({ title: "pages.version-viewer.mods.notifications.all-mods-already-installed.title", desc: "pages.version-viewer.mods.notifications.all-mods-already-installed.description" });
@@ -172,10 +143,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const importMods = (files: string[]): void => {
setModsDropZoneOpen(false);
modsManager.importMods(files, version);
modsManager.importMods(files, version).then(() => {
loadMods();
});
};
const uninstallMod = (mod: Mod): void => {
const uninstallMod = (mod: BbmFullMod): void => {
setUninstalling(() => true);
lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => {
loadMods();
@@ -206,17 +179,23 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return Promise.resolve();
}
const promise = async () => {
const promise = async (): Promise<[BbmFullMod[], BbmModVersion[]]> => {
const available = await lastValueFrom(modsManager.getAvailableMods(version));
const installed = await lastValueFrom(modsManager.getInstalledMods(version));
return [available, installed];
}
return promise().then(([available, installed]) => {
const defaultMods = installed?.length ? [] : configService.get<string[]>("default_mods" as DefaultConfigKey);
const defaultMods = installed?.length ? [] : available.filter(m => m.mod.category === BbmCategories.Core || m.mod.category === BbmCategories.Essential);
setModsAvailable(() => modsToCategoryMap(available));
setModsSelected(() => available.filter(m => m.required || defaultMods.some(d => m.name?.toLowerCase() === d?.toLowerCase()) || installed.some(i => m.name === i.name)));
setModsInstalled(() => modsToCategoryMap(installed));
const installedMods: BbmFullMod[] = installed.map(version => {
const mod = available.find(m => m.mod.id === version.modId);
return mod ? { ...mod, version } : null;
}).filter(mod => mod);
setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installedMods.some(i => m.mod.id === i.mod.id)));
setModsInstalled(modsToCategoryMap(installedMods));
});
};
@@ -120,10 +120,10 @@ export function VersionViewer() {
const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, {data: state});
if(exitCode !== ModalExitCode.COMPLETED){ return; }
lastValueFrom(bsLauncher.createLaunchShortcut(data)).then(() => {
lastValueFrom(bsLauncher.createLaunchShortcut(data.launchOption, data.steamShortcut)).then(() => {
notification.notifySuccess({
title: "notifications.create-launch-shortcut.success.title",
desc: "notifications.create-launch-shortcut.success.msg"
desc: `notifications.create-launch-shortcut.success.${data.steamShortcut ? "msg-steam" : "msg"}`
});
}).catch(() => {
notification.notifyError({
+2 -2
View File
@@ -125,9 +125,9 @@ export class BSLauncherService {
}
public createLaunchShortcut(launchOptions: LaunchOption): Observable<boolean>{
public createLaunchShortcut(launchOptions: LaunchOption, steamShortcut: boolean): Observable<boolean>{
const options: LaunchOption = {...launchOptions, version: {...launchOptions.version, color: launchOptions.version.color || this.theme.getBsmColors()[1]}};
return this.ipcService.sendV2("create-launch-shortcut", options);
return this.ipcService.sendV2("create-launch-shortcut", { options, steamShortcut });
}
public restoreSteamVR(): Promise<void>{
@@ -1,12 +1,12 @@
import { Observable, BehaviorSubject, throwError, of, lastValueFrom } from "rxjs";
import { catchError, map, tap } from "rxjs/operators";
import { BSVersion } from "shared/bs-version.interface";
import { Mod } from "shared/models/mods";
import { IpcService } from "./ipc.service";
import { ProgressBarService } from "./progress-bar.service";
import { NotificationService } from "./notification.service";
import { Progression } from "main/helpers/fs.helpers";
import { ProgressionInterface } from "shared/models/progress-bar";
import { BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -32,15 +32,15 @@ export class BsModsManagerService {
this.notifications = NotificationService.getInstance();
}
public getAvailableMods(version: BSVersion): Observable<Mod[]> {
public getAvailableMods(version: BSVersion): Observable<BbmFullMod[]> {
return this.ipcService.sendV2("bs-mods.get-available-mods", version);
}
public getInstalledMods(version: BSVersion): Observable<Mod[]> {
public getInstalledMods(version: BSVersion): Observable<BbmModVersion[]> {
return this.ipcService.sendV2("bs-mods.get-installed-mods", version);
}
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
public installMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return throwError(() => new Error("Action already in progress"));
@@ -72,7 +72,7 @@ export class BsModsManagerService {
});
}
public uninstallMod(mod: Mod, version: BSVersion): Observable<Progression> {
public uninstallMod(mod: BbmFullMod, version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return throwError(() => new Error("Action already in progress"));
}
+6 -1
View File
@@ -1,4 +1,4 @@
import { coerce, lt, valid } from "semver";
import { coerce, lt, gt, valid } from "semver";
import { tryit } from "./error.helpers";
export function safeLt(a: string, b: string): boolean {
@@ -6,3 +6,8 @@ export function safeLt(a: string, b: string): boolean {
return result ?? false;
}
export function safeGt(a: string, b: string): boolean {
const { result } = tryit(() => gt(valid(coerce(a)), valid(coerce(b))));
return result ?? false;
}
+8
View File
@@ -0,0 +1,8 @@
export enum Comparison {
EQUAL = 0,
GREATER = 1,
LESSER = -1,
};
export type Comparator<T> = (object1: T, object2: T) => number;
+6 -7
View File
@@ -11,7 +11,6 @@ import { DepotDownloaderEvent } from "../bs-version-download/depot-downloader.mo
import { MSGetQuery, MSModel, MSModelType } from "../models/model-saber.model";
import { ModelDownload } from "renderer/services/models-management/models-downloader.service";
import { BsmLocalModel } from "../models/bsm-local-model.interface";
import { Mod } from "../mods";
import { BPList, DownloadPlaylistProgressionData } from "../playlists/playlist.interface";
import { VersionLinkerAction } from "renderer/services/version-folder-linker.service";
import { FileFilter, OpenDialogOptions, OpenDialogReturnValue } from "electron";
@@ -20,7 +19,7 @@ import { Supporter } from "../supporters";
import { AppWindow } from "../window-manager/app-window.model";
import { LocalBPList, LocalBPListsDetails } from "../playlists/local-playlist.models";
import { StaticConfigGetIpcRequestResponse, StaticConfigKeys, StaticConfigSetIpcRequest } from "main/services/static-configuration.service";
import { ExternalMod } from "../mods/mod.interface";
import { BbmFullMod, BbmModVersion, ExternalMod } from "../mods/mod.interface";
import { OculusDownloadInfo } from "main/services/bs-version-download/bs-oculus-downloader.service";
export type IpcReplier<T> = (data: Observable<T>) => void;
@@ -54,7 +53,7 @@ export interface IpcChannelMapping {
"bs-installer.set-install-path": { request: { path: string, move: boolean }, response: string};
/* ** bs-launcher-ipcs ** */
"create-launch-shortcut": { request: LaunchOption, response: boolean };
"create-launch-shortcut": { request: { options: LaunchOption, steamShortcut?: boolean }, response: boolean };
"bs-launch.need-start-as-admin": { request: void, response: boolean };
"bs-launch.launch": { request: LaunchOption, response: BSLaunchEventData };
"bs-launch.restore-steamvr": { request: void, response: void };
@@ -83,11 +82,11 @@ export interface IpcChannelMapping {
"delete-models": { request: BsmLocalModel[], response: Progression<BsmLocalModel[]> };
/* ** bs-mods-ipcs ** */
"bs-mods.get-available-mods": { request: BSVersion, response: Mod[] };
"bs-mods.get-installed-mods": { request: BSVersion, response: Mod[] };
"bs-mods.get-available-mods": { request: BSVersion, response: BbmFullMod[] };
"bs-mods.get-installed-mods": { request: BSVersion, response: BbmModVersion[] };
"bs-mods.import-mods": { request: { paths: string[]; version: BSVersion; }, response: Progression<ExternalMod> };
"bs-mods.install-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
"bs-mods.uninstall-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
"bs-mods.install-mods": { request: { mods: BbmFullMod[]; version: BSVersion }, response: Progression };
"bs-mods.uninstall-mods": { request: { mods: BbmFullMod[]; version: BSVersion }, response: Progression };
"bs-mods.uninstall-all-mods": { request: BSVersion, response: Progression };
/* ** bs-playlist-ipcs ** */
@@ -1,5 +1,7 @@
import { ObjectValues } from "shared/helpers/type.helpers";
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
import { BsmLocalMap } from "./bsm-local-map.interface";
import { Comparator } from "../comparator.type";
export interface BsvMapDetail {
automapper: boolean;
@@ -250,6 +252,11 @@ export interface MapFilter {
excludedTags?: Set<MapTag>;
}
export interface MapSort {
compare: Comparator<BsmLocalMap>;
ascending: boolean;
}
export interface SearchResponse {
docs: BsvMapDetail[];
redirect: string;
+42
View File
@@ -0,0 +1,42 @@
import { Comparison } from "../comparator.type";
import { Sorter } from "../sorter.model";
import { BsmLocalMap } from "./bsm-local-map.interface";
const sortName = (map1: BsmLocalMap, map2: BsmLocalMap) => map1.mapInfo.songName.localeCompare(map2.mapInfo.songName);
export const mapSorter = new Sorter<BsmLocalMap>({
comparators: {
name: sortName,
"song-author": (map1, map2) => map1.mapInfo.songAuthorName.localeCompare(map2.mapInfo.songAuthorName),
"map-author": (map1, map2) => {
// Compare the number of mappers, else compare the first mapper
const difference = map1.mapInfo.levelMappers.length - map2.mapInfo.levelMappers.length;
return difference || map1.mapInfo.levelMappers[0].localeCompare(map2.mapInfo.levelMappers[0]);
},
bpm: (map1, map2) => map1.mapInfo.beatsPerMinute - map2.mapInfo.beatsPerMinute,
duration: (map1, map2) => {
if (!map1.songDetails) {
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
}
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.duration - map2.songDetails.duration;
},
likes: (map1, map2) => {
if (!map1.songDetails) {
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
}
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.upVotes - map2.songDetails.upVotes;
},
"date-uploaded": (map1, map2) => {
if (!map1.songDetails) {
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
}
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.uploadedAt - map2.songDetails.uploadedAt;
},
},
tiebreak: sortName,
defaultKey: "name"
});
-1
View File
@@ -1,2 +1 @@
export { ModInstallProgression, InstallModsResult, UninstallModsResult } from "./mod-ipc.model";
export { Mod, DownloadLink, DownloadLinkType, FileHashes, ModAuthor } from "./mod.interface";
+96 -37
View File
@@ -1,42 +1,101 @@
export interface Mod {
_id: string;
name: string;
version: string;
gameVersion: string;
authorId: string;
uploadedDate: string;
updatedDate: string;
author: ModAuthor;
description: string;
link: string;
category: string;
downloads: DownloadLink[];
required: boolean;
dependencies: Mod[];
status: string;
}
export interface ModAuthor {
_id: string;
username: string;
lastLogin: string;
}
export interface DownloadLink {
type: DownloadLinkType;
url: string;
hashMd5: FileHashes[];
}
export type DownloadLinkType = "universal" | "steam" | "oculus";
export interface FileHashes {
hash: string;
file: string;
}
// Any mods that are not supported in beatmods
export interface ExternalMod {
name: string;
files: string[];
}
// BBM Mods
export interface BbmFullMod {
mod: BbmMod;
version: BbmModVersion;
}
export interface BbmMod {
id: number;
name: string;
summary: string;
description: string;
gameName: "BeatSaber";
category: BbmCategories;
authors: BbmUserAPIResponse[];
status: BbmStatus;
iconFileName: string;
gitUrl: string;
lastApprovedById: number;
lastUpdatedById: number;
createdAt: Date;
updatedAt: Date;
}
export interface BbmModVersion {
id: number;
modId: number;
author: BbmUserAPIResponse;
modVersion: string;
platform: BbmPlatform;
zipHash: string;
status: BbmStatus;
dependencies: number[];
contentHashes: BbmContentHash[];
supportedGameVersions: BbmGameVersion[];
downloadCount: number;
lastApprovedById?: number;
lastUpdatedById?: number;
createdAt?: Date;
updatedAt?: Date;
}
export interface BbmContentHash {
path: string;
hash: string;
}
export enum BbmStatus {
Private = "private",
Removed = "removed",
Unverified = "unverified",
Verified = "verified",
}
export enum BbmPlatform {
SteamPC = `steampc`,
OculusPC = `oculuspc`,
UniversalPC = `universalpc`,
UniversalQuest = `universalquest`,
}
export interface BbmGameVersion {
readonly id: number;
gameName: "BeatSaber";
version: string; // semver
defaultVersion: boolean;
}
export enum BbmCategories {
Core = "core",
Essential = "essential",
Library = "library",
Cosmetic = "cosmetic",
PracticeTraining = "practice",
Gameplay = "gameplay",
StreamTools = "streamtools",
UIEnhancements = "ui",
Lighting = "lighting",
TweaksTools = "tweaks",
Multiplayer = "multiplayer",
TextChanges = "text",
Editor = "editor",
Other = "other",
}
export interface BbmUserAPIResponse {
id: number;
username: string;
githubId: string;
sponsorUrl: string;
displayName: string;
bio: string;
createdAt?: Date;
updatedAt?: Date;
}

Some files were not shown because too many files have changed in this diff Show More