├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── dotnet.yml │ └── dotnet_build_v2.yml ├── LICENSE ├── ModAssistant.sln ├── ModAssistant ├── App.config ├── App.xaml ├── App.xaml.cs ├── Classes │ ├── Diagnostics.cs │ ├── External Interfaces │ │ ├── BeatSaver.cs │ │ ├── ModelSaber.cs │ │ ├── Playlists.cs │ │ └── Utils.cs │ ├── Http.cs │ ├── HyperlinkExtensions.cs │ ├── Languages.cs │ ├── Mod.cs │ ├── OneClickInstaller.cs │ ├── Promotions.cs │ ├── Server.cs │ ├── Themes.cs │ ├── Updater.cs │ ├── Utils.cs │ └── ZeyuCount.cs ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── Libs │ └── semver │ │ ├── IntExtensions.cs │ │ └── SemVersion.cs ├── Localisation │ ├── cs.xaml │ ├── de.xaml │ ├── en-DEBUG.xaml │ ├── en.xaml │ ├── es.xaml │ ├── fr.xaml │ ├── it.xaml │ ├── ja.xaml │ ├── ko.xaml │ ├── nb.xaml │ ├── nl.xaml │ ├── pl.xaml │ ├── pt.xaml │ ├── ru.xaml │ ├── sv.xaml │ ├── th.xaml │ └── zh.xaml ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── ModAssistant.csproj ├── OneClickStatus.xaml ├── OneClickStatus.xaml.cs ├── Pages │ ├── About.xaml │ ├── About.xaml.cs │ ├── Intro.xaml │ ├── Intro.xaml.cs │ ├── Invalid.xaml │ ├── Invalid.xaml.cs │ ├── Loading.xaml │ ├── Loading.xaml.cs │ ├── Mods.xaml │ ├── Mods.xaml.cs │ ├── Options.xaml │ └── Options.xaml.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── Resources │ ├── Icons.xaml │ └── icon.ico ├── Styles │ ├── Button.xaml │ ├── CheckBox.xaml │ ├── ComboBox.xaml │ ├── ComboBoxItem.xaml │ ├── GridViewColumnHeader.xaml │ ├── Label.xaml │ ├── ListView.xaml │ ├── ListViewItem.xaml │ ├── Menu.xaml │ ├── MenuItem.xaml │ ├── RepeatButton.xaml │ ├── ScrollBar.xaml │ ├── TextBlock.xaml │ ├── Thumb.xaml │ └── ToggleButton.xaml ├── Themes │ ├── Anniversary.xaml │ ├── Anniversary │ │ └── Background.png │ ├── BSMG.xaml │ ├── BSMG │ │ └── Sidebar.png │ ├── Dark.xaml │ ├── Default Scrollbar.xaml │ ├── Light Pink.xaml │ ├── Light.xaml │ └── Ugly Kulu-Ya-Ku.xaml └── packages.config ├── README.md └── tools ├── README.md └── generate_translation_stubs.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.cs] 12 | dotnet_sort_system_directives_first = true 13 | dotnet_separate_import_directive_groups = false 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.yml] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: BeatSaberMods 2 | ko_fi: N4N8JX7B 3 | liberapay: Assistant 4 | custom: ['https://paypal.me/AssistantMoe', 'https://bs.assistant.moe/Donate/'] 5 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build 2 | on: 3 | push: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup msbuild 15 | uses: microsoft/setup-msbuild@v1.1.3 16 | - name: Install dependencies 17 | run: msbuild -t:restore 18 | - name: Build project 19 | run: msbuild ModAssistant/ModAssistant.csproj /t:Build /p:Configuration=Release 20 | - name: Cleanup release 21 | shell: bash 22 | run: | 23 | find "ModAssistant/bin/Release" -type f ! -name "ModAssistant.exe" -delete 24 | cp "LICENSE" "ModAssistant/bin/Release/LICENSE.ModAssistant.txt" 25 | - name: Upload Build 26 | if: startsWith(github.ref, 'refs/tags/') == false 27 | uses: actions/upload-artifact@v3 28 | with: 29 | name: ModAssistant-${{ github.sha }} 30 | path: ./ModAssistant/bin/Release/ 31 | - name: Extract Release Version 32 | if: startsWith(github.ref, 'refs/tags/') 33 | id: get_version 34 | shell: bash 35 | run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 36 | - name: Release 37 | if: startsWith(github.ref, 'refs/tags/') 38 | uses: softprops/action-gh-release@v1 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | with: 42 | name: Mod Assistant v${{ steps.get_version.outputs.version }} 43 | files: ./ModAssistant/bin/Release/ModAssistant.exe 44 | -------------------------------------------------------------------------------- /.github/workflows/dotnet_build_v2.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build v2 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup msbuild 18 | uses: microsoft/setup-msbuild@v1.3.1 19 | 20 | - name: Restore dependencies 21 | run: msbuild -t:restore 22 | 23 | - name: Setup NuGet.exe 24 | uses: nuget/setup-nuget@v1 25 | with: 26 | nuget-version: latest 27 | 28 | - name: Restore Nuget Dependencies 29 | run: nuget restore ModAssistant.sln 30 | 31 | - name: Build project 32 | run: msbuild ModAssistant/ModAssistant.csproj /t:Build /p:Configuration=Release 33 | 34 | - name: Cleanup release 35 | shell: bash 36 | run: | 37 | find "ModAssistant/bin/Release" -type f ! -name "ModAssistant.exe" -delete 38 | cp "LICENSE" "ModAssistant/bin/Release/LICENSE.ModAssistant.txt" 39 | - name: Upload Build 40 | if: startsWith(github.ref, 'refs/tags/') == false 41 | uses: actions/upload-artifact@v3 42 | with: 43 | name: ModAssistant-${{ github.sha }} 44 | path: ./ModAssistant/bin/Release/ 45 | 46 | - name: Extract Release Version 47 | if: startsWith(github.ref, 'refs/tags/') 48 | id: get_version 49 | shell: bash 50 | run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 51 | 52 | - name: Release 53 | if: startsWith(github.ref, 'refs/tags/') 54 | uses: softprops/action-gh-release@v1 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | with: 58 | name: Mod Assistant v${{ steps.get_version.outputs.version }} 59 | files: ./ModAssistant/bin/Release/ModAssistant.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Assistant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ModAssistant.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2027 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModAssistant", "ModAssistant\ModAssistant.csproj", "{6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {6A224B82-40DA-40B3-94DC-EFBEC2BDDA39}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {ACC0D20B-78C5-40AC-BF48-553C41D9987B} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /ModAssistant/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | False 22 | 23 | 24 | True 25 | 26 | 27 | False 28 | 29 | 30 | False 31 | 32 | 33 | False 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | True 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | True 52 | 53 | 54 | False 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 国际源@BeatMods 64 | 65 | 66 | 67 | 68 | 69 | False 70 | 71 | 72 | 默认@default 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ModAssistant/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ModAssistant/Classes/Diagnostics.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Windows; 4 | 5 | namespace ModAssistant 6 | { 7 | class Diagnostics 8 | { 9 | public static string[] ReadFolder(string path, int level = 0) 10 | { 11 | List entries = new List(); 12 | 13 | foreach (string file in Directory.GetFileSystemEntries(path)) 14 | { 15 | string line = string.Empty; 16 | 17 | if (File.Exists(file)) 18 | { 19 | line = Utils.CalculateMD5(file) + " " + LevelSeparator(level) + "├─ " + Path.GetFileName(file); 20 | entries.Add(line); 21 | 22 | } 23 | else if (Directory.Exists(file)) 24 | { 25 | line = Utils.Constants.MD5Spacer + LevelSeparator(level) + "├─ " + Path.GetFileName(file); 26 | entries.Add(line); 27 | 28 | foreach (string entry in ReadFolder(file.Replace(@"\", @"\\"), level + 1)) 29 | { 30 | //MessageBox.Show(entry); 31 | entries.Add(entry); 32 | } 33 | 34 | } 35 | else 36 | { 37 | MessageBox.Show("! " + file); 38 | } 39 | } 40 | if (entries.Count > 0) 41 | { 42 | entries[entries.Count - 1] = entries[entries.Count - 1].Replace("├", "└"); 43 | } 44 | 45 | return entries.ToArray(); 46 | } 47 | 48 | private static string LevelSeparator(int level) 49 | { 50 | string separator = string.Empty; 51 | for (int i = 0; i < level; i++) 52 | { 53 | separator += "│ "; 54 | } 55 | return separator; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ModAssistant/Classes/External Interfaces/ModelSaber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows; 4 | 5 | namespace ModAssistant.API 6 | { 7 | class ModelSaber 8 | { 9 | private static string ModelSaberURLPrefix 10 | { 11 | get => 12 | Properties.Settings.Default.AssetsDownloadServer == AssetsServer.Default ? ModAssistant.Utils.Constants.ModelSaberURLPrefix_default : 13 | (Properties.Settings.Default.AssetsDownloadServer == AssetsServer.WGZeyu ? ModAssistant.Utils.Constants.ModelSaberURLPrefix_wgzeyu : 14 | ModAssistant.Utils.Constants.ModelSaberURLPrefix_beatsaberchina); 15 | } 16 | private const string CustomAvatarsFolder = "CustomAvatars"; 17 | private const string CustomSabersFolder = "CustomSabers"; 18 | private const string CustomPlatformsFolder = "CustomPlatforms"; 19 | private const string CustomBloqsFolder = "CustomNotes"; 20 | 21 | public static async Task GetModel(Uri uri) 22 | { 23 | string proxyURL = ModelSaberURLPrefix; 24 | bool throughProxy = true; 25 | bool fallback = false; 26 | 27 | if (Properties.Settings.Default.AssetsDownloadServer == "国内源@WGzeyu" && !ModAssistant.ZeyuCount.checkModelSaberSingle()) 28 | { 29 | proxyURL = ModAssistant.Utils.Constants.ModelSaberURLPrefix_default; 30 | Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Fallback"), "默认@default")}"); 31 | throughProxy = false; 32 | fallback = true; 33 | } 34 | 35 | switch (uri.Host) 36 | { 37 | case "avatar": 38 | await Utils.DownloadAsset(proxyURL + uri.Host + uri.AbsolutePath, CustomAvatarsFolder, null, null, true, false, false, throughProxy, fallback); 39 | break; 40 | case "saber": 41 | await Utils.DownloadAsset(proxyURL + uri.Host + uri.AbsolutePath, CustomSabersFolder, null, null, true, false, false, throughProxy, fallback); 42 | break; 43 | case "platform": 44 | await Utils.DownloadAsset(proxyURL + uri.Host + uri.AbsolutePath, CustomPlatformsFolder, null, null, true, false, false, throughProxy, fallback); 45 | break; 46 | case "bloq": 47 | await Utils.DownloadAsset(proxyURL + uri.Host + uri.AbsolutePath, CustomBloqsFolder, null, null, true, false, false, throughProxy, fallback); 48 | break; 49 | } 50 | } 51 | public static string proxyURL() 52 | { 53 | if (Properties.Settings.Default.AssetsDownloadServer == "国内源@WGzeyu" && !ModAssistant.ZeyuCount.checkModelSaberSingle()) 54 | { 55 | return ModAssistant.Utils.Constants.ModelSaberURLPrefix_default; 56 | } 57 | 58 | return ModelSaberURLPrefix; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ModAssistant/Classes/External Interfaces/Playlists.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Windows; 6 | using static ModAssistant.Http; 7 | 8 | namespace ModAssistant.API 9 | { 10 | public class Playlists 11 | { 12 | private const string BSaberURLPrefix = "https://bsaber.com/PlaylistAPI/"; 13 | private const string PlaylistsFolder = "Playlists"; 14 | private static readonly string BeatSaberPath = Utils.BeatSaberPath; 15 | 16 | public static void CreatePlaylistsFolder() 17 | { 18 | string playlistsPath = Path.Combine(BeatSaberPath, PlaylistsFolder); 19 | Directory.CreateDirectory(playlistsPath); 20 | } 21 | 22 | public static async Task DownloadAll(Uri uri) 23 | { 24 | switch (uri.Host) 25 | { 26 | case "playlist": 27 | Uri url = new Uri($"{uri.PathAndQuery.Trim('/')}"); 28 | string filename = await Get(url); 29 | await DownloadFrom(filename); 30 | break; 31 | } 32 | } 33 | 34 | public static async Task Get(Uri url) 35 | { 36 | try 37 | { 38 | CreatePlaylistsFolder(); 39 | string filename = await Utils.DownloadAsset(url.ToString(), PlaylistsFolder, preferContentDisposition: true); 40 | return Path.Combine(BeatSaberPath, PlaylistsFolder, filename); 41 | } 42 | catch 43 | { 44 | return null; 45 | } 46 | } 47 | 48 | public static async Task DownloadFrom(string file) 49 | { 50 | CreatePlaylistsFolder(); 51 | 52 | if (Path.Combine(BeatSaberPath, PlaylistsFolder) != Path.GetDirectoryName(file)) 53 | { 54 | string destination = Path.Combine(BeatSaberPath, PlaylistsFolder, Path.GetFileName(file)); 55 | File.Copy(file, destination, true); 56 | } 57 | 58 | int Errors = 0; 59 | int Minimum = 0; 60 | int Value = 0; 61 | string fallback = string.Empty; 62 | 63 | Playlist playlist = JsonSerializer.Deserialize(File.ReadAllText(file)); 64 | int Maximum = playlist.songs.Length; 65 | if (ModAssistant.Properties.Settings.Default.AssetsDownloadServer == "国内源@WGzeyu") 66 | { 67 | if (playlist.songs.Length > 20) 68 | { 69 | fallback = ModAssistant.Properties.Settings.Default.AssetsDownloadServer; 70 | ModAssistant.Properties.Settings.Default.AssetsDownloadServer = "默认@default"; 71 | ModAssistant.Properties.Settings.Default.Save(); 72 | Utils.SetMessage((string)Application.Current.FindResource("Options:PlaylistsFallback")); 73 | } 74 | } 75 | 76 | foreach (Playlist.Song song in playlist.songs) 77 | { 78 | API.BeatSaver.BeatSaverMap response = new BeatSaver.BeatSaverMap(); 79 | if (!string.IsNullOrEmpty(song.hash)) 80 | { 81 | response = await BeatSaver.GetFromHash(song.hash, false, true); 82 | } 83 | else if (!string.IsNullOrEmpty(song.key)) 84 | { 85 | response = await BeatSaver.GetFromKey(song.key, false, true); 86 | } 87 | Value++; 88 | 89 | if (response.Success) 90 | { 91 | Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:InstallingPlaylist"), TextProgress(Minimum, Maximum, Value))} {response.Name}"); 92 | } 93 | else 94 | { 95 | Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FailedPlaylistSong"), song.songName)}"); 96 | ModAssistant.Utils.Log($"Failed installing BeatSaver map: {song.songName} | {song.key} | {song.hash} | ({response?.response?.ratelimit?.Remaining})"); 97 | App.CloseWindowOnFinish = false; 98 | await Task.Delay(3 * 1000); 99 | Errors++; 100 | } 101 | } 102 | Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FinishedPlaylist"), Errors, playlist.playlistTitle)}"); 103 | 104 | if (!string.IsNullOrEmpty(fallback)) 105 | { 106 | ModAssistant.Properties.Settings.Default.AssetsDownloadServer = fallback; 107 | ModAssistant.Properties.Settings.Default.Save(); 108 | } 109 | } 110 | 111 | private static string TextProgress(int min, int max, int value) 112 | { 113 | if (max == value) 114 | { 115 | return $" {string.Concat(Enumerable.Repeat("▒", 10))} [{value}/{max}]"; 116 | } 117 | int interval = (int)Math.Floor((double)value / (((double)max - (double)min) / (double)10)); 118 | return $" {string.Concat(Enumerable.Repeat("▒", interval))}{string.Concat(Enumerable.Repeat("░", 10 - interval))} [{value}/{max}]"; 119 | } 120 | 121 | #pragma warning disable IDE1006 // Naming Styles 122 | class Playlist 123 | { 124 | public string playlistTitle { get; set; } 125 | public string playlistAuthor { get; set; } 126 | public string image { get; set; } 127 | public Song[] songs { get; set; } 128 | 129 | public class Song 130 | { 131 | public string key { get; set; } 132 | public string hash { get; set; } 133 | public string songName { get; set; } 134 | public string uploader { get; set; } 135 | } 136 | } 137 | } 138 | } 139 | #pragma warning restore IDE1006 // Naming Styles 140 | -------------------------------------------------------------------------------- /ModAssistant/Classes/External Interfaces/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | 8 | namespace ModAssistant.API 9 | { 10 | public class Utils 11 | { 12 | public static readonly string BeatSaberPath = App.BeatSaberInstallDirectory; 13 | 14 | public static void SetMessage(string message) 15 | { 16 | if (App.OCIWindow != "No" || App.OCIWindow != "Notify") 17 | { 18 | if (App.window == null) 19 | { 20 | if (App.OCIWindow == "No" || App.OCIWindow == "Notify") OneClickStatus.Instance = null; 21 | if (OneClickStatus.Instance == null) return; 22 | 23 | OneClickStatus.Instance.MainText = message; 24 | } 25 | else 26 | { 27 | MainWindow.Instance.MainText = message; 28 | } 29 | } 30 | } 31 | 32 | public static async Task DownloadAsset(string link, string folder, string fileName = null, string displayName = null, bool showNotification = true, bool beatsaver = false, bool preferContentDisposition = false, bool fromPlaylist = false, bool fallback = false) 33 | { 34 | if (string.IsNullOrEmpty(BeatSaberPath)) 35 | { 36 | ModAssistant.Utils.SendNotify((string)Application.Current.FindResource("OneClick:InstallDirNotFound")); 37 | } 38 | try 39 | { 40 | var parentDir = Path.Combine(BeatSaberPath, folder); 41 | Directory.CreateDirectory(parentDir); 42 | 43 | if (string.IsNullOrEmpty(fileName)) 44 | { 45 | fileName = new Uri(link).Segments.Last(); 46 | } 47 | 48 | if (beatsaver) 49 | { 50 | fileName = WebUtility.UrlDecode(Path.Combine(parentDir, fileName)); 51 | await BeatSaver.Download(link, fileName, fromPlaylist, fallback); 52 | } 53 | else 54 | { 55 | fileName = await ModAssistant.Utils.Download(link, parentDir, fileName, preferContentDisposition, true, fallback); 56 | } 57 | 58 | if (string.IsNullOrEmpty(displayName)) 59 | { 60 | displayName = Path.GetFileNameWithoutExtension(fileName); 61 | } 62 | 63 | if (showNotification) 64 | { 65 | SetMessage(string.Format((string)Application.Current.FindResource("OneClick:InstalledAsset"), displayName)); 66 | } 67 | 68 | return fileName; 69 | } 70 | catch 71 | { 72 | SetMessage((string)Application.Current.FindResource("OneClick:AssetInstallFailed")); 73 | App.CloseWindowOnFinish = false; 74 | return null; 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ModAssistant/Classes/Http.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Threading.Tasks; 7 | using System.Threading; 8 | using System.Web.Script.Serialization; 9 | using BrotliSharpLib; 10 | using System.Linq; 11 | using System.IO.Compression; 12 | using System.IO; 13 | 14 | namespace ModAssistant 15 | { 16 | static class Http 17 | { 18 | private static HttpClient _client = null; 19 | 20 | public static HttpClient HttpClient 21 | { 22 | get 23 | { 24 | if (_client != null) return _client; 25 | 26 | /*var handler = new HttpClientHandler() 27 | { 28 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, 29 | };*/ 30 | 31 | _client = new HttpClient(new BrotliCompressionHandler()) 32 | { 33 | Timeout = TimeSpan.FromSeconds(360), 34 | }; 35 | 36 | ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault; 37 | _client.DefaultRequestHeaders.Add("User-Agent", "ModAssistant/" + App.Version); 38 | 39 | return _client; 40 | } 41 | } 42 | 43 | public static JavaScriptSerializer JsonSerializer = new JavaScriptSerializer() 44 | { 45 | MaxJsonLength = int.MaxValue, 46 | }; 47 | 48 | private class BrotliCompressionHandler : DelegatingHandler 49 | { 50 | public BrotliCompressionHandler() : base(new HttpClientHandler()) 51 | { 52 | } 53 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 54 | { 55 | // Specify supported encodings in the request 56 | request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("br")); 57 | request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); 58 | request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate")); 59 | 60 | return base.SendAsync(request, cancellationToken).ContinueWith(responseTask => 61 | { 62 | HttpResponseMessage response = responseTask.Result; 63 | 64 | // Check if the response is compressed 65 | if (response.Content.Headers.ContentEncoding != null) 66 | { 67 | foreach (var encoding in response.Content.Headers.ContentEncoding) 68 | { 69 | switch (encoding.ToLowerInvariant()) 70 | { 71 | case "br": 72 | // Decompress Brotli content 73 | byte[] brotliCompressedBytes = response.Content.ReadAsByteArrayAsync().Result; 74 | byte[] brotliDecompressedBytes = Brotli.DecompressBuffer(brotliCompressedBytes, 0, brotliCompressedBytes.Length); 75 | response.Content = new ByteArrayContent(brotliDecompressedBytes); 76 | response.Content.Headers.Remove("Content-Encoding"); 77 | break; 78 | 79 | case "gzip": 80 | // Decompress GZip content 81 | using (var stream = new GZipStream(response.Content.ReadAsStreamAsync().Result, CompressionMode.Decompress)) 82 | { 83 | var resultStream = new MemoryStream(); 84 | stream.CopyTo(resultStream); 85 | response.Content = new ByteArrayContent(resultStream.ToArray()); 86 | } 87 | response.Content.Headers.Remove("Content-Encoding"); 88 | break; 89 | 90 | case "deflate": 91 | // Decompress Deflate content 92 | using (var stream = new DeflateStream(response.Content.ReadAsStreamAsync().Result, CompressionMode.Decompress)) 93 | { 94 | var resultStream = new MemoryStream(); 95 | stream.CopyTo(resultStream); 96 | response.Content = new ByteArrayContent(resultStream.ToArray()); 97 | } 98 | response.Content.Headers.Remove("Content-Encoding"); 99 | break; 100 | } 101 | } 102 | } 103 | 104 | return response; 105 | }, cancellationToken); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /ModAssistant/Classes/HyperlinkExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Windows; 3 | using System.Windows.Documents; 4 | 5 | namespace ModAssistant 6 | { 7 | public static class HyperlinkExtensions 8 | { 9 | public static bool GetIsExternal(DependencyObject obj) 10 | { 11 | return (bool)obj.GetValue(IsExternalProperty); 12 | } 13 | 14 | public static void SetIsExternal(DependencyObject obj, bool value) 15 | { 16 | obj.SetValue(IsExternalProperty, value); 17 | } 18 | 19 | public static readonly DependencyProperty IsExternalProperty = DependencyProperty.RegisterAttached("IsExternal", typeof(bool), typeof(HyperlinkExtensions), new UIPropertyMetadata(false, OnIsExternalChanged)); 20 | 21 | private static void OnIsExternalChanged(object sender, DependencyPropertyChangedEventArgs args) 22 | { 23 | var hyperlink = sender as Hyperlink; 24 | 25 | if ((bool)args.NewValue) 26 | hyperlink.RequestNavigate += Hyperlink_RequestNavigate; 27 | else 28 | hyperlink.RequestNavigate -= Hyperlink_RequestNavigate; 29 | } 30 | 31 | private static void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) 32 | { 33 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); 34 | e.Handled = true; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ModAssistant/Classes/Languages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Windows; 7 | using ModAssistant.Pages; 8 | 9 | namespace ModAssistant 10 | { 11 | internal class Languages 12 | { 13 | public static string LoadedLanguage { get; private set; } 14 | public static List LoadedLanguages => availableCultures.ToList(); 15 | public static bool FirstRun = true; 16 | private static readonly string[] availableLanguageCodes = { "cs", "de", "en", "es", "fr", "it", "ja", "ko", "nb", "nl", "pl", "pt", "ru", "sv", "th", "zh" }; 17 | private static IEnumerable availableCultures; 18 | 19 | public static void LoadLanguages() 20 | { 21 | var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures); 22 | 23 | // Get CultureInfo for any of the available translations 24 | availableCultures = allCultures.Where(cultureInfo => availableLanguageCodes.Any(code => code.Equals(cultureInfo.Name))); 25 | 26 | string savedLanguageCode = Properties.Settings.Default.LanguageCode; 27 | if (!LoadLanguage(savedLanguageCode)) 28 | { 29 | // If no language code was saved, load system language 30 | if (!LoadLanguage(CultureInfo.CurrentUICulture.Name)) 31 | { 32 | _ = LoadLanguage("en"); 33 | } 34 | } 35 | 36 | UpdateUI(LoadedLanguage); 37 | } 38 | 39 | public static void UpdateUI(string languageCode) 40 | { 41 | if (Options.Instance != null && Options.Instance.LanguageSelectComboBox != null) 42 | { 43 | Options.Instance.LanguageSelectComboBox.ItemsSource = availableCultures.Select(cultureInfo => cultureInfo.NativeName).ToList(); 44 | Options.Instance.LanguageSelectComboBox.SelectedIndex = LoadedLanguages.FindIndex(cultureInfo => cultureInfo.Name.Equals(languageCode)); 45 | } 46 | } 47 | 48 | public static ResourceDictionary LanguagesDict 49 | { 50 | get 51 | { 52 | return Application.Current.Resources.MergedDictionaries[1]; 53 | } 54 | } 55 | 56 | public static bool LoadLanguage(string languageCode) 57 | { 58 | if (string.IsNullOrEmpty(languageCode)) 59 | { 60 | return false; 61 | } 62 | 63 | try 64 | { 65 | LanguagesDict.Source = new Uri($"Localisation/{languageCode}.xaml", UriKind.Relative); 66 | LoadedLanguage = languageCode; 67 | return true; 68 | } 69 | catch (IOException) 70 | { 71 | if (languageCode.Contains("-")) 72 | { 73 | return LoadLanguage(languageCode.Split('-').First()); 74 | } 75 | 76 | return false; 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ModAssistant/Classes/Mod.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ModAssistant.Pages; 3 | 4 | namespace ModAssistant 5 | { 6 | public class Mod 7 | { 8 | public string name; 9 | public string nameWithTranslation; 10 | public string version; 11 | public string gameVersion; 12 | public string trueGameVersion; 13 | public string _id; 14 | public string status; 15 | public string authorId; 16 | public string uploadedDate; 17 | public string updatedDate; 18 | public Author author; 19 | public string description; 20 | public string descriptionWithTranslation; 21 | public string link; 22 | public string category; 23 | public DownloadLink[] downloads; 24 | public bool required; 25 | public Dependency[] dependencies; 26 | public List Dependents = new List(); 27 | public Mods.ModListItem ListItem; 28 | public Translation[] translations; 29 | 30 | public class Author 31 | { 32 | public string _id; 33 | public string username; 34 | public string lastLogin; 35 | } 36 | 37 | public class DownloadLink 38 | { 39 | public string type; 40 | public string url; 41 | public FileHashes[] hashMd5; 42 | } 43 | 44 | public class FileHashes 45 | { 46 | public string hash; 47 | public string file; 48 | } 49 | 50 | public class Dependency 51 | { 52 | public string name; 53 | public string _id; 54 | public Mod Mod; 55 | } 56 | 57 | public class Translation { 58 | public string name; 59 | public string description; 60 | public string language; 61 | } 62 | 63 | public class TranslationWGzeyu 64 | { 65 | public string name; 66 | public string description; 67 | public string newname; 68 | public string newdescription; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ModAssistant/Classes/OneClickInstaller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using System.Drawing; 6 | using Microsoft.Win32; 7 | using System.Windows.Forms; 8 | using Application = System.Windows.Application; 9 | using MessageBox = System.Windows.MessageBox; 10 | 11 | namespace ModAssistant 12 | { 13 | class OneClickInstaller 14 | { 15 | private static readonly string[] Protocols = new[] { "modelsaber", "beatsaver", "bsplaylist" }; 16 | public static OneClickStatus Status = new OneClickStatus(); 17 | 18 | public static async Task InstallAsset(string link) 19 | { 20 | Uri uri = new Uri(link); 21 | if (!Protocols.Contains(uri.Scheme)) return; 22 | 23 | switch (uri.Scheme) 24 | { 25 | case "modelsaber": 26 | await ModelSaber(uri); 27 | break; 28 | case "beatsaver": 29 | await BeatSaver(uri); 30 | break; 31 | case "bsplaylist": 32 | await Playlist(uri); 33 | break; 34 | } 35 | if (App.OCIWindow != "No") 36 | { 37 | Status.StopRotation(); 38 | API.Utils.SetMessage((string)Application.Current.FindResource("OneClick:Done")); 39 | } 40 | if (App.OCIWindow == "Notify") 41 | { 42 | Utils.SendNotify((string)Application.Current.FindResource("OneClick:DoneNotify")); 43 | Application.Current.Shutdown(); 44 | } 45 | if (App.OCIWindow == "Close") 46 | { 47 | Application.Current.Shutdown(); 48 | } 49 | 50 | 51 | } 52 | 53 | private static async Task BeatSaver(Uri uri) 54 | { 55 | string Key = uri.Host; 56 | await API.BeatSaver.GetFromKey(Key); 57 | } 58 | 59 | private static async Task ModelSaber(Uri uri) 60 | { 61 | if (App.OCIWindow != "No" && App.OCIWindow != "Notify") Status.Show(); 62 | API.Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Installing"), System.Web.HttpUtility.UrlDecode(uri.Segments.Last()))}"); 63 | await API.ModelSaber.GetModel(uri); 64 | } 65 | 66 | private static async Task Playlist(Uri uri) 67 | { 68 | if (App.OCIWindow != "No" && App.OCIWindow != "Notify") Status.Show(); 69 | if (App.OCIWindow == "Notify") Utils.SendNotify((string)Application.Current.FindResource("OneClick:StartDownloadPlaylist")); 70 | await API.Playlists.DownloadAll(uri); 71 | } 72 | 73 | public static void Register(string Protocol, bool Background = false, string Description = null) 74 | { 75 | if (IsRegistered(Protocol) == true) 76 | return; 77 | try 78 | { 79 | if (Utils.IsAdmin) 80 | { 81 | RegistryKey ProtocolKey = Registry.ClassesRoot.OpenSubKey(Protocol, true); 82 | if (ProtocolKey == null) 83 | ProtocolKey = Registry.ClassesRoot.CreateSubKey(Protocol, true); 84 | RegistryKey CommandKey = ProtocolKey.CreateSubKey(@"shell\open\command", true); 85 | if (CommandKey == null) 86 | CommandKey = Registry.ClassesRoot.CreateSubKey(@"shell\open\command", true); 87 | 88 | if (ProtocolKey.GetValue("OneClick-Provider", "").ToString() != "ModAssistant") 89 | { 90 | if (Description != null) 91 | { 92 | ProtocolKey.SetValue("", Description, RegistryValueKind.String); 93 | } 94 | ProtocolKey.SetValue("URL Protocol", "", RegistryValueKind.String); 95 | ProtocolKey.SetValue("OneClick-Provider", "ModAssistant", RegistryValueKind.String); 96 | CommandKey.SetValue("", $"\"{Utils.ExePath}\" \"--install\" \"%1\""); 97 | } 98 | 99 | Utils.SendNotify(string.Format((string)Application.Current.FindResource("OneClick:ProtocolHandler:Registered"), Protocol)); 100 | } 101 | else 102 | { 103 | Utils.StartAsAdmin($"\"--register\" \"{Protocol}\" \"{Description}\""); 104 | } 105 | } 106 | catch (Exception e) 107 | { 108 | MessageBox.Show(e.ToString()); 109 | } 110 | 111 | if (Background) 112 | Application.Current.Shutdown(); 113 | else 114 | Pages.Options.Instance.UpdateHandlerStatus(); 115 | } 116 | 117 | public static void Unregister(string Protocol, bool Background = false) 118 | { 119 | if (IsRegistered(Protocol) == false) 120 | return; 121 | try 122 | { 123 | if (Utils.IsAdmin) 124 | { 125 | using (RegistryKey ProtocolKey = Registry.ClassesRoot.OpenSubKey(Protocol, true)) 126 | { 127 | if (ProtocolKey != null 128 | && ProtocolKey.GetValue("OneClick-Provider", "").ToString() == "ModAssistant") 129 | { 130 | Registry.ClassesRoot.DeleteSubKeyTree(Protocol); 131 | } 132 | } 133 | 134 | Utils.SendNotify(string.Format((string)Application.Current.FindResource("OneClick:ProtocolHandler:Unregistered"), Protocol)); 135 | } 136 | else 137 | { 138 | Utils.StartAsAdmin($"\"--unregister\" \"{Protocol}\""); 139 | } 140 | 141 | } 142 | catch (Exception e) 143 | { 144 | MessageBox.Show(e.ToString()); 145 | } 146 | 147 | if (Background) 148 | Application.Current.Shutdown(); 149 | else 150 | Pages.Options.Instance.UpdateHandlerStatus(); 151 | } 152 | 153 | public static bool IsRegistered(string Protocol) 154 | { 155 | RegistryKey ProtocolKey = Registry.ClassesRoot.OpenSubKey(Protocol); 156 | if (ProtocolKey != null 157 | && ProtocolKey.GetValue("OneClick-Provider", "").ToString() == "ModAssistant") 158 | return true; 159 | else 160 | return false; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /ModAssistant/Classes/Promotions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ModAssistant 4 | { 5 | class Promotions 6 | { 7 | public static Promotion[] List = 8 | { 9 | new Promotion 10 | { 11 | ModName = "YUR Fit Calorie Tracker", 12 | Links = new List(){ 13 | new PromotionLink{Text = "加入我们的Discord", Link = "https://yur.chat", TextAfterLink = ",或下载"}, 14 | new PromotionLink{Text = "iOS", Link = "https://bit.ly/yuriphone", TextAfterLink = "、" }, 15 | new PromotionLink{Text = "Android", Link = "https://play.google.com/store/apps/details?id=com.yur", TextAfterLink = "APP!" } 16 | }, 17 | Active = false 18 | } 19 | }; 20 | } 21 | 22 | class Promotion 23 | { 24 | public string ModName; 25 | public List Links; 26 | public bool Active; 27 | } 28 | 29 | class PromotionLink 30 | { 31 | public string Text; 32 | public string Link; 33 | public string TextAfterLink; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ModAssistant/Classes/Server.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ModAssistant 8 | { 9 | public static class Server 10 | { 11 | public const string BeatMods = "国际源@BeatMods"; 12 | public const string WGZeyu = "国内源@WGzeyu"; 13 | public const string BeatModsTop = "包子源@BeatTop"; 14 | } 15 | 16 | public static class AssetsServer 17 | { 18 | public const string Default = "默认@default"; 19 | public const string WGZeyu = "国内源@WGzeyu"; 20 | public const string BeatSaberChina = "光剑中文社区源@BeatSaberChina"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ModAssistant/Classes/Updater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using System.Windows; 6 | using static ModAssistant.Http; 7 | 8 | namespace ModAssistant 9 | { 10 | class Updater 11 | { 12 | private static readonly string APILatestURL = "https://api.github.com/repos/beatmods-top/ModAssistant/releases/latest"; 13 | 14 | private static Update LatestUpdate; 15 | private static Version CurrentVersion; 16 | private static Version LatestVersion; 17 | private static bool NeedsUpdate = false; 18 | private static readonly string NewExe = Path.Combine(Path.GetDirectoryName(Utils.ExePath), "ModAssistant.exe"); 19 | private static readonly string Arguments = App.Arguments; 20 | 21 | #pragma warning disable CS0162 // Unreachable code detected 22 | public static async Task CheckForUpdate() 23 | { 24 | #if DEBUG 25 | return false; 26 | #endif 27 | 28 | var resp = await HttpClient.GetAsync(APILatestURL); 29 | var body = await resp.Content.ReadAsStringAsync(); 30 | LatestUpdate = JsonSerializer.Deserialize(body); 31 | 32 | LatestVersion = new Version(LatestUpdate.tag_name.Substring(1)); 33 | CurrentVersion = new Version(App.Version); 34 | 35 | return (LatestVersion > CurrentVersion); 36 | } 37 | #pragma warning restore CS0162 // Unreachable code detected 38 | 39 | public static async Task Run() 40 | { 41 | if (Path.GetFileName(Utils.ExePath).Equals("ModAssistant.old.exe")) RunNew(); 42 | try 43 | { 44 | NeedsUpdate = await CheckForUpdate(); 45 | } 46 | catch 47 | { 48 | Utils.SendNotify((string)Application.Current.FindResource("Updater:CheckFailed")); 49 | } 50 | 51 | if (NeedsUpdate) await StartUpdate(); 52 | } 53 | 54 | public static async Task StartUpdate() 55 | { 56 | string OldExe = Path.Combine(Path.GetDirectoryName(Utils.ExePath), "ModAssistant.old.exe"); 57 | string DownloadLink = null; 58 | 59 | foreach (Update.Asset asset in LatestUpdate.assets) 60 | { 61 | if (asset.name == "ModAssistant.exe") 62 | { 63 | DownloadLink = asset.browser_download_url; 64 | } 65 | } 66 | 67 | if (string.IsNullOrEmpty(DownloadLink)) 68 | { 69 | Utils.SendNotify((string)Application.Current.FindResource("Updater:DownloadFailed")); 70 | } 71 | else 72 | { 73 | if (File.Exists(OldExe)) 74 | { 75 | File.Delete(OldExe); 76 | } 77 | 78 | File.Move(Utils.ExePath, OldExe); 79 | 80 | await Utils.Download(DownloadLink, "", NewExe); 81 | RunNew(); 82 | } 83 | } 84 | 85 | private static void RunNew() 86 | { 87 | Process.Start(NewExe, Arguments); 88 | Application.Current.Dispatcher.Invoke(() => { Application.Current.Shutdown(); }); 89 | } 90 | } 91 | 92 | public class Update 93 | { 94 | public string url; 95 | public string assets_url; 96 | public string upload_url; 97 | public string html_url; 98 | public int id; 99 | public string node_id; 100 | public string tag_name; 101 | public string target_commitish; 102 | public string name; 103 | public bool draft; 104 | public User author; 105 | public bool prerelease; 106 | public string created_at; 107 | public string published_at; 108 | public Asset[] assets; 109 | public string tarball_url; 110 | public string zipball_url; 111 | public string body; 112 | 113 | public class Asset 114 | { 115 | public string url; 116 | public int id; 117 | public string node_id; 118 | public string name; 119 | public string label; 120 | public User uploader; 121 | public string content_type; 122 | public string state; 123 | public int size; 124 | public string created_at; 125 | public string updated_at; 126 | public string browser_download_url; 127 | } 128 | 129 | public class User 130 | { 131 | public string login; 132 | public int id; 133 | public string node_id; 134 | public string avatar_url; 135 | public string gravatar_id; 136 | public string url; 137 | public string html_url; 138 | public string followers_url; 139 | public string following_url; 140 | public string gists_url; 141 | public string starred_url; 142 | public string subscriptions_url; 143 | public string organizations_url; 144 | public string repos_url; 145 | public string events_url; 146 | public string received_events_url; 147 | public string type; 148 | public bool site_admin; 149 | 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /ModAssistant/Classes/ZeyuCount.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Forms; 8 | using ModAssistant.Properties; 9 | using static ModAssistant.Http; 10 | 11 | namespace ModAssistant 12 | { 13 | public static class ZeyuCount 14 | { 15 | private static int limit = 200; 16 | private static int count = 0; 17 | private static string date = $"{DateTime.Now.Date.Year}-{DateTime.Now.Date.Month}-{DateTime.Now.Date.Day}"; 18 | 19 | private const int beatsaver_single_multiplier = 1; 20 | private const int beatsaver_playlist_multiplier = 3; 21 | private const int modelsaber_multiplier = 1; 22 | 23 | public class ZeyuCountData 24 | { 25 | public string date { get; set; } 26 | public int count { get; set; } 27 | } 28 | 29 | public static void loadData() 30 | { 31 | string prev_data = Settings.Default.ZeyuCount; 32 | if (!string.IsNullOrEmpty(prev_data)) 33 | { 34 | ZeyuCountData data = JsonSerializer.Deserialize(Settings.Default.ZeyuCount); 35 | date = data.date; 36 | count = data.count; 37 | } 38 | else { 39 | saveData(); 40 | } 41 | } 42 | 43 | public static void saveData() 44 | { 45 | ZeyuCountData data = new ZeyuCountData(); 46 | data.date = date; 47 | data.count = count; 48 | Settings.Default.ZeyuCount = JsonSerializer.Serialize(data); 49 | Settings.Default.Save(); 50 | } 51 | 52 | private static bool CheckCount(int chance) 53 | { 54 | updateCount(0); 55 | return (count + chance <= limit); 56 | } 57 | 58 | private static bool updateCount(int chance) 59 | { 60 | loadData(); 61 | if (string.IsNullOrEmpty(date)) 62 | { 63 | date = $"{DateTime.Now.Date.Year}-{DateTime.Now.Date.Month}-{DateTime.Now.Date.Day}"; 64 | } 65 | 66 | if (date != $"{DateTime.Now.Date.Year}-{DateTime.Now.Date.Month}-{DateTime.Now.Date.Day}") 67 | { 68 | date = $"{DateTime.Now.Date.Year}-{DateTime.Now.Date.Month}-{DateTime.Now.Date.Day}"; 69 | count = 0; 70 | } 71 | 72 | if (count + chance > limit) 73 | { 74 | return false; 75 | } 76 | else 77 | { 78 | count += chance; 79 | saveData(); 80 | Utils.UpdateCountIndicator(); 81 | return true; 82 | } 83 | } 84 | 85 | public static int getCount() { 86 | return limit - count; 87 | } 88 | 89 | public static bool checkBeatsaverSingle() 90 | { 91 | return CheckCount(beatsaver_single_multiplier); 92 | } 93 | 94 | public static bool checkBeatsaverMultiple() 95 | { 96 | return CheckCount(beatsaver_playlist_multiplier); 97 | } 98 | 99 | public static bool checkModelSaberSingle() 100 | { 101 | return CheckCount(modelsaber_multiplier); 102 | } 103 | 104 | public static void downloadBeatsaverSingle() 105 | { 106 | if (updateCount(beatsaver_single_multiplier)) 107 | { 108 | // When Success 109 | } 110 | else 111 | { 112 | // When False 113 | } 114 | } 115 | 116 | public static void downloadBeatsaverMultiple() 117 | { 118 | if (updateCount(beatsaver_playlist_multiplier)) 119 | { 120 | // When Success 121 | } 122 | else 123 | { 124 | // When False 125 | } 126 | } 127 | 128 | public static void downloadModelSaberSingle() 129 | { 130 | if (updateCount(modelsaber_multiplier)) 131 | { 132 | // When Success 133 | } 134 | else 135 | { 136 | // When False 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /ModAssistant/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ModAssistant/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks 13 | 14 | 15 | 16 | 17 | A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. 18 | 19 | 20 | 21 | 22 | A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks 23 | 24 | 25 | 26 | 27 | A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. 28 | 29 | 30 | 31 | 32 | A list of unmanaged 32 bit assembly names to include, delimited with line breaks. 33 | 34 | 35 | 36 | 37 | A list of unmanaged 64 bit assembly names to include, delimited with line breaks. 38 | 39 | 40 | 41 | 42 | The order of preloaded assemblies, delimited with line breaks. 43 | 44 | 45 | 46 | 47 | 48 | This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. 49 | 50 | 51 | 52 | 53 | Controls if .pdbs for reference assemblies are also embedded. 54 | 55 | 56 | 57 | 58 | Controls if runtime assemblies are also embedded. 59 | 60 | 61 | 62 | 63 | Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. 64 | 65 | 66 | 67 | 68 | Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. 69 | 70 | 71 | 72 | 73 | As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. 74 | 75 | 76 | 77 | 78 | Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. 79 | 80 | 81 | 82 | 83 | Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. 84 | 85 | 86 | 87 | 88 | A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | 89 | 90 | 91 | 92 | 93 | A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. 94 | 95 | 96 | 97 | 98 | A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | 99 | 100 | 101 | 102 | 103 | A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. 104 | 105 | 106 | 107 | 108 | A list of unmanaged 32 bit assembly names to include, delimited with |. 109 | 110 | 111 | 112 | 113 | A list of unmanaged 64 bit assembly names to include, delimited with |. 114 | 115 | 116 | 117 | 118 | The order of preloaded assemblies, delimited with |. 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 127 | 128 | 129 | 130 | 131 | A comma-separated list of error codes that can be safely ignored in assembly verification. 132 | 133 | 134 | 135 | 136 | 'false' to turn off automatic generation of the XML Schema file. 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /ModAssistant/Libs/semver/IntExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2013 Max Hauser 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | using System.Text; 24 | 25 | namespace ModAssistant.Libs 26 | { 27 | internal static class IntExtensions 28 | { 29 | /// 30 | /// The number of digits in a non-negative number. Returns 1 for all 31 | /// negative numbers. That is ok because we are using it to calculate 32 | /// string length for a for numbers that 33 | /// aren't supposed to be negative, but when they are it is just a little 34 | /// slower. 35 | /// 36 | /// 37 | /// This approach is based on https://stackoverflow.com/a/51099524/268898 38 | /// where the poster offers performance benchmarks showing this is the 39 | /// fastest way to get a number of digits. 40 | /// 41 | public static int Digits(this int n) 42 | { 43 | if (n < 10) return 1; 44 | if (n < 100) return 2; 45 | if (n < 1_000) return 3; 46 | if (n < 10_000) return 4; 47 | if (n < 100_000) return 5; 48 | if (n < 1_000_000) return 6; 49 | if (n < 10_000_000) return 7; 50 | if (n < 100_000_000) return 8; 51 | if (n < 1_000_000_000) return 9; 52 | return 10; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ModAssistant/OneClickStatus.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 57 | 63 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 86 | 91 | 92 | 97 | 102 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /ModAssistant/OneClickStatus.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Data; 4 | 5 | namespace ModAssistant 6 | { 7 | /// 8 | /// Interaction logic for OneClickStatus.xaml 9 | /// 10 | public partial class OneClickStatus : Window 11 | { 12 | public static OneClickStatus Instance; 13 | 14 | public string HistoryText 15 | { 16 | get 17 | { 18 | return HistoryTextBlock.Text; 19 | } 20 | set 21 | { 22 | Dispatcher.Invoke(new Action(() => { Instance.HistoryTextBlock.Text = value; })); 23 | } 24 | } 25 | public string MainText 26 | { 27 | get 28 | { 29 | return MainTextBlock.Text; 30 | } 31 | set 32 | { 33 | Dispatcher.Invoke(new Action(() => 34 | { 35 | Instance.MainTextBlock.Text = value; 36 | Instance.HistoryTextBlock.Text = string.IsNullOrEmpty(MainText) ? $"{value}" : $"{value}\n{HistoryText}"; 37 | })); 38 | } 39 | } 40 | 41 | public OneClickStatus() 42 | { 43 | InitializeComponent(); 44 | Instance = (App.OCIWindow == "No" && App.OCIWindow == "Notify") ? null : this; 45 | } 46 | 47 | public void StopRotation() 48 | { 49 | Ring.Style = null; 50 | } 51 | } 52 | 53 | [ValueConversion(typeof(double), typeof(double))] 54 | public class DivideDoubleByTwoConverter : IValueConverter 55 | { 56 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 57 | { 58 | if (targetType != typeof(double)) 59 | { 60 | throw new InvalidOperationException("The target must be a double"); 61 | } 62 | double d = (double)value; 63 | return ((double)d) / 2; 64 | } 65 | 66 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 67 | { 68 | throw new NotSupportedException(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ModAssistant/Pages/About.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Forms; 7 | using System.Windows.Navigation; 8 | using static ModAssistant.Http; 9 | using MessageBox = System.Windows.Forms.MessageBox; 10 | 11 | namespace ModAssistant.Pages 12 | { 13 | /// 14 | /// Interaction logic for Page1.xaml 15 | /// 16 | public partial class About : Page 17 | { 18 | public static About Instance = new About(); 19 | 20 | public About() 21 | { 22 | InitializeComponent(); 23 | } 24 | 25 | private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) 26 | { 27 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); 28 | e.Handled = true; 29 | } 30 | 31 | private async void HeadpatsButton_Click(object sender, RoutedEventArgs e) 32 | { 33 | PatButton.IsEnabled = false; 34 | var success = await Task.Run(async () => await HeadPat()); 35 | 36 | PatUp.IsOpen = success; 37 | if (!success) PatButton.IsEnabled = true; 38 | } 39 | 40 | private async void HugsButton_Click(object sender, RoutedEventArgs e) 41 | { 42 | HugButton.IsEnabled = false; 43 | var success = await Task.Run(async () => await Hug()); 44 | 45 | HugUp.IsOpen = success; 46 | if (!success) HugButton.IsEnabled = true; 47 | } 48 | 49 | private async Task WeebCDN(string type) 50 | { 51 | var resp = await HttpClient.GetAsync(Utils.Constants.WeebCDNAPIURL + type + "/random"); 52 | var body = await resp.Content.ReadAsStringAsync(); 53 | 54 | var response = JsonSerializer.Deserialize(body); 55 | return response.url; 56 | } 57 | 58 | private async Task HeadPat() 59 | { 60 | try 61 | { 62 | var image = await WeebCDN("pats"); 63 | PatImage.Load(image); 64 | 65 | return true; 66 | } 67 | catch (Exception ex) 68 | { 69 | var buttonType = (string)FindResource("About:HeadpatsButton"); 70 | var title = string.Format((string)FindResource("About:ImageError:Title"), buttonType); 71 | var message = string.Format((string)FindResource("About:ImageError:Message"), buttonType); 72 | 73 | MessageBox.Show($"{message}\n{ex.Message}", title, MessageBoxButtons.OK, MessageBoxIcon.Error); 74 | return false; 75 | } 76 | } 77 | 78 | private async Task Hug() 79 | { 80 | try 81 | { 82 | var image = await WeebCDN("hugs"); 83 | HugImage.Load(image); 84 | 85 | return true; 86 | } 87 | catch (Exception ex) 88 | { 89 | var buttonType = (string)FindResource("About:HugsButton"); 90 | var title = string.Format((string)FindResource("About:ImageError:Title"), buttonType); 91 | var message = string.Format((string)FindResource("About:ImageError:Message"), buttonType); 92 | 93 | MessageBox.Show($"{message}\n{ex.Message}", title, MessageBoxButtons.OK, MessageBoxIcon.Error); 94 | return false; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ModAssistant/Pages/Intro.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 42 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | 63 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 87 | 88 | 89 | 90 | 95 | 96 | 97 | 98 | 103 | 104 | 105 | 106 | 112 | 126 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /ModAssistant/Pages/Intro.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Navigation; 5 | 6 | namespace ModAssistant.Pages 7 | { 8 | /// 9 | /// Interaction logic for Intro.xaml 10 | /// 11 | public partial class Intro : Page 12 | { 13 | public static Intro Instance = new Intro(); 14 | 15 | public Intro() 16 | { 17 | InitializeComponent(); 18 | } 19 | 20 | private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) 21 | { 22 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); 23 | e.Handled = true; 24 | } 25 | 26 | private void Disagree_Click(object sender, RoutedEventArgs e) 27 | { 28 | MainWindow.Instance.ModsButton.IsEnabled = false; 29 | Properties.Settings.Default.Agreed = false; 30 | Properties.Settings.Default.Save(); 31 | MessageBox.Show((string)FindResource("Intro:ClosingApp")); 32 | Application.Current.Shutdown(); 33 | } 34 | 35 | private void Agree_Click(object sender, RoutedEventArgs e) 36 | { 37 | if (string.IsNullOrEmpty(MainWindow.GameVersion)) 38 | { 39 | string line1 = (string)FindResource("Intro:VersionDownloadFailed"); 40 | string line2 = (string)FindResource("Intro:ModsTabDisabled"); 41 | 42 | MessageBox.Show($"{line1}.\n{line2}"); 43 | } 44 | else 45 | { 46 | MainWindow.Instance.ModsButton.IsEnabled = true; 47 | 48 | string text = (string)FindResource("Intro:ModsTabEnabled"); 49 | Utils.SendNotify(text); 50 | MainWindow.Instance.MainText = text; 51 | } 52 | Properties.Settings.Default.Agreed = true; 53 | Properties.Settings.Default.Save(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ModAssistant/Pages/Invalid.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 47 | 54 | 55 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 81 | 82 | 88 | 94 | 95 | : 96 | 97 | 102 | 109 | 110 |