using System; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Security.Cryptography; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.ReactiveUI; using Avalonia.Threading; using ReactiveUI; using VaultSmpInstaller.Data; using VaultSmpInstaller.ViewModels; namespace VaultSmpInstaller.Views; public partial class MainWindow : ReactiveWindow { public MainWindow() { InitializeComponent(); this.WhenActivated(action => action(ViewModel!.ShowProfileSelectionDialog.RegisterHandler(DoShowDialogAsync))); this.WhenActivated(_ => StartDownload()); } private async Task ProcessInstance(String instancePath) { await using FileStream fs = new FileStream(Path.Combine(instancePath, "instance.json"), FileMode.Open); ViewModel!.LatestInstanceConfig = await JsonSerializer.DeserializeAsync(fs, JsonContext.Default.InstanceConfig); if (ViewModel!.LatestInstanceConfig != null) { ViewModel!.LatestInstanceConfig.InstancePath = instancePath; foreach (var modProfile in ViewModel!.LatestInstanceConfig.ModProfiles.Values) if (modProfile.Required || modProfile.DefaultState) modProfile.IsEnabled = true; ViewModel!.LatestVersion = ViewModel!.LatestInstanceConfig.Version; ViewModel!.RaisePropertyChanged(nameof(ViewModel.LatestInstanceConfig)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfiles)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfiles)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfileNames)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfileNames)); ViewModel!.SelectProfileCommand.Execute(null); } else { throw new Exception("Failed to download new version"); } } private async Task DoShowDialogAsync(InteractionContext interaction) { var dialog = new ProfileWindow1(); dialog.DataContext = interaction.Input; var result = await dialog.ShowDialog(this); interaction.SetOutput(result); } private readonly DownloadingWindow _downloadingWindow = new DownloadingWindow(); private void StartDownload() { var downloadThread = new Thread(async (arg) => { if (arg is CancellationTokenSource) { var tokenSource = (CancellationTokenSource)arg; var archivePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".zip"); var extractPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); try { HttpClient httpClient = new HttpClient(); using var response = await httpClient.GetAsync("https://holenode.cdnbcn.net/LatestProfile.zip", tokenSource.Token); await using var fs = new FileStream(archivePath, FileMode.OpenOrCreate); await response.Content.CopyToAsync(fs, tokenSource.Token); fs.Close(); ZipFile.ExtractToDirectory(archivePath, extractPath); tokenSource.Token.ThrowIfCancellationRequested(); File.Delete(archivePath); await Dispatcher.UIThread.Invoke(() => ProcessInstance(extractPath)); } catch (OperationCanceledException e) { try { File.Delete(archivePath); Directory.Delete(extractPath, true); } catch (Exception ignored) { // ignored } Dispatcher.UIThread.Invoke(Close); } Dispatcher.UIThread.Invoke(() => _downloadingWindow.Close()); } }); CancellationTokenSource source = new CancellationTokenSource(); downloadThread.Start(source); _downloadingWindow.DataContext = new DownloadingWindowViewModel(); _downloadingWindow.Closed += (_, _) => source.Cancel(); _downloadingWindow.ShowDialog(this); } private string _disabledSelected = ""; private void DisabledSelectionChanged(object? sender, SelectionChangedEventArgs e) { if (e.AddedItems is [String]) _disabledSelected = (String)e.AddedItems[0]!; } private void EnableSelected(object? sender, RoutedEventArgs e) { if (ViewModel!.LatestInstanceConfig!.ModProfiles.ContainsKey(_disabledSelected)) { ViewModel!.LatestInstanceConfig!.ModProfiles[_disabledSelected].IsEnabled = true; ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfiles)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfileNames)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfiles)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfileNames)); _disabledSelected = ""; } } private string _enabledSelected = ""; private void EnabledSelectionChanged(object? sender, SelectionChangedEventArgs e) { if (e.AddedItems is [String]) _enabledSelected = (String)e.AddedItems[0]!; } private void DisableSelected(object? sender, RoutedEventArgs e) { if (ViewModel!.LatestInstanceConfig!.ModProfiles.ContainsKey(_enabledSelected) && !ViewModel!.LatestInstanceConfig!.ModProfiles[_enabledSelected].Required) { ViewModel!.LatestInstanceConfig!.ModProfiles[_enabledSelected].IsEnabled = false; ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfiles)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfileNames)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfiles)); ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfileNames)); _enabledSelected = ""; } } public ManualResetEvent ContinueInstalling = new ManualResetEvent(false); private void InstallProfile(object? sender, RoutedEventArgs e) { Thread installThread = new Thread(() => { var md5 = MD5.Create(); var viewModel = Dispatcher.UIThread.Invoke(() => ViewModel); if (viewModel!.InstalledInstanceConfig == null) { viewModel.InstalledInstanceConfig = viewModel.LatestInstanceConfig!; viewModel.InstalledVersion = viewModel.LatestInstanceConfig!.Version; File.WriteAllText(Path.Combine(viewModel.SelectedInstance!.InstancePath, "installedInstance.json"), JsonSerializer.Serialize(viewModel.LatestInstanceConfig, JsonContext.Default.InstanceConfig)); foreach (var (path, md5Hash) in viewModel.LatestInstanceConfig.ReplaceFiles) { var stream = File.ReadAllBytes(Path.Combine(viewModel.SelectedInstance.MinecraftPath, path)); if (BitConverter.ToString(md5.ComputeHash(stream)).Replace("-","").ToLower() != md5Hash) { Dispatcher.UIThread.Invoke(() => { var instanceNotIntactWindow = new InstanceNotIntactWindow(this); instanceNotIntactWindow.DataContext = new ThemeViewModel(); instanceNotIntactWindow.ShowDialog(this); }); ContinueInstalling.WaitOne(); break; } } if(!Directory.Exists(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles"))) { Directory.CreateDirectory(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles")); } foreach (var path in viewModel.LatestInstanceConfig.ReplaceFiles.Keys) { var pathParts = path.Split('/'); var currentPath = ""; foreach (var part in pathParts[0..^1]) { currentPath = Path.Combine(currentPath, part); if(!Directory.Exists(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", currentPath))) Directory.CreateDirectory(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", currentPath)); } File.Copy(Path.Combine(viewModel.SelectedInstance.MinecraftPath, path), Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", path)); } } CopyAll(Path.Combine(viewModel.LatestInstanceConfig!.InstancePath, "config"), viewModel.SelectedInstance!.ConfigPath); CopyAll(Path.Combine(viewModel.LatestInstanceConfig.InstancePath, "scripts"), viewModel.SelectedInstance.ScriptsPath); if(!Directory.Exists(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles"))) { Directory.CreateDirectory(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles")); } if(!Directory.Exists(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", "mods"))) { Directory.CreateDirectory(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", "mods")); } foreach (var (profileName, modProfile) in viewModel.LatestInstanceConfig.ModProfiles) { if (modProfile.IsEnabled) { foreach (var (modName, md5Hash) in modProfile.Replaces) { if (File.Exists(Path.Combine(viewModel.SelectedInstance.ModsPath, modName))) { var stream = File.ReadAllBytes(Path.Combine(viewModel.SelectedInstance.ModsPath, modName)); if (BitConverter.ToString(md5.ComputeHash(stream)).Replace("-","").ToLower() == md5Hash) { File.Copy(Path.Combine(viewModel.SelectedInstance.ModsPath, modName), Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", "mods", modName)); File.Delete(Path.Combine(viewModel.SelectedInstance.ModsPath, modName)); } } } foreach (var modName in modProfile.Mods.Keys) { File.Copy(Path.Combine(viewModel.LatestInstanceConfig.InstancePath, "mods", modName), Path.Combine(viewModel.SelectedInstance.ModsPath, modName), true); } } else if (viewModel.InstalledInstanceConfig.ModProfiles[profileName].IsEnabled) { foreach (var modName in modProfile.Mods.Keys) { if(File.Exists(Path.Combine(viewModel.SelectedInstance.ModsPath, modName))) File.Delete(Path.Combine(viewModel.SelectedInstance.ModsPath, modName)); } } } Dispatcher.UIThread.Invoke(() => { var successWindow = new SuccessWindow(this); successWindow.DataContext = new ThemeViewModel(); successWindow.ShowDialog(this); }); viewModel.InstalledInstanceConfig = viewModel.LatestInstanceConfig!; viewModel.InstalledVersion = viewModel.LatestInstanceConfig!.Version; viewModel.RaisePropertyChanged(nameof(viewModel.InstalledInstanceConfig)); viewModel.RaisePropertyChanged(nameof(viewModel.InstalledModProfiles)); viewModel.RaisePropertyChanged(nameof(viewModel.InstalledModProfileNames)); ContinueInstalling.Reset(); ContinueInstalling.WaitOne(); }); installThread.Start(); } public static void CopyAll(string source, string target) { CopyAll(new DirectoryInfo(source), new DirectoryInfo(target)); } public static void CopyAll(DirectoryInfo source, DirectoryInfo target) { if (source.FullName.ToLower() == target.FullName.ToLower()) { return; } // Check if the target directory exists, if not, create it. if (Directory.Exists(target.FullName) == false) { Directory.CreateDirectory(target.FullName); } // Copy each file into it's new directory. foreach (FileInfo fi in source.GetFiles()) { fi.CopyTo(Path.Combine(target.ToString(), fi.Name), true); } // Copy each subdirectory using recursion. foreach (DirectoryInfo diSourceSubDir in source.GetDirectories()) { DirectoryInfo nextTargetSubDir = target.CreateSubdirectory(diSourceSubDir.Name); CopyAll(diSourceSubDir, nextTargetSubDir); } } }