From 09d58215c552848f4036f2400051fb5f5f4fee5f Mon Sep 17 00:00:00 2001 From: milo Date: Wed, 27 May 2020 04:52:14 -0400 Subject: [PATCH] remove annoying windows --- .editorconfig | 16 + .github/FUNDING.yml | 4 + .github/workflows/dotnet.yml | 52 ++ .vs/ModAssistant/v16/.suo | Bin 0 -> 120832 bytes LICENSE | 21 + ModAssistant.sln | 25 + ModAssistant/App.config | 66 ++ ModAssistant/App.xaml | 43 + ModAssistant/App.xaml.cs | 241 ++++++ ModAssistant/Classes/Diagnostics.cs | 59 ++ .../Classes/External Interfaces/BeatSaver.cs | 361 ++++++++ .../Classes/External Interfaces/ModelSaber.cs | 34 + .../Classes/External Interfaces/Playlists.cs | 114 +++ .../Classes/External Interfaces/Utils.cs | 76 ++ ModAssistant/Classes/Http.cs | 40 + ModAssistant/Classes/HyperlinkExtensions.cs | 38 + ModAssistant/Classes/Mod.cs | 54 ++ ModAssistant/Classes/OneClickInstaller.cs | 138 ++++ ModAssistant/Classes/Promotions.cs | 24 + ModAssistant/Classes/Themes.cs | 524 ++++++++++++ ModAssistant/Classes/Updater.cs | 146 ++++ ModAssistant/Classes/Utils.cs | 443 ++++++++++ ModAssistant/Libs/semver/IntExtensions.cs | 55 ++ ModAssistant/Libs/semver/SemVersion.cs | 598 ++++++++++++++ ModAssistant/Localisation/de.xaml | 244 ++++++ ModAssistant/Localisation/en-DEBUG.xaml | 185 +++++ ModAssistant/Localisation/en.xaml | 244 ++++++ ModAssistant/Localisation/fr.xaml | 251 ++++++ ModAssistant/Localisation/it.xaml | 244 ++++++ ModAssistant/Localisation/ko.xaml | 243 ++++++ ModAssistant/Localisation/nl.xaml | 242 ++++++ ModAssistant/Localisation/ru.xaml | 244 ++++++ ModAssistant/Localisation/zh.xaml | 235 ++++++ ModAssistant/MainWindow.xaml | 229 ++++++ ModAssistant/MainWindow.xaml.cs | 323 ++++++++ ModAssistant/ModAssistant.csproj | 303 +++++++ ModAssistant/OneClickStatus.xaml | 98 +++ ModAssistant/OneClickStatus.xaml.cs | 64 ++ ModAssistant/Pages/About.xaml | 292 +++++++ ModAssistant/Pages/About.xaml.cs | 62 ++ ModAssistant/Pages/Intro.xaml | 141 ++++ ModAssistant/Pages/Intro.xaml.cs | 57 ++ ModAssistant/Pages/Invalid.xaml | 127 +++ ModAssistant/Pages/Invalid.xaml.cs | 36 + ModAssistant/Pages/Loading.xaml | 74 ++ ModAssistant/Pages/Loading.xaml.cs | 38 + ModAssistant/Pages/Mods.xaml | 137 +++ ModAssistant/Pages/Mods.xaml.cs | 777 ++++++++++++++++++ ModAssistant/Pages/Options.xaml | 364 ++++++++ ModAssistant/Pages/Options.xaml.cs | 361 ++++++++ ModAssistant/Properties/AssemblyInfo.cs | 55 ++ ModAssistant/Properties/Resources.Designer.cs | 71 ++ ModAssistant/Properties/Resources.resx | 117 +++ ModAssistant/Properties/Settings.Designer.cs | 194 +++++ ModAssistant/Properties/Settings.settings | 48 ++ ModAssistant/Resources/Icons.xaml | 46 ++ ModAssistant/Resources/icon.ico | Bin 0 -> 124093 bytes ModAssistant/Styles/Button.xaml | 90 ++ ModAssistant/Styles/CheckBox.xaml | 75 ++ ModAssistant/Styles/ComboBox.xaml | 103 +++ ModAssistant/Styles/ComboBoxItem.xaml | 81 ++ ModAssistant/Styles/GridViewColumnHeader.xaml | 51 ++ ModAssistant/Styles/Label.xaml | 15 + ModAssistant/Styles/ListView.xaml | 7 + ModAssistant/Styles/ListViewItem.xaml | 42 + ModAssistant/Styles/Menu.xaml | 8 + ModAssistant/Styles/MenuItem.xaml | 66 ++ ModAssistant/Styles/RepeatButton.xaml | 58 ++ ModAssistant/Styles/ScrollBar.xaml | 215 +++++ ModAssistant/Styles/TextBlock.xaml | 7 + ModAssistant/Styles/Thumb.xaml | 26 + ModAssistant/Styles/ToggleButton.xaml | 80 ++ ModAssistant/Themes/BSMG.xaml | 103 +++ ModAssistant/Themes/BSMG/Sidebar.png | Bin 0 -> 6156 bytes ModAssistant/Themes/Dark.xaml | 97 +++ ModAssistant/Themes/Default Scrollbar.xaml | 16 + ModAssistant/Themes/Light Pink.xaml | 85 ++ ModAssistant/Themes/Light.xaml | 96 +++ ModAssistant/Themes/Ugly Kulu-Ya-Ku.xaml | 117 +++ ModAssistant/bin/Debug/ModAssistant.exe | Bin 0 -> 681472 bytes .../bin/Debug/ModAssistant.exe.config | 66 ++ ModAssistant/bin/Debug/ModAssistant.pdb | Bin 0 -> 333312 bytes ModAssistant/bin/Release/ModAssistant.exe | Bin 0 -> 615936 bytes .../bin/Release/ModAssistant.exe.config | 66 ++ ModAssistant/bin/Release/ModAssistant.pdb | Bin 0 -> 310784 bytes ModAssistant/obj/Debug/App.baml | Bin 0 -> 1743 bytes ModAssistant/obj/Debug/App.g.cs | 89 ++ ModAssistant/obj/Debug/App.g.i.cs | 89 ++ .../Debug/GeneratedInternalTypeHelper.g.cs | 62 ++ .../Debug/GeneratedInternalTypeHelper.g.i.cs | 62 ++ ModAssistant/obj/Debug/Localisation/de.baml | Bin 0 -> 17305 bytes .../obj/Debug/Localisation/en-DEBUG.baml | Bin 0 -> 14068 bytes ModAssistant/obj/Debug/Localisation/en.baml | Bin 0 -> 16353 bytes ModAssistant/obj/Debug/Localisation/fr.baml | Bin 0 -> 17896 bytes ModAssistant/obj/Debug/Localisation/it.baml | Bin 0 -> 17610 bytes ModAssistant/obj/Debug/Localisation/ko.baml | Bin 0 -> 18328 bytes ModAssistant/obj/Debug/Localisation/nl.baml | Bin 0 -> 17102 bytes ModAssistant/obj/Debug/Localisation/ru.baml | Bin 0 -> 21426 bytes ModAssistant/obj/Debug/Localisation/zh.baml | Bin 0 -> 15986 bytes ModAssistant/obj/Debug/MainWindow.baml | Bin 0 -> 5991 bytes ModAssistant/obj/Debug/MainWindow.g.cs | 306 +++++++ ModAssistant/obj/Debug/MainWindow.g.i.cs | 306 +++++++ ...odAssistant.Properties.Resources.resources | Bin 0 -> 180 bytes ...odAssistant.csproj.CoreCompileInputs.cache | 1 + .../ModAssistant.csproj.FileListAbsolute.txt | 61 ++ ...ModAssistant.csproj.GenerateResource.cache | Bin 0 -> 954 bytes ...ModAssistant.csprojAssemblyReference.cache | Bin 0 -> 424 bytes ModAssistant/obj/Debug/ModAssistant.exe | Bin 0 -> 681472 bytes .../obj/Debug/ModAssistant.g.resources | Bin 0 -> 390939 bytes ModAssistant/obj/Debug/ModAssistant.pdb | Bin 0 -> 333312 bytes .../Debug/ModAssistant_MarkupCompile.cache | 20 + .../obj/Debug/ModAssistant_MarkupCompile.lref | 20 + ModAssistant/obj/Debug/OneClickStatus.baml | Bin 0 -> 3501 bytes ModAssistant/obj/Debug/OneClickStatus.g.cs | 90 ++ ModAssistant/obj/Debug/OneClickStatus.g.i.cs | 90 ++ ModAssistant/obj/Debug/Pages/About.baml | Bin 0 -> 7555 bytes ModAssistant/obj/Debug/Pages/About.g.cs | 211 +++++ ModAssistant/obj/Debug/Pages/About.g.i.cs | 211 +++++ ModAssistant/obj/Debug/Pages/Intro.baml | Bin 0 -> 3628 bytes ModAssistant/obj/Debug/Pages/Intro.g.cs | 113 +++ ModAssistant/obj/Debug/Pages/Intro.g.i.cs | 113 +++ ModAssistant/obj/Debug/Pages/Invalid.baml | Bin 0 -> 3381 bytes ModAssistant/obj/Debug/Pages/Invalid.g.cs | 98 +++ ModAssistant/obj/Debug/Pages/Invalid.g.i.cs | 98 +++ ModAssistant/obj/Debug/Pages/Loading.baml | Bin 0 -> 2904 bytes ModAssistant/obj/Debug/Pages/Loading.g.cs | 76 ++ ModAssistant/obj/Debug/Pages/Loading.g.i.cs | 76 ++ ModAssistant/obj/Debug/Pages/Mods.baml | Bin 0 -> 3971 bytes ModAssistant/obj/Debug/Pages/Mods.g.cs | 232 ++++++ ModAssistant/obj/Debug/Pages/Mods.g.i.cs | 232 ++++++ ModAssistant/obj/Debug/Pages/Options.baml | Bin 0 -> 8621 bytes ModAssistant/obj/Debug/Pages/Options.g.cs | 410 +++++++++ ModAssistant/obj/Debug/Pages/Options.g.i.cs | 410 +++++++++ ModAssistant/obj/Debug/Resources/Icons.baml | Bin 0 -> 7504 bytes ModAssistant/obj/Debug/Styles/Button.baml | Bin 0 -> 2884 bytes ModAssistant/obj/Debug/Styles/CheckBox.baml | Bin 0 -> 3047 bytes ModAssistant/obj/Debug/Styles/ComboBox.baml | Bin 0 -> 3855 bytes .../obj/Debug/Styles/ComboBoxItem.baml | Bin 0 -> 3113 bytes .../Debug/Styles/GridViewColumnHeader.baml | Bin 0 -> 1987 bytes ModAssistant/obj/Debug/Styles/Label.baml | Bin 0 -> 1262 bytes ModAssistant/obj/Debug/Styles/ListView.baml | Bin 0 -> 743 bytes .../obj/Debug/Styles/ListViewItem.baml | Bin 0 -> 1496 bytes ModAssistant/obj/Debug/Styles/Menu.baml | Bin 0 -> 906 bytes ModAssistant/obj/Debug/Styles/MenuItem.baml | Bin 0 -> 4076 bytes .../obj/Debug/Styles/RepeatButton.baml | Bin 0 -> 2259 bytes ModAssistant/obj/Debug/Styles/ScrollBar.baml | Bin 0 -> 7383 bytes ModAssistant/obj/Debug/Styles/TextBlock.baml | Bin 0 -> 713 bytes ModAssistant/obj/Debug/Styles/Thumb.baml | Bin 0 -> 1437 bytes .../obj/Debug/Styles/ToggleButton.baml | Bin 0 -> 2946 bytes ModAssistant/obj/Debug/Themes/BSMG.baml | Bin 0 -> 5161 bytes ModAssistant/obj/Debug/Themes/Dark.baml | Bin 0 -> 5030 bytes .../obj/Debug/Themes/Default Scrollbar.baml | Bin 0 -> 1414 bytes ModAssistant/obj/Debug/Themes/Light Pink.baml | Bin 0 -> 4732 bytes ModAssistant/obj/Debug/Themes/Light.baml | Bin 0 -> 5051 bytes ModAssistant/obj/Release/App.baml | Bin 0 -> 1391 bytes ModAssistant/obj/Release/App.g.cs | 89 ++ ModAssistant/obj/Release/App.g.i.cs | 89 ++ .../Release/GeneratedInternalTypeHelper.g.cs | 62 ++ .../GeneratedInternalTypeHelper.g.i.cs | 62 ++ ModAssistant/obj/Release/Localisation/de.baml | Bin 0 -> 14508 bytes .../obj/Release/Localisation/en-DEBUG.baml | Bin 0 -> 11775 bytes ModAssistant/obj/Release/Localisation/en.baml | Bin 0 -> 13556 bytes ModAssistant/obj/Release/Localisation/fr.baml | Bin 0 -> 14985 bytes ModAssistant/obj/Release/Localisation/it.baml | Bin 0 -> 14813 bytes ModAssistant/obj/Release/Localisation/ko.baml | Bin 0 -> 15531 bytes ModAssistant/obj/Release/Localisation/nl.baml | Bin 0 -> 14313 bytes ModAssistant/obj/Release/Localisation/ru.baml | Bin 0 -> 18629 bytes ModAssistant/obj/Release/Localisation/zh.baml | Bin 0 -> 13264 bytes ModAssistant/obj/Release/MainWindow.baml | Bin 0 -> 3906 bytes ModAssistant/obj/Release/MainWindow.g.cs | 306 +++++++ ModAssistant/obj/Release/MainWindow.g.i.cs | 306 +++++++ ...odAssistant.Properties.Resources.resources | Bin 0 -> 180 bytes ...odAssistant.csproj.CoreCompileInputs.cache | 1 + .../ModAssistant.csproj.FileListAbsolute.txt | 61 ++ ...ModAssistant.csproj.GenerateResource.cache | Bin 0 -> 954 bytes ...ModAssistant.csprojAssemblyReference.cache | Bin 0 -> 424 bytes ModAssistant/obj/Release/ModAssistant.exe | Bin 0 -> 615936 bytes .../obj/Release/ModAssistant.g.resources | Bin 0 -> 338863 bytes ModAssistant/obj/Release/ModAssistant.pdb | Bin 0 -> 310784 bytes .../Release/ModAssistant_MarkupCompile.cache | 20 + .../Release/ModAssistant_MarkupCompile.lref | 20 + ModAssistant/obj/Release/OneClickStatus.baml | Bin 0 -> 2623 bytes ModAssistant/obj/Release/OneClickStatus.g.cs | 90 ++ .../obj/Release/OneClickStatus.g.i.cs | 90 ++ ModAssistant/obj/Release/Pages/About.baml | Bin 0 -> 4834 bytes ModAssistant/obj/Release/Pages/About.g.cs | 211 +++++ ModAssistant/obj/Release/Pages/About.g.i.cs | 211 +++++ ModAssistant/obj/Release/Pages/Intro.baml | Bin 0 -> 2389 bytes ModAssistant/obj/Release/Pages/Intro.g.cs | 113 +++ ModAssistant/obj/Release/Pages/Intro.g.i.cs | 113 +++ ModAssistant/obj/Release/Pages/Invalid.baml | Bin 0 -> 2269 bytes ModAssistant/obj/Release/Pages/Invalid.g.cs | 98 +++ ModAssistant/obj/Release/Pages/Invalid.g.i.cs | 98 +++ ModAssistant/obj/Release/Pages/Loading.baml | Bin 0 -> 2297 bytes ModAssistant/obj/Release/Pages/Loading.g.cs | 76 ++ ModAssistant/obj/Release/Pages/Loading.g.i.cs | 76 ++ ModAssistant/obj/Release/Pages/Mods.baml | Bin 0 -> 2776 bytes ModAssistant/obj/Release/Pages/Mods.g.cs | 232 ++++++ ModAssistant/obj/Release/Pages/Mods.g.i.cs | 232 ++++++ ModAssistant/obj/Release/Pages/Options.baml | Bin 0 -> 5368 bytes ModAssistant/obj/Release/Pages/Options.g.cs | 410 +++++++++ ModAssistant/obj/Release/Pages/Options.g.i.cs | 410 +++++++++ ModAssistant/obj/Release/Resources/Icons.baml | Bin 0 -> 6998 bytes ModAssistant/obj/Release/Styles/Button.baml | Bin 0 -> 2067 bytes ModAssistant/obj/Release/Styles/CheckBox.baml | Bin 0 -> 2275 bytes ModAssistant/obj/Release/Styles/ComboBox.baml | Bin 0 -> 2873 bytes .../obj/Release/Styles/ComboBoxItem.baml | Bin 0 -> 2260 bytes .../Release/Styles/GridViewColumnHeader.baml | Bin 0 -> 1504 bytes ModAssistant/obj/Release/Styles/Label.baml | Bin 0 -> 1062 bytes ModAssistant/obj/Release/Styles/ListView.baml | Bin 0 -> 660 bytes .../obj/Release/Styles/ListViewItem.baml | Bin 0 -> 1124 bytes ModAssistant/obj/Release/Styles/Menu.baml | Bin 0 -> 804 bytes ModAssistant/obj/Release/Styles/MenuItem.baml | Bin 0 -> 3148 bytes .../obj/Release/Styles/RepeatButton.baml | Bin 0 -> 1676 bytes .../obj/Release/Styles/ScrollBar.baml | Bin 0 -> 5252 bytes .../obj/Release/Styles/TextBlock.baml | Bin 0 -> 630 bytes ModAssistant/obj/Release/Styles/Thumb.baml | Bin 0 -> 1176 bytes .../obj/Release/Styles/ToggleButton.baml | Bin 0 -> 2133 bytes .../Properties.Resources.Designer.cs.dll | Bin 0 -> 6144 bytes ModAssistant/obj/Release/Themes/BSMG.baml | Bin 0 -> 4144 bytes ModAssistant/obj/Release/Themes/Dark.baml | Bin 0 -> 4055 bytes .../obj/Release/Themes/Default Scrollbar.baml | Bin 0 -> 1237 bytes .../obj/Release/Themes/Light Pink.baml | Bin 0 -> 3897 bytes ModAssistant/obj/Release/Themes/Light.baml | Bin 0 -> 4090 bytes README.md | 141 ++++ 225 files changed, 17981 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/dotnet.yml create mode 100644 .vs/ModAssistant/v16/.suo create mode 100644 LICENSE create mode 100644 ModAssistant.sln create mode 100644 ModAssistant/App.config create mode 100644 ModAssistant/App.xaml create mode 100644 ModAssistant/App.xaml.cs create mode 100644 ModAssistant/Classes/Diagnostics.cs create mode 100644 ModAssistant/Classes/External Interfaces/BeatSaver.cs create mode 100644 ModAssistant/Classes/External Interfaces/ModelSaber.cs create mode 100644 ModAssistant/Classes/External Interfaces/Playlists.cs create mode 100644 ModAssistant/Classes/External Interfaces/Utils.cs create mode 100644 ModAssistant/Classes/Http.cs create mode 100644 ModAssistant/Classes/HyperlinkExtensions.cs create mode 100644 ModAssistant/Classes/Mod.cs create mode 100644 ModAssistant/Classes/OneClickInstaller.cs create mode 100644 ModAssistant/Classes/Promotions.cs create mode 100644 ModAssistant/Classes/Themes.cs create mode 100644 ModAssistant/Classes/Updater.cs create mode 100644 ModAssistant/Classes/Utils.cs create mode 100644 ModAssistant/Libs/semver/IntExtensions.cs create mode 100644 ModAssistant/Libs/semver/SemVersion.cs create mode 100644 ModAssistant/Localisation/de.xaml create mode 100644 ModAssistant/Localisation/en-DEBUG.xaml create mode 100644 ModAssistant/Localisation/en.xaml create mode 100644 ModAssistant/Localisation/fr.xaml create mode 100644 ModAssistant/Localisation/it.xaml create mode 100644 ModAssistant/Localisation/ko.xaml create mode 100644 ModAssistant/Localisation/nl.xaml create mode 100644 ModAssistant/Localisation/ru.xaml create mode 100644 ModAssistant/Localisation/zh.xaml create mode 100644 ModAssistant/MainWindow.xaml create mode 100644 ModAssistant/MainWindow.xaml.cs create mode 100644 ModAssistant/ModAssistant.csproj create mode 100644 ModAssistant/OneClickStatus.xaml create mode 100644 ModAssistant/OneClickStatus.xaml.cs create mode 100644 ModAssistant/Pages/About.xaml create mode 100644 ModAssistant/Pages/About.xaml.cs create mode 100644 ModAssistant/Pages/Intro.xaml create mode 100644 ModAssistant/Pages/Intro.xaml.cs create mode 100644 ModAssistant/Pages/Invalid.xaml create mode 100644 ModAssistant/Pages/Invalid.xaml.cs create mode 100644 ModAssistant/Pages/Loading.xaml create mode 100644 ModAssistant/Pages/Loading.xaml.cs create mode 100644 ModAssistant/Pages/Mods.xaml create mode 100644 ModAssistant/Pages/Mods.xaml.cs create mode 100644 ModAssistant/Pages/Options.xaml create mode 100644 ModAssistant/Pages/Options.xaml.cs create mode 100644 ModAssistant/Properties/AssemblyInfo.cs create mode 100644 ModAssistant/Properties/Resources.Designer.cs create mode 100644 ModAssistant/Properties/Resources.resx create mode 100644 ModAssistant/Properties/Settings.Designer.cs create mode 100644 ModAssistant/Properties/Settings.settings create mode 100644 ModAssistant/Resources/Icons.xaml create mode 100644 ModAssistant/Resources/icon.ico create mode 100644 ModAssistant/Styles/Button.xaml create mode 100644 ModAssistant/Styles/CheckBox.xaml create mode 100644 ModAssistant/Styles/ComboBox.xaml create mode 100644 ModAssistant/Styles/ComboBoxItem.xaml create mode 100644 ModAssistant/Styles/GridViewColumnHeader.xaml create mode 100644 ModAssistant/Styles/Label.xaml create mode 100644 ModAssistant/Styles/ListView.xaml create mode 100644 ModAssistant/Styles/ListViewItem.xaml create mode 100644 ModAssistant/Styles/Menu.xaml create mode 100644 ModAssistant/Styles/MenuItem.xaml create mode 100644 ModAssistant/Styles/RepeatButton.xaml create mode 100644 ModAssistant/Styles/ScrollBar.xaml create mode 100644 ModAssistant/Styles/TextBlock.xaml create mode 100644 ModAssistant/Styles/Thumb.xaml create mode 100644 ModAssistant/Styles/ToggleButton.xaml create mode 100644 ModAssistant/Themes/BSMG.xaml create mode 100644 ModAssistant/Themes/BSMG/Sidebar.png create mode 100644 ModAssistant/Themes/Dark.xaml create mode 100644 ModAssistant/Themes/Default Scrollbar.xaml create mode 100644 ModAssistant/Themes/Light Pink.xaml create mode 100644 ModAssistant/Themes/Light.xaml create mode 100644 ModAssistant/Themes/Ugly Kulu-Ya-Ku.xaml create mode 100644 ModAssistant/bin/Debug/ModAssistant.exe create mode 100644 ModAssistant/bin/Debug/ModAssistant.exe.config create mode 100644 ModAssistant/bin/Debug/ModAssistant.pdb create mode 100644 ModAssistant/bin/Release/ModAssistant.exe create mode 100644 ModAssistant/bin/Release/ModAssistant.exe.config create mode 100644 ModAssistant/bin/Release/ModAssistant.pdb create mode 100644 ModAssistant/obj/Debug/App.baml create mode 100644 ModAssistant/obj/Debug/App.g.cs create mode 100644 ModAssistant/obj/Debug/App.g.i.cs create mode 100644 ModAssistant/obj/Debug/GeneratedInternalTypeHelper.g.cs create mode 100644 ModAssistant/obj/Debug/GeneratedInternalTypeHelper.g.i.cs create mode 100644 ModAssistant/obj/Debug/Localisation/de.baml create mode 100644 ModAssistant/obj/Debug/Localisation/en-DEBUG.baml create mode 100644 ModAssistant/obj/Debug/Localisation/en.baml create mode 100644 ModAssistant/obj/Debug/Localisation/fr.baml create mode 100644 ModAssistant/obj/Debug/Localisation/it.baml create mode 100644 ModAssistant/obj/Debug/Localisation/ko.baml create mode 100644 ModAssistant/obj/Debug/Localisation/nl.baml create mode 100644 ModAssistant/obj/Debug/Localisation/ru.baml create mode 100644 ModAssistant/obj/Debug/Localisation/zh.baml create mode 100644 ModAssistant/obj/Debug/MainWindow.baml create mode 100644 ModAssistant/obj/Debug/MainWindow.g.cs create mode 100644 ModAssistant/obj/Debug/MainWindow.g.i.cs create mode 100644 ModAssistant/obj/Debug/ModAssistant.Properties.Resources.resources create mode 100644 ModAssistant/obj/Debug/ModAssistant.csproj.CoreCompileInputs.cache create mode 100644 ModAssistant/obj/Debug/ModAssistant.csproj.FileListAbsolute.txt create mode 100644 ModAssistant/obj/Debug/ModAssistant.csproj.GenerateResource.cache create mode 100644 ModAssistant/obj/Debug/ModAssistant.csprojAssemblyReference.cache create mode 100644 ModAssistant/obj/Debug/ModAssistant.exe create mode 100644 ModAssistant/obj/Debug/ModAssistant.g.resources create mode 100644 ModAssistant/obj/Debug/ModAssistant.pdb create mode 100644 ModAssistant/obj/Debug/ModAssistant_MarkupCompile.cache create mode 100644 ModAssistant/obj/Debug/ModAssistant_MarkupCompile.lref create mode 100644 ModAssistant/obj/Debug/OneClickStatus.baml create mode 100644 ModAssistant/obj/Debug/OneClickStatus.g.cs create mode 100644 ModAssistant/obj/Debug/OneClickStatus.g.i.cs create mode 100644 ModAssistant/obj/Debug/Pages/About.baml create mode 100644 ModAssistant/obj/Debug/Pages/About.g.cs create mode 100644 ModAssistant/obj/Debug/Pages/About.g.i.cs create mode 100644 ModAssistant/obj/Debug/Pages/Intro.baml create mode 100644 ModAssistant/obj/Debug/Pages/Intro.g.cs create mode 100644 ModAssistant/obj/Debug/Pages/Intro.g.i.cs create mode 100644 ModAssistant/obj/Debug/Pages/Invalid.baml create mode 100644 ModAssistant/obj/Debug/Pages/Invalid.g.cs create mode 100644 ModAssistant/obj/Debug/Pages/Invalid.g.i.cs create mode 100644 ModAssistant/obj/Debug/Pages/Loading.baml create mode 100644 ModAssistant/obj/Debug/Pages/Loading.g.cs create mode 100644 ModAssistant/obj/Debug/Pages/Loading.g.i.cs create mode 100644 ModAssistant/obj/Debug/Pages/Mods.baml create mode 100644 ModAssistant/obj/Debug/Pages/Mods.g.cs create mode 100644 ModAssistant/obj/Debug/Pages/Mods.g.i.cs create mode 100644 ModAssistant/obj/Debug/Pages/Options.baml create mode 100644 ModAssistant/obj/Debug/Pages/Options.g.cs create mode 100644 ModAssistant/obj/Debug/Pages/Options.g.i.cs create mode 100644 ModAssistant/obj/Debug/Resources/Icons.baml create mode 100644 ModAssistant/obj/Debug/Styles/Button.baml create mode 100644 ModAssistant/obj/Debug/Styles/CheckBox.baml create mode 100644 ModAssistant/obj/Debug/Styles/ComboBox.baml create mode 100644 ModAssistant/obj/Debug/Styles/ComboBoxItem.baml create mode 100644 ModAssistant/obj/Debug/Styles/GridViewColumnHeader.baml create mode 100644 ModAssistant/obj/Debug/Styles/Label.baml create mode 100644 ModAssistant/obj/Debug/Styles/ListView.baml create mode 100644 ModAssistant/obj/Debug/Styles/ListViewItem.baml create mode 100644 ModAssistant/obj/Debug/Styles/Menu.baml create mode 100644 ModAssistant/obj/Debug/Styles/MenuItem.baml create mode 100644 ModAssistant/obj/Debug/Styles/RepeatButton.baml create mode 100644 ModAssistant/obj/Debug/Styles/ScrollBar.baml create mode 100644 ModAssistant/obj/Debug/Styles/TextBlock.baml create mode 100644 ModAssistant/obj/Debug/Styles/Thumb.baml create mode 100644 ModAssistant/obj/Debug/Styles/ToggleButton.baml create mode 100644 ModAssistant/obj/Debug/Themes/BSMG.baml create mode 100644 ModAssistant/obj/Debug/Themes/Dark.baml create mode 100644 ModAssistant/obj/Debug/Themes/Default Scrollbar.baml create mode 100644 ModAssistant/obj/Debug/Themes/Light Pink.baml create mode 100644 ModAssistant/obj/Debug/Themes/Light.baml create mode 100644 ModAssistant/obj/Release/App.baml create mode 100644 ModAssistant/obj/Release/App.g.cs create mode 100644 ModAssistant/obj/Release/App.g.i.cs create mode 100644 ModAssistant/obj/Release/GeneratedInternalTypeHelper.g.cs create mode 100644 ModAssistant/obj/Release/GeneratedInternalTypeHelper.g.i.cs create mode 100644 ModAssistant/obj/Release/Localisation/de.baml create mode 100644 ModAssistant/obj/Release/Localisation/en-DEBUG.baml create mode 100644 ModAssistant/obj/Release/Localisation/en.baml create mode 100644 ModAssistant/obj/Release/Localisation/fr.baml create mode 100644 ModAssistant/obj/Release/Localisation/it.baml create mode 100644 ModAssistant/obj/Release/Localisation/ko.baml create mode 100644 ModAssistant/obj/Release/Localisation/nl.baml create mode 100644 ModAssistant/obj/Release/Localisation/ru.baml create mode 100644 ModAssistant/obj/Release/Localisation/zh.baml create mode 100644 ModAssistant/obj/Release/MainWindow.baml create mode 100644 ModAssistant/obj/Release/MainWindow.g.cs create mode 100644 ModAssistant/obj/Release/MainWindow.g.i.cs create mode 100644 ModAssistant/obj/Release/ModAssistant.Properties.Resources.resources create mode 100644 ModAssistant/obj/Release/ModAssistant.csproj.CoreCompileInputs.cache create mode 100644 ModAssistant/obj/Release/ModAssistant.csproj.FileListAbsolute.txt create mode 100644 ModAssistant/obj/Release/ModAssistant.csproj.GenerateResource.cache create mode 100644 ModAssistant/obj/Release/ModAssistant.csprojAssemblyReference.cache create mode 100644 ModAssistant/obj/Release/ModAssistant.exe create mode 100644 ModAssistant/obj/Release/ModAssistant.g.resources create mode 100644 ModAssistant/obj/Release/ModAssistant.pdb create mode 100644 ModAssistant/obj/Release/ModAssistant_MarkupCompile.cache create mode 100644 ModAssistant/obj/Release/ModAssistant_MarkupCompile.lref create mode 100644 ModAssistant/obj/Release/OneClickStatus.baml create mode 100644 ModAssistant/obj/Release/OneClickStatus.g.cs create mode 100644 ModAssistant/obj/Release/OneClickStatus.g.i.cs create mode 100644 ModAssistant/obj/Release/Pages/About.baml create mode 100644 ModAssistant/obj/Release/Pages/About.g.cs create mode 100644 ModAssistant/obj/Release/Pages/About.g.i.cs create mode 100644 ModAssistant/obj/Release/Pages/Intro.baml create mode 100644 ModAssistant/obj/Release/Pages/Intro.g.cs create mode 100644 ModAssistant/obj/Release/Pages/Intro.g.i.cs create mode 100644 ModAssistant/obj/Release/Pages/Invalid.baml create mode 100644 ModAssistant/obj/Release/Pages/Invalid.g.cs create mode 100644 ModAssistant/obj/Release/Pages/Invalid.g.i.cs create mode 100644 ModAssistant/obj/Release/Pages/Loading.baml create mode 100644 ModAssistant/obj/Release/Pages/Loading.g.cs create mode 100644 ModAssistant/obj/Release/Pages/Loading.g.i.cs create mode 100644 ModAssistant/obj/Release/Pages/Mods.baml create mode 100644 ModAssistant/obj/Release/Pages/Mods.g.cs create mode 100644 ModAssistant/obj/Release/Pages/Mods.g.i.cs create mode 100644 ModAssistant/obj/Release/Pages/Options.baml create mode 100644 ModAssistant/obj/Release/Pages/Options.g.cs create mode 100644 ModAssistant/obj/Release/Pages/Options.g.i.cs create mode 100644 ModAssistant/obj/Release/Resources/Icons.baml create mode 100644 ModAssistant/obj/Release/Styles/Button.baml create mode 100644 ModAssistant/obj/Release/Styles/CheckBox.baml create mode 100644 ModAssistant/obj/Release/Styles/ComboBox.baml create mode 100644 ModAssistant/obj/Release/Styles/ComboBoxItem.baml create mode 100644 ModAssistant/obj/Release/Styles/GridViewColumnHeader.baml create mode 100644 ModAssistant/obj/Release/Styles/Label.baml create mode 100644 ModAssistant/obj/Release/Styles/ListView.baml create mode 100644 ModAssistant/obj/Release/Styles/ListViewItem.baml create mode 100644 ModAssistant/obj/Release/Styles/Menu.baml create mode 100644 ModAssistant/obj/Release/Styles/MenuItem.baml create mode 100644 ModAssistant/obj/Release/Styles/RepeatButton.baml create mode 100644 ModAssistant/obj/Release/Styles/ScrollBar.baml create mode 100644 ModAssistant/obj/Release/Styles/TextBlock.baml create mode 100644 ModAssistant/obj/Release/Styles/Thumb.baml create mode 100644 ModAssistant/obj/Release/Styles/ToggleButton.baml create mode 100644 ModAssistant/obj/Release/TempPE/Properties.Resources.Designer.cs.dll create mode 100644 ModAssistant/obj/Release/Themes/BSMG.baml create mode 100644 ModAssistant/obj/Release/Themes/Dark.baml create mode 100644 ModAssistant/obj/Release/Themes/Default Scrollbar.baml create mode 100644 ModAssistant/obj/Release/Themes/Light Pink.baml create mode 100644 ModAssistant/obj/Release/Themes/Light.baml create mode 100644 README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..191a51d8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.cs] +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..121233f1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +patreon: BeatSaberMods +ko_fi: N4N8JX7B +liberapay: Assistant +custom: ['https://paypal.me/AssistantMoe', 'https://bs.assistant.moe/Donate/'] diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 00000000..c5c5328d --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,52 @@ +name: .NET Build +on: [push] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v1 + # Set Path workaround for https://github.com/actions/virtual-environments/issues/263 + - name: "Temp step to Set Path for Windows" + run: | + echo "::add-path::C:\Program Files\Git\mingw64\bin" + echo "::add-path::C:\Program Files\Git\usr\bin" + echo "::add-path::C:\Program Files\Git\bin" + - name: Setup MSBuild + uses: warrenbuckley/Setup-MSBuild@v1 + - name: Install dependencies + run: msbuild -t:restore + - name: Build project + run: msbuild ModAssistant/ModAssistant.csproj /t:Build /p:Configuration=Release + - name: Cleanup release + shell: bash + run: | + find "ModAssistant/bin/Release" -type f ! -name "ModAssistant.exe" -delete + cp "LICENSE" "ModAssistant/bin/Release/LICENSE.ModAssistant.txt" + - name: Upload Build + if: contains(github.ref, 'refs/tags/') == false + uses: actions/upload-artifact@v1 + with: + name: ModAssistant-${{ github.sha }} + path: ./ModAssistant/bin/Release + - name: Create Release + if: contains(github.ref, 'refs/tags/') == true + id: create_release + uses: actions/create-release@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Mod Assistant ${{ github.ref }} + draft: false + prerelease: false + - name: Upload Release Asset + if: contains(github.ref, 'refs/tags/') == true + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./ModAssistant/bin/Release/ModAssistant.exe + asset_name: ModAssistant.exe + asset_content_type: application/vnd.microsoft.portable-executable diff --git a/.vs/ModAssistant/v16/.suo b/.vs/ModAssistant/v16/.suo new file mode 100644 index 0000000000000000000000000000000000000000..278fdb62bcf171188bfff605ce2b0504c7b63bcd GIT binary patch literal 120832 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 literal 0 HcmV?d00001 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..9d3b4edf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2020 Assistant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ModAssistant.sln b/ModAssistant.sln new file mode 100644 index 00000000..5643d233 --- /dev/null +++ b/ModAssistant.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2027 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModAssistant", "ModAssistant\ModAssistant.csproj", "{6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ACC0D20B-78C5-40AC-BF48-553C41D9987B} + EndGlobalSection +EndGlobal diff --git a/ModAssistant/App.config b/ModAssistant/App.config new file mode 100644 index 00000000..1eb1102b --- /dev/null +++ b/ModAssistant/App.config @@ -0,0 +1,66 @@ + + + + +
+
+ + + + + + + + + + + + + + + False + + + True + + + False + + + False + + + False + + + + + + + + + True + + + + + + + + + True + + + False + + + + + + + + + + + + \ No newline at end of file diff --git a/ModAssistant/App.xaml b/ModAssistant/App.xaml new file mode 100644 index 00000000..2b4ab8a6 --- /dev/null +++ b/ModAssistant/App.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ModAssistant/App.xaml.cs b/ModAssistant/App.xaml.cs new file mode 100644 index 00000000..4b9d3510 --- /dev/null +++ b/ModAssistant/App.xaml.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows; + +namespace ModAssistant +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + public static string BeatSaberInstallDirectory; + public static string BeatSaberInstallType; + public static bool SaveModSelection; + public static bool CheckInstalledMods; + public static bool SelectInstalledMods; + public static bool ReinstallInstalledMods; + public static bool CloseWindowOnFinish; + public static string Version = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + public static List SavedMods = ModAssistant.Properties.Settings.Default.SavedMods.Split(',').ToList(); + public static MainWindow window; + public static string Arguments; + public static bool Update = true; + public static bool GUI = true; + + + private async void Application_Startup(object sender, StartupEventArgs e) + { + // Set SecurityProtocol to prevent crash with TLS + System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + + // Load localisation languages + LoadLanguage(CultureInfo.CurrentCulture.Name); + + // Uncomment the next line to debug localisation + // LoadLanguage("en-DEBUG"); + + if (ModAssistant.Properties.Settings.Default.UpgradeRequired) + { + ModAssistant.Properties.Settings.Default.Upgrade(); + ModAssistant.Properties.Settings.Default.UpgradeRequired = false; + ModAssistant.Properties.Settings.Default.Save(); + } + + Version = Version.Substring(0, Version.Length - 2); + BeatSaberInstallDirectory = Utils.GetInstallDir(); + + while (string.IsNullOrEmpty(App.BeatSaberInstallDirectory)) + { + string title = (string)Current.FindResource("App:InstallDirDialog:Title"); + string body = (string)Current.FindResource("App:InstallDirDialog:OkCancel"); + + if (System.Windows.Forms.MessageBox.Show(body, title, System.Windows.Forms.MessageBoxButtons.OKCancel) == System.Windows.Forms.DialogResult.OK) + { + App.BeatSaberInstallDirectory = Utils.GetManualDir(); + } + else + { + Environment.Exit(0); + } + } + + BeatSaberInstallType = ModAssistant.Properties.Settings.Default.StoreType; + SaveModSelection = ModAssistant.Properties.Settings.Default.SaveSelected; + CheckInstalledMods = ModAssistant.Properties.Settings.Default.CheckInstalled; + SelectInstalledMods = ModAssistant.Properties.Settings.Default.SelectInstalled; + ReinstallInstalledMods = ModAssistant.Properties.Settings.Default.ReinstallInstalled; + CloseWindowOnFinish = ModAssistant.Properties.Settings.Default.CloseWindowOnFinish; + + await ArgumentHandler(e.Args); + await Init(); + } + + private async Task Init() + { + if (Update) + { + try + { + await Task.Run(async () => await Updater.Run()); + } + catch (UnauthorizedAccessException e) + { + Utils.StartAsAdmin(Arguments, true); + } + } + + if (GUI) + { + window = new MainWindow(); + window.Show(); + } + else + { + //Application.Current.Shutdown(); + } + } + + private async Task ArgumentHandler(string[] args) + { + Arguments = string.Join(" ", args); + while (args.Length > 0) + { + switch (args[0]) + { + case "--install": + if (args.Length < 2 || string.IsNullOrEmpty(args[1])) + { + Utils.SendNotify(string.Format((string)Current.FindResource("App:InvalidArgument"), "--install")); + } + else + { + await OneClickInstaller.InstallAsset(args[1]); + } + + if (CloseWindowOnFinish) + { + await Task.Delay(5 * 1000); + Current.Shutdown(); + } + + Update = false; + GUI = false; + args = Shift(args, 2); + break; + + case "--no-update": + Update = false; + args = Shift(args); + break; + + case "--language": + if (args.Length < 2 || string.IsNullOrEmpty(args[1])) + { + Utils.SendNotify(string.Format((string)Current.FindResource("App:InvalidArgument"), "--language")); + } + else + { + LoadLanguage(args[1]); + } + + args = Shift(args, 2); + break; + + case "--register": + if (args.Length < 2 || string.IsNullOrEmpty(args[1])) + { + Utils.SendNotify(string.Format((string)Current.FindResource("App:InvalidArgument"), "--register")); + } + else + { + OneClickInstaller.Register(args[1], true); + } + + Update = false; + GUI = false; + args = Shift(args, 2); + break; + + case "--unregister": + if (args.Length < 2 || string.IsNullOrEmpty(args[1])) + { + Utils.SendNotify(string.Format((string)Current.FindResource("App:InvalidArgument"), "--unregister")); + } + else + { + OneClickInstaller.Unregister(args[1], true); + } + + Update = false; + GUI = false; + args = Shift(args, 2); + break; + + case "--runforever": + while (true) + { + + } + + default: + Utils.SendNotify((string)Current.FindResource("App:UnrecognizedArgument")); + args = Shift(args); + break; + } + } + } + + private static string[] Shift(string[] array, int places = 1) + { + if (places >= array.Length) return Array.Empty(); + string[] newArray = new string[array.Length - places]; + for(int i = places; i < array.Length; i++) + { + newArray[i - places] = array[i]; + } + + return newArray; + } + + private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) + { + string title = (string)Current.FindResource("App:Exception"); + string body = (string)Current.FindResource("App:UnhandledException"); + MessageBox.Show($"{body}: {e.Exception}", "Exception", MessageBoxButton.OK, MessageBoxImage.Warning); + + e.Handled = true; + Application.Current.Shutdown(); + } + + private ResourceDictionary LanguagesDict + { + get + { + return Resources.MergedDictionaries[1]; + } + } + + private void LoadLanguage(string culture) + { + try + { + LanguagesDict.Source = new Uri($"Localisation/{culture}.xaml", UriKind.Relative); + } + catch (IOException) + { + if (culture.Contains("-")) + { + LoadLanguage(culture.Split('-').First()); + } + // Can't load language file + } + } + } +} diff --git a/ModAssistant/Classes/Diagnostics.cs b/ModAssistant/Classes/Diagnostics.cs new file mode 100644 index 00000000..136d4d64 --- /dev/null +++ b/ModAssistant/Classes/Diagnostics.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Windows; + +namespace ModAssistant +{ + class Diagnostics + { + public static string[] ReadFolder(string path, int level = 0) + { + List entries = new List(); + + foreach (string file in Directory.GetFileSystemEntries(path)) + { + string line = string.Empty; + + if (File.Exists(file)) + { + line = Utils.CalculateMD5(file) + " " + LevelSeparator(level) + "├─ " + Path.GetFileName(file); + entries.Add(line); + + } + else if (Directory.Exists(file)) + { + line = Utils.Constants.MD5Spacer + LevelSeparator(level) + "├─ " + Path.GetFileName(file); + entries.Add(line); + + foreach (string entry in ReadFolder(file.Replace(@"\", @"\\"), level + 1)) + { + //MessageBox.Show(entry); + entries.Add(entry); + } + + } + else + { + MessageBox.Show("! " + file); + } + + + } + if (entries.Count > 0) + entries[entries.Count - 1] = entries[entries.Count - 1].Replace("├", "└"); + + return entries.ToArray(); + } + + private static string LevelSeparator(int level) + { + string separator = string.Empty; + for (int i = 0; i < level; i++) + { + separator += "│ "; + } + return separator; + } + } +} diff --git a/ModAssistant/Classes/External Interfaces/BeatSaver.cs b/ModAssistant/Classes/External Interfaces/BeatSaver.cs new file mode 100644 index 00000000..09cd93b3 --- /dev/null +++ b/ModAssistant/Classes/External Interfaces/BeatSaver.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using System.Web; +using System.Windows; +using static ModAssistant.Http; + +namespace ModAssistant.API +{ + public class BeatSaver + { + private const string BeatSaverURLPrefix = "https://beatsaver.com"; + private static readonly string CustomSongsFolder = Path.Combine("Beat Saber_Data", "CustomLevels"); + private const bool BypassDownloadCounter = false; + + public static async Task GetFromKey(string Key, bool showNotification = true) + { + // if (showNotification) OneClickInstaller.Status.Show(); + return await GetMap(Key, "key", showNotification); + } + + public static async Task GetFromHash(string Hash, bool showNotification = true) + { + //if (showNotification) OneClickInstaller.Status.Show(); + return await GetMap(Hash, "hash", showNotification); + } + + private static async Task GetMap(string id, string type, bool showNotification) + { + string urlSegment; + switch (type) + { + case "hash": + urlSegment = "/api/maps/by-hash/"; + break; + case "key": + urlSegment = "/api/maps/detail/"; + break; + default: + return null; + } + + BeatSaverMap map = new BeatSaverMap(); + map.Success = false; + if (showNotification) Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Installing"), id)}"); + try + { + BeatSaverApiResponse beatsaver = await GetResponse(BeatSaverURLPrefix + urlSegment + id); + if (beatsaver != null && beatsaver.map != null) + { + map.response = beatsaver; + map.Name = await InstallMap(beatsaver.map, showNotification); + map.Success = true; + } + } + catch (Exception e) + { + ModAssistant.Utils.Log($"Failed downloading BeatSaver map: {id} | Error: {e.Message}", "ERROR"); + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Failed"), (map.Name ?? id))}"); + App.CloseWindowOnFinish = false; + } + return map; + } + + private static async Task GetResponse(string url, bool showNotification = true, int retries = 3) + { + if (retries == 0) + { + ModAssistant.Utils.Log($"Max tries reached: Skipping {url}", "ERROR"); + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitSkip"), url)}"); + App.CloseWindowOnFinish = false; + throw new Exception("Max retries allowed"); + } + + BeatSaverApiResponse response = new BeatSaverApiResponse(); + try + { + var resp = await HttpClient.GetAsync(url); + response.statusCode = resp.StatusCode; + response.ratelimit = GetRatelimit(resp.Headers); + string body = await resp.Content.ReadAsStringAsync(); + + if ((int)resp.StatusCode == 429) + { + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitHit"), response.ratelimit.ResetTime)}"); + await response.ratelimit.Wait(); + return await GetResponse(url, showNotification, retries - 1); + } + + if (response.statusCode == HttpStatusCode.OK) + { + response.map = JsonSerializer.Deserialize(body); + return response; + } + else + { + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Failed"), url)}"); + App.CloseWindowOnFinish = false; + return response; + } + } + catch (Exception e) + { + if (showNotification) + { + MessageBox.Show($"{Application.Current.FindResource("OneClick:MapDownloadFailed")}\n\n" + e); + } + return null; + } + } + + public static async Task InstallMap(BeatSaverApiResponseMap Map, bool showNotification = true) + { + string zip = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, Map.hash) + ".zip"; + string mapName = string.Concat(($"{Map.key} ({Map.metadata.songName} - {Map.metadata.levelAuthorName})") + .Split(ModAssistant.Utils.Constants.IllegalCharacters)); + string directory = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, mapName); + +#pragma warning disable CS0162 // Unreachable code detected + if (BypassDownloadCounter) + { + await Utils.DownloadAsset(BeatSaverURLPrefix + Map.directDownload, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification, true); + } + else + { + await Utils.DownloadAsset(BeatSaverURLPrefix + Map.downloadURL, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification, true); + } +#pragma warning restore CS0162 // Unreachable code detected + + if (File.Exists(zip)) + { + string mimeType = MimeMapping.GetMimeMapping(zip); + + if (!mimeType.StartsWith("application/x-zip")) + { + ModAssistant.Utils.Log($"Failed extracting BeatSaver map: {zip} \n| Content: {string.Join("\n", File.ReadAllLines(zip))}", "ERROR"); + throw new Exception("File not a zip."); + } + + try + { + using (FileStream stream = new FileStream(zip, FileMode.Open)) + using (ZipArchive archive = new ZipArchive(stream)) + { + foreach (ZipArchiveEntry file in archive.Entries) + { + string fileDirectory = Path.GetDirectoryName(Path.Combine(directory, file.FullName)); + if (!Directory.Exists(fileDirectory)) + { + Directory.CreateDirectory(fileDirectory); + } + + if (!string.IsNullOrEmpty(file.Name)) + { + file.ExtractToFile(Path.Combine(directory, file.FullName), true); + } + } + } + } + catch (Exception e) + { + File.Delete(zip); + ModAssistant.Utils.Log($"Failed extracting BeatSaver map: {zip} | Error: {e} \n| Content: {string.Join("\n", File.ReadAllLines(zip))}", "ERROR"); + throw new Exception("File extraction failed."); + } + File.Delete(zip); + } + else + { + if (showNotification) + { + string line1 = (string)Application.Current.FindResource("OneClick:SongDownload:Failed"); + string line2 = (string)Application.Current.FindResource("OneClick:SongDownload:NetworkIssues"); + string title = (string)Application.Current.FindResource("OneClick:SongDownload:FailedTitle"); + MessageBox.Show($"{line1}\n{line2}", title); + } + throw new Exception("Zip file not found."); + } + return mapName; + } + + public static BeatSaverRatelimit GetRatelimit(HttpResponseHeaders headers) + { + BeatSaverRatelimit ratelimit = new BeatSaverRatelimit(); + + + if (headers.TryGetValues("Rate-Limit-Remaining", out IEnumerable _remaining)) + { + var Remaining = _remaining.GetEnumerator(); + Remaining.MoveNext(); + ratelimit.Remaining = Int32.Parse(Remaining.Current); + Remaining.Dispose(); + } + + if (headers.TryGetValues("Rate-Limit-Reset", out IEnumerable _reset)) + { + var Reset = _reset.GetEnumerator(); + Reset.MoveNext(); + ratelimit.Reset = Int32.Parse(Reset.Current); + ratelimit.ResetTime = UnixTimestampToDateTime((long)ratelimit.Reset); + Reset.Dispose(); + } + + if (headers.TryGetValues("Rate-Limit-Total", out IEnumerable _total)) + { + var Total = _total.GetEnumerator(); + Total.MoveNext(); + ratelimit.Total = Int32.Parse(Total.Current); + Total.Dispose(); + } + + return ratelimit; + } + + public static DateTime UnixTimestampToDateTime(double unixTime) + { + DateTime unixStart = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc); + long unixTimeStampInTicks = (long)(unixTime * TimeSpan.TicksPerSecond); + return new DateTime(unixStart.Ticks + unixTimeStampInTicks, System.DateTimeKind.Utc); + } + + public static async Task Download(string url, string output, int retries = 3) + { + if (retries == 0) + { + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitSkip"), url)}"); + App.CloseWindowOnFinish = false; + ModAssistant.Utils.Log($"Max tries reached: Couldn't download {url}", "ERROR"); + throw new Exception("Max retries allowed"); + } + + var resp = await HttpClient.GetAsync(url); + + if ((int)resp.StatusCode == 429) + { + var ratelimit = new BeatSaver.BeatSaverRatelimit(); + ratelimit = GetRatelimit(resp.Headers); + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitHit"), ratelimit.ResetTime)}"); + await ratelimit.Wait(); + await Download(url, output, retries - 1); + } + + using (var stream = await resp.Content.ReadAsStreamAsync()) + using (var fs = new FileStream(output, FileMode.OpenOrCreate, FileAccess.Write)) + { + await stream.CopyToAsync(fs); + } + } + + public class BeatSaverMap + { + public BeatSaverApiResponse response { get; set; } + public bool Success { get; set; } + public string Name { get; set; } + } + + public class BeatSaverApiResponse + { + public HttpStatusCode statusCode { get; set; } + public BeatSaverRatelimit ratelimit { get; set;} + public BeatSaverApiResponseMap map { get; set; } + } + + public class BeatSaverRatelimit + { + public int? Remaining { get; set; } + public int? Total { get; set; } + public int? Reset { get; set; } + public DateTime ResetTime { get; set; } + public async Task Wait() + { + await Task.Delay(new TimeSpan(ResetTime.Ticks - DateTime.Now.Ticks)); + } + } + + public class BeatSaverApiResponseMap + { + public Metadata metadata { get; set; } + public Stats stats { get; set; } + public string description { get; set; } + public DateTime? deletedAt { get; set; } + public string _id { get; set; } + public string key { get; set; } + public string name { get; set; } + public Uploader uploader { get; set; } + public DateTime uploaded { get; set; } + public string hash { get; set; } + public string directDownload { get; set; } + public string downloadURL { get; set; } + public string coverURL { get; set; } + + public class Difficulties + { + public bool easy { get; set; } + public bool normal { get; set; } + public bool hard { get; set; } + public bool expert { get; set; } + public bool expertPlus { get; set; } + } + + public class Metadata + { + public Difficulties difficulties { get; set; } + public Characteristic[] characteristics { get; set; } + public double duration { get; set; } + public string songName { get; set; } + public string songSubName { get; set; } + public string songAuthorName { get; set; } + public string levelAuthorName { get; set; } + public double bpm { get; set; } + } + + public class Characteristic + { + public string name { get; set; } + public CharacteristicDifficulties difficulties { get; set; } + } + + public class CharacteristicDifficulties + { + public Difficulty easy { get; set; } + public Difficulty normal { get; set; } + public Difficulty hard { get; set; } + public Difficulty expert { get; set; } + public Difficulty expertPlus { get; set; } + } + + public class Difficulty + { + public double? duration { get; set; } + public double? length { get; set; } + public double bombs { get; set; } + public double notes { get; set; } + public double obstacles { get; set; } + public double njs { get; set; } + public double njsOffset { get; set; } + } + + public class Stats + { + public int downloads { get; set; } + public int plays { get; set; } + public int downVotes { get; set; } + public int upVotes { get; set; } + public double heat { get; set; } + public double rating { get; set; } + } + + public class Uploader + { + public string _id { get; set; } + public string username { get; set; } + } + } + } +} diff --git a/ModAssistant/Classes/External Interfaces/ModelSaber.cs b/ModAssistant/Classes/External Interfaces/ModelSaber.cs new file mode 100644 index 00000000..f3da0828 --- /dev/null +++ b/ModAssistant/Classes/External Interfaces/ModelSaber.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace ModAssistant.API +{ + class ModelSaber + { + private const string ModelSaberURLPrefix = "https://modelsaber.com/files/"; + private const string CustomAvatarsFolder = "CustomAvatars"; + private const string CustomSabersFolder = "CustomSabers"; + private const string CustomPlatformsFolder = "CustomPlatforms"; + private const string CustomBloqsFolder = "CustomNotes"; + + public static async Task GetModel(Uri uri) + { + switch (uri.Host) + { + case "avatar": + await Utils.DownloadAsset(ModelSaberURLPrefix + uri.Host + uri.AbsolutePath, CustomAvatarsFolder); + break; + case "saber": + await Utils.DownloadAsset(ModelSaberURLPrefix + uri.Host + uri.AbsolutePath, CustomSabersFolder); + break; + case "platform": + await Utils.DownloadAsset(ModelSaberURLPrefix + uri.Host + uri.AbsolutePath, CustomPlatformsFolder); + break; + case "bloq": + await Utils.DownloadAsset(ModelSaberURLPrefix + uri.Host + uri.AbsolutePath, CustomBloqsFolder); + break; + } + } + + } +} diff --git a/ModAssistant/Classes/External Interfaces/Playlists.cs b/ModAssistant/Classes/External Interfaces/Playlists.cs new file mode 100644 index 00000000..30b52bc5 --- /dev/null +++ b/ModAssistant/Classes/External Interfaces/Playlists.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using static ModAssistant.Http; +using System.Windows; + +namespace ModAssistant.API +{ + public class Playlists + { + private const string BSaberURLPrefix = "https://bsaber.com/PlaylistAPI/"; + private const string PlaylistsFolder = "Playlists"; + private static readonly string BeatSaberPath = Utils.BeatSaberPath; + + public static async Task DownloadAll(Uri uri) + { + switch (uri.Host) + { + case "playlist": + Uri url = new Uri($"{uri.LocalPath.Trim('/')}"); + string filename = await Get(url); + await DownloadFrom(filename); + break; + + } + } + + public static async Task Get(Uri url) + { + string filename = url.Segments.Last(); + string absolutePath = Path.Combine(BeatSaberPath, PlaylistsFolder, filename); + try + { + await Utils.DownloadAsset(url.ToString(), PlaylistsFolder, filename); + return absolutePath; + } + catch + { + return null; + } + } + + public static async Task DownloadFrom(string file) + { + if (Path.Combine(BeatSaberPath, PlaylistsFolder) != Path.GetDirectoryName(file)) + { + string destination = Path.Combine(BeatSaberPath, PlaylistsFolder, Path.GetFileName(file)); + File.Copy(file, destination, true); + } + + int Errors = 0; + int Minimum = 0; + int Value = 0; + + Playlist playlist = JsonSerializer.Deserialize(File.ReadAllText(file)); + int Maximum = playlist.songs.Length; + + foreach (Playlist.Song song in playlist.songs) + { + API.BeatSaver.BeatSaverMap response = new BeatSaver.BeatSaverMap(); + if (!string.IsNullOrEmpty(song.hash)) + { + response = await BeatSaver.GetFromHash(song.hash, false); + } + else if (!string.IsNullOrEmpty(song.key)) + { + response = await BeatSaver.GetFromKey(song.key, false); + } + Value++; + + if (response.Success) + { + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:InstallingPlaylist"), TextProgress(Minimum, Maximum, Value))} {response.Name}"); + } + else + { + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FailedPlaylistSong"), song.songName)}"); + ModAssistant.Utils.Log($"Failed installing BeatSaver map: {song.songName} | {song.key} | {song.hash} | ({response?.response?.ratelimit?.Remaining})"); + App.CloseWindowOnFinish = false; + await Task.Delay(3 * 1000); + Errors++; + } + } + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FinishedPlaylist"), Errors, playlist.playlistTitle)}"); + } + + private static string TextProgress(int min, int max, int value) + { + if (max == value) + { + return $" {string.Concat(Enumerable.Repeat("▒", 10))} [{value}/{max}]"; + } + int interval = (int)Math.Floor((double)value / ( ((double)max - (double)min ) / (double)10)); + return $" {string.Concat(Enumerable.Repeat("▒", interval))}{string.Concat(Enumerable.Repeat("░", 10 - interval))} [{value}/{max}]"; + } + + class Playlist + { + public string playlistTitle { get; set; } + public string playlistAuthor { get; set; } + public string image { get; set; } + public Song[] songs { get; set; } + + public class Song + { + public string key { get; set; } + public string hash { get; set; } + public string songName { get; set; } + public string uploader { get; set; } + } + } + } +} diff --git a/ModAssistant/Classes/External Interfaces/Utils.cs b/ModAssistant/Classes/External Interfaces/Utils.cs new file mode 100644 index 00000000..c2c04ecb --- /dev/null +++ b/ModAssistant/Classes/External Interfaces/Utils.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Windows; +using static ModAssistant.Http; + +namespace ModAssistant.API +{ + public class Utils + { + public static readonly string BeatSaberPath = App.BeatSaberInstallDirectory; + + public static void SetMessage(string message) + { + if (App.window == null) + { + OneClickStatus.Instance.MainText = message; + } + else + { + MainWindow.Instance.MainText = message; + } + } + + public static async Task DownloadAsset(string link, string folder, bool showNotifcation, string fileName = null) + { + await DownloadAsset(link, folder, fileName, null, showNotifcation); + } + + public static async Task DownloadAsset(string link, string folder, string fileName = null, string displayName = null) + { + await DownloadAsset(link, folder, fileName, displayName, true); + } + + public static async Task DownloadAsset(string link, string folder, string fileName, string displayName, bool showNotification, bool beatsaver = false) + { + if (string.IsNullOrEmpty(BeatSaberPath)) + { + ModAssistant.Utils.SendNotify((string)Application.Current.FindResource("OneClick:InstallDirNotFound")); + } + try + { + Directory.CreateDirectory(Path.Combine(BeatSaberPath, folder)); + if (string.IsNullOrEmpty(fileName)) + { + fileName = WebUtility.UrlDecode(Path.Combine(BeatSaberPath, folder, new Uri(link).Segments.Last())); + } + else + { + fileName = WebUtility.UrlDecode(Path.Combine(BeatSaberPath, folder, fileName)); + } + if (string.IsNullOrEmpty(displayName)) + { + displayName = Path.GetFileNameWithoutExtension(fileName); + } + + if (beatsaver) await BeatSaver.Download(link, fileName); + else await ModAssistant.Utils.Download(link, fileName); + + if (showNotification) + { + SetMessage(string.Format((string)Application.Current.FindResource("OneClick:InstalledAsset"), displayName)); + } + } + catch + { + SetMessage((string)Application.Current.FindResource("OneClick:AssetInstallFailed")); + App.CloseWindowOnFinish = false; + } + } + } +} diff --git a/ModAssistant/Classes/Http.cs b/ModAssistant/Classes/Http.cs new file mode 100644 index 00000000..6ccac42e --- /dev/null +++ b/ModAssistant/Classes/Http.cs @@ -0,0 +1,40 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Web.Script.Serialization; + +namespace ModAssistant +{ + static class Http + { + private static HttpClient _client = null; + + public static HttpClient HttpClient + { + get + { + if (_client != null) return _client; + + var handler = new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + }; + + _client = new HttpClient(handler) + { + Timeout = TimeSpan.FromSeconds(240), + }; + + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + _client.DefaultRequestHeaders.Add("User-Agent", "ModAssistant/" + App.Version); + + return _client; + } + } + + public static JavaScriptSerializer JsonSerializer = new JavaScriptSerializer() + { + MaxJsonLength = int.MaxValue, + }; + } +} diff --git a/ModAssistant/Classes/HyperlinkExtensions.cs b/ModAssistant/Classes/HyperlinkExtensions.cs new file mode 100644 index 00000000..8b1f7120 --- /dev/null +++ b/ModAssistant/Classes/HyperlinkExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Documents; + +namespace ModAssistant +{ + public static class HyperlinkExtensions + { + public static bool GetIsExternal(DependencyObject obj) + { + return (bool)obj.GetValue(IsExternalProperty); + } + + public static void SetIsExternal(DependencyObject obj, bool value) + { + obj.SetValue(IsExternalProperty, value); + } + + public static readonly DependencyProperty IsExternalProperty = DependencyProperty.RegisterAttached("IsExternal", typeof(bool), typeof(HyperlinkExtensions), new UIPropertyMetadata(false, OnIsExternalChanged)); + + private static void OnIsExternalChanged(object sender, DependencyPropertyChangedEventArgs args) + { + var hyperlink = sender as Hyperlink; + + if ((bool)args.NewValue) + hyperlink.RequestNavigate += Hyperlink_RequestNavigate; + else + hyperlink.RequestNavigate -= Hyperlink_RequestNavigate; + } + + private static void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) + { + Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); + e.Handled = true; + } + } +} diff --git a/ModAssistant/Classes/Mod.cs b/ModAssistant/Classes/Mod.cs new file mode 100644 index 00000000..fee155b9 --- /dev/null +++ b/ModAssistant/Classes/Mod.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using ModAssistant.Pages; + +namespace ModAssistant +{ + public class Mod + { + public string name; + public string version; + public string gameVersion; + public string _id; + public string status; + public string authorId; + public string uploadedDate; + public string updatedDate; + public Author author; + public string description; + public string link; + public string category; + public DownloadLink[] downloads; + public bool required; + public Dependency[] dependencies; + public List Dependents = new List(); + public Mods.ModListItem ListItem; + + public class Author + { + public string _id; + public string username; + public string lastLogin; + } + + public class DownloadLink + { + public string type; + public string url; + public FileHashes[] hashMd5; + } + + public class FileHashes + { + public string hash; + public string file; + } + + public class Dependency + { + public string name; + public string _id; + public Mod Mod; + } + } +} diff --git a/ModAssistant/Classes/OneClickInstaller.cs b/ModAssistant/Classes/OneClickInstaller.cs new file mode 100644 index 00000000..bcdb0a52 --- /dev/null +++ b/ModAssistant/Classes/OneClickInstaller.cs @@ -0,0 +1,138 @@ +using Microsoft.Win32; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace ModAssistant +{ + class OneClickInstaller + { + private static readonly string[] Protocols = new[] { "modelsaber", "beatsaver", "bsplaylist" }; + public static OneClickStatus Status = new OneClickStatus(); + + public static async Task InstallAsset(string link) + { + Uri uri = new Uri(link); + if (!Protocols.Contains(uri.Scheme)) return; + + switch (uri.Scheme) + { + case "modelsaber": + await ModelSaber(uri); + break; + case "beatsaver": + await BeatSaver(uri); + break; + case "bsplaylist": + await Playlist(uri); + break; + } + } + + private static async Task BeatSaver(Uri uri) + { + string Key = uri.Host; + await API.BeatSaver.GetFromKey(Key); + } + + private static async Task ModelSaber(Uri uri) + { + Status.Show(); + API.Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Installing"), System.Web.HttpUtility.UrlDecode(uri.Segments.Last()))}"); + await API.ModelSaber.GetModel(uri); + } + + private static async Task Playlist(Uri uri) + { + Status.Show(); + await API.Playlists.DownloadAll(uri); + } + + public static void Register(string Protocol, bool Background = false) + { + if (IsRegistered(Protocol) == true) + return; + try + { + if (Utils.IsAdmin) + { + RegistryKey ProtocolKey = Registry.ClassesRoot.OpenSubKey(Protocol, true); + if (ProtocolKey == null) + ProtocolKey = Registry.ClassesRoot.CreateSubKey(Protocol, true); + RegistryKey CommandKey = ProtocolKey.CreateSubKey(@"shell\open\command", true); + if (CommandKey == null) + CommandKey = Registry.ClassesRoot.CreateSubKey(@"shell\open\command", true); + + if (ProtocolKey.GetValue("OneClick-Provider", "").ToString() != "ModAssistant") + { + ProtocolKey.SetValue("URL Protocol", "", RegistryValueKind.String); + ProtocolKey.SetValue("OneClick-Provider", "ModAssistant", RegistryValueKind.String); + CommandKey.SetValue("", $"\"{Utils.ExePath}\" \"--install\" \"%1\""); + } + + Utils.SendNotify(string.Format((string)Application.Current.FindResource("OneClick:ProtocolHandler:Registered"), Protocol)); + } + else + { + Utils.StartAsAdmin($"\"--register\" \"{Protocol}\""); + } + } + catch (Exception e) + { + MessageBox.Show(e.ToString()); + } + + if (Background) + Application.Current.Shutdown(); + else + Pages.Options.Instance.UpdateHandlerStatus(); + } + + public static void Unregister(string Protocol, bool Background = false) + { + if (IsRegistered(Protocol) == false) + return; + try + { + if (Utils.IsAdmin) + { + using (RegistryKey ProtocolKey = Registry.ClassesRoot.OpenSubKey(Protocol, true)) + { + if (ProtocolKey != null + && ProtocolKey.GetValue("OneClick-Provider", "").ToString() == "ModAssistant") + { + Registry.ClassesRoot.DeleteSubKeyTree(Protocol); + } + } + + Utils.SendNotify(string.Format((string)Application.Current.FindResource("OneClick:ProtocolHandler:Unregistered"), Protocol)); + } + else + { + Utils.StartAsAdmin($"\"--unregister\" \"{Protocol}\""); + } + + } + catch (Exception e) + { + MessageBox.Show(e.ToString()); + } + + if (Background) + Application.Current.Shutdown(); + else + Pages.Options.Instance.UpdateHandlerStatus(); + } + + public static bool IsRegistered(string Protocol) + { + RegistryKey ProtocolKey = Registry.ClassesRoot.OpenSubKey(Protocol); + if (ProtocolKey != null + && ProtocolKey.GetValue("OneClick-Provider", "").ToString() == "ModAssistant") + return true; + else + return false; + } + } +} diff --git a/ModAssistant/Classes/Promotions.cs b/ModAssistant/Classes/Promotions.cs new file mode 100644 index 00000000..40200787 --- /dev/null +++ b/ModAssistant/Classes/Promotions.cs @@ -0,0 +1,24 @@ +using System; + +namespace ModAssistant +{ + class Promotions + { + public static Promotion[] ActivePromotions = + { + new Promotion + { + ModName = "YUR Fit Calorie Tracker", + Text = "Join our Discord!", + Link = "https://yur.chat" + } + }; + } + + class Promotion + { + public string ModName; + public string Text; + public string Link; + } +} diff --git a/ModAssistant/Classes/Themes.cs b/ModAssistant/Classes/Themes.cs new file mode 100644 index 00000000..5e839963 --- /dev/null +++ b/ModAssistant/Classes/Themes.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.IO; +using System.Windows.Media; +using ModAssistant.Pages; +using System.Reflection; +using Microsoft.Win32; +using System.Windows.Media.Imaging; +using System.IO.Compression; +using System.Windows.Markup; + +namespace ModAssistant +{ + public class Themes + { + public static string LoadedTheme { get; private set; } + public static List LoadedThemes { get => loadedThemes.Keys.ToList(); } + public static string ThemeDirectory = Path.Combine(Path.GetDirectoryName(Utils.ExePath), "Themes"); + + /// + /// Local dictionary of Resource Dictionaries mapped by their names. + /// + private static Dictionary loadedThemes = new Dictionary(); + private static List preInstalledThemes = new List { "Light", "Dark", "BSMG", "Light Pink" }; + + /// + /// Index of "LoadedTheme" in App.xaml + /// + private static readonly int LOADED_THEME_INDEX = 3; + + private static List supportedVideoExtensions = new List() { ".mp4", ".webm", ".mkv", ".avi", ".m2ts" }; + + /// + /// Load all themes from local Themes subfolder and from embedded resources. + /// This also refreshes the Themes dropdown in the Options screen. + /// + public static void LoadThemes() + { + loadedThemes.Clear(); + + /* + * Begin by loading local themes. We should always load these first. + * I am doing loading here to prevent the LoadTheme function from becoming too crazy. + */ + foreach (string localTheme in preInstalledThemes) + { + string location = $"Themes/{localTheme}.xaml"; + Uri local = new Uri(location, UriKind.Relative); + + ResourceDictionary localDictionary = new ResourceDictionary(); + localDictionary.Source = local; + + /* + * Load any Waifus that come with these built-in themes, too. + * The format must be: Background.png and Sidebar.png as a subfolder with the same name as the theme name. + * For example: "Themes/Dark/Background.png", or "Themes/Ugly Kulu-Ya-Ku/Sidebar.png" + */ + Waifus waifus = new Waifus(); + waifus.Background = GetImageFromEmbeddedResources(localTheme, "Background"); + waifus.Sidebar = GetImageFromEmbeddedResources(localTheme, "Sidebar"); + + Theme theme = new Theme(localTheme, localDictionary); + theme.Waifus = waifus; + loadedThemes.Add(localTheme, theme); + } + + // Load themes from Themes subfolder if it exists. + if (Directory.Exists(ThemeDirectory)) + { + foreach (string file in Directory.EnumerateFiles(ThemeDirectory)) + { + FileInfo info = new FileInfo(file); + string name = Path.GetFileNameWithoutExtension(info.Name); + + if (info.Extension.ToLower().Equals(".mat")) + { + Theme theme = LoadZipTheme(ThemeDirectory, name, ".mat"); + if (theme is null) continue; + + AddOrModifyTheme(name, theme); + } + } + + // Finally load any loose theme files in subfolders. + foreach (string directory in Directory.EnumerateDirectories(ThemeDirectory)) + { + string name = directory.Split('\\').Last(); + Theme theme = LoadTheme(directory, name); + + if (theme is null) continue; + AddOrModifyTheme(name, theme); + } + } + + // Refresh Themes dropdown in Options screen. + if (Options.Instance != null && Options.Instance.ApplicationThemeComboBox != null) + { + Options.Instance.ApplicationThemeComboBox.ItemsSource = LoadedThemes; + Options.Instance.ApplicationThemeComboBox.SelectedIndex = LoadedThemes.IndexOf(LoadedTheme); + } + } + + /// + /// Runs once at the start of the program, performs settings checking. + /// + /// Theme name retrieved from the settings file. + public static void FirstLoad(string savedTheme) + { + if (string.IsNullOrEmpty(savedTheme)) + { + try + { + Themes.ApplyWindowsTheme(); + } + catch + { + Themes.ApplyTheme("Light", false); + } + return; + } + + try + { + Themes.ApplyTheme(savedTheme, false); + } + catch (ArgumentException) + { + Themes.ApplyWindowsTheme(); + MainWindow.Instance.MainText = (string)Application.Current.FindResource("Themes:ThemeNotFound"); + } + } + + /// + /// Applies a loaded theme to ModAssistant. + /// + /// Name of the theme. + /// Send message to MainText (default: true). + public static void ApplyTheme(string theme, bool sendMessage = true) + { + if (loadedThemes.TryGetValue(theme, out Theme newTheme)) + { + LoadedTheme = theme; + MainWindow.Instance.BackgroundVideo.Pause(); + MainWindow.Instance.BackgroundVideo.Visibility = Visibility.Hidden; + + if (newTheme.ThemeDictionary != null) + { + // TODO: Search by name + Application.Current.Resources.MergedDictionaries.RemoveAt(LOADED_THEME_INDEX); + Application.Current.Resources.MergedDictionaries.Insert(LOADED_THEME_INDEX, newTheme.ThemeDictionary); + } + + Properties.Settings.Default.SelectedTheme = theme; + Properties.Settings.Default.Save(); + + if (sendMessage) + { + MainWindow.Instance.MainText = string.Format((string)Application.Current.FindResource("Themes:ThemeSet"), theme); + } + + ApplyWaifus(); + + if (File.Exists(newTheme.VideoLocation)) + { + Uri videoUri = new Uri(newTheme.VideoLocation, UriKind.Absolute); + MainWindow.Instance.BackgroundVideo.Visibility = Visibility.Visible; + + // Load the source video if it's not the same as what's playing, or if the theme is loading for the first time. + if (!sendMessage || MainWindow.Instance.BackgroundVideo.Source?.AbsoluteUri != videoUri.AbsoluteUri) + { + MainWindow.Instance.BackgroundVideo.Stop(); + MainWindow.Instance.BackgroundVideo.Source = videoUri; + } + + MainWindow.Instance.BackgroundVideo.Play(); + } + + ReloadIcons(); + } + else + { + throw new ArgumentException(string.Format((string)Application.Current.FindResource("Themes:ThemeMissing"), theme)); + } + } + + /// + /// Writes an Embedded Resource theme to disk. You cannot write an outside theme to disk. + /// + /// Name of local theme. + public static void WriteThemeToDisk(string themeName) + { + Directory.CreateDirectory(ThemeDirectory); + Directory.CreateDirectory($"{ThemeDirectory}\\{themeName}"); + + if (File.Exists($@"{ThemeDirectory}\\{themeName}.xaml") == false) + { + /* + * Any theme that you want to write must be set as an Embedded Resource instead of the default Page. + * This is so that we can grab its exact content from Manifest, shown below. + * Writing it as is instead of using XAMLWriter keeps the source as is with comments, spacing, and organization. + * Using XAMLWriter would compress it into an unorganized mess. + */ + using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream($"ModAssistant.Themes.{themeName}.xaml")) + using (FileStream writer = new FileStream($@"{ThemeDirectory}\\{themeName}\\{themeName}.xaml", FileMode.Create)) + { + byte[] buffer = new byte[s.Length]; + int read = s.Read(buffer, 0, (int)s.Length); + writer.Write(buffer, 0, buffer.Length); + } + + MainWindow.Instance.MainText = string.Format((string)Application.Current.FindResource("Themes:SavedTemplateTheme"), themeName); + } + else + { + MessageBox.Show((string)Application.Current.FindResource("Themes:TemplateThemeExists")); + } + } + + /// + /// Finds the theme set on Windows and applies it. + /// + public static void ApplyWindowsTheme() + { + using (RegistryKey key = Registry.CurrentUser + .OpenSubKey("Software").OpenSubKey("Microsoft") + .OpenSubKey("Windows").OpenSubKey("CurrentVersion") + .OpenSubKey("Themes").OpenSubKey("Personalize")) + { + object registryValueObject = key?.GetValue("AppsUseLightTheme"); + if (registryValueObject != null) + { + if ((int)registryValueObject <= 0) + { + ApplyTheme("Dark", false); + return; + } + } + + ApplyTheme("Light", false); + } + } + + /// + /// Loads a Theme from a directory location. + /// + /// The full directory path to the theme. + /// Name of the containing folder. + /// + private static Theme LoadTheme(string directory, string name) + { + Theme theme = new Theme(name, null); + theme.Waifus = new Waifus(); + + foreach (string file in Directory.EnumerateFiles(directory).OrderBy(x => x)) + { + FileInfo info = new FileInfo(file); + bool isPng = info.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase); + bool isSidePng = info.Name.EndsWith(".side.png", StringComparison.OrdinalIgnoreCase); + bool isXaml = info.Name.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase); + + if (isPng && !isSidePng) + { + theme.Waifus.Background = new BitmapImage(new Uri(info.FullName)); + } + + if (isSidePng) + { + theme.Waifus.Sidebar = new BitmapImage(new Uri(info.FullName)); + } + + if (isXaml) + { + try + { + Uri resourceSource = new Uri(info.FullName); + ResourceDictionary dictionary = new ResourceDictionary(); + dictionary.Source = resourceSource; + theme.ThemeDictionary = dictionary; + } + catch (Exception ex) + { + string message = string.Format((string)Application.Current.FindResource("Themes:FailedToLoadXaml"), name, ex.Message); + MessageBox.Show(message); + } + } + + if (supportedVideoExtensions.Contains(info.Extension)) + { + if (info.Name != $"_{name}{info.Extension}" || theme.VideoLocation is null) + { + theme.VideoLocation = info.FullName; + } + } + } + + return theme; + } + + /// + /// Modifies an already existing theme, or adds the theme if it doesn't exist + /// + /// Name of the theme. + /// Theme to modify/apply + private static void AddOrModifyTheme(string name, Theme theme) + { + if (loadedThemes.TryGetValue(name, out _)) + { + if (theme.ThemeDictionary != null) + { + loadedThemes[name].ThemeDictionary = theme.ThemeDictionary; + } + + if (theme.Waifus?.Background != null) + { + if (loadedThemes[name].Waifus is null) loadedThemes[name].Waifus = new Waifus(); + loadedThemes[name].Waifus.Background = theme.Waifus.Background; + } + + if (theme.Waifus?.Sidebar != null) + { + if (loadedThemes[name].Waifus is null) loadedThemes[name].Waifus = new Waifus(); + loadedThemes[name].Waifus.Sidebar = theme.Waifus.Sidebar; + } + + if (!string.IsNullOrEmpty(theme.VideoLocation)) + { + loadedThemes[name].VideoLocation = theme.VideoLocation; + } + } + else + { + loadedThemes.Add(name, theme); + } + } + + /// + /// Loads themes from pre-packged zips. + /// + /// Theme directory + /// Theme name + /// Theme extension + private static Theme LoadZipTheme(string directory, string name, string extension) + { + Waifus waifus = new Waifus(); + ResourceDictionary dictionary = null; + + using (FileStream stream = new FileStream(Path.Combine(directory, name + extension), FileMode.Open, FileAccess.Read)) + using (ZipArchive archive = new ZipArchive(stream)) + { + foreach (ZipArchiveEntry file in archive.Entries) + { + bool isPng = file.Name.EndsWith(".png", StringComparison.OrdinalIgnoreCase); + bool isSidePng = file.Name.EndsWith(".side.png", StringComparison.OrdinalIgnoreCase); + bool isXaml = file.Name.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase); + + if (isPng && !isSidePng) + { + waifus.Background = GetImageFromStream(Utils.StreamToArray(file.Open())); + } + + if (isSidePng) + { + waifus.Sidebar = GetImageFromStream(Utils.StreamToArray(file.Open())); + } + + string videoExtension = $".{file.Name.Split('.').Last()}"; + if (supportedVideoExtensions.Contains(videoExtension)) + { + string videoName = $"{ThemeDirectory}\\{name}\\_{name}{videoExtension}"; + Directory.CreateDirectory($"{ThemeDirectory}\\{name}"); + + if (File.Exists(videoName) == false) + { + file.ExtractToFile(videoName, false); + } + else + { + /* + * Check to see if the lengths of each file are different. If they are, overwrite what currently exists. + * The reason we are also checking LoadedTheme against the name variable is to prevent overwriting a file that's + * already being used by ModAssistant and causing a System.IO.IOException. + */ + FileInfo existingInfo = new FileInfo(videoName); + if (existingInfo.Length != file.Length && LoadedTheme != name) + { + file.ExtractToFile(videoName, true); + } + } + } + + if (isXaml && loadedThemes.ContainsKey(name) == false) + { + try + { + dictionary = (ResourceDictionary)XamlReader.Load(file.Open()); + } + catch (Exception ex) + { + string message = string.Format((string)Application.Current.FindResource("Themes:FailedToLoadXaml"), name, ex.Message); + MessageBox.Show(message); + } + } + } + } + + Theme theme = new Theme(name, dictionary); + theme.Waifus = waifus; + + return theme; + } + + /// + /// Returns a BeatmapImage from a byte array. + /// + /// byte array containing an image. + /// + private static BitmapImage GetImageFromStream(byte[] array) + { + using (var mStream = new MemoryStream(array)) + { + BitmapImage image = new BitmapImage(); + image.BeginInit(); + image.CacheOption = BitmapCacheOption.OnLoad; + image.StreamSource = mStream; + image.EndInit(); + if (image.CanFreeze) image.Freeze(); + + return image; + } + } + + private static BitmapImage GetImageFromEmbeddedResources(string themeName, string imageName) + { + try + { + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"ModAssistant.Themes.{themeName}.{imageName}.png")) + { + byte[] imageBytes = new byte[stream.Length]; + stream.Read(imageBytes, 0, (int)stream.Length); + return GetImageFromStream(imageBytes); + } + } + catch { return null; } //We're going to ignore errors here because backgrounds/sidebars should be optional. + } + + /// + /// Applies waifus from currently loaded Theme. + /// + private static void ApplyWaifus() + { + Waifus waifus = loadedThemes[LoadedTheme].Waifus; + + if (waifus?.Background is null) + { + MainWindow.Instance.BackgroundImage.Opacity = 0; + } + else + { + MainWindow.Instance.BackgroundImage.Opacity = 1; + MainWindow.Instance.BackgroundImage.ImageSource = waifus.Background; + } + + if (waifus?.Sidebar is null) + { + MainWindow.Instance.SideImage.Visibility = Visibility.Hidden; + } + else + { + MainWindow.Instance.SideImage.Visibility = Visibility.Visible; + MainWindow.Instance.SideImage.Source = waifus.Sidebar; + } + } + + /// + /// Reload the icon colors for the About, Info, Options, and Mods buttons from the currently loaded theme. + /// + private static void ReloadIcons() + { + ResourceDictionary icons = Application.Current.Resources.MergedDictionaries.First(x => x.Source?.ToString() == "Resources/Icons.xaml"); + + ChangeColor(icons, "AboutIconColor", "heartDrawingGroup"); + ChangeColor(icons, "InfoIconColor", "info_circleDrawingGroup"); + ChangeColor(icons, "OptionsIconColor", "cogDrawingGroup"); + ChangeColor(icons, "ModsIconColor", "microchipDrawingGroup"); + ChangeColor(icons, "LoadingIconColor", "loadingInnerDrawingGroup"); + ChangeColor(icons, "LoadingIconColor", "loadingMiddleDrawingGroup"); + ChangeColor(icons, "LoadingIconColor", "loadingOuterDrawingGroup"); + } + + /// + /// Change the color of an image from the loaded theme. + /// + /// ResourceDictionary that contains the image. + /// Resource name of the color to change. + /// DrawingGroup name for the image. + private static void ChangeColor(ResourceDictionary icons, string ResourceColorName, string DrawingGroupName) + { + Application.Current.Resources[ResourceColorName] = loadedThemes[LoadedTheme].ThemeDictionary[ResourceColorName]; + ((GeometryDrawing)((DrawingGroup)icons[DrawingGroupName]).Children[0]).Brush = (Brush)Application.Current.Resources[ResourceColorName]; + } + + private class Waifus + { + public BitmapImage Background = null; + public BitmapImage Sidebar = null; + } + + private class Theme + { + public string Name; + public ResourceDictionary ThemeDictionary; + public Waifus Waifus = null; + public string VideoLocation = null; + + public Theme(string name, ResourceDictionary dictionary) + { + Name = name; + ThemeDictionary = dictionary; + } + } + } +} diff --git a/ModAssistant/Classes/Updater.cs b/ModAssistant/Classes/Updater.cs new file mode 100644 index 00000000..0b0acf0a --- /dev/null +++ b/ModAssistant/Classes/Updater.cs @@ -0,0 +1,146 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using System.Windows; +using static ModAssistant.Http; + +namespace ModAssistant +{ + class Updater + { + private static readonly string APILatestURL = "https://api.github.com/repos/Assistant/ModAssistant/releases/latest"; + + private static Update LatestUpdate; + private static Version CurrentVersion; + private static Version LatestVersion; + private static bool NeedsUpdate = false; + private static string NewExe = Path.Combine(Path.GetDirectoryName(Utils.ExePath), "ModAssistant.exe"); + private static string Arguments = App.Arguments; + + public static async Task CheckForUpdate() + { + var resp = await HttpClient.GetAsync(APILatestURL); + var body = await resp.Content.ReadAsStringAsync(); + LatestUpdate = JsonSerializer.Deserialize(body); + + LatestVersion = new Version(LatestUpdate.tag_name.Substring(1)); + CurrentVersion = new Version(App.Version); + + return (LatestVersion > CurrentVersion); + } + + public static async Task Run() + { + if (Path.GetFileName(Utils.ExePath).Equals("ModAssistant.old.exe")) RunNew(); + try + { + NeedsUpdate = await CheckForUpdate(); + } + catch + { + Utils.SendNotify((string)Application.Current.FindResource("Updater:CheckFailed")); + } + + if (NeedsUpdate) await StartUpdate(); + } + + public static async Task StartUpdate() + { + string OldExe = Path.Combine(Path.GetDirectoryName(Utils.ExePath), "ModAssistant.old.exe"); + string DownloadLink = null; + + foreach (Update.Asset asset in LatestUpdate.assets) + { + if (asset.name == "ModAssistant.exe") + { + DownloadLink = asset.browser_download_url; + } + } + + if (string.IsNullOrEmpty(DownloadLink)) + { + Utils.SendNotify((string)Application.Current.FindResource("Updater:DownloadFailed")); + } + else + { + if (File.Exists(OldExe)) + { + File.Delete(OldExe); + } + + File.Move(Utils.ExePath, OldExe); + + await Utils.Download(DownloadLink, NewExe); + RunNew(); + } + } + + private static void RunNew() + { + Process.Start(NewExe, Arguments); + Application.Current.Dispatcher.Invoke(() => { Application.Current.Shutdown(); }); + } + } + + public class Update + { + public string url; + public string assets_url; + public string upload_url; + public string html_url; + public int id; + public string node_id; + public string tag_name; + public string target_commitish; + public string name; + public bool draft; + public User author; + public bool prerelease; + public string created_at; + public string published_at; + public Asset[] assets; + public string tarball_url; + public string zipball_url; + public string body; + + public class Asset + { + public string url; + public int id; + public string node_id; + public string name; + public string label; + public User uploader; + public string content_type; + public string state; + public int size; + public string created_at; + public string updated_at; + public string browser_download_url; + } + + public class User + { + public string login; + public int id; + public string node_id; + public string avatar_url; + public string gravatar_id; + public string url; + public string html_url; + public string followers_url; + public string following_url; + public string gists_url; + public string starred_url; + public string subscriptions_url; + public string organizations_url; + public string repos_url; + public string events_url; + public string received_events_url; + public string type; + public bool site_admin; + + } + } +} diff --git a/ModAssistant/Classes/Utils.cs b/ModAssistant/Classes/Utils.cs new file mode 100644 index 00000000..4f1c4251 --- /dev/null +++ b/ModAssistant/Classes/Utils.cs @@ -0,0 +1,443 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows; +using static ModAssistant.Http; + +namespace ModAssistant +{ + public class Utils + { + public static bool IsAdmin = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + public static string ExePath = Process.GetCurrentProcess().MainModule.FileName; + + public class Constants + { + public const string BeatSaberAPPID = "620980"; + public const string BeatModsAPIUrl = "https://beatmods.com/api/v1/"; + public const string TeknikAPIUrl = "https://api.teknik.io/v1/"; + public const string BeatModsURL = "https://beatmods.com"; + public const string BeatModsVersions = "https://versions.beatmods.com/versions.json"; + public const string BeatModsAlias = "https://alias.beatmods.com/aliases.json"; + public const string WeebCDNAPIURL = "https://pat.assistant.moe/api/v1.0/"; + public const string BeatModsModsOptions = "mod?status=approved"; + public const string MD5Spacer = " "; + public static readonly char[] IllegalCharacters = new char[] + { + '<', '>', ':', '/', '\\', '|', '?', '*', '"', + '\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\u0007', + '\u0008', '\u0009', '\u000a', '\u000b', '\u000c', '\u000d', '\u000e', '\u000d', + '\u000f', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', + '\u0017', '\u0018', '\u0019', '\u001a', '\u001b', '\u001c', '\u001d', '\u001f', + }; + } + + public class TeknikPasteResponse + { + public Result result; + public class Result + { + public string id; + public string url; + public string title; + public string syntax; + public DateTime? expiration; + public string password; + } + } + + public class WeebCDNRandomResponse + { + public int index; + public string url; + public string ext; + } + + public static void SendNotify(string message, string title = null) + { + string defaultTitle = (string)Application.Current.FindResource("Utils:NotificationTitle"); + + var notification = new System.Windows.Forms.NotifyIcon() + { + Visible = true, + Icon = System.Drawing.SystemIcons.Information, + BalloonTipTitle = title ?? defaultTitle, + BalloonTipText = message + }; + + notification.ShowBalloonTip(5000); + + notification.Dispose(); + } + + public static void StartAsAdmin(string Arguments, bool Close = false) + { + using (Process process = new Process()) + { + process.StartInfo.FileName = Process.GetCurrentProcess().MainModule.FileName; + process.StartInfo.Arguments = Arguments; + process.StartInfo.UseShellExecute = true; + process.StartInfo.Verb = "runas"; + + try + { + process.Start(); + + if (!Close) + { + process.WaitForExit(); + } + } + catch + { + MessageBox.Show((string)Application.Current.FindResource("Utils:RunAsAdmin")); + } + + if (Close) Application.Current.Shutdown(); + } + } + + public static string CalculateMD5(string filename) + { + using (var md5 = MD5.Create()) + { + using (var stream = File.OpenRead(filename)) + { + var hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + + public static string GetInstallDir() + { + string InstallDir = Properties.Settings.Default.InstallFolder; + + if (!string.IsNullOrEmpty(InstallDir) + && Directory.Exists(InstallDir) + && Directory.Exists(Path.Combine(InstallDir, "Beat Saber_Data", "Plugins")) + && File.Exists(Path.Combine(InstallDir, "Beat Saber.exe"))) + { + return InstallDir; + } + + try + { + InstallDir = GetSteamDir(); + } + catch { } + if (!string.IsNullOrEmpty(InstallDir)) + { + return InstallDir; + } + + try + { + InstallDir = GetOculusDir(); + } + catch { } + if (!string.IsNullOrEmpty(InstallDir)) + { + return InstallDir; + } + + MessageBox.Show((string)Application.Current.FindResource("Utils:NoInstallFolder")); + + InstallDir = GetManualDir(); + if (!string.IsNullOrEmpty(InstallDir)) + { + return InstallDir; + } + + return null; + } + + public static string SetDir(string directory, string store) + { + App.BeatSaberInstallDirectory = directory; + App.BeatSaberInstallType = store; + Pages.Options.Instance.InstallDirectory = directory; + Pages.Options.Instance.InstallType = store; + Properties.Settings.Default.InstallFolder = directory; + Properties.Settings.Default.StoreType = store; + Properties.Settings.Default.Save(); + return directory; + } + + public static string GetSteamDir() + { + + string SteamInstall = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)?.OpenSubKey("SOFTWARE")?.OpenSubKey("WOW6432Node")?.OpenSubKey("Valve")?.OpenSubKey("Steam")?.GetValue("InstallPath").ToString(); + if (string.IsNullOrEmpty(SteamInstall)) + { + SteamInstall = Registry.LocalMachine.OpenSubKey("SOFTWARE")?.OpenSubKey("WOW6432Node")?.OpenSubKey("Valve")?.OpenSubKey("Steam")?.GetValue("InstallPath").ToString(); + } + + if (string.IsNullOrEmpty(SteamInstall)) return null; + + string vdf = Path.Combine(SteamInstall, @"steamapps\libraryfolders.vdf"); + if (!File.Exists(@vdf)) return null; + + Regex regex = new Regex("\\s\"\\d\"\\s+\"(.+)\""); + List SteamPaths = new List + { + Path.Combine(SteamInstall, @"steamapps") + }; + + using (StreamReader reader = new StreamReader(@vdf)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + Match match = regex.Match(line); + if (match.Success) + { + SteamPaths.Add(Path.Combine(match.Groups[1].Value.Replace(@"\\", @"\"), @"steamapps")); + } + } + } + + regex = new Regex("\\s\"installdir\"\\s+\"(.+)\""); + foreach (string path in SteamPaths) + { + if (File.Exists(Path.Combine(@path, @"appmanifest_" + Constants.BeatSaberAPPID + ".acf"))) + { + using (StreamReader reader = new StreamReader(Path.Combine(@path, @"appmanifest_" + Constants.BeatSaberAPPID + ".acf"))) + { + string line; + while ((line = reader.ReadLine()) != null) + { + Match match = regex.Match(line); + if (match.Success) + { + if (File.Exists(Path.Combine(@path, @"common", match.Groups[1].Value, "Beat Saber.exe"))) + { + return SetDir(Path.Combine(@path, @"common", match.Groups[1].Value), "Steam"); + } + } + } + } + } + } + return null; + } + + public static string GetVersion() + { + string filename = Path.Combine(App.BeatSaberInstallDirectory, "Beat Saber_Data", "globalgamemanagers"); + using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read)) + { + byte[] file = File.ReadAllBytes(filename); + byte[] bytes = new byte[32]; + + fs.Read(file, 0, Convert.ToInt32(fs.Length)); + fs.Close(); + int index = Encoding.UTF8.GetString(file).IndexOf("public.app-category.games") + 136; + + Array.Copy(file, index, bytes, 0, 32); + string version = Encoding.UTF8.GetString(bytes).Trim(Utils.Constants.IllegalCharacters); + + return version; + } + } + + public static string GetOculusDir() + { + string OculusInstall = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)?.OpenSubKey("SOFTWARE")?.OpenSubKey("Wow6432Node")?.OpenSubKey("Oculus VR, LLC")?.OpenSubKey("Oculus")?.OpenSubKey("Config")?.GetValue("InitialAppLibrary").ToString(); + if (string.IsNullOrEmpty(OculusInstall)) return null; + + if (!string.IsNullOrEmpty(OculusInstall)) + { + if (File.Exists(Path.Combine(OculusInstall, "Software", "hyperbolic-magnetism-beat-saber", "Beat Saber.exe"))) + { + return SetDir(Path.Combine(OculusInstall, "Software", "hyperbolic-magnetism-beat-saber"), "Oculus"); + } + } + + // Yoinked this code from Umbranox's Mod Manager. Lot's of thanks and love for Umbra <3 + using (RegistryKey librariesKey = Registry.CurrentUser.OpenSubKey("Software")?.OpenSubKey("Oculus VR, LLC")?.OpenSubKey("Oculus")?.OpenSubKey("Libraries")) + { + // Oculus libraries uses GUID volume paths like this "\\?\Volume{0fea75bf-8ad6-457c-9c24-cbe2396f1096}\Games\Oculus Apps", we need to transform these to "D:\Game"\Oculus Apps" + WqlObjectQuery wqlQuery = new WqlObjectQuery("SELECT * FROM Win32_Volume"); + using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(wqlQuery)) + { + Dictionary guidLetterVolumes = new Dictionary(); + + foreach (ManagementBaseObject disk in searcher.Get()) + { + var diskId = ((string)disk.GetPropertyValue("DeviceID")).Substring(11, 36); + var diskLetter = ((string)disk.GetPropertyValue("DriveLetter")) + @"\"; + + if (!string.IsNullOrWhiteSpace(diskLetter)) + { + guidLetterVolumes.Add(diskId, diskLetter); + } + } + + // Search among the library folders + foreach (string libraryKeyName in librariesKey.GetSubKeyNames()) + { + using (RegistryKey libraryKey = librariesKey.OpenSubKey(libraryKeyName)) + { + string libraryPath = (string)libraryKey.GetValue("Path"); + // Yoinked this code from Megalon's fix. <3 + string GUIDLetter = guidLetterVolumes.FirstOrDefault(x => libraryPath.Contains(x.Key)).Value; + if (!string.IsNullOrEmpty(GUIDLetter)) + { + string finalPath = Path.Combine(GUIDLetter, libraryPath.Substring(49), @"Software\hyperbolic-magnetism-beat-saber"); + if (File.Exists(Path.Combine(finalPath, "Beat Saber.exe"))) + { + return SetDir(finalPath, "Oculus"); + } + } + } + } + } + } + + return null; + } + + public static string GetManualDir() + { + var dialog = new SaveFileDialog() + { + Title = (string)Application.Current.FindResource("Utils:InstallDir:DialogTitle"), + Filter = "Directory|*.this.directory", + FileName = "select" + }; + + if (dialog.ShowDialog() == true) + { + string path = dialog.FileName; + path = path.Replace("\\select.this.directory", ""); + path = path.Replace(".this.directory", ""); + path = path.Replace("\\select.directory", ""); + if (File.Exists(Path.Combine(path, "Beat Saber.exe"))) + { + string store; + if (File.Exists(Path.Combine(path, "Beat Saber_Data", "Plugins", "steam_api64.dll"))) + { + store = "Steam"; + } + else + { + store = "Oculus"; + } + return SetDir(path, store); + } + } + return null; + } + + public static string GetManualFile(string filter = "", string title = "Open File") + { + var dialog = new OpenFileDialog() + { + Title = title, + Filter = filter, + Multiselect = false, + }; + + if (dialog.ShowDialog() == true) + { + return dialog.FileName; + } + return null; + } + + public static bool IsVoid() + { + string directory = App.BeatSaberInstallDirectory; + + if (File.Exists(Path.Combine(directory, "IGG-GAMES.COM.url")) || + File.Exists(Path.Combine(directory, "SmartSteamEmu.ini")) || + File.Exists(Path.Combine(directory, "GAMESTORRENT.CO.url")) || + File.Exists(Path.Combine(directory, "Beat Saber_Data", "Plugins", "BSteam crack.dll")) || + File.Exists(Path.Combine(directory, "Beat Saber_Data", "Plugins", "HUHUVR_steam_api64.dll")) || + Directory.GetFiles(Path.Combine(directory, "Beat Saber_Data", "Plugins"), "*.ini", SearchOption.TopDirectoryOnly).Length > 0) + return true; + return false; + } + + public static byte[] StreamToArray(Stream input) + { + byte[] buffer = new byte[16 * 1024]; + using (MemoryStream ms = new MemoryStream()) + { + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } + } + + public static void OpenFolder(string location) + { + if (!location.EndsWith(Path.DirectorySeparatorChar.ToString())) location += Path.DirectorySeparatorChar; + if (Directory.Exists(location)) + { + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo() + { + FileName = location, + UseShellExecute = true, + Verb = "open" + }); + return; + } + catch { } + } + MessageBox.Show($"{string.Format((string)Application.Current.FindResource("Utils:CannotOpenFolder"), location)}."); + } + + public static void Log(string message, string severity = "LOG") + { + string path = Path.GetDirectoryName(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath); + string logFile = $"{path}{Path.DirectorySeparatorChar}log.log"; + File.AppendAllText(logFile, $"[{DateTime.UtcNow.ToString("yyyy-mm-dd HH:mm:ss.ffffff")}][{severity.ToUpper()}] {message}\n"); + } + + public static async Task Download(string link, string output) + { + var resp = await HttpClient.GetAsync(link); + using (var stream = await resp.Content.ReadAsStreamAsync()) + using (var fs = new FileStream(output, FileMode.OpenOrCreate, FileAccess.Write)) + { + await stream.CopyToAsync(fs); + } + } + + private delegate void ShowMessageBoxDelegate(string Message, string Caption); + + private static void ShowMessageBox(string Message, string Caption) + { + MessageBox.Show(Message, Caption); + } + + public static void ShowMessageBoxAsync(string Message, string Caption) + { + ShowMessageBoxDelegate caller = new ShowMessageBoxDelegate(ShowMessageBox); + caller.BeginInvoke(Message, Caption, null, null); + } + + public static void ShowMessageBoxAsync(string Message) + { + ShowMessageBoxDelegate caller = new ShowMessageBoxDelegate(ShowMessageBox); + caller.BeginInvoke(Message, null, null, null); + } + } +} diff --git a/ModAssistant/Libs/semver/IntExtensions.cs b/ModAssistant/Libs/semver/IntExtensions.cs new file mode 100644 index 00000000..b715259f --- /dev/null +++ b/ModAssistant/Libs/semver/IntExtensions.cs @@ -0,0 +1,55 @@ +/* +Copyright (c) 2013 Max Hauser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +using System.Text; + +namespace ModAssistant.Libs +{ + internal static class IntExtensions + { + /// + /// The number of digits in a non-negative number. Returns 1 for all + /// negative numbers. That is ok because we are using it to calculate + /// string length for a for numbers that + /// aren't supposed to be negative, but when they are it is just a little + /// slower. + /// + /// + /// This approach is based on https://stackoverflow.com/a/51099524/268898 + /// where the poster offers performance benchmarks showing this is the + /// fastest way to get a number of digits. + /// + public static int Digits(this int n) + { + if (n < 10) return 1; + if (n < 100) return 2; + if (n < 1_000) return 3; + if (n < 10_000) return 4; + if (n < 100_000) return 5; + if (n < 1_000_000) return 6; + if (n < 10_000_000) return 7; + if (n < 100_000_000) return 8; + if (n < 1_000_000_000) return 9; + return 10; + } + } +} diff --git a/ModAssistant/Libs/semver/SemVersion.cs b/ModAssistant/Libs/semver/SemVersion.cs new file mode 100644 index 00000000..94a25418 --- /dev/null +++ b/ModAssistant/Libs/semver/SemVersion.cs @@ -0,0 +1,598 @@ +/* +Copyright (c) 2013 Max Hauser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +using System; +using System.Globalization; +using System.Text; +#if !NETSTANDARD +using System.Runtime.Serialization; +using System.Security.Permissions; +#endif +using System.Text.RegularExpressions; + +namespace ModAssistant.Libs +{ + /// + /// A semantic version implementation. + /// Conforms with v2.0.0 of http://semver.org + /// +#if NETSTANDARD + public sealed class SemVersion : IComparable, IComparable +#else + [Serializable] + public sealed class SemVersion : IComparable, IComparable, ISerializable +#endif + { + private static readonly Regex ParseEx = + new Regex(@"^(?\d+)" + + @"(?>\.(?\d+))?" + + @"(?>\.(?\d+))?" + + @"(?>\-(?
[0-9A-Za-z\-\.]+))?" +
+                @"(?>\+(?[0-9A-Za-z\-\.]+))?$",
+#if NETSTANDARD
+                RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture,
+#else
+                RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture,
+#endif
+                TimeSpan.FromSeconds(0.5));
+
+#if !NETSTANDARD
+#pragma warning disable CA1801 // Parameter unused
+        /// 
+        /// Deserialize a .
+        /// 
+        /// The  parameter is null.
+        private SemVersion(SerializationInfo info, StreamingContext context)
+#pragma warning restore CA1801 // Parameter unused
+        {
+            if (info == null) throw new ArgumentNullException(nameof(info));
+            var semVersion = Parse(info.GetString("SemVersion"));
+            Major = semVersion.Major;
+            Minor = semVersion.Minor;
+            Patch = semVersion.Patch;
+            Prerelease = semVersion.Prerelease;
+            Build = semVersion.Build;
+        }
+#endif
+
+        /// 
+        /// Constructs a new instance of the  class.
+        /// 
+        /// The major version.
+        /// The minor version.
+        /// The patch version.
+        /// The prerelease version (e.g. "alpha").
+        /// The build metadata (e.g. "nightly.232").
+        public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = "", string build = "")
+        {
+            Major = major;
+            Minor = minor;
+            Patch = patch;
+
+            Prerelease = prerelease ?? "";
+            Build = build ?? "";
+        }
+
+        /// 
+        /// Constructs a new instance of the  class from
+        /// a .
+        /// 
+        /// The  that is used to initialize
+        /// the Major, Minor, Patch and Build.
+        /// A  with the same Major and Minor version.
+        /// The Patch version will be the fourth part of the version number. The
+        /// build meta data will contain the third part of the version number if
+        /// it is greater than zero.
+        public SemVersion(Version version)
+        {
+            if (version == null)
+                throw new ArgumentNullException(nameof(version));
+
+            Major = version.Major;
+            Minor = version.Minor;
+
+            if (version.Revision >= 0)
+                Patch = version.Revision;
+
+            Prerelease = "";
+
+            Build = version.Build > 0 ? version.Build.ToString(CultureInfo.InvariantCulture) : "";
+        }
+
+        /// 
+        /// Converts the string representation of a semantic version to its  equivalent.
+        /// 
+        /// The version string.
+        /// If set to  minor and patch version are required,
+        /// otherwise they are optional.
+        /// The  object.
+        /// The  is .
+        /// The  has an invalid format.
+        /// The  is missing Minor or Patch versions and  is .
+        /// The Major, Minor, or Patch versions are larger than int.MaxValue.
+        public static SemVersion Parse(string version, bool strict = false)
+        {
+            var match = ParseEx.Match(version);
+            if (!match.Success)
+                throw new ArgumentException($"Invalid version '{version}'.", nameof(version));
+
+            var major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
+
+            var minorMatch = match.Groups["minor"];
+            int minor = 0;
+            if (minorMatch.Success)
+                minor = int.Parse(minorMatch.Value, CultureInfo.InvariantCulture);
+            else if (strict)
+                throw new InvalidOperationException("Invalid version (no minor version given in strict mode)");
+
+            var patchMatch = match.Groups["patch"];
+            int patch = 0;
+            if (patchMatch.Success)
+                patch = int.Parse(patchMatch.Value, CultureInfo.InvariantCulture);
+            else if (strict)
+                throw new InvalidOperationException("Invalid version (no patch version given in strict mode)");
+
+            var prerelease = match.Groups["pre"].Value;
+            var build = match.Groups["build"].Value;
+
+            return new SemVersion(major, minor, patch, prerelease, build);
+        }
+
+        /// 
+        /// Converts the string representation of a semantic version to its 
+        /// equivalent and returns a value that indicates whether the conversion succeeded.
+        /// 
+        /// The version string.
+        /// When the method returns, contains a  instance equivalent
+        /// to the version string passed in, if the version string was valid, or  if the
+        /// version string was not valid.
+        /// If set to  minor and patch version are required,
+        /// otherwise they are optional.
+        ///  when a invalid version string is passed, otherwise .
+        public static bool TryParse(string version, out SemVersion semver, bool strict = false)
+        {
+            semver = null;
+            if (version is null) return false;
+
+            var match = ParseEx.Match(version);
+            if (!match.Success) return false;
+
+            if (!int.TryParse(match.Groups["major"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var major))
+                return false;
+
+            var minorMatch = match.Groups["minor"];
+            int minor = 0;
+            if (minorMatch.Success)
+            {
+                if (!int.TryParse(minorMatch.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out minor))
+                    return false;
+            }
+            else if (strict) return false;
+
+            var patchMatch = match.Groups["patch"];
+            int patch = 0;
+            if (patchMatch.Success)
+            {
+                if (!int.TryParse(patchMatch.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out patch))
+                    return false;
+            }
+            else if (strict) return false;
+
+            var prerelease = match.Groups["pre"].Value;
+            var build = match.Groups["build"].Value;
+
+            semver = new SemVersion(major, minor, patch, prerelease, build);
+            return true;
+        }
+
+        /// 
+        /// Checks whether two semantic versions are equal.
+        /// 
+        /// The first version to compare.
+        /// The second version to compare.
+        ///  if the two values are equal, otherwise .
+        public static bool Equals(SemVersion versionA, SemVersion versionB)
+        {
+            if (ReferenceEquals(versionA, versionB)) return true;
+            if (versionA is null || versionB is null) return false;
+            return versionA.Equals(versionB);
+        }
+
+        /// 
+        /// Compares the specified versions.
+        /// 
+        /// The first version to compare.
+        /// The second version to compare.
+        /// A signed number indicating the relative values of  and .
+        public static int Compare(SemVersion versionA, SemVersion versionB)
+        {
+            if (ReferenceEquals(versionA, versionB)) return 0;
+            if (versionA is null) return -1;
+            if (versionB is null) return 1;
+            return versionA.CompareTo(versionB);
+        }
+
+        /// 
+        /// Make a copy of the current instance with changed properties.
+        /// 
+        /// The value to replace the major version or  to leave it unchanged.
+        /// The value to replace the minor version or  to leave it unchanged.
+        /// The value to replace the patch version or  to leave it unchanged.
+        /// The value to replace the prerelease version or  to leave it unchanged.
+        /// The value to replace the build metadata or  to leave it unchanged.
+        /// The new version object.
+        /// 
+        /// The change method is intended to be called using named argument syntax, passing only
+        /// those fields to be changed.
+        /// 
+        /// 
+        /// To change only the patch version:
+        /// version.Change(patch: 4)
+        /// 
+        public SemVersion Change(int? major = null, int? minor = null, int? patch = null,
+            string prerelease = null, string build = null)
+        {
+            return new SemVersion(
+                major ?? Major,
+                minor ?? Minor,
+                patch ?? Patch,
+                prerelease ?? Prerelease,
+                build ?? Build);
+        }
+
+        /// 
+        /// Gets the major version.
+        /// 
+        /// 
+        /// The major version.
+        /// 
+        public int Major { get; }
+
+        /// 
+        /// Gets the minor version.
+        /// 
+        /// 
+        /// The minor version.
+        /// 
+        public int Minor { get; }
+
+        /// 
+        /// Gets the patch version.
+        /// 
+        /// 
+        /// The patch version.
+        /// 
+        public int Patch { get; }
+
+        /// 
+        /// Gets the prerelease version.
+        /// 
+        /// 
+        /// The prerelease version. Empty string if this is a release version.
+        /// 
+        public string Prerelease { get; }
+
+        /// 
+        /// Gets the build metadata.
+        /// 
+        /// 
+        /// The build metadata. Empty string if there is no build metadata.
+        /// 
+        public string Build { get; }
+
+        /// 
+        /// Returns the  equivalent of this version.
+        /// 
+        /// 
+        /// The  equivalent of this version.
+        /// 
+        public override string ToString()
+        {
+            // Assume all separators ("..-+"), at most 2 extra chars
+            var estimatedLength = 4 + Major.Digits() + Minor.Digits() + Patch.Digits()
+                                  + Prerelease.Length + Build.Length;
+            var version = new StringBuilder(estimatedLength);
+            version.Append(Major);
+            version.Append('.');
+            version.Append(Minor);
+            version.Append('.');
+            version.Append(Patch);
+            if (Prerelease.Length > 0)
+            {
+                version.Append('-');
+                version.Append(Prerelease);
+            }
+            if (Build.Length > 0)
+            {
+                version.Append('+');
+                version.Append(Build);
+            }
+            return version.ToString();
+        }
+
+        /// 
+        /// Compares the current instance with another object of the same type and returns an integer that indicates
+        /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
+        /// other object.
+        /// 
+        /// An object to compare with this instance.
+        /// 
+        /// A value that indicates the relative order of the objects being compared.
+        /// The return value has these meanings:
+        ///  Less than zero: This instance precedes  in the sort order.
+        ///  Zero: This instance occurs in the same position in the sort order as .
+        ///  Greater than zero: This instance follows  in the sort order.
+        /// 
+        /// The  is not a .
+        public int CompareTo(object obj)
+        {
+            return CompareTo((SemVersion)obj);
+        }
+
+        /// 
+        /// Compares the current instance with another object of the same type and returns an integer that indicates
+        /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
+        /// other object.
+        /// 
+        /// An object to compare with this instance.
+        /// 
+        /// A value that indicates the relative order of the objects being compared.
+        /// The return value has these meanings:
+        ///  Less than zero: This instance precedes  in the sort order.
+        ///  Zero: This instance occurs in the same position in the sort order as .
+        ///  Greater than zero: This instance follows  in the sort order.
+        /// 
+        public int CompareTo(SemVersion other)
+        {
+            var r = CompareByPrecedence(other);
+            if (r != 0) return r;
+
+#pragma warning disable CA1062 // Validate arguments of public methods
+            // If other is null, CompareByPrecedence() returns 1
+            return CompareComponent(Build, other.Build);
+#pragma warning restore CA1062 // Validate arguments of public methods
+        }
+
+        /// 
+        /// Returns whether two semantic versions have the same precedence. Versions
+        /// that differ only by build metadata have the same precedence.
+        /// 
+        /// The semantic version to compare to.
+        ///  if the version precedences are equal.
+        public bool PrecedenceMatches(SemVersion other)
+        {
+            return CompareByPrecedence(other) == 0;
+        }
+
+        /// 
+        /// Compares two semantic versions by precedence as defined in the SemVer spec. Versions
+        /// that differ only by build metadata have the same precedence.
+        /// 
+        /// The semantic version.
+        /// 
+        /// A value that indicates the relative order of the objects being compared.
+        /// The return value has these meanings:
+        ///  Less than zero: This instance precedes  in the sort order.
+        ///  Zero: This instance occurs in the same position in the sort order as .
+        ///  Greater than zero: This instance follows  in the sort order.
+        /// 
+        public int CompareByPrecedence(SemVersion other)
+        {
+            if (other is null)
+                return 1;
+
+            var r = Major.CompareTo(other.Major);
+            if (r != 0) return r;
+
+            r = Minor.CompareTo(other.Minor);
+            if (r != 0) return r;
+
+            r = Patch.CompareTo(other.Patch);
+            if (r != 0) return r;
+
+            return CompareComponent(Prerelease, other.Prerelease, true);
+        }
+
+        private static int CompareComponent(string a, string b, bool nonemptyIsLower = false)
+        {
+            var aEmpty = string.IsNullOrEmpty(a);
+            var bEmpty = string.IsNullOrEmpty(b);
+            if (aEmpty && bEmpty)
+                return 0;
+
+            if (aEmpty)
+                return nonemptyIsLower ? 1 : -1;
+            if (bEmpty)
+                return nonemptyIsLower ? -1 : 1;
+
+            var aComps = a.Split('.');
+            var bComps = b.Split('.');
+
+            var minLen = Math.Min(aComps.Length, bComps.Length);
+            for (int i = 0; i < minLen; i++)
+            {
+                var ac = aComps[i];
+                var bc = bComps[i];
+                var aIsNum = int.TryParse(ac, out var aNum);
+                var bIsNum = int.TryParse(bc, out var bNum);
+                int r;
+                if (aIsNum && bIsNum)
+                {
+                    r = aNum.CompareTo(bNum);
+                    if (r != 0) return r;
+                }
+                else
+                {
+                    if (aIsNum)
+                        return -1;
+                    if (bIsNum)
+                        return 1;
+                    r = string.CompareOrdinal(ac, bc);
+                    if (r != 0)
+                        return r;
+                }
+            }
+
+            return aComps.Length.CompareTo(bComps.Length);
+        }
+
+        /// 
+        /// Determines whether the specified  is equal to this instance.
+        /// 
+        /// The  to compare with this instance.
+        /// 
+        ///    if the specified  is equal to this instance, otherwise .
+        /// 
+        /// The  is not a .
+        public override bool Equals(object obj)
+        {
+            if (obj is null)
+                return false;
+
+            if (ReferenceEquals(this, obj))
+                return true;
+
+            var other = (SemVersion)obj;
+
+            return Major == other.Major
+                && Minor == other.Minor
+                && Patch == other.Patch
+                && string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal)
+                && string.Equals(Build, other.Build, StringComparison.Ordinal);
+        }
+
+        /// 
+        /// Returns a hash code for this instance.
+        /// 
+        /// 
+        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+        /// 
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                // TODO verify this. Some versions start result = 17. Some use 37 instead of 31
+                int result = Major.GetHashCode();
+                result = result * 31 + Minor.GetHashCode();
+                result = result * 31 + Patch.GetHashCode();
+                result = result * 31 + Prerelease.GetHashCode();
+                result = result * 31 + Build.GetHashCode();
+                return result;
+            }
+        }
+
+#if !NETSTANDARD
+        /// 
+        /// Populates a  with the data needed to serialize the target object.
+        /// 
+        /// The  to populate with data.
+        /// The destination (see ) for this serialization.
+        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
+        public void GetObjectData(SerializationInfo info, StreamingContext context)
+        {
+            if (info == null) throw new ArgumentNullException(nameof(info));
+            info.AddValue("SemVersion", ToString());
+        }
+#endif
+
+#pragma warning disable CA2225 // Operator overloads have named alternates
+        /// 
+        /// Implicit conversion from  to .
+        /// 
+        /// The semantic version.
+        /// The  object.
+        /// The  is .
+        /// The version number has an invalid format.
+        /// The Major, Minor, or Patch versions are larger than int.MaxValue.
+        public static implicit operator SemVersion(string version)
+#pragma warning restore CA2225 // Operator overloads have named alternates
+        {
+            return Parse(version);
+        }
+
+        /// 
+        /// Compares two semantic versions for equality.
+        /// 
+        /// The left value.
+        /// The right value.
+        /// If left is equal to right , otherwise .
+        public static bool operator ==(SemVersion left, SemVersion right)
+        {
+            return Equals(left, right);
+        }
+
+        /// 
+        /// Compares two semantic versions for inequality.
+        /// 
+        /// The left value.
+        /// The right value.
+        /// If left is not equal to right , otherwise .
+        public static bool operator !=(SemVersion left, SemVersion right)
+        {
+            return !Equals(left, right);
+        }
+
+        /// 
+        /// Compares two semantic versions.
+        /// 
+        /// The left value.
+        /// The right value.
+        /// If left is greater than right , otherwise .
+        public static bool operator >(SemVersion left, SemVersion right)
+        {
+            return Compare(left, right) > 0;
+        }
+
+        /// 
+        /// Compares two semantic versions.
+        /// 
+        /// The left value.
+        /// The right value.
+        /// If left is greater than or equal to right , otherwise .
+        public static bool operator >=(SemVersion left, SemVersion right)
+        {
+            return Equals(left, right) || Compare(left, right) > 0;
+        }
+
+        /// 
+        /// Compares two semantic versions.
+        /// 
+        /// The left value.
+        /// The right value.
+        /// If left is less than right , otherwise .
+        public static bool operator <(SemVersion left, SemVersion right)
+        {
+            return Compare(left, right) < 0;
+        }
+
+        /// 
+        /// Compares two semantic versions.
+        /// 
+        /// The left value.
+        /// The right value.
+        /// If left is less than or equal to right , otherwise .
+        public static bool operator <=(SemVersion left, SemVersion right)
+        {
+            return Equals(left, right) || Compare(left, right) < 0;
+        }
+    }
+}
diff --git a/ModAssistant/Localisation/de.xaml b/ModAssistant/Localisation/de.xaml
new file mode 100644
index 00000000..1d02ce20
--- /dev/null
+++ b/ModAssistant/Localisation/de.xaml
@@ -0,0 +1,244 @@
+
+    i18n:de-DE
+
+    
+    Der Beat Saber Installationsordner konnte nicht gefunden werden!
+    Drücke OK um es erneut zu versuchen, oder Abbrechen um das Programm zu beenden.
+    Ungültiges Argument! '{0}' benötigt eine Option.
+    Unbekanntes Argument. Beende Mod Assistant.
+    Eine nicht behandelte Ausnahme ist aufgetreten
+    Ausnahme
+
+    
+    ModAssistant
+    Intro
+    Mods
+    Über
+    Optionen
+    Spiel Version
+    Version
+    Mod Info
+    Installieren/
+    Aktualisieren
+    Spielversion konnte nicht geladen werden, der Mods Tab wird nicht verfügbar sein.
+    Neue Spielversion gefunden!
+    Es scheint ein Spiel Update gegeben zu haben.
+    Bitte prüfe ob unten links die richtige Version ausgewählt ist!
+    Kein Mod ausgewählt!
+    {0} hat keine Informationsseite.
+
+    
+    Intro
+    Willkommen bei Mod Assistant
+    Bitte lies diese Seite vollständig und aufmerksam!
+    
+        Durch Nutzung des Programms wird bestätigt, dass folgende Bedingungen gelesen und akzeptiert wurden:
+    
+    
+        Beat Saber
+        unterstützt normalerweise keine Mods. Das heißt:
+    
+    
+        Mods
+        werden nach jedem Update nicht mehr funktionieren. Dies ist normal, und
+        die Schuld liegt nicht bei Beat Games.
+    
+    
+        Mods
+        werden Fehler und Leistungsprobleme verursachen. Die Schuld
+        liegt nicht bei Beat Games.
+    
+    
+        Mods werden
+        kostenlos von Leuten in deren
+        Freizeit erstellt. Bitte sei geduldig und verständnisvoll.
+    
+    
+        Bitte gib KEINE schlechten Bewertungen weil die Mods nicht funktionieren. Die Schuld
+        liegt nicht bei Beat Games.
+         Sie versuchen nicht die Mods zu unterbinden.
+    
+    
+        Wenn ich weiterhin schlecht Bewertungen
+        wegen nicht funktionierenden Mods sehe,
+        
+        Werde ich persönlich die Mods mit einem rostigen Löffel töten
+    
+    
+        Bitte lies den Einsteiger Leitfaden im
+        
+            Wiki
+        .
+    
+    Annehmen
+    Ablehnen
+    Programm wird beendet: Du hast den Bedingungen nicht zugestimmt.
+    Versionsliste konnte nicht geladen werden
+    Mods Tab deaktiviert. Bitte Programm neu starten um es nochmal zu versuchen.
+    Du kannst jetzt den Mods Tab benutzen!
+
+    
+    Mods
+    Name
+    Installiert
+    Neuste
+    Beschreibung
+    Entfernen
+    Entfernen
+    Modliste konnte nicht geladen werden
+    Prüfe installierte Mods
+    Lade Mods
+    Laden der Mods abgeschlossen
+    Installiere {0}
+    {0} installiert
+    Mod Installation abgeschlossen
+    Downloadlink für {0} konnte nicht gefunden werden
+    {0} entfernen?
+    Bist du dir sicher das du {0} entfernen möchtest?
+    Dies kann die anderen Mods unbrauchbar machen
+    Fehler beim Extrahieren von {0}, neuer Versuch in {1} Sekunden. ({2}/{3})
+    Fehler beim Extrahieren von {0} nach {1} Versuchen, wird übersprungen. Dieser Mod funktioniert möglicherweise nicht richtig, also gehe auf eigenes Risiko vor
+    Suchen...
+
+    
+    Über
+    Über Mod Assistant
+    Ich bin Assistant und ich habe Mod Assistant als Assistent für Mods mit ein paar Prinzipen im Auge gemacht:
+    Einfachheit
+    Portabilität
+    Nur eine Datei
+    Verantwortungsbewusster Umgang
+    
+        Wenn dir das Programm gefällt und du mich unterstützen möchtest, dann besuche meine 
+        
+            Spendenseite
+        
+         oder mein 
+        
+            Patreon
+        
+    
+    Besonderer Dank ♥
+    Spenden
+    Kopf tätscheln
+    Umarmungen
+
+    
+    Optionen
+    Einstellungen
+    Installationsordner
+    Ordner wählen
+    Ordner öffnen
+    Ausgewählte Mods speichern
+    Installierte Mods prüfen
+    Installierte Mods auswählen
+    Installierte Mods neu installieren
+    OneClick™ Installation aktivieren
+    BeatSaver
+    ModelSaber
+    Playlists 
+    Close window when finished 
+    Spiel Typ
+    Steam
+    Oculus
+    Werkzeuge
+    Playlist installieren
+    Installiere Playlist: {0}
+    Titel fehlgeschlagen: {0}
+    [{0} Fehler] Playlist Installation abgeschlossen: {1}
+    Diagnose
+    Log öffnen
+    AppData öffnen
+    BSIPA entfernen
+    Mods entfernen
+    Design
+    Exportieren
+    Log wird hochgeladen
+    Log URL in die Zwischenablage kopiert!
+    Log Hochladen fehlgeschlagen!
+    Log Hochladen fehlgeschlagen!
+    Log Datei konnte nicht zu Teknik hochgeladen werden, bitte nochmal versuchen oder die Datei manuell senden.
+    Lade Liste der Mods
+    Suche BSIPA Version
+    BSIPA entfernt
+    Alle Mods entfernen?
+    Bist du dir sicher das du ALLE Mods entfernen möchtest?
+    Dies kann nicht rückgängig gemacht werden.
+    Alle Mods entfernt
+    Aktuelles Design wurde entfernt, gehe zurück zum Standart...
+    Designs Ordner nicht gefunden! Versuche die Vorlage zu exportieren...
+    AppData Ordner nicht gefunden! Versuche dein Spiel zu starten.
+
+    
+    Lade Mods
+
+    
+    Ungültig
+    Ungültige Installation erkannt
+    Die SPielinstallation ist beschädigt oder anderweitig ungültig
+    Dies kann passieren wenn dein Spiel eine Raubkopie ist oder eine Raubkopie über eine legitime Version kopiert wurde
+    
+        Falls dein Spiel eine Raubkopie ist,
+        bitte kaufe das Spiel 
+            
+                HIER
+            
+        .
+    
+    
+        Wenn dein Spiel
+        keine Raubkopie ist, bitte
+        
+            mach eine saubere Neuinstallation
+        .
+    
+    
+        Falls das nicht hilft, frage im
+        #support Kanal in der
+        
+            BSMG
+        .
+    
+    Falls du eine Raubkopie hattest aber das Spiel jetzt gekauft hast
+    Ordner auswählen
+    Muss Mod Assistant neu gestartet werden wenn eine legitime Version installiert wurde
+
+    
+    Map Details konnten nicht geladen werden.
+    Titel konnte nicht geladen werden.
+    Titel konnte nicht geladen werden.
+    Möglicherweise gibt es Probleme mit BeatSaver oder deiner Internetverbindung.
+    Herunterladen der Titel ZIP fehlgeschlagen
+    Beat Saber Installationspfad nicht gefunden.
+    Installiert: {0}
+    Installation fehlgeschlagen.
+    {0} OneClick™ Installation Handler registriert!
+    {0} OneClick™ Installation Handler entfernt!
+    Installing: {0} 
+    Max tries reached: Skipping {0} 
+    Ratelimit hit. Resuming in {0} 
+    Download failed: {0} 
+
+    
+    Design nicht gefunden, gehe zurück zum Standard Design...
+    Design gesetzt auf: {0}.
+    {0} existiert nicht.
+    Designvorlage "{0}" in Design Ordner gespeichert.
+    Designvorlage existiert bereits!
+    Fehler beim Laden der .xaml Datei von Design {0}: {1}
+
+    
+    Konnte nicht auf Aktualisierungen prüfen.
+    Konnte Aktualisierung nicht herunterladen.
+
+    
+    Mod Assistant
+    Beat Saber Installationsordner konnte nicht erkannt werden. Bitte manuell auswählen.
+    Mod Assistant muss diese Aufgabe mit Administrator Rechten ausführen. Bitte nochmal versuchen.
+    Wähle den Beat Saber Installationsordner aus
+    Ordner konnte nicht geöffnet werden: {0}
+
diff --git a/ModAssistant/Localisation/en-DEBUG.xaml b/ModAssistant/Localisation/en-DEBUG.xaml
new file mode 100644
index 00000000..d7daea69
--- /dev/null
+++ b/ModAssistant/Localisation/en-DEBUG.xaml
@@ -0,0 +1,185 @@
+
+    i18n:en-DEBUG
+
+    
+    App:InstallDirDialog:Title
+    App:InstallDirDialog:OkCancel
+    {0} App:InvalidArgument
+    App:UnrecognizedArgument
+    App:UnhandledException
+    App:Exception
+
+    
+    MainWindow:WindowTitle
+    MainWindow:IntroButton
+    MainWindow:ModsButton
+    MainWindow:AboutButton
+    MainWindow:OptionsButton
+    MainWindow:GameVersionLabel
+    MainWindow:VersionLabel
+    MainWindow:ModInfoButton
+    MainWindow:InstallButtonTop
+    MainWindow:InstallButtonBottom
+    MainWindow:GameVersionLoadFailed
+    MainWindow:GameUpdateDialog:Title
+    MainWindow:GameUpdateDialog:Line1
+    MainWindow:GameUpdateDialog:Line2
+    MainWindow:NoModSelected
+    {0} MainWindow:NoModInfoPage
+
+    
+    Intro:Title
+    Intro:PageTitle
+    Intro:Terms:Header
+    Intro:Terms:Line1
+    Intro:Terms:Line2
+    Intro:Terms:Term1
+    Intro:Terms:Term2
+    Intro:Terms:Term3
+    Intro:ReviewsBeatGamesFault
+    Intro:ReviewsRustySpoon
+    Intro:WikiGuide
+    Intro:AgreeButton
+    Intro:DisagreeButton
+    Intro:ClosingApp
+    Intro:VersionDownloadFailed
+    Intro:ModsTabDisabled
+    Intro:ModsTabEnabled
+
+    
+    Mods:Title
+    Mods:Header:Name
+    Mods:Header:Installed
+    Mods:Header:Latest
+    Mods:Header:Description
+    Mods:Header:Uninstall
+    Mods:UninstallButton
+    Mods:LoadFailed
+    Mods:CheckingInstalledMods
+    Mods:LoadingMods
+    Mods:FinishedLoadingMods
+    {0} Mods:InstallingMod
+    {0} Mods:InstalledMod
+    Mods:FinishedInstallingMods
+    Mods:ModDownloadLinkMissing
+    {0} Mods:UninstallBox:Title
+    {0} Mods:UninstallBox:Body1
+    Mods:UninstallBox:Body2
+    {0} {1} {2} {3} Mods:FailedExtract
+    {0} {1} Mods:FailedExtractMaxReached
+    Mods:SearchLabel
+
+    
+    About:Title
+    About:PageTitle
+    About:List:Header
+    About:List:Item1
+    About:List:Item2
+    About:List:Item3
+    About:List:Item4
+    About:SupportAssistant
+    About:SpecialThanks
+    About:Donate
+    About:HeadpatsButton
+    About:HugsButton
+
+    
+    Options:Title
+    Options:PageTitle
+    Options:InstalFolder
+    Options:SelectFolderButton
+    Options:OpenFolderButton
+    Options:SaveSelectedMods
+    Options:CheckInstalledMods
+    Options:SelectInstalledMods
+    Options:Reinstall Installed Mods
+    Options:EnableOneClickInstalls
+    Options:BeatSaver
+    Options:ModelSaber
+    Options:Playlists
+    Options:CloseWindow
+    Options:GameType
+    Options:GameType:Steam
+    Options:GameType:Oculus
+    Options:Tools
+    Options:InstallPlaylist
+    {0} Options:InstallingPlaylist
+    {0} Options:FailedPlaylistSong
+    {0} {1} Options:FinishedPlaylist
+    Options:Diagnostics
+    Options:OpenLogsButton
+    Options:OpenAppDataButton
+    Options:UninstallBSIPAButton
+    Options:RemoveAllModsButton
+    Options:ApplicationTheme
+    Options:ExportTemplateButton
+    Options:UploadingLog
+    Options:LogUrlCopied
+    Options:LogUploadFailed
+    Options:LogUploadFailed:Title
+    Options:LogUploadFailed:Body
+    Options:GettingModList
+    Options:FindingBSIPAVersion
+    Options:BSIPAUninstalled
+    Options:YeetModsBox:Title
+    Options:YeetModsBox:RemoveAllMods
+    Options:YeetModsBox:CannotBeUndone
+    Options:AllModsUninstalled
+    Options:CurrentThemeRemoved
+    Options:ThemeFolderNotFound
+    Options:AppDataNotFound
+
+    
+    Loading:Loading
+
+    
+    Invalid:Title
+    Invalid:PageTitle
+    Invalid:PageSubtitle
+    Invalid:List:Header
+    Invalid:List:Line1
+    Invalid:List:Line2
+    Invalid:List:Line3
+    Invalid:BoughtGame1
+    Invalid:SelectFolderButton
+    Invalid:BoughtGame2
+
+    
+    OneClick:MapDownloadFailed
+    OneClick:SongDownloadFailed
+    OneClick:SongDownload:Failed
+    OneClick:SongDownload:NetworkIssues
+    OneClick:SongDownload:FailedTitle
+    OneClick:InstallDirNotFound
+    {0} OneClick:InstalledAsset
+    OneClick:AssetInstallFailed
+    {0} OneClick:ProtocolHandler:Registered
+    {0} OneClick:ProtocolHandler:Unregistered
+    {0} OneClick:Installing
+    {0} OneClick:RatelimitSkip
+    {0} OneClick:RatelimitHit
+    {0} OneClick:Failed
+
+    
+    Themes:ThemeNotFound
+    Themes:ThemeSet {0}
+    {0} Themes:ThemeMissing
+    Themes:SavedTemplateTheme {0}
+    Themes:TemplateThemeExists
+    Themes:FailedToLoadXaml {0} {1}
+
+    
+    Updater:CheckFailed
+    Updater:DownloadFailed
+
+    
+    Utils:NotificationTitle
+    Utils:NoInstallFolder
+    Utils:RunAsAdmin
+    Utils:InstallDir:DialogTitle
+    Utils:CannotOpenFolder {0}
+
diff --git a/ModAssistant/Localisation/en.xaml b/ModAssistant/Localisation/en.xaml
new file mode 100644
index 00000000..d59414c9
--- /dev/null
+++ b/ModAssistant/Localisation/en.xaml
@@ -0,0 +1,244 @@
+
+    i18n:en-US
+
+    
+    Couldn't find your Beat Saber install folder!
+    Press OK to try again, or Cancel to close application.
+    Invalid argument! '{0}' requires an option.
+    Unrecognized argument. Closing Mod Assistant.
+    An unhandled exception just occurred
+    Exception
+
+    
+    ModAssistant
+    Intro
+    Mods
+    About
+    Options
+    Game Version
+    Version
+    Mod Info
+    Install
+    or Update
+    Could not load game versions, Mods tab will be unavailable.
+    New Game Version Detected!
+    It looks like there's been a game update.
+    Please double check that the correct version is selected at the bottom left corner!
+    No mod selected!
+    {0} does not have an info page.
+
+    
+    Intro
+    Welcome to Mod Assistant
+    Please read this page entirely and carefully
+    
+        By using this program attest to have read and agree to the following terms:
+    
+    
+        Beat Saber
+        does not natively support mods. This means:
+    
+    
+        Mods
+        will break every update. This is normal, and
+        not Beat Games' fault.
+    
+    
+        Mods
+        will cause bugs and performance issues. This is
+        not Beat Games' fault.
+    
+    
+        Mods are made for
+        free by people in their
+        free time. Please be patient and understanding.
+    
+    
+        DO NOT leave negative reviews because mods broke. This is
+        not Beat Games' fault.
+         They are not trying to kill mods.
+    
+    
+        If I keep seeing people leave negative reviews
+        because mods broke,
+        
+        I will personally kill mods with a rusty spoon
+    
+    
+        Please read the Beginners Guide on the
+        
+            Wiki
+        .
+    
+    I Agree
+    Disagree
+    Closing Application: You did not agree to terms and conditions.
+    Could not download versions list
+    Mods tab disabled. Please restart to try again.
+    You can now use the Mods tab!
+
+    
+    Mods
+    Name
+    Installed
+    Latest
+    Description
+    Uninstall
+    Uninstall
+    Could not load mods list
+    Checking installed mods
+    Loading Mods
+    Finished loading mods
+    Installing {0}
+    Installed {0}
+    Finished installing mods
+    Could not find download link for {0}
+    Uninstall {0}?
+    Are you sure you want to remove {0}?
+    This could break your other mods
+    Failed to extract {0}, trying again in {1} seconds. ({2}/{3})
+    Failed to extract {0} after max attempts ({1}), skipping. This mod might not work properly so proceed at your own risk
+    Search...
+
+    
+    About
+    About Mod Assistant
+    I'm Assistant, and I made Mod Assistant for mod assistance, with a few principles in mind:
+    Simplicity
+    Portability
+    Single Executable
+    Responsible use
+    
+        If you enjoy this program and would like to support me, please visit my
+        
+            donation page
+        
+        or my
+        
+            Patreon
+        
+    
+    Special Thanks ♥
+    Donate
+    Headpats
+    Hugs
+
+    
+    Options
+    Settings
+    Install Folder
+    Select Folder
+    Open Folder
+    Save Selected Mods
+    Detect Installed Mods
+    Select Installed Mods
+    Reinstall Installed Mods
+    Enable OneClick™ Installs
+    BeatSaver
+    ModelSaber
+    Playlists
+    Close window when finished
+    Game Type
+    Steam
+    Oculus
+    Tools
+    Install Playlist
+    Installing Playlist: {0}
+    Failed song: {0}
+    [{0} fails] Finished Installing Playlist: {1}
+    Diagnostics
+    Open Logs
+    Open AppData
+    Uninstall BSIPA
+    Remove All Mods
+    Application Theme
+    Export Template
+    Uploading Log
+    Log URL Copied To Clipboard!
+    Uploading Log Failed
+    Uploading log failed!
+    Could not upload log file to Teknik, please try again or send the file manually.
+    Getting Mod List
+    Finding BSIPA Version
+    BSIPA Uninstalled
+    Uninstall All Mods?
+    Are you sure you want to remove ALL mods?
+    This cannot be undone.
+    All Mods Uninstalled
+    Current theme has been removed, reverting to default...
+    Themes folder not found! Try exporting the template...
+    AppData folder not found! Try launching your game.
+
+    
+    Loading Mods
+
+    
+    Invalid
+    Invalid Installation Detected
+    Your game installation is corrupted or otherwise invalid
+    This can happen if your copy of the game is pirated, or if you copied a pirated copy over your legit install
+    
+        If your copy of the game is pirated,
+        please purchase the game
+            
+                HERE
+            
+        .
+    
+    
+        If your copy of the game is
+        not pirated, please
+        
+            do a clean install
+        .
+    
+    
+        If those don't help, ask for support in the
+        #support channel in
+        
+            BSMG
+        .
+    
+    If you used to have a pirated version but have since bought the game
+    Select Folder
+    You will need to restart Mod Assistant after changing to the legit install
+
+    
+    Could not get map details.
+    Could not download the song.
+    Could not download the song.
+    There might be issues with BeatSaver or your internet connection.
+    Failed to download song ZIP
+    Beat Saber installation path not found.
+    Installed: {0}
+    Failed to install.
+    {0} OneClick™ Install handlers registered!
+    {0} OneClick™ Install handlers unregistered!
+    Installing: {0}
+    Max tries reached: Skipping {0}
+    Ratelimit hit. Resuming in {0}
+    Download failed: {0}
+
+    
+    Theme not found, reverting to default theme...
+    Theme set to {0}.
+    {0} does not exist.
+    Template theme "{0}" saved to Themes folder.
+    Template theme already exists!
+    Failed to load .xaml file for theme {0}: {1}
+
+    
+    Couldn't check for updates.
+    Couldn't download update.
+
+    
+    Mod Assistant
+    Could not detect your Beat Saber install folder. Please select it manually.
+    Mod Assistant needs to run this task as Admin. Please try again.
+    Select your Beat Saber install folder
+    Can't open folder: {0}
+
diff --git a/ModAssistant/Localisation/fr.xaml b/ModAssistant/Localisation/fr.xaml
new file mode 100644
index 00000000..7cc8269a
--- /dev/null
+++ b/ModAssistant/Localisation/fr.xaml
@@ -0,0 +1,251 @@
+
+    i18n:fr-FR
+
+    
+    Impossible de trouver le dossier d'installation de Beat Saber !
+    Appuyez sur OK pour réessayer, ou Annuler pour fermer l'application.
+    Argument invalide ! '{0}' nécessite une option.
+    Argument non reconnu. Fermeture de Mod Assistant.
+    Une exception non gérée est survenue
+    Exception
+
+    
+    ModAssistant
+    Intro
+    Mods
+    À propos
+    Options
+    Version du jeu
+    Version
+    Info sur le mod
+    Installer
+    ou Mettre à jour
+    Impossible de charger les versions du jeu, l'onglet Mods sera indisponible.
+    Nouvelle version du jeu détectée !
+    Il semble que le jeu a été mis à jour.
+    Veuillez vous assurer que la bonne version soit sélectionnée en bas à gauche !
+    Aucun mod sélectionné !
+    {0} n'a pas de page informative.
+
+    
+    Intro
+    Bienvenue sur Mod Assistant
+    Veuillez lire cette page entièrement et avec attention
+    
+        En utilisant ce programme, vous attestez que vous avez lu et accepté les modalités suivantes :
+    
+    
+        Beat Saber
+        ne supporte
+        pas nativement les mods. Cela signifie que :
+    
+    
+        Les mods
+        dysfonctionneront à chaque mise à jour. C'est normal, et ce
+        n'est
+        pas la faute de Beat Games.
+    
+    
+        Les mods
+        causeront des bugs et des problèmes de performance. Ce
+        n'est
+        pas la faute de Beat Games.
+    
+    
+        Les mods sont créés
+        gratuitement par des gens sur leur
+        temps libre. Veuillez être patient et compréhensif.
+    
+    
+        NE laissez
+        PAS de commentaires négatifs parce que les mods ne fonctionnent plus. Ce
+        n'est
+        pas la faute de Beat Games.
+         Ils n'essaient pas de faire disparaître les mods.
+    
+    
+        Si je continue de voir des gens laisser des commentaires négatifs
+        parce que les mods ne fonctionnent plus,
+        
+        J'irai personnellement liquider les mods avec une cuillère rouillée
+    
+    
+        Veuillez lire le Guide du Débutant sur le
+        
+            Wiki
+        .
+    
+    J'accepte
+    Je refuse
+    Fermeture de l'application : vous n'avez pas accepté les modalités et conditions.
+    Impossible de télécharger la liste des versions
+    Onglet Mods désactivé. Veuillez relancer pour réessayer.
+    Vous pouvez désormais utiliser l'onglet Mods !
+
+    
+    Mods
+    Nom
+    Installé
+    Récent
+    Description
+    Désinstaller
+    Désinstaller
+    Impossible de charger la liste des mods
+    Vérification des mods installés
+    Chargement des mods
+    Fin : mods chargés
+    Installation de {0}
+    {0} installé
+    Fin : mods installés
+    Impossible de trouver le lien de téléchargement de {0}
+    Désinstaller {0} ?
+    Êtes-vous sûr de vouloir supprimer {0} ?
+    Cela pourrait faire dysfonctionner d'autres mods installés
+    Échec de l'extraction de {0}, nouvelle tentative dans {1} secondes. ({2}/{3})
+    Échec de l'extraction de {0} après le maximum de tentatives ({1}), abandon. Ce mod pourrait ne pas fonctionner correctement, continuez à vos propres risques
+    Recherche...
+
+
+    
+    À propos
+    À propos de Mod Assistant
+    Je suis Assistant, et je réalise Mod Assistant pour l'assistance aux mods, avec quelques principes en tête :
+    Simplicité
+    Portabilité
+    Exécutable unique
+    Utilisation responsable
+    
+        Si vous aimez ce programme et souhaitez me soutenir, veuillez visiter ma
+        
+            page de don
+        
+        ou mon
+        
+            Patreon
+        
+    
+    Remerciements particuliers ♥
+    Faire un don
+    Caresses-tête
+    Câlins
+
+    
+    Options
+    Paramètres
+    Dossier d'installation
+    Sélectionner un dossier
+    Ouvrir le dossier
+    Sauvegarder les mods sélectionnés
+    Détecter les mods installés
+    Sélectionner les mods installés
+    Réinstaller les mods installés
+    Activer les installations OneClick™
+    BeatSaver
+    ModelSaber
+    Playlists 
+    Close window when finished 
+    Type du jeu
+    Steam
+    Oculus
+    Outils
+    Installer une playlist
+    Installation de la playlist : {0}
+    Échec de la musique : {0}
+    [{0} échecs] Installation de la playlist terminée : {1}
+    Diagnostic
+    Ouvrir les logs
+    Ouvrir AppData
+    Désinstaller BSIPA
+    Supprimer tous les mods
+    Thème de l'application
+    Exporter le modèle
+    Envoi des logs
+    URL des logs copiée dans le presse-papier !
+    L'envoi des logs a échoué
+    L'envoi des logs a échoué !
+    Impossible d'envoyer le fichier de logs à Teknik, veuillez réessayer ou envoyer le fichier manuellement.
+    Récupération de la liste des mods
+    Découverte de la version de BSIPA
+    BSIPA désinstallé
+    Désinstaller les mods ?
+    Êtes-vous sûr de vouloir supprimer TOUS les mods ?
+    Cela ne peut pas être annulé.
+    Tous les mods ont été désinstallés
+    Le thème actuel a été supprimé, passage au thème par défaut...
+    Dossier Themes non trouvé ! Essayez d'exporter le modèle...
+    Dossier AppData non trouvé ! Essayez de lancer votre jeu.
+
+    
+    Chargement des mods
+
+    
+    Invalide
+    Installation invalide détectée
+    Votre installation du jeu est corrompue sinon invalide
+    Cela peut survenir si votre copie du jeu est piratée, ou si vous copiez une copie piratée sur votre installation légitime
+    
+        Si votre copie du jeu est piratée,
+        veuillez acheter le jeu
+            
+                ICI
+            
+        .
+    
+    
+        Si votre copie du jeu
+        n'est
+        pas piratée, veuillez
+        
+            faire une installation propre
+        .
+    
+    
+        Si cela n'a pas fonctionné, demandez de l'aide au support dans le canal textuel
+        #support dans
+        
+            BSMG
+        .
+    
+    Si vous utilisiez une version piratée mais avez acheté le jeu depuis
+    Sélectionner un dossier
+    Vous devez relancer Mod Assistant après avoir choisi l'installation légitime
+
+    
+    Impossible de récupérer les détails de la map.
+    Impossible de télécharger la musique.
+    Impossible de télécharger la musique.
+    Il pourrait y avoir des problèmes avec BeatSaver ou votre connexion Internet.
+    Échec du téléchargement du ZIP de la musique
+    Chemin de l'installation de Beat Saber non trouvé.
+    Installé : {0}
+    Échec de l'installation.
+    {0} : gestionnaires d'installation OneClick™ inscrits !
+    {0} : gestionnaires d'installation OneClick™ désinscrits !
+    Installing: {0} 
+    Max tries reached: Skipping {0} 
+    Ratelimit hit. Resuming in {0} 
+    Download failed: {0} 
+    
+    
+    Thème non trouvé, passage au thème par défaut...
+    Thème défini sur {0}.
+    {0} n'existe pas.
+    Modèle du thème "{0}" sauvegardé dans le dossier Themes.
+    Le modèle du thème existe déjà !
+    Échec du chargement du fichier .xaml pour le thème {0} : {1}
+
+    
+    Impossible de vérifier les mises à jour.
+    Impossible de télécharger la mise à jour.
+
+    
+    Mod Assistant
+    Impossible de détecter le dossier d'installation de Beat Saber. Veuillez le sélectionner manuellement.
+    Mod Assistant a besoin de lancer cette tâche en administrateur. Veuillez réessayer.
+    Sélectionnez le dossier d'installation de Beat Saber
+    Impossible d'ouvrir le dossier : {0}
+
diff --git a/ModAssistant/Localisation/it.xaml b/ModAssistant/Localisation/it.xaml
new file mode 100644
index 00000000..d01fde6f
--- /dev/null
+++ b/ModAssistant/Localisation/it.xaml
@@ -0,0 +1,244 @@
+
+    i18n:it-IT
+
+    
+    Impossibile trovare la directory d'installazione di Beat Saber
+    Premi OK per riprovare, oppure Cancel per chiudere l'applicazione.
+    Argomento non valido! '{0}' ha bisogno di un'opzione.
+    Argomento non riconosciuto. Chiusura di Mod Assistant.
+    Si è appena verificata un'eccezione non gestita
+    Eccezione
+
+    
+    ModAssistant
+    Introduzione
+    Mod
+    Info
+    Opzioni
+    Versione del gioco
+    Versione
+    Info sulla mod
+    Installa
+    o Aggiorna
+    Non è stato possibile caricare le versioni del gioco, il menù Mod non sarà disponibile.
+    Rilevata nuova versione del gioco!
+    Sembra che ci sia stato un aggiornamento del gioco.
+    Per piacere verifica che sia selezionata la versione corretta nel menù in basso a sinistra
+    Nessuna mod selezionata!
+    {0} non ha una pagina di informazioni.
+
+    
+    Introduzione
+    Benvenuto/a in Mod Assistant
+    Ti invitiamo a leggere questa pagina fino alla fine e con cautela
+    
+        Utilizzando questo programma, attesti di accettare i seguenti termini:
+    
+    
+        Beat Saber
+        non supporta nativamente le mod. Ciò significa che:
+    
+    
+        Le mod
+        smetteranno di funzionare ad ogni aggiornamento. È assolutamente normale, e
+        non è colpa di Beat Games.
+    
+    
+        Le mod
+        causeranno bug e problemi di prestazioni. Questa
+        non è colpa di Beat Games.
+    
+    
+        Le mod sono fatte
+        gratuitamente da sviluppatori nel loro
+        tempo libero. Ti invitiamo ad essere paziente e di comprendere la situazione.
+    
+    
+        NON lasciare feedback negativi solo perche le mod smettono di funzionare. Questa
+        non è colpa Beat Games.
+         Non è loro intenzione "rompere" le mod.
+    
+    
+        Se continuerò a trovare persone che lasciano feedback negativi 
+        perchè le mod smettono di funzionare,
+        
+        Mi assicurerò di farle sparire dalla circolazione
+    
+    
+        Ti invitiamo a leggere la guida introduttiva sulla 
+        
+            Wiki
+        .
+    
+    Accetto
+    Non accetto
+    Chiusura dell'app: Non hai accettato i termini e condizioni.
+    Non sono riuscito a scaricare la lista delle versioni
+    Menù delle mod disattivato. Ti invitiamo a riprovare riavviando il programma.
+    Ora puoi utilizzare il menù Mod!
+
+    
+    Mod
+    Nome
+    Versione installata
+    Ultima versione
+    Descrizione
+    Disinstalla
+    Disinstalla
+    Impossibile caricare la lista delle mod
+    Controllo le mod installate
+    Carico le mod
+    Caricamento delle mod completato
+    Installazione di {0} in corso
+    {0} installato
+    Installazione delle mod completata
+    Impossibile trovare il link di download per {0}
+    Vuoi disinstallare {0}?
+    Sei sicuro di voler disinstallare {0}?
+    Continuando, altre mod potrebbero smettere di funzionare
+    Impossibile estrarre {0}, prossimo tentativo in {1} secondi. ({2}/{3})
+    Non sono riuscito ad estrarre {0} dopo aver raggiunto il numero massimo di tentativi ({1}), salto questa mod. Questa protrebbe anche non funzionare correttamente, quindi procedi a tuo rischio e pericolo
+    Cerca...
+    
+    
+    Info
+    Info su Mod Assistant
+    Ciao, sono Assistant, ed ho creato Mod Assistant per aiutarmi con le mod, con alcuni principi in testa:
+    Semplicità d'uso
+    Portabilità
+    Un solo eseguibile
+    Uso responsabile
+    
+        Se ti piace questo programma, e volessi supportarmi, puoi visitare la mia 
+        
+            pagina delle donazioni
+        
+        oppure il mio 
+        
+            Patreon
+        
+    
+    Ringraziamenti speciali ♥
+    Donazioni
+    Carezze
+    Abbracci
+
+    
+    Opzioni
+    Impostazioni
+    Directory d'Installazione
+    Seleziona Cartella
+    Apri Cartella
+    Salva le Mod Selezionate
+    Rileva le Mod Installate
+    Seleziona le Mod Installate
+    Reinstalla le Mod
+    Attiva OneClick™
+    BeatSaver
+    ModelSaber
+    Playlists 
+    Close window when finished 
+    Tipo di Installazione
+    Steam
+    Oculus
+    Strumenti
+    Installa Playlist
+    Installazione Playlist: {0}
+    Installazione canzone fallita: {0}
+    [{0} fails] Installazione playlist terminata: {1}
+    Diagnostica
+    Apri il Log
+    Apri AppData
+    Disinstalla BSIPA
+    Rimuovi Tutte le Mod
+    Tema dell'app
+    Esporta Template
+    Caricamento del Log
+    URL del Log copiato negli Appunti!
+    Caricamento del Log Fallito
+    Caricamento del Log Fallito!
+    Non sono riuscito a caricare il Log su Teknik, ti invitiamo a riprovare oppure puoi caricare il file manualmente.
+    Prendo la lista delle Mod
+    Cerco la versione di BSIPA
+    BSIPA Disinstallato
+    Disinstallare tutte le Mod?
+    Sei sicuro di voler rimuovere TUTTE le mod?
+    Questa azione non può essere annullata.
+    Tutte le Mod sono state Disinstallate
+    Il tema corrente è stato rimosso, torno al principale...
+    Cartella del tema non trovata! Prova ad esportare il template...
+    Cartella AppData non trovata! Prova ad avviare il gioco.
+
+    
+    Caricamento delle Mod
+
+    
+    Non Valida
+    Installazione non valida rilevata
+    La tua installazione di BeatSaber è corrotta o invalida
+    Ciò accade se hai una copia piratata, oppure se hai installato una versione originale senza rimuovere quella piratata
+    
+        Se la tua copia è piratata,
+        ti invitiamo a compare il gioco
+            
+                QUI
+            
+        .
+    
+    
+        Se la tua copia del gioco
+        non è piratata, ti invitiamo a
+        
+            reinstallare il gioco
+        .
+    
+    
+        Se questo non aiuta, puoi sempre chiedere aiuto nel canale
+        #support all'interno del
+        
+            Server Discord di BSMG
+        .
+    
+    Se avevi la versione piratata, ma hai comprato il gioco
+    Seleziona cartella
+    Dovrai riavviare ModAssistant dopo aver cambiato la directory d'Installazione
+
+    
+    Impossibile estrarre i dettagli della mappa.
+    Impossibile scaricare il brano.
+    Non sono riuscito a scaricare il brano.
+    Ci possono essere problemi con BeatSaver e/o la tua connessione ad Internet.
+    Download del file ZIP della mappa fallito
+    Directory d'installazione di Beat Saber non trovata.
+    Installato: {0}
+    Non sono riuscito ad installare la mappa.
+    {0} Registrazione dei gestori OneClick™ riuscita!
+    {0} De-Regitrazione dei gestori OneClick™ riuscita!
+    Installing: {0} 
+    Max tries reached: Skipping {0} 
+    Ratelimit hit. Resuming in {0} 
+    Download failed: {0} 
+    
+    
+    Tema non trovato, ritorno al tema predefinito...
+    Tema impostato su {0}.
+    {0} non esiste.
+    Template del tema "{0}" salvato nella cartella Themes.
+    Template del tema già esistente!
+    Impossibile caricare il file .xaml per il tema {0}: {1}
+
+    
+    Impossibile controllare gli aggiornamenti.
+    Impossibile scaricare l'aggiornamento.
+
+    
+    Mod Assistant
+    Impossibile determinare automaticamente la directory d'installazione di Beat Saber. Ti invitiamo a selezionarla manualmente.
+    Mod Assistant ha bisogno di eseguire questa azione come Amministratore. Ti invitiamo a riprovare.
+    Seleziona la directory d'installazione di Beat Saber
+    Impossibile aprire la seguente cartella: {0}
+
diff --git a/ModAssistant/Localisation/ko.xaml b/ModAssistant/Localisation/ko.xaml
new file mode 100644
index 00000000..c2885356
--- /dev/null
+++ b/ModAssistant/Localisation/ko.xaml
@@ -0,0 +1,243 @@
+
+    i18n:ko-KR
+
+    
+    비트세이버 설치 폴더를 찾을 수 없습니다!
+    OK를 눌러 재시도하거나, Cancel을 눌러 프로그램을 종료할 수 있습니다.
+    유효하지 않은 인수입니다! '{0}'은 옵션을 필요로 합니다.
+    인식할 수 없는 인수입니다. 모드 어시스턴트를 종료합니다.
+    처리되지 않은 예외가 발생했습니다.
+    예외
+
+    
+    모드 어시스턴트
+    인트로
+    모드
+    정보
+    옵션
+    게임 버전
+    버전
+    모드 설명
+    설치
+    또는 업데이트
+    게임 버전을 로드할 수 없었기 때문에 모드 탭이 비활성화됩니다.
+    새로운 게임 버전이 감지되었습니다!
+    게임 업데이트가 있었던 것 같습니다.
+    왼쪽 아래에 선택된 버전이 맞는 버전인지 다시 한번 확인해주세요!
+    아무런 모드도 선택되지 않았습니다!
+    {0}는 설명 페이지를 가지고 있지 않습니다.
+
+    
+    인트로
+    모드 어시스턴트에 어서오세요
+    이 페이지를 정독해주세요
+    
+        이 프로그램을 사용하려면 다음 약관을 읽고 동의해야 합니다:
+    
+    
+        비트세이버는 공식적으로 모드를
+        지원하지 않습니다.
+    
+    
+        이것은 모드들이 매 업데이트마다
+        망가진다는 것을 의미합니다. 이것은 일반적이며, Beat Games'의 탓이
+        아닙니다.
+    
+    
+        모드들은 버그와 성능 문제를
+        발생시킵니다. 이것은 Beat Games'의 탓이
+        아닙니다. 
+    
+    
+        모드들은
+        무료로 만들어졌으며 모더들의
+        소중한 시간의 결과물입니다.기다림을 갖고 이해해주세요.
+    
+    
+        모드가 망가진 것 때문에 게임에 대한 부정적인 의견을남기지 마세요. 이것은 Beat Games'의 탓이
+        아닙니다.
+         Beat Games'는 모드를 죽이려 하지 않습니다.
+    
+    
+        만일 사람들이 모드가
+        망가진 것을 이유로 부정적인 의견을 남기는 것을 계속 보게 된다면,
+        
+        제가 개인적으로 모드를 터트릴지도요..?
+    
+    
+        
+            위키
+        에 있는 초보자 가이드를 읽어주세요.
+    
+    동의합니다
+    거부합니다
+    어플리케이션을 종료합니다: 당신은 약관에 동의하지 않았습니다.
+    버전 리스트를 다운로드할 수 없었습니다
+    모드 탭이 비활성화되었습니다. 어시스턴트를 재시작해주세요.
+    이제 모드 탭을 사용할 수 있습니다!
+
+    
+    모드
+    이름
+    설치 버전
+    최신 버전
+    설명
+    제거
+    제거
+    모드 목록을 불러올 수 없었습니다
+    설치된 모드들을 확인하고 있습니다
+    모드들을 불러오고 있습니다
+    모드들을 불러왔습니다
+    Installing {0}
+    Installed {0}
+    모드 설치를 마쳤습니다
+    {0}를 위한 다운로드 링크를 찾을 수 없었습니다
+    {0}를 제거하시겠습니까?
+    정말로 {0}를 제거하시겠습니까?
+    다른 모드를 사용 못하게 만들 수도 있습니다.
+    {0}를 추출하는데 실패했습니다. {1}초 안에 재시도합니다. ({2}/{3})
+    ({1})회동안 {0}를 추출하는데 실패했습니다. 이 모드가 제대로 작동하지 않을지도 모릅니다
+    Search... 
+
+    
+    정보
+    모드 어시스턴트에 대하여
+    I'm Assistant, and I made Mod Assistant for mod assistance, with a few principles in mind:
+    Simplicity
+    Portability
+    Single Executable
+    Responsible use
+    
+        If you enjoy this program and would like to support me, please visit my
+        
+            donation page
+        
+        or my
+        
+            Patreon
+        
+    
+    Special Thanks ♥
+    Donate
+    Headpats
+    Hugs
+
+    
+    옵션
+    설정
+    설치 폴더
+    폴더 선택
+    폴더 열기
+    선택된 모드 저장
+    설치된 모드 감지
+    설치된 모드 선택
+    설치된 모드 재설치
+    OneClick™ 설치 활성화
+    BeatSaver
+    ModelSaber
+    Playlists 
+    Close window when finished 
+    게임 유형
+    Steam
+    Oculus
+    Tools 
+    Install Playlist 
+    Installing Playlist: {0} 
+    Failed song: {0} 
+    [{0} fails] Finished Installing Playlist: {1} 
+    진단
+    로그 열기
+    앱데이터 열기
+    BSIPA 삭제
+    모든 모드 삭제
+    어플리케이션 테마
+    템플릿 추출
+    로그 제출
+    로그 URL이 클립보드에 복사되었습니다!
+    로그 제출에 실패하였습니다
+    로그 제출에 실패하였습니다!
+    Teknik에게 로그 파일을 제출하는데에 실패하였습니다. 재시도하거나 수동으로 파일을 보내주세요.
+    모드 목록을 얻는중입니다
+    BSIPA 버전을 찾는중입니다
+    BSIPA가 제거되었습니다
+    모든 모드를 제거할까요?
+    정말로 모든 모드를 제거할까요?
+    이것은 취소할 수 없습니다.
+    모든 모드가 제거되었습니다
+    현재 테마를 제거중입니다. 원래 테마로 돌아갑니다...
+    테마 폴더를 찾을 수 없습니다! 템플릿으로 내보냅니다...
+    AppData 폴더를 찾을 수 없습니다! 게임을 실행해보세요.
+
+    
+    모드 로딩중
+
+    
+    잘못된 프로그램
+    잘못된 프로그램 설치가 감지되었습니다
+    게임이 손상되었거나 다른 이유로 잘못된 것 같습니다
+    이 오류는 게임이 불법적인 경로로 받아졌거나, 불법적인 경로로 받아진 게임을 당신의 정상적인 게임에 덮어씌워졌을 때에 발생합니다
+    
+        만일 당신이 게임을 불법적인 경로로 받았다면,
+        
+            
+                여기서
+            
+        게임을 구매해주세요.
+    
+    
+        만일 당신의 게임이 불법적인 경로로 받아진게
+        아니라면,
+        
+            클린재설치
+        를 해주세요.
+    
+    
+        만일 그것들이 도움되지 않았다면,
+        
+            BSMG
+        의
+        #support 채널에서 도움을 구하세요.
+    
+    만일 불법적인 경로로 받아진 게임을 가지고 있었다가 게임을 구매했다면,
+    폴더를 선택해주세요
+    정상적인 설치 이후 모드 어시스턴트를 재시작할 필요가 있습니다
+
+    
+    맵의 세부정보를 얻어올 수 없었습니다.
+    곡을 받아올 수 없었습니다.
+    곡을 받아올 수 없었습니다.
+    BeatSaver 또는 당신의 인터넷 연결에 문제가 있는 것 같습니다.
+    노래 ZIP 파일을 받는 데에 실패했습니다.
+    비트세이버 설치 폴더를 찾을 수 없었습니다.
+    설치됨: {0}
+    설치에 실패하였습니다.
+    {0} OneClick™ 설치 관리자가 등록되었습니다!
+    {0} OneClick™ 설치 관리자가 등록 취소되었습니다!
+    Installing: {0} 
+    Max tries reached: Skipping {0} 
+    Ratelimit hit. Resuming in {0} 
+    Download failed: {0} 
+    
+    
+    테마를 찾을 수 없어, 기본 테마로 돌아갑니다...
+    {0} 테마로 설정합니다.
+    {0}는 존재하지 않습니다.
+    템플릿 테마 "{0}"가 템플릿 폴더에 저장되었습니다.
+    템플릿 테마가 이미 존재합니다!
+    테마를 위한 .xaml 파일을 불러오지 못했습니다 {0}: {1}
+
+    
+    업데이트를 확인할 수 없습니다.
+    업데이트를 다운로드할 수 없습니다.
+
+    
+    모드 어시스턴트
+    비트세이버 설치 폴더를 찾을 수 없습니다. 수동으로 선택해주세요.
+    모드 어시스턴트는 이 작업을 관리자 권한으로 실행하는 것을 필요로 합니다. 다시 시도해주세요.
+    비트세이버 설치 폴더를 선택해주세요
+    폴더를 열 수 없습니다: {0}
+
diff --git a/ModAssistant/Localisation/nl.xaml b/ModAssistant/Localisation/nl.xaml
new file mode 100644
index 00000000..dc9dd5d7
--- /dev/null
+++ b/ModAssistant/Localisation/nl.xaml
@@ -0,0 +1,242 @@
+
+    i18n:en-US
+
+    
+    Beat Saber installatie map niet gevonden!
+    Klik OK om opnieuw te proberen, of Annuleren om de applicatie af te sluiten.
+    Ongeldig argument! '{0}' heeft een optie nodig.
+    Niet herkend argument. Mod Assistant sluit af.
+    Een onverwerkte foutcode is zojuist opgetreden
+    Foutcode
+
+    
+    ModAssistant
+    Introductie
+    Mods
+    Over
+    Opties
+    Game Versie
+    Versie
+    Mod Info
+    Installeren
+    of Update
+    Kan de game versie niet laden, Mods tabblad zal onbeschikbaar zijn.
+    Nieuwe Game Versie Gedetecteerd!
+    Het lijkt erop dat er een game update is geweest.
+    Controleer alstublieft of de correcte versie geselecteerd is linksonder in de hoek
+    Geen mod geselecteerd!
+    {0} heeft geen info pagina
+
+    
+    Introductie
+    Welkom bij Mod Assistant
+    Lees deze pagina alstublieft volledig en aandachtig
+    
+        Door het gebruiken van dit programma verklaar ik de volgende voorwaarden te hebben gelezen en hier mee akkoord te gaan:
+    
+    
+        Beat Saber
+        Ondersteund mods niet van zichzelf, dit betekent dat:
+    
+    
+        Mods
+        Elke update niet meer werken, dit is normaal en niet de fout van Beat Games.
+    
+    
+        Mods
+        zullen bugs en prestatievermindering veroorzaken. Dit is niet een fout van Beat Games.
+    
+    
+        Mods worden
+        gratis gemaakt door mensen in hun
+        vrije tijd. Wees alstublieft geduldig en begripvol.
+    
+    
+        Laat GEEN negatieve beoordelingen achter op beat saber omdat mods niet meer werken. Dit is
+        niet de fout van Beat Games.
+         Ze proberen niet mods ontoegankelijk te maken.
+    
+    
+        Als ik blijf zien dat mensen negatieve reviews achterlaten
+        omdat mods niet meer werken,
+        
+        zal ik persoonlijk met een roestige lepel mods niet meer laten werken
+    
+    
+        Lees alstublieft de 'Beginners Guide' op de
+        
+            Wiki
+        . (engels)
+    
+    Accepteer
+    Accepteer niet
+    Sluit applicatie af: U accepteerde de voorwaarden niet.
+    Kon de versie lijst niet downloaden
+    Mods tabblad uitgeschakeld. Herstart het programma om opnieuw te proberen.
+    U kunt nu het mods tabblad gebruiken!
+
+    
+    Mods
+    Naam
+    Geïnstalleerd
+    Recentst
+    Beschrijving
+    Deïnstalleer
+    Deïnstalleer
+    Kon de mod lijst niet laden
+    Geïnstalleerde mods controleren
+    Mods Laden
+    Klaar met mods laden
+    {0} wordt geïnstalleerd
+    {0} is geïnstalleerd
+    Klaar met mods installeren
+    Kon de download link voor {0} niet vinden
+    {0} deïnstalleren?
+     Weet U zeker dat U {0} wilt verwijderen?
+    Dit zou uw andere mods niet meer kunnen laten werken
+    Kon {0} niet extracten, probeer opniew over {1} seconden. ({2}/{3})
+    Kon {0} niet extracten na maximaal aantal pogingen ({1}), Overslaan. Deze mod werkt msischien niet goed dus ga verder op eigen risico
+    Search... 
+
+    
+    Over
+    Over Mod Assistant
+    Ik ben Assistant, en ik heb Mod Assistant gemaakt om te assisteren met mods, met een aantal principes als basis:
+    Eenvoud
+    Draagbaarheid
+    Één enkel uitvoerbaar bestand
+    Verantwoordelijk gebruik
+    
+        Als U dit programma nuttig vind en mij graag wilt steunen, ga dan naar mijn
+        
+            donatie pagina
+        
+        of mijn
+        
+            Patreon
+        
+    
+    Bijzondere dank ♥
+    Doneer
+    Tik op hoofd
+    Knuffels
+
+    
+    Opties
+    Instellingen
+    Installatie map
+    Selecteer map
+    Open map
+    Sla geselecteerde mods op
+    Detecteer geïnstalleerde mods
+    Selecteer geïnstalleerde mods
+    Geïnstalleerde mods herinstalleren
+    Activeer OneClick™ Installaties
+    BeatSaver
+    ModelSaber
+    Playlists 
+    Close window when finished 
+    Game Type
+    Steam
+    Oculus
+    Tools 
+    Install Playlist 
+    Installing Playlist: {0} 
+    Failed song: {0} 
+    [{0} fails] Finished Installing Playlist: {1} 
+    Diagnostiek
+    Open Logs
+    Open AppData
+    Deïnstalleer BSIPA
+    Verwijder Alle Mods
+    Applicatie thema
+    Exporteer sjabloon
+    Log aan het uploaden
+    Log URL gekopieerd naar klembord!
+    Log Upload Mislukt
+    Log upload mislukt!
+    Kon het log bestand niet uploaden naar teknik, probeer alstublieft opnieuw of upload handmatig.
+    Mod Lijst Ophalen
+    BSIPA versie vinden
+    BSIPA Gedeïnstalleerd
+    Deïnstalleer ALLE mods?
+    Weet U zeker dat U ALLE mods wilt deïnstalleren?
+    Dit kan niet ongedaan gemaakt worden.
+    Alle mods gedeïnstalleerd
+    Huidig thema is verwijderd, terugvallen op standaard...
+    Thema map niet gevonden! probeer het sjabloon te exporteren...
+    Appdata map niet gevonden! probeer Uw spel te starten.
+
+    
+    Mods Laden
+
+    
+    Ongeldig
+    Ongeldige Installatie Gedetecteerd
+    Uw game installatie is corrupt of ongeldig
+    Dit kan gebeuren als U een gepirateerde versie heeft, of een gepirateerde versie over de legitieme versie heeft gekopieerd
+    
+        Als Uw game versie is gepirateerd,
+        Koop alstublieft de game
+            
+                HIER
+            
+        .
+    
+    
+        Als Uw game versie
+        niet gepirateerd is, doe dan alstublieft 
+        
+            een "schone" installatie
+        .
+    
+    
+        Als dat allebei niet helpt, vraag om hulp in de 
+        #support channel in
+        
+            BSMG
+        . (Engels)
+    
+    Als U een gepirateerde versie van het spel had maar nu het spel hebt gekocht
+    Selecteer map
+    Dan moet U Mod Assistant opnieuw starten na het wisselen naar de legitieme installatie
+
+    
+    Kon de Map details niet vinden.
+    Kon het nummer niet downloaden.
+    Kon het nummer niet downlaoden.
+    Er kunnen problemen zijn met BeatSaver of Uw internet verbinding.
+    Kon de ZIP van het nummer niet downloaden
+    Kon het Beat Saber installatie pad niet vinden
+    {0} Geïnstalleerd
+    Installatie mislukt
+    {0} OneClick™ Installeer afhandelingen geregistreerd!
+    {0} OneClick™ Install afhandelingen uitgeregistreerd!
+    Installing: {0} 
+    Max tries reached: Skipping {0} 
+    Ratelimit hit. Resuming in {0} 
+    Download failed: {0} 
+    
+    
+    Thema niet gevonden, terugvallen op standaard thema...
+    Theme ingesteld op {0}.
+    {0} bestaat niet.
+    Thema sjabloon "{0}" opgeslagen in de thema map.
+    Thema sjabloon bestaat al!
+    .xaml bestand laden voor {0} mislukt: {1}
+
+    
+    Kon niet controleren voor updates
+    Kon update niet downloaden
+
+    
+    Mod Assistant
+    Kon Uw Beat Saber installatie map niet vinden. Selecteer deze alstublieft handmatig.
+    Mod Assistant moet deze taak als adminstrator uitvoeren. Probeer alstublieft opnieuw.
+    Selecteer Uw Beat Saber installatie map
+    Kan map niet openen: {0}
+
diff --git a/ModAssistant/Localisation/ru.xaml b/ModAssistant/Localisation/ru.xaml
new file mode 100644
index 00000000..8c8090e9
--- /dev/null
+++ b/ModAssistant/Localisation/ru.xaml
@@ -0,0 +1,244 @@
+
+    i18n:ru-RU
+
+    
+    Не получается найти папку с установленным Beat Saber!
+    Нажмите ОК, чтобы попробовать снова или ЗАКРЫТЬ, чтобы закрыть приложение.
+    Недопустимый аргумент! '{0}' требуется выбор.
+    Нераспознанный аргумент. Закрытие Mod Assistant.
+    Произошло необработанное исключениеd
+    Исключение
+
+    
+    ModAssistant
+    Вступление
+    Модификации
+    Информация
+    Настройки
+    Версия игры
+    Версия
+    Информация о модификации
+    Установить
+    или обновить
+    Не удаётся загрузить список версий игры, Вкладка с модификациями недоступна.
+    Обнаружена новая версия игры!
+    Похоже на то, что было обновление игры.
+    Пожалуйста, проверьте дважды, что выбрана корректная версия игры в нижнем левом углу!
+    Нет выбранных модификаций!
+    {0} Не имеет страницы с информацией.
+
+ 
+    Вступление
+    Добро пожаловать в Mod Assistant
+    Прочитайте эту страницу полностью и внимательно
+    
+        Используя эту программу, вы прочитали и принимаете эти условия:
+    
+    
+        Beat Saber
+        не имеет нативной поддержки модификаций. Это означает:
+    
+    
+        Модификации
+        могут прекращать работоспособность каждое обновление. Это нормально, и
+        это не вина Beat Games.
+    
+    
+        Модификации
+        могут вызывать ошибки и проблемы с производительностью. Это
+        не вина Beat Games.
+    
+    
+        Модификации создаются
+        бесплатно людьми в их
+        свободное время. Пожалуйста, будьте терпиливыми и взаимопонимающими.
+    
+    
+        НЕ оставляйте отрицательные отзывы потому что модификация не работает. Это 
+        не вина Beat Games.
+         Beat Games не пытается уничтожить модификации.
+    
+    
+        Если я увижу людей, которые продолжают оставлять негативные комментарии
+        потому что модификации не работают,
+        
+        Я лично уничтожу модификации ржавой ложкой
+    
+    
+        Пожалуйста, прочитайте инструкцию для начинающих на
+        
+            Вики
+        .
+    
+    Я согласен
+    Я не согласен
+    Приложение закрывается: Вы не приняли пользовательскую политику соглашений.
+    Не удаётся получить список версий
+    Вкладка с модификациями недоступна. Пожалуйста перезапустите, чтобы попробовать снова.
+    Теперь вы можете использовать вкладку с модификациями!
+
+    
+    Модификации
+    Название
+    Установленная
+    Последняя
+    Описание
+    Удалить
+    Удалить
+    Не удается загрузить список модификаций
+    Проверка установленных модификаций
+    Загрузка модификаций
+    Загрузка модификаций окончена
+    Установка {0}
+    Установлено {0}
+    Установка модификаций окончена.
+    Не удаётся получить ссылку на скачивание {0}
+    Удаление {0}?
+    Вы уверены, что хотите удалить {0}?
+    Это может сломать остальные модификации
+    Не удалось извлечь {0}, попробуйте снова через {1} секунд. ({2}/{3})
+    Не удалось извлечь {0} после попыток ({1}), пропускается. Эта модификация может работать некорректно, используйте на свой страх и риск
+    Поиск...
+
+  
+    Информация
+    Про Mod Assistant
+    Я ассистент, и я создал Mod Assistant для помощи модификациям с некоторыми личными принципами:
+    Простота
+    Портативность 
+    Приложение одним файлом
+    Ответственное использование
+    
+        Если вам нравится программа и вы хотите меня поддержать, пожалуйста, посетите
+        
+            страницу для пожертвований 
+        
+        или мой
+        
+            Patreon
+        
+    
+    Отдельное спасибо ♥
+    Поддержать
+    Погладить по голове
+    Обнять
+
+    
+    Опции
+    Настройки
+    Папка установки
+    Выбрать папку
+    Открыть папку
+    Сохранить выбранные модификации
+    Обнаружение установленных модификаций
+    Select Installed Mods
+    Переустановить установленные модификации
+    Включить OneClick™ установки
+    BeatSaver
+    ModelSaber
+    Playlists 
+    Close window when finished 
+    Тип игры
+    Steam
+    Oculus
+    Инструменты
+    Установить плейлист
+    Установка плейлиста: {0}
+    Ошибка с песней: {0}
+    [{0} ошибок] Установка плейлиста окончена: {1}
+    Диагностика
+    Открыть логи
+    Открыть AppData
+    Удалить BSIPA
+    Удалить все модификации
+    Тема приложения
+    Экспортировать шаблон
+    Загрузка логов
+    Ссылка на лог успешно скопирована!
+    Загрузка логов не удалась 
+    Загрузка логов не удалась!
+    Не удаётся загрузить файл с логом на Teknik, пожалуйста, попробуйте снова или отправьте файл вручную.
+    Получаем список модификаций
+    Поиск BSIPA версии
+    BSIPA удалён
+    Удалить все модификации?
+    Вы уверены, что хотите удалить ВСЕ модификации?
+    Это действие нельзя отменить.
+    Все модификации удалены!
+    Текущая тема была удалена, возвращаемся к стандартной теме...
+    Папка с темами не найдена! Пробую экспортировать шаблон...
+    Папка AppData не найдена! Попробуйте запустить игру.
+
+    
+    Загрузка модификаций
+
+    
+    Недействительно
+    Обнаружена недействительная установка
+    Ваша установленная игра сломана или недействительная
+    Это могло произойти, если у вас нелицензионная копия игры, или вы скопировали нелицензионную копию поверх лицензионной
+    
+        Если ваша копия игры нелицензионная,
+        пожалуйста, купите игру
+            
+                ТУТ
+            
+        .
+    
+    
+        Если ваша копия игры
+        лицензионная, пожалуйста
+        
+            переустановите игру заново
+        .
+    
+    
+        Если из этого ничего вам не помогает, попросите помощи в
+        #support канал в
+        
+            BSMG
+        .
+    
+    Если вы использовали нелицензионную версию игру, но купили игру
+    выберете папку
+    Вам будет необходимо перезапустить Mod Assistant после того, как вы выберете папку с лицензионной копией игры
+
+    
+    Не удаётся получить информацию о карте.
+    Не удаётся загрузить песню.
+    Не удаётся загрузить песню.
+    Возможно это ошибки с BeatSaver или вашим интернет соединением.
+    Ошибка с загрузкой песни ZIP
+    Установочный путь к Beat Saber не найден.
+    Установлено: {0}
+    Ошибка установки.
+    {0} OneClick™ установки зарегистрированы
+    {0} OneClick™ установки не зарегистрированы!
+    Installing: {0} 
+    Max tries reached: Skipping {0} 
+    Ratelimit hit. Resuming in {0} 
+    Download failed: {0} 
+    
+    
+    Тема не найдена, возвращаемся к стандартной теме...
+    Установлена тема: {0}.
+    {0} не существует.
+    Шаблон темы "{0}" сохранён в папку с темами.
+    Шаблон темы уже существует!
+    Не удаётся загрузить .xaml файл для темы {0}: {1}
+
+    
+    Не удаётся проверить обновления.
+    Не удаётся загрузить обновление.
+
+    
+    Mod Assistant
+    Не удаётся обнаружить папку с Beat Saber. Пожалуйста, укажите путь вручную.
+    Mod Assistant требует запустить эту задачу с правами администратора. Пожалуйста, попробуйте заново.
+    Укажите папку с установленным Beat Saber
+    Не удаётся открыть папку: {0}
+
diff --git a/ModAssistant/Localisation/zh.xaml b/ModAssistant/Localisation/zh.xaml
new file mode 100644
index 00000000..c5b26e07
--- /dev/null
+++ b/ModAssistant/Localisation/zh.xaml
@@ -0,0 +1,235 @@
+
+    i18n:zh-Hans
+
+    
+    找不到您的Beat Saber安装路径!
+    点确定重试,或点取消关闭软件。
+    无效参数!'{0}'需要一个选项。
+    无法识别的参数。关闭Mod Assistant。
+    刚刚发生了一个未处理的异常
+    例外
+
+    
+    ModAssistant
+    简介
+    Mod
+    关于
+    选项
+    游戏版本
+    软件版本
+    Mod信息
+    安装
+    或更新
+    无法获取游戏版本,Mod选项卡将不可用。
+    检测到新的游戏版本!
+    似乎游戏更新了,
+    请仔细检查左下角是否选择了正确的游戏版本!
+    没有选择Mod!
+    {0}没有信息页。
+
+    
+    简介
+    欢迎使用Mod Assistant
+    请仔细阅读本页
+    
+        通过使用此程序,证明您已阅读并同意以下条款:
+    
+    
+        Beat Saber并不能原生支持Mod。 这意味着:
+    
+    
+        Mod将会在每次游戏更新后无法使用。这很正常,并不是Beat Games的错。
+    
+    
+        Mod会导致出现BUG或者性能问题。这并不是Beat Games的错。
+    
+    
+        爱好者在空闲时间用爱发电制作了这些Mod,请保持耐心,等待Mod更新。
+    
+    
+        请勿由于Mod不可用而发表差评,Beat Games不会封杀Mod。
+    
+    
+        如果我继续看到因为Mod不可用而留下的差评,我会亲自干掉Mod。
+    
+    
+        请务必阅读
+        
+            BS中文教程及常见问题解答
+        
+        ,以及
+        
+            新手指南(英文)
+        。
+    
+    同意
+    拒绝
+    关闭软件:您不同意此条款。
+    无法下载版本列表
+    已禁用Mod选项卡,请尝试重新打开软件。
+    你现在可以使用Mod选项卡了!
+
+    
+    Mod
+    名称
+    已安装
+    最新
+    介绍
+    卸载
+    卸载
+    无法加载Mod列表
+    正在检测已安装的Mod
+    正在加载Mod列表
+    Mod列表加载完成
+    正在安装{0}
+    已安装{0}
+    Mod安装完成
+    {0}找不到下载地址!
+    卸载{0}?
+    你确定要移除{0}?
+    这可能会导致其他Mod不可用。
+    {0}解压失败,将在{1}秒后重试。({2}/{3})
+    {0}在重试{1}次后仍然无法解压,将被跳过。注意,这个Mod可能无法使用。
+    搜索... 
+
+    
+    关于
+    关于Mod Assistant
+    我是Assistant,为了方便安装MOD,我制作了Mod Assistant。有以下特性:
+    简单易用
+    可移植性
+    单一文件
+    负责任地使用
+    
+        如果你喜欢这个项目并且想支持我,请访问我的
+        
+            捐助页面
+        
+        或我的
+        
+            Patreon
+        
+    
+    特别感谢 ♥
+    捐助
+    摸摸头
+    抱抱
+
+    
+    选项
+    设置
+    安装路径
+    选择路径
+    打开路径
+    保存选中的Mod
+    检查已安装的Mod
+    选中已安装的Mod
+    重新安装已有Mod
+    在以下站点启用OneClick™一键安装:
+    BeatSaver
+    ModelSaber
+    Playlists 
+    Close window when finished 
+    游戏类型
+    Steam
+    Oculus
+    工具 
+    添加歌单(Playlist) 
+    正在添加歌单:{0} 
+    失败歌曲:{0} 
+    [{0}失败]添加{1}完成 
+    诊断工具
+    打开日志
+    打开游戏存档
+    卸载BSIPA
+    移除所有Mod
+    软件主题
+    导出模板
+    正在上传日志
+    日志网址已复制到剪贴板!
+    日志上传完成!
+    日志上传失败!
+    无法将日志文件上传到Teknik,请重试或手动发送文件。
+    正在获取Mod列表
+    正在检查BSIPA版本
+    已卸载BSIPA
+    卸载所有Mod?
+    你确定要移除所有Mod?
+    这将无法撤销。
+    已卸载所有Mod
+    当前主题已被删除,恢复为默认...
+    找不到主题文件夹!请尝试导出模板
+    找不到游戏存档路径!请尝试启动游戏后重试。
+
+    
+    正在加载Mod...
+
+    
+    无效
+    检测到无效的安装
+    您的游戏安装已损坏或无效
+    如果您的游戏是盗版的,或者您使用盗版游戏替换了正版,则可能会发生这种情况。
+    
+        如果您的游戏是盗版的,
+        请在
+            
+                这里
+            
+        购买游戏。
+    
+    
+        如果您的游戏
+        不是盗版,请
+        
+            清洁安装
+        。
+    
+    
+        如果这些方法没有帮助,请在BSMG的
+        
+            Discord
+        #support频道寻求帮助。
+    
+    如果您曾经使用盗版,但之后购买了正版游戏
+    选择路径
+    更改为正版游戏之后,您需要重新启动Mod Assistant
+
+    
+    无法获取地图详情。
+    无法下载歌曲。
+    无法下载歌曲。
+    可能您的互联网连接或BeatSaver存在问题。
+    下载歌曲压缩包失败。
+    找不到Beat Saber安装路径。
+    已安装:{0}
+    安装失败。
+    {0} OneClick™ 一键安装处理程序已注册!
+    {0} OneClick™ 一键安装处理程序已移除!
+    Installing: {0} 
+    Max tries reached: Skipping {0} 
+    Ratelimit hit. Resuming in {0} 
+    Download failed: {0} 
+    
+    
+    找不到主题,恢复为默认主题...
+    主题设置为{0}.
+    {0}不存在
+    模板主题"{0}"保存到主题文件夹。
+    模板主题已存在!
+    无法加载主题的.xaml文件 {0}: {1}
+
+    
+    无法检查更新。
+    无法下载更新。
+
+    
+    Mod Assistant
+    检测不到您的Beat Saber安装路径,请手动选择。
+    Mod Assistant需要以管理员身份运行此任务,请重试。
+    选择您的Beat Saber安装路径
+    无法打开路径:{0}
+
diff --git a/ModAssistant/MainWindow.xaml b/ModAssistant/MainWindow.xaml
new file mode 100644
index 00000000..d0f2f2d9
--- /dev/null
+++ b/ModAssistant/MainWindow.xaml
@@ -0,0 +1,229 @@
+
+    
+        
+        
+            
+                
+            
+        
+        
+        
+        
+            
+                
+                
+            
+            
+                
+                
+            
+
+            
+                
+                    
+                    
+                    
+                    
+                    
+                    
+                
+
+                
+
+                
+
+                
+
+                
+
+                
+                    
+                        
+                        :
+                    
+                    
+                    
+                
+
+            
+
+            
+                
+                
+            
+
+            
+
+            
+                
+                    
+                    
+                    
+                
+
+                
+                    
+                
+                
+
+                
+                    
+                
+            
+        
+    
+
diff --git a/ModAssistant/MainWindow.xaml.cs b/ModAssistant/MainWindow.xaml.cs
new file mode 100644
index 00000000..eafd1613
--- /dev/null
+++ b/ModAssistant/MainWindow.xaml.cs
@@ -0,0 +1,323 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using ModAssistant.Pages;
+using static ModAssistant.Http;
+
+namespace ModAssistant
+{
+    /// 
+    /// Interaction logic for MainWindow.xaml
+    /// 
+    public partial class MainWindow : Window
+    {
+        public static MainWindow Instance;
+        public static bool ModsOpened = false;
+        public static bool ModsLoading = false;
+        public static string GameVersion;
+        public static string GameVersionOverride;
+        public TaskCompletionSource VersionLoadStatus = new TaskCompletionSource();
+
+        public string MainText
+        {
+            get
+            {
+                return MainTextBlock.Text;
+            }
+            set
+            {
+                Dispatcher.Invoke(new Action(() => { MainWindow.Instance.MainTextBlock.Text = value; }));
+            }
+        }
+
+        public MainWindow()
+        {
+            InitializeComponent();
+            Instance = this;
+
+            const int ContentWidth = 1280;
+            const int ContentHeight = 720;
+
+            double ChromeWidth = SystemParameters.WindowNonClientFrameThickness.Left + SystemParameters.WindowNonClientFrameThickness.Right;
+            double ChromeHeight = SystemParameters.WindowNonClientFrameThickness.Top + SystemParameters.WindowNonClientFrameThickness.Bottom;
+            double ResizeBorder = SystemParameters.ResizeFrameVerticalBorderWidth;
+
+            Width = ChromeWidth + ContentWidth + 2 * ResizeBorder;
+            Height = ChromeHeight + ContentHeight + 2 * ResizeBorder;
+
+            VersionText.Text = App.Version;
+
+            if (Utils.IsVoid())
+            {
+                Main.Content = Invalid.Instance;
+                MainWindow.Instance.ModsButton.IsEnabled = false;
+                MainWindow.Instance.OptionsButton.IsEnabled = false;
+                MainWindow.Instance.IntroButton.IsEnabled = false;
+                MainWindow.Instance.AboutButton.IsEnabled = false;
+                MainWindow.Instance.GameVersionsBox.IsEnabled = false;
+                return;
+            }
+
+            Themes.LoadThemes();
+            Themes.FirstLoad(Properties.Settings.Default.SelectedTheme);
+
+            Task.Run(() => LoadVersionsAsync());
+
+            if (!Properties.Settings.Default.Agreed || string.IsNullOrEmpty(Properties.Settings.Default.LastTab))
+            {
+                Main.Content = Intro.Instance;
+            }
+            else
+            {
+                switch (Properties.Settings.Default.LastTab)
+                {
+                    case "Intro":
+                        Main.Content = Intro.Instance;
+                        break;
+                    case "Mods":
+                        _ = ShowModsPage();
+                        break;
+                    case "About":
+                        Main.Content = About.Instance;
+                        break;
+                    case "Options":
+                        Main.Content = Options.Instance;
+                        Themes.LoadThemes();
+                        break;
+                    default:
+                        Main.Content = Intro.Instance;
+                        break;
+                }
+            }
+        }
+
+        private async void LoadVersionsAsync()
+        {
+            try
+            {
+                var resp = await HttpClient.GetAsync(Utils.Constants.BeatModsVersions);
+                var body = await resp.Content.ReadAsStringAsync();
+                List versions = JsonSerializer.Deserialize(body).ToList();
+
+                resp = await HttpClient.GetAsync(Utils.Constants.BeatModsAlias);
+                body = await resp.Content.ReadAsStringAsync();
+                object jsonObject = JsonSerializer.DeserializeObject(body);
+
+                Dispatcher.Invoke(() =>
+                {
+                    GameVersion = GetGameVersion(versions, jsonObject);
+
+                    GameVersionsBox.ItemsSource = versions;
+                    GameVersionsBox.SelectedValue = GameVersion;
+
+                    if (!string.IsNullOrEmpty(GameVersionOverride))
+                    {
+                        GameVersionsBox.Visibility = Visibility.Collapsed;
+                        GameVersionsBoxOverride.Visibility = Visibility.Visible;
+                        GameVersionsBoxOverride.Text = GameVersionOverride;
+                        GameVersionsBoxOverride.IsEnabled = false;
+                    }
+
+                    if (!string.IsNullOrEmpty(GameVersion) && Properties.Settings.Default.Agreed)
+                    {
+                        MainWindow.Instance.ModsButton.IsEnabled = true;
+                    }
+                });
+
+                VersionLoadStatus.SetResult(true);
+            }
+            catch (Exception e)
+            {
+                Dispatcher.Invoke(() =>
+                {
+                    GameVersionsBox.IsEnabled = false;
+                    MessageBox.Show($"{Application.Current.FindResource("MainWindow:GameVersionLoadFailed")}\n{e}");
+                });
+
+                VersionLoadStatus.SetResult(false);
+            }
+        }
+
+        private string GetGameVersion(List versions, object aliases)
+        {
+            string version = Utils.GetVersion();
+            if (!string.IsNullOrEmpty(version) && versions.Contains(version))
+            {
+                return version;
+            }
+
+            string aliasOf = CheckAliases(versions, aliases, version);
+            if (!string.IsNullOrEmpty(aliasOf))
+            {
+                return aliasOf;
+            }
+
+            string versionsString = String.Join(",", versions.ToArray());
+            if (Properties.Settings.Default.AllGameVersions != versionsString)
+            {
+                Properties.Settings.Default.AllGameVersions = versionsString;
+                Properties.Settings.Default.Save();
+
+                string title = (string)Application.Current.FindResource("MainWindow:GameUpdateDialog:Title");
+                string line1 = (string)Application.Current.FindResource("MainWindow:GameUpdateDialog:Line1");
+                string line2 = (string)Application.Current.FindResource("MainWindow:GameUpdateDialog:Line2");
+
+                Utils.ShowMessageBoxAsync($"{line1}\n\n{line2}", title);
+                return versions[0];
+            }
+
+            if (!string.IsNullOrEmpty(Properties.Settings.Default.GameVersion) && versions.Contains(Properties.Settings.Default.GameVersion))
+                return Properties.Settings.Default.GameVersion;
+            return versions[0];
+        }
+
+        private string CheckAliases(List versions, object aliases, string detectedVersion)
+        {
+            Dictionary Objects = (Dictionary)aliases;
+            foreach (string version in versions)
+            {
+                object[] aliasArray = (object[])Objects[version];
+                foreach (object alias in aliasArray)
+                {
+                    if (alias.ToString() == detectedVersion)
+                    {
+                        GameVersionOverride = detectedVersion;
+                        return version;
+                    }
+                }
+            }
+            return string.Empty;
+        }
+
+        private async Task ShowModsPage()
+        {
+            void OpenModsPage()
+            {
+                Main.Content = Mods.Instance;
+                Properties.Settings.Default.LastTab = "Mods";
+                Properties.Settings.Default.Save();
+                Mods.Instance.RefreshColumns();
+            }
+
+            if (ModsOpened == true && Mods.Instance.PendingChanges == false)
+            {
+                OpenModsPage();
+                return;
+            }
+
+            Main.Content = Loading.Instance;
+
+            if (ModsLoading) return;
+            ModsLoading = true;
+            await Mods.Instance.LoadMods();
+            ModsLoading = false;
+
+            if (ModsOpened == false) ModsOpened = true;
+            if (Mods.Instance.PendingChanges == true) Mods.Instance.PendingChanges = false;
+
+            if (Main.Content == Loading.Instance)
+            {
+                OpenModsPage();
+            }
+        }
+
+        private void ModsButton_Click(object sender, RoutedEventArgs e)
+        {
+            _ = ShowModsPage();
+        }
+
+        private void IntroButton_Click(object sender, RoutedEventArgs e)
+        {
+            Main.Content = Intro.Instance;
+            Properties.Settings.Default.LastTab = "Intro";
+            Properties.Settings.Default.Save();
+        }
+
+        private void AboutButton_Click(object sender, RoutedEventArgs e)
+        {
+            Main.Content = About.Instance;
+            Properties.Settings.Default.LastTab = "About";
+            Properties.Settings.Default.Save();
+        }
+
+        private void OptionsButton_Click(object sender, RoutedEventArgs e)
+        {
+            Main.Content = Options.Instance;
+            Themes.LoadThemes();
+            Properties.Settings.Default.LastTab = "Options";
+            Properties.Settings.Default.Save();
+        }
+
+        private void InstallButton_Click(object sender, RoutedEventArgs e)
+        {
+            Mods.Instance.InstallMods();
+        }
+
+        private void InfoButton_Click(object sender, RoutedEventArgs e)
+        {
+            if ((Mods.ModListItem)Mods.Instance.ModsListView.SelectedItem == null)
+            {
+                MessageBox.Show((string)Application.Current.FindResource("MainWindow:NoModSelected"));
+                return;
+            }
+            Mods.ModListItem mod = ((Mods.ModListItem)Mods.Instance.ModsListView.SelectedItem);
+            string infoUrl = mod.ModInfo.link;
+            if (string.IsNullOrEmpty(infoUrl))
+            {
+                MessageBox.Show(string.Format((string)Application.Current.FindResource("MainWindow:NoModInfoPage"), mod.ModName));
+            }
+            else
+            {
+                System.Diagnostics.Process.Start(infoUrl);
+            }
+        }
+
+        private async void GameVersionsBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            string oldGameVersion = GameVersion;
+
+            GameVersion = (sender as ComboBox).SelectedItem.ToString();
+
+            if (string.IsNullOrEmpty(oldGameVersion)) return;
+
+            Properties.Settings.Default.GameVersion = GameVersion;
+            Properties.Settings.Default.Save();
+
+            if (ModsOpened)
+            {
+                var prevPage = Main.Content;
+
+                Mods.Instance.PendingChanges = true;
+                await ShowModsPage();
+
+                Main.Content = prevPage;
+            }
+        }
+
+        private void Window_PreviewMouseDown(object sender, MouseButtonEventArgs e)
+        {
+            About.Instance.PatUp.IsOpen = false;
+            About.Instance.PatButton.IsEnabled = true;
+            About.Instance.HugUp.IsOpen = false;
+            About.Instance.HugButton.IsEnabled = true;
+        }
+
+        private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
+        {
+            if (Main.Content == Mods.Instance)
+            {
+                Mods.Instance.RefreshColumns();
+            }
+        }
+
+        private void BackgroundVideo_MediaEnded(object sender, RoutedEventArgs e)
+        {
+            BackgroundVideo.Position = TimeSpan.Zero;
+            BackgroundVideo.Play();
+        }
+    }
+}
diff --git a/ModAssistant/ModAssistant.csproj b/ModAssistant/ModAssistant.csproj
new file mode 100644
index 00000000..9291398a
--- /dev/null
+++ b/ModAssistant/ModAssistant.csproj
@@ -0,0 +1,303 @@
+
+
+  
+  
+    Debug
+    AnyCPU
+    {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}
+    WinExe
+    ModAssistant
+    ModAssistant
+    v4.6.1
+    512
+    {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+    4
+    true
+  
+  
+    AnyCPU
+    true
+    full
+    false
+    bin\Debug\
+    DEBUG;TRACE
+    prompt
+    4
+  
+  
+    AnyCPU
+    pdbonly
+    true
+    bin\Release\
+    TRACE
+    prompt
+    4
+  
+  
+    Resources\icon.ico
+  
+  
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+      4.0
+    
+    
+    
+    
+    
+  
+  
+    
+      MSBuild:Compile
+      Designer
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+      OneClickStatus.xaml
+    
+    
+      Intro.xaml
+    
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Invalid.xaml
+    
+    
+      Loading.xaml
+    
+    
+      Mods.xaml
+    
+    
+      About.xaml
+    
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      App.xaml
+      Code
+    
+    
+    
+      MainWindow.xaml
+      Code
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      MSBuild:Compile
+      Designer
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+    
+      Designer
+      MSBuild:Compile
+    
+  
+  
+    
+      Options.xaml
+    
+    
+      Code
+    
+    
+      True
+      True
+      Resources.resx
+    
+    
+      True
+      Settings.settings
+      True
+    
+    
+      ResXFileCodeGenerator
+      Resources.Designer.cs
+    
+    
+      PublicSettingsSingleFileGenerator
+      Settings.Designer.cs
+    
+  
+  
+    
+  
+  
+    
+  
+  
+    
+  
+  
+
\ No newline at end of file
diff --git a/ModAssistant/OneClickStatus.xaml b/ModAssistant/OneClickStatus.xaml
new file mode 100644
index 00000000..74d0bcde
--- /dev/null
+++ b/ModAssistant/OneClickStatus.xaml
@@ -0,0 +1,98 @@
+
+
+    
+        
+        
+    
+
+    
+        
+            
+            
+        
+        
+
+        
+            
+                
+                
+            
+
+            
+            
+            
+        
+        
+            
+                
+                
+            
+            
+                
+                    
+                
+            
+
+        
+    
+
diff --git a/ModAssistant/OneClickStatus.xaml.cs b/ModAssistant/OneClickStatus.xaml.cs
new file mode 100644
index 00000000..6b5d4462
--- /dev/null
+++ b/ModAssistant/OneClickStatus.xaml.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Windows;
+using System.Windows.Data;
+
+namespace ModAssistant
+{
+    /// 
+    /// Interaction logic for OneClickStatus.xaml
+    /// 
+    public partial class OneClickStatus : Window
+    {
+        public static OneClickStatus Instance;
+
+        public string HistoryText
+        {
+            get
+            {
+                return HistoryTextBlock.Text;
+            }
+            set
+            {
+                Dispatcher.Invoke(new Action(() => { OneClickStatus.Instance.HistoryTextBlock.Text = value; }));
+            }
+        }
+        public string MainText
+        {
+            get
+            {
+                return HistoryTextBlock.Text;
+            }
+            set
+            {
+                Dispatcher.Invoke(new Action(() => {
+                    OneClickStatus.Instance.HistoryTextBlock.Text = string.IsNullOrEmpty(MainText) ? $"{value}" : $"{value}\n{MainText}";
+                }));
+            }
+        }
+
+        public OneClickStatus()
+        {
+            InitializeComponent();
+            Instance = this;
+        }
+    }
+
+    [ValueConversion(typeof(double), typeof(double))]
+    public class DivideDoubleByTwoConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+        {
+            if (targetType != typeof(double))
+            {
+                throw new InvalidOperationException("The target must be a double");
+            }
+            double d = (double)value;
+            return ((double)d) / 2;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+        {
+            throw new NotSupportedException();
+        }
+    }
+}
diff --git a/ModAssistant/Pages/About.xaml b/ModAssistant/Pages/About.xaml
new file mode 100644
index 00000000..188d3ecc
--- /dev/null
+++ b/ModAssistant/Pages/About.xaml
@@ -0,0 +1,292 @@
+
+
+    
+        
+            
+            
+            
+            
+            
+            
+            
+            
+            
+            
+            
+            
+
+        
+
+        
+        
+        
+            •
+            
+        
+        
+            •
+            
+        
+        
+            •
+            
+        
+        
+            •
+            
+        
+
+        
+            
+        
+
+        
+
+        
+
+            
+                
+                    
+                        Umbranox
+                    
+                
+                
+                    Inspiration
+                
+                
+                    Creating the Mod Manager
+                
+                
+                    
+                        
+                    
+                
+            
+
+            
+                
+                    
+                        lolPants
+                    
+                
+                
+                    Inspiration
+                
+                
+                    Creating ModSaber
+                
+                
+                    The first Mod repository
+                
+                
+                    
+                        
+                    
+                
+            
+
+            
+                
+                    
+                        Caeden117
+                    
+                
+                
+                    Theme Support
+                
+                
+                    
+                        
+                    
+                
+            
+
+            
+                
+                    
+                        Interz
+                    
+                
+                
+                    Logos and icon design
+                
+                
+                    
+                        
+                    
+                
+            
+
+            
+                
+                    
+                        Megalon2D
+                    
+                
+                
+                    BSMG Theme
+                
+                
+                    Lots of fixes
+                
+                
+                    
+                        
+                    
+                
+            
+
+        
+        
+            
+            
+        
+    
+
diff --git a/ModAssistant/Pages/Intro.xaml.cs b/ModAssistant/Pages/Intro.xaml.cs
new file mode 100644
index 00000000..25029dcd
--- /dev/null
+++ b/ModAssistant/Pages/Intro.xaml.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Navigation;
+
+namespace ModAssistant.Pages
+{
+    /// 
+    /// Interaction logic for Intro.xaml
+    /// 
+    public partial class Intro : Page
+    {
+        public static Intro Instance = new Intro();
+
+        public Intro()
+        {
+            InitializeComponent();
+        }
+
+        private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
+        {
+            Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
+            e.Handled = true;
+        }
+
+        private void Disagree_Click(object sender, RoutedEventArgs e)
+        {
+            MainWindow.Instance.ModsButton.IsEnabled = false;
+            Properties.Settings.Default.Agreed = false;
+            Properties.Settings.Default.Save();
+            MessageBox.Show((string)FindResource("Intro:ClosingApp"));
+            System.Windows.Application.Current.Shutdown();
+        }
+
+        private void Agree_Click(object sender, RoutedEventArgs e)
+        {
+            if (string.IsNullOrEmpty(MainWindow.GameVersion))
+            {
+                string line1 = (string)FindResource("Intro:VersionDownloadFailed");
+                string line2 = (string)FindResource("Intro:ModsTabDisabled");
+
+                MessageBox.Show($"{line1}.\n{line2}");
+            }
+            else
+            {
+                MainWindow.Instance.ModsButton.IsEnabled = true;
+
+                string text = (string)FindResource("Intro:ModsTabEnabled");
+                Utils.SendNotify(text);
+                MainWindow.Instance.MainText = text;
+            }
+            Properties.Settings.Default.Agreed = true;
+            Properties.Settings.Default.Save();
+        }
+    }
+}
diff --git a/ModAssistant/Pages/Invalid.xaml b/ModAssistant/Pages/Invalid.xaml
new file mode 100644
index 00000000..c65f5050
--- /dev/null
+++ b/ModAssistant/Pages/Invalid.xaml
@@ -0,0 +1,127 @@
+
+
+    
+        
+            
+            
+            
+            
+            
+            
+            
+            
+            
+            
+        
+
+        
+            
+            
+        
+
+        
+        
+        
+
+        
+            
+        
+
+        
+            
+        
+
+        
+            
+        
+
+        
+        
+            
+            :
+        
+        
+            
+        
+