diff --git a/BadBuilder/BadBuilder.csproj b/BadBuilder/BadBuilder.csproj index 533bf2d..e2620de 100644 --- a/BadBuilder/BadBuilder.csproj +++ b/BadBuilder/BadBuilder.csproj @@ -14,10 +14,12 @@ - + + + diff --git a/BadBuilder/BadBuilder.sln b/BadBuilder/BadBuilder.sln new file mode 100644 index 0000000..9154beb --- /dev/null +++ b/BadBuilder/BadBuilder.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BadBuilder", "BadBuilder.csproj", "{7F5ABAE3-7695-CBAC-99AA-67F395914E74}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7F5ABAE3-7695-CBAC-99AA-67F395914E74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F5ABAE3-7695-CBAC-99AA-67F395914E74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F5ABAE3-7695-CBAC-99AA-67F395914E74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F5ABAE3-7695-CBAC-99AA-67F395914E74}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {262F9D09-5BF9-4841-A008-0F212104F6B0} + EndGlobalSection +EndGlobal diff --git a/BadBuilder/ConsoleExperiences/DiskMenu.cs b/BadBuilder/ConsoleExperiences/DiskExperience.cs similarity index 82% rename from BadBuilder/ConsoleExperiences/DiskMenu.cs rename to BadBuilder/ConsoleExperiences/DiskExperience.cs index fbae1e1..bebf627 100644 --- a/BadBuilder/ConsoleExperiences/DiskMenu.cs +++ b/BadBuilder/ConsoleExperiences/DiskExperience.cs @@ -11,7 +11,7 @@ namespace BadBuilder { internal partial class Program { - static string PromptForDiskSelection(List disks) + static string PromptDiskSelection(List disks) { var choices = new List(); foreach (var disk in disks) @@ -38,14 +38,14 @@ namespace BadBuilder ); } - static async Task FormatDisk(List disks, string selectedDisk) + static void FormatDisk(List disks, string selectedDisk) { int diskIndex = disks.FindIndex(disk => $"{disk.DriveLetter} ({disk.SizeFormatted}) - {disk.Type}" == selectedDisk); - await AnsiConsole.Status().StartAsync($"[#76B900]Formatting disk[/] {selectedDisk}", async ctx => + AnsiConsole.Status().SpinnerStyle(LightOrangeStyle).Start($"[#76B900]Formatting disk[/] {selectedDisk}", ctx => { if (diskIndex == -1) return; - await DiskHelper.FormatDisk(disks[diskIndex]); + DiskHelper.FormatDisk(disks[diskIndex]).Wait(); }); } } diff --git a/BadBuilder/ConsoleExperiences/DownloadExperience.cs b/BadBuilder/ConsoleExperiences/DownloadExperience.cs new file mode 100644 index 0000000..c216de6 --- /dev/null +++ b/BadBuilder/ConsoleExperiences/DownloadExperience.cs @@ -0,0 +1,111 @@ +using Spectre.Console; +using BadBuilder.Helpers; + +using static BadBuilder.Constants; + +namespace BadBuilder +{ + internal partial class Program + { + static async Task> DownloadRequiredFiles() + { + List items = new() + { + ("XEXMenu", "https://consolemods.org/wiki/images/3/35/XeXmenu_12.7z"), + ("Rock Band Blitz", "https://download.digiex.net/Consoles/Xbox360/Arcade-games/RBBlitz.zip"), + ("Simple 360 NAND Flasher", "https://www.consolemods.org/wiki/images/f/ff/Simple_360_NAND_Flasher.7z"), + }; + await DownloadHelper.GetGitHubAssets(items); + + List existingFiles = items.Where(item => + File.Exists(Path.Combine(DOWNLOAD_DIR, item.url.Split('/').Last()))).ToList(); + + List choices = items.Select(item => + existingFiles.Any(e => e.name == item.name) + ? $"{item.name} [italic gray](already exists)[/]" + : item.name).ToList(); + + var prompt = new MultiSelectionPrompt() + .Title("Which files do you already have? [gray](Select all that apply)[/]") + .PageSize(10) + .NotRequired() + .HighlightStyle(GreenStyle) + .AddChoices(choices); + + foreach (string choice in choices) + { + if (existingFiles.Any(e => choice.StartsWith(e.name))) + prompt.Select(choice); + } + + List selectedItems = AnsiConsole.Prompt(prompt) + .Select(choice => choice.Split(" [")[0]) + .ToList(); + + + List itemsToDownload = items.Where(item => !selectedItems.Contains(item.name)).ToList(); + + itemsToDownload.Sort((a, b) => b.name.Length.CompareTo(a.name.Length)); + + if (itemsToDownload.Any()) + { + if (!Directory.Exists($"{DOWNLOAD_DIR}")) + Directory.CreateDirectory($"{DOWNLOAD_DIR}"); + + HttpClient downloadClient = new(); + + await AnsiConsole.Progress() + .Columns( + new TaskDescriptionColumn(), + new ProgressBarColumn().FinishedStyle(GreenStyle).CompletedStyle(LightOrangeStyle), + new PercentageColumn().CompletedStyle(GreenStyle), + new RemainingTimeColumn().Style(GrayStyle), + new TransferSpeedColumn() + ) + .StartAsync(async ctx => + { + AnsiConsole.MarkupLine("[#76B900]{0}[/] Downloading required files.", Markup.Escape("[*]")); + await Task.WhenAll(itemsToDownload.Select(async item => + { + var task = ctx.AddTask(item.name, new ProgressTaskSettings { AutoStart = false }); + await DownloadHelper.DownloadFileAsync(downloadClient, task, item.url); + })); + }); + + string status = "[+]"; + AnsiConsole.MarkupInterpolated($"[#76B900]{status}[/] [bold]{itemsToDownload.Count()}[/] download(s) completed.\n"); + } + else + { + AnsiConsole.MarkupLine("[italic #76B900]No downloads required. All files already exist.[/]"); + } + + + Console.WriteLine(); + foreach (string selectedItem in selectedItems) + { + string expectedFileName = items.First(i => i.name == selectedItem).url.Split('/').Last(); + string destinationPath = Path.Combine(DOWNLOAD_DIR, expectedFileName); + + if (File.Exists(destinationPath)) continue; + + string existingPath = AnsiConsole.Prompt( + new TextPrompt($"Enter the path for the [bold]{selectedItem}[/] archive:") + .PromptStyle(LightOrangeStyle) + .Validate(path => + { + return File.Exists(path.Trim().Trim('"')) + ? ValidationResult.Success() + : ValidationResult.Error("[red]File does not exist.[/]"); + }) + ).Trim().Trim('"'); + + File.Copy(existingPath, destinationPath, overwrite: true); + AnsiConsole.MarkupLine($"[italic #76B900]Successfully copied [bold]{selectedItem}[/] to the working directory.[/]\n"); + } + + + return items.Select(item => new ArchiveItem(item.name, Path.Combine(DOWNLOAD_DIR, item.url.Split('/').Last()))).ToList(); + } + } +} \ No newline at end of file diff --git a/BadBuilder/ConsoleExperiences/ExtractExperience.cs b/BadBuilder/ConsoleExperiences/ExtractExperience.cs new file mode 100644 index 0000000..7a469b3 --- /dev/null +++ b/BadBuilder/ConsoleExperiences/ExtractExperience.cs @@ -0,0 +1,33 @@ +using Spectre.Console; +using BadBuilder.Helpers; + +namespace BadBuilder +{ + internal partial class Program + { + internal static async Task ExtractFiles(List filesToExtract) + { + filesToExtract.Sort((a, b) => b.name.Length.CompareTo(a.name.Length)); + + await AnsiConsole.Progress() + .Columns( + new TaskDescriptionColumn(), + new ProgressBarColumn().FinishedStyle(GreenStyle).CompletedStyle(LightOrangeStyle), + new PercentageColumn().CompletedStyle(GreenStyle), + new RemainingTimeColumn().Style(GrayStyle) + ) + .StartAsync(async ctx => + { + AnsiConsole.MarkupLine("[#76B900]{0}[/] Extracting files.", Markup.Escape("[*]")); + await Task.WhenAll(filesToExtract.Select(async item => + { + var task = ctx.AddTask(item.name, new ProgressTaskSettings { AutoStart = false }); + await ArchiveHelper.ExtractFileAsync(item.name, item.path, task); + })); + }); + + string status = "[+]"; + AnsiConsole.MarkupInterpolated($"[#76B900]{status}[/] [bold]{filesToExtract.Count()}[/] files extracted."); + } + } +} diff --git a/BadBuilder/ConsoleExperiences/HomebrewExperience.cs b/BadBuilder/ConsoleExperiences/HomebrewExperience.cs new file mode 100644 index 0000000..b098e81 --- /dev/null +++ b/BadBuilder/ConsoleExperiences/HomebrewExperience.cs @@ -0,0 +1,170 @@ +using Spectre.Console; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BadBuilder +{ + internal partial class Program + { + static bool PromptAddHomebrew() + { + return AnsiConsole.Prompt( + new TextPrompt("Would you like to add homebrew programs?") + .AddChoice(true) + .AddChoice(false) + .DefaultValue(false) + .ChoicesStyle(GreenStyle) + .DefaultValueStyle(OrangeStyle) + .WithConverter(choice => choice ? "y" : "n") + ); + } + + public static List ManageHomebrewApps() + { + var homebrewApps = new List(); + + while (true) + { + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .PageSize(4) + .HighlightStyle(GreenStyle) + .AddChoices("Add Homebrew App", "View Added Apps", "Remove App", "Finish & Save") + ); + + switch (choice) + { + case "Add Homebrew App": + ClearConsole(); + + var newApp = AddHomebrewApp(); + if (newApp != null) + { + homebrewApps.Add(newApp.Value); + AnsiConsole.Status() + .Start("Adding...", ctx => ctx.Spinner(Spinner.Known.Dots2)); + } + break; + + case "View Added Apps": + ClearConsole(); + DisplayApps(homebrewApps); + break; + + case "Remove App": + ClearConsole(); + RemoveHomebrewApp(homebrewApps); + break; + + case "Finish & Save": + if (homebrewApps.Count == 0) + { + AnsiConsole.MarkupLine("[#ffac4d]No apps added.[/]"); + return homebrewApps; + } + + if (AnsiConsole.Prompt( + new TextPrompt("Save and exit?") + .AddChoice(true) + .AddChoice(false) + .DefaultValue(true) + .ChoicesStyle(GreenStyle) + .DefaultValueStyle(OrangeStyle) + .WithConverter(choice => choice ? "y" : "n"))) + { + string status = "[+]"; + AnsiConsole.MarkupInterpolated($"[#76B900]{status}[/] Saved [bold]{homebrewApps.Count}[/] app(s).\n"); + return homebrewApps; + } + break; + } + } + } + + private static HomebrewApp? AddHomebrewApp() + { + AnsiConsole.MarkupLine("[bold #76B900]Add a new homebrew app[/]\n"); + string folderPath = AnsiConsole.Ask("[#FFA500]Enter the folder path for the app:[/]"); + if (!Directory.Exists(folderPath)) + { + AnsiConsole.MarkupLine("[#ffac4d]Invalid folder path. Please try again.[/]"); + return null; + } + + string[] xexFiles = Directory.GetFiles(folderPath, "*.xex"); + if (xexFiles.Length == 0) + { + AnsiConsole.MarkupLine("[#ffac4d]No XEX files found in this folder.[/]"); + return null; + } + + string entryPoint = xexFiles.Length switch + { + 0 => AnsiConsole.Prompt( + new TextPrompt("[#ffac4d]No .xex files found.[/] Enter entry point:") + .Validate(path => File.Exists(path) ? ValidationResult.Success() : ValidationResult.Error("File not found")) + ), + 1 => xexFiles[0], + _ => AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[#FFA500]Select entry point:[/]") + .HighlightStyle(PeachStyle) + .AddChoices(xexFiles.Select(Path.GetFileName)) + ) + }; + + ClearConsole(); + + AnsiConsole.MarkupLine($"[#76B900]Added:[/] {folderPath.Split('\\').Last()} -> [#ffac4d]{Path.GetFileName(entryPoint)}[/]\n"); + return (folderPath.Split('\\').Last(), folderPath, Path.Combine(folderPath, entryPoint)); + } + + + + private static void DisplayApps(List apps) + { + if (apps.Count == 0) + { + AnsiConsole.MarkupLine("[#ffac4d]No homebrew apps added.[/]\n"); + return; + } + + var table = new Table() + .Title("[bold #76B900]Added Homebrew Apps[/]") + .AddColumn("[#4D8C00]Folder[/]") + .AddColumn("[#ff7200]Entry Point[/]"); + + foreach (var app in apps) + table.AddRow($"[#A1CF3E]{app.folder}[/]", $"[#ffac4d]{Path.GetFileName(app.entryPoint)}[/]"); + + AnsiConsole.Write(table); + Console.WriteLine(); + } + + private static void RemoveHomebrewApp(List apps) + { + if (apps.Count == 0) + { + AnsiConsole.MarkupLine("[#ffac4d]No apps to remove.[/]\n"); + return; + } + + var appToRemove = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[bold #76B900]Select an app to remove:[/]") + .PageSize(5) + .HighlightStyle(LightOrangeStyle) + .MoreChoicesText("[grey](Move up/down to scroll)[/]") + .AddChoices(apps.Select(app => $"{Path.GetFileName(app.folder)}")) + ); + + var selectedApp = apps.First(app => $"{Path.GetFileName(app.folder)}" == appToRemove); + apps.Remove(selectedApp); + + AnsiConsole.MarkupLine($"[#ffac4d]Removed:[/] {selectedApp.folder.Split('\\').Last()}\n"); + } + } +} diff --git a/BadBuilder/Constants.cs b/BadBuilder/Constants.cs new file mode 100644 index 0000000..1d0aa23 --- /dev/null +++ b/BadBuilder/Constants.cs @@ -0,0 +1,11 @@ +namespace BadBuilder +{ + internal static class Constants + { + internal const string WORKING_DIR = "Work"; + internal const string DOWNLOAD_DIR = $@"{WORKING_DIR}\\Download"; + internal const string EXTRACTED_DIR = $@"{WORKING_DIR}\\Extract"; + + internal const string ContentFolder = "Content\\0000000000000000\\"; + } +} \ No newline at end of file diff --git a/BadBuilder/Helpers/ArchiveHelper.cs b/BadBuilder/Helpers/ArchiveHelper.cs new file mode 100644 index 0000000..ef731a0 --- /dev/null +++ b/BadBuilder/Helpers/ArchiveHelper.cs @@ -0,0 +1,35 @@ +using SharpCompress.Archives; +using SharpCompress.Common; +using Spectre.Console; +using System.Diagnostics; +using static BadBuilder.Constants; + +namespace BadBuilder.Helpers +{ + internal static class ArchiveHelper + { + internal static async Task ExtractFileAsync(string friendlyName, string archivePath, ProgressTask task) + { + string subFolder = Path.Combine(EXTRACTED_DIR, friendlyName); + Directory.CreateDirectory(subFolder); + + try + { + var archive = ArchiveFactory.Open(archivePath); + task.MaxValue = archive.Entries.Count(); + foreach (var entry in archive.Entries) + { + if (!entry.IsDirectory) + entry.WriteToDirectory(subFolder, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }); + + task.Increment(1); + } + } + catch (Exception ex) + { + // Sorry, but these exceptions are invalid. SharpCompress is mad because there's nested ZIPs in xexmenu. + Debug.WriteLine(ex); + } + } + } +} \ No newline at end of file diff --git a/BadBuilder/Helpers/DownloadHelper.cs b/BadBuilder/Helpers/DownloadHelper.cs index 80db9f1..f19ded5 100644 --- a/BadBuilder/Helpers/DownloadHelper.cs +++ b/BadBuilder/Helpers/DownloadHelper.cs @@ -1,15 +1,42 @@ -using Spectre.Console; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Octokit; +using Spectre.Console; + +using static BadBuilder.Constants; namespace BadBuilder.Helpers { internal static class DownloadHelper { - internal static async Task DownloadFile(HttpClient client, ProgressTask task, string url) + internal static async Task GetGitHubAssets(List items) + { + GitHubClient gitClient = new(new ProductHeaderValue("BadBuilder-Downloader")); + List repos = + [ + "grimdoomer/Xbox360BadUpdate", + "FreeMyXe/FreeMyXe" + ]; + + foreach (var repo in repos) + { + string[] splitRepo = repo.Split('/'); + var latestRelease = await gitClient.Repository.Release.GetLatest(splitRepo[0], splitRepo[1]); + + foreach (var asset in latestRelease.Assets) + { + string friendlyName = asset.Name switch + { + var name when name.Contains("Free", StringComparison.OrdinalIgnoreCase) => "FreeMyXe", + var name when name.Contains("Tools", StringComparison.OrdinalIgnoreCase) => "BadUpdate Tools", + var name when name.Contains("BadUpdate", StringComparison.OrdinalIgnoreCase) => "BadUpdate", + _ => asset.Name.Substring(0, asset.Name.Length - 4) + }; + + items.Add(new(friendlyName, asset.BrowserDownloadUrl)); + } + } + } + + internal static async Task DownloadFileAsync(HttpClient client, ProgressTask task, string url) { try { @@ -22,10 +49,10 @@ namespace BadBuilder.Helpers string filename = url.Substring(url.LastIndexOf('/') + 1); - using (var contentStream = await response.Content.ReadAsStreamAsync()) - using (var fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true)) + using (Stream contentStream = await response.Content.ReadAsStreamAsync()) + using (FileStream fileStream = new($"{DOWNLOAD_DIR}/{filename}", System.IO.FileMode.Create, FileAccess.Write, FileShare.None, 8192, true)) { - var buffer = new byte[8192]; + byte[] buffer = new byte[8192]; while (true) { var read = await contentStream.ReadAsync(buffer, 0, buffer.Length); diff --git a/BadBuilder/Helpers/FileSystemHelper.cs b/BadBuilder/Helpers/FileSystemHelper.cs new file mode 100644 index 0000000..38b90a7 --- /dev/null +++ b/BadBuilder/Helpers/FileSystemHelper.cs @@ -0,0 +1,39 @@ +using BadBuilder.Models; + +namespace BadBuilder.Helpers +{ + internal static class FileSystemHelper + { + internal static async Task MirrorDirectoryAsync(string sourceDir, string destDir) + { + Directory.CreateDirectory(destDir); + + string[] files = Directory.GetFiles(sourceDir); + foreach (var file in files) + { + string relativePath = Path.GetRelativePath(sourceDir, file); + string destFile = Path.Combine(destDir, relativePath); + + Directory.CreateDirectory(Path.GetDirectoryName(destFile)); + + await CopyFileAsync(file, destFile); + } + + string[] directories = Directory.GetDirectories(sourceDir); + foreach (var dir in directories) + { + var relativePath = Path.GetRelativePath(sourceDir, dir); + var destSubDir = Path.Combine(destDir, relativePath); + + await MirrorDirectoryAsync(dir, destSubDir); + } + } + + internal static async Task CopyFileAsync(string sourceFile, string destFile) + { + using (var sourceStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read)) + using (var destStream = new FileStream(destFile, FileMode.Create, FileAccess.Write)) + await sourceStream.CopyToAsync(destStream); + } + } +} \ No newline at end of file diff --git a/BadBuilder/Helpers/PatchHelper.cs b/BadBuilder/Helpers/PatchHelper.cs new file mode 100644 index 0000000..2aff559 --- /dev/null +++ b/BadBuilder/Helpers/PatchHelper.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BadBuilder.Helpers +{ + internal static class PatchHelper + { + internal static async Task PatchXexAsync(string xexPath, string xexToolPath) + { + Process process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = xexToolPath, + Arguments = $"-m r -r a \"{xexPath}\"", + RedirectStandardOutput = false, + RedirectStandardError = false, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + await process.WaitForExitAsync(); + } + } +} \ No newline at end of file diff --git a/BadBuilder/Helpers/ResourceHelper.cs b/BadBuilder/Helpers/ResourceHelper.cs index 9670667..92bc13e 100644 --- a/BadBuilder/Helpers/ResourceHelper.cs +++ b/BadBuilder/Helpers/ResourceHelper.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; +using System.Reflection; namespace BadBuilder.Helpers { @@ -12,7 +7,7 @@ namespace BadBuilder.Helpers internal static void ExtractEmbeddedBinary(string resourceName) { var assembly = Assembly.GetExecutingAssembly(); - string fullResourceName = $"BadBuilder.Tools.{resourceName}"; + string fullResourceName = $"BadBuilder.Resources.{resourceName}"; using (Stream resourceStream = assembly.GetManifestResourceStream(fullResourceName)) { diff --git a/BadBuilder/Models/ActionQueue.cs b/BadBuilder/Models/ActionQueue.cs new file mode 100644 index 0000000..7bb263c --- /dev/null +++ b/BadBuilder/Models/ActionQueue.cs @@ -0,0 +1,29 @@ +namespace BadBuilder.Models +{ + internal class ActionQueue + { + private SortedDictionary>> priorityQueue = new SortedDictionary>>(); + + internal void EnqueueAction(Func action, int priority) + { + if (!priorityQueue.ContainsKey(priority)) + { + priorityQueue[priority] = new Queue>(); + } + priorityQueue[priority].Enqueue(action); + } + + internal async Task ExecuteActionsAsync() + { + foreach (var priority in priorityQueue.Keys.OrderByDescending(p => p)) + { + var actions = priorityQueue[priority]; + while (actions.Count > 0) + { + var action = actions.Dequeue(); + await action(); + } + } + } + } +} \ No newline at end of file diff --git a/BadBuilder/Program.cs b/BadBuilder/Program.cs index d133e66..3dca2ff 100644 --- a/BadBuilder/Program.cs +++ b/BadBuilder/Program.cs @@ -1,15 +1,29 @@ -using Spectre.Console; +global using DownloadItem = (string name, string url); +global using ArchiveItem = (string name, string path); +global using HomebrewApp = (string name, string folder, string entryPoint); + +using Spectre.Console; using BadBuilder.Models; using BadBuilder.Helpers; +using static BadBuilder.Constants; + namespace BadBuilder { internal partial class Program { static readonly Style OrangeStyle = new Style(new Color(255, 114, 0)); + static readonly Style LightOrangeStyle = new Style(new Color(255, 172, 77)); + static readonly Style PeachStyle = new Style(new Color(255, 216, 153)); + static readonly Style GreenStyle = new Style(new Color(118, 185, 0)); static readonly Style GrayStyle = new Style(new Color(132, 133, 137)); + static string XexToolPath = string.Empty; + static string TargetDriveLetter = string.Empty; + + static ActionQueue actionQueue = new(); + static void Main(string[] args) { ShowWelcomeMessage(); @@ -22,61 +36,146 @@ namespace BadBuilder if (action == "Exit") Environment.Exit(0); List disks = DiskHelper.GetDisks(); - string selectedDisk = PromptForDiskSelection(disks); + string selectedDisk = PromptDiskSelection(disks); + TargetDriveLetter = selectedDisk.Substring(0, 3); bool confirmation = PromptFormatConfirmation(selectedDisk); if (confirmation) { - FormatDisk(disks, selectedDisk).Wait(); + FormatDisk(disks, selectedDisk); break; } } + ClearConsole(); + + List downloadedFiles = DownloadRequiredFiles().Result; + ExtractFiles(downloadedFiles).Wait(); + + + AnsiConsole.MarkupLine("\n\n[#76B900]{0}[/] Copying requried files and folders.", Markup.Escape("[*]")); + foreach (var folder in Directory.GetDirectories($@"{EXTRACTED_DIR}")) + { + var folderName = folder.Split("\\").Last(); + + switch (folderName) + { + case "XEXMenu": + EnqueueMirrorDirectory( + Path.Combine(folder, $"{ContentFolder}C0DE9999"), + Path.Combine(TargetDriveLetter, $"{ContentFolder}C0DE9999"), + 7 + ); + break; + + case "FreeMyXe": + EnqueueFileCopy( + Path.Combine(folder, "FreeMyXe.xex"), + Path.Combine(TargetDriveLetter, "BadUpdatePayload", "default.xex"), + 9 + ); + break; + + case "BadUpdate": + actionQueue.EnqueueAction(async () => + { + using (StreamWriter writer = new(Path.Combine(TargetDriveLetter, "name.txt"))) + { + writer.WriteLine("USB Storage Device"); + } + + using (StreamWriter writer = new(Path.Combine(TargetDriveLetter, "info.txt"))) + { + writer.WriteLine("This drive was created with BadBuilder by Pdawg.\nFind more info here: https://github.com/Pdawg-bytes/BadBuilder"); + } + + Directory.CreateDirectory(Path.Combine(TargetDriveLetter, "Apps")); + await FileSystemHelper.MirrorDirectoryAsync(Path.Combine(folder, "Rock Band Blitz"), TargetDriveLetter); + }, 10); + break; + + case "BadUpdate Tools": + XexToolPath = Path.Combine(folder, "XePatcher", "XexTool.exe"); + break; + + case "Rock Band Blitz": + EnqueueMirrorDirectory( + Path.Combine(folder, $"{ContentFolder}5841122D\\000D0000"), + Path.Combine(TargetDriveLetter, $"{ContentFolder}5841122D\\000D0000"), + 8 + ); + break; + + case "Simple 360 NAND Flasher": + actionQueue.EnqueueAction(async () => + { + await PatchHelper.PatchXexAsync(Path.Combine(folder, "Simple 360 NAND Flasher", "Default.xex"), XexToolPath); + await FileSystemHelper.MirrorDirectoryAsync(Path.Combine(folder, "Simple 360 NAND Flasher"), Path.Combine(TargetDriveLetter, "Apps", "Simple 360 NAND Flasher")); + }, 6); + break; + + default: throw new Exception($"Unexpected directory in working folder: {folder}"); + } + } + actionQueue.ExecuteActionsAsync().Wait(); + + ClearConsole(); + if (!PromptAddHomebrew()) + { + AnsiConsole.MarkupLine("\n[#76B900]{0}[/] Your USB drive is ready to go.", Markup.Escape("[+]")); + Environment.Exit(0); + } + Console.WriteLine(); - var items = new (string name, string url)[] - { - ("rbblitz", "https://download.digiex.net/Consoles/Xbox360/Arcade-games/RBBlitz.zip"), - ("BadUpdate", "https://github.com/grimdoomer/Xbox360BadUpdate/releases/download/v1.1/Xbox360BadUpdate-Retail-USB-v1.1.zip"), - ("BadUpdate Tools", "https://github.com/grimdoomer/Xbox360BadUpdate/releases/download/v1.1/Tools.zip"), - ("FreeMyXe", "https://github.com/FreeMyXe/FreeMyXe/releases/download/beta4/FreeMyXe-beta4.zip") - }; + List homebrewApps = ManageHomebrewApps(); - HttpClient client = new(); - - AnsiConsole.Progress() - .Columns( - [ - new TaskDescriptionColumn(), - new ProgressBarColumn().FinishedStyle(GreenStyle), - new PercentageColumn().CompletedStyle(GreenStyle), - new RemainingTimeColumn().Style(GrayStyle), - new SpinnerColumn(Spinner.Known.Dots12).Style(OrangeStyle) - ]) - .StartAsync(async ctx => + AnsiConsole.Status() + .SpinnerStyle(OrangeStyle) + .StartAsync("Copying and patching homebrew apps.", async ctx => { - await Task.WhenAll(items.Select(async item => + await Task.WhenAll(homebrewApps.Select(async item => { - var task = ctx.AddTask(item.name, new ProgressTaskSettings { AutoStart = false}); - await DownloadHelper.DownloadFile(client, task, item.url); + await FileSystemHelper.MirrorDirectoryAsync(item.folder, Path.Combine(TargetDriveLetter, "Apps", item.name)); + await PatchHelper.PatchXexAsync(Path.Combine(TargetDriveLetter, "Apps", item.name, Path.GetFileName(item.entryPoint)), XexToolPath); })); }).Wait(); - Console.WriteLine("Download completed!"); + string status = "[+]"; + AnsiConsole.MarkupInterpolated($"[#76B900]{status}[/] [bold]{homebrewApps.Count()}[/] apps copied.\n"); + + AnsiConsole.MarkupLine("\n[#76B900]{0}[/] Your USB drive is ready to go.", Markup.Escape("[+]")); } + static void EnqueueMirrorDirectory(string sourcePath, string destinationPath, int priority) + { + actionQueue.EnqueueAction(async () => + { + await FileSystemHelper.MirrorDirectoryAsync(sourcePath, destinationPath); + }, priority); + } + + static void EnqueueFileCopy(string sourceFile, string destinationFile, int priority) + { + actionQueue.EnqueueAction(async () => + { + await FileSystemHelper.CopyFileAsync(sourceFile, destinationFile); + }, priority); + } + + static void ShowWelcomeMessage() => AnsiConsole.Markup( """ - [#107c10]██████╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██╗██╗ ██████╗ ███████╗██████╗[/] - [#2ca243]██╔══██╗██╔══██╗██╔══██╗██╔══██╗██║ ██║██║██║ ██╔══██╗██╔════╝██╔══██╗[/] + [#4D8C00]██████╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██╗██╗ ██████╗ ███████╗██████╗[/] + [#65A800]██╔══██╗██╔══██╗██╔══██╗██╔══██╗██║ ██║██║██║ ██╔══██╗██╔════╝██╔══██╗[/] [#76B900]██████╔╝███████║██║ ██║██████╔╝██║ ██║██║██║ ██║ ██║█████╗ ██████╔╝[/] - [#92C83E]██╔══██╗██╔══██║██║ ██║██╔══██╗██║ ██║██║██║ ██║ ██║██╔══╝ ██╔══██╗[/] - [#a1d156]██████╔╝██║ ██║██████╔╝██████╔╝╚██████╔╝██║███████╗██████╔╝███████╗██║ ██║[/] - [#a1d156]╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚══════╝╚═╝ ╚═╝[/] + [#A1CF3E]██╔══██╗██╔══██║██║ ██║██╔══██╗██║ ██║██║██║ ██║ ██║██╔══╝ ██╔══██╗[/] + [#CCE388]██████╔╝██║ ██║██████╔╝██████╔╝╚██████╔╝██║███████╗██████╔╝███████╗██║ ██║[/] + [#CCE388]╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚══════╝╚═╝ ╚═╝[/] [#76B900]────────────────────────────────────────────────────────────────────────────[/] - Xbox 360 [#FF7200]BadUpdate[/] USB Builder - [#848589]Created by Pdawg[/] + ───────────────────────Xbox 360 [#FF7200]BadUpdate[/] USB Builder─────────────────────── + [#848589]Created by Pdawg[/] [#76B900]────────────────────────────────────────────────────────────────────────────[/] """); @@ -89,5 +188,12 @@ namespace BadBuilder "Exit" ) ); + + static void ClearConsole() + { + AnsiConsole.Clear(); + ShowWelcomeMessage(); + Console.WriteLine(); + } } } \ No newline at end of file diff --git a/BadBuilder/Tools/fat32format.exe b/BadBuilder/Resources/fat32format.exe similarity index 100% rename from BadBuilder/Tools/fat32format.exe rename to BadBuilder/Resources/fat32format.exe