Compare commits

..

10 Commits

Author SHA1 Message Date
993a27a33a Update default payload and bundle Aurora dashboard 2025-11-08 20:45:49 +01:00
Pdawg11239
fca7d4daa4 Update download links 2025-08-07 14:19:55 -04:00
Pdawg
d120ad1357 Prepare README for v0.3 2025-04-02 01:56:26 -04:00
Pdawg11239
52ee1324f2 Update XeXMenu name styling 2025-04-02 01:55:30 -04:00
Pdawg11239
d9a76cc483 Add 'unsupported' homebrew support & 'XeUnshackle' 2025-04-02 01:40:06 -04:00
Pdawg11239
39316e3078 Update link for RBB 2025-03-30 15:11:28 -04:00
Pdawg11239
e27b272aa6 Fix copying bug 2025-03-29 20:53:04 -04:00
Pdawg11239
ac42c67478 Add native format for small FAT32; fix MBR bugs 2025-03-29 20:06:48 -04:00
Pdawg
8505c06e66 Update README.md 2025-03-17 21:39:47 -04:00
Pdawg11239
b5ba06a1b4 Create README.md 2025-03-17 21:32:13 -04:00
14 changed files with 367 additions and 124 deletions

View File

@@ -6,6 +6,12 @@
internal const string ANSI_RESET = "\u001b[0m";
internal const long KB = 1024L;
internal const long MB = 1048576L;
internal const long GB = 1073741824L;
internal const long TB = 1099511627776L;
internal const int FMIFS_HARDDISK = 0xC;
internal const uint GENERIC_READ = 0x80000000;
internal const uint GENERIC_WRITE = 0x40000000;

View File

@@ -1,5 +1,6 @@
#pragma warning disable CA1416
using System.Text;
using System.Diagnostics;
using Windows.Win32.System.Ioctl;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
@@ -13,12 +14,34 @@ namespace BadBuilder.Formatter
{
public static class DiskFormatter
{
public static unsafe string FormatVolume(char driveLetter)
public static unsafe string FormatVolume(char driveLetter, long diskSize)
{
if (diskSize < 31 * GB) // Just a safeguard to ensure that we never run into close calls with disk size.
{
Process process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "format.com",
Arguments = $"\"{driveLetter}:\" /Q /X /Y /FS:FAT32 /V:BADUPDATE",
RedirectStandardOutput = false,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit();
if (process.ExitCode == 0) return "";
else return Error($"Native format failed with exit code: {process.ExitCode}");
}
string devicePath = $"\\\\.\\{driveLetter}:";
uint volumeID = GetVolumeID();
using SafeFileHandle driveHandle = OpenDeviceHandle(devicePath);
SafeFileHandle driveHandle = OpenDeviceHandle(devicePath);
if (driveHandle.IsInvalid) return Error("Unable to open device. GetLastError: " + Marshal.GetLastWin32Error());
if (!EnableExtendedDASDIO(driveHandle) || !LockDevice(driveHandle))
@@ -48,6 +71,10 @@ namespace BadBuilder.Formatter
if (!UnlockDevice(driveHandle) || !DismountVolume(driveHandle))
return Error($"Failed to release the device. GetLastError: {Marshal.GetLastWin32Error()}");
driveHandle.Dispose();
if (!SetVolumeLabel($"{driveLetter}:", "BADUPDATE"))
return Error($"Unable to set volume label. GetLastError: {Marshal.GetLastWin32Error()}");
return string.Empty;
}
@@ -109,14 +136,18 @@ namespace BadBuilder.Formatter
private static unsafe FAT32BootSector InitializeBootSector(DISK_GEOMETRY diskGeometry, PARTITION_INFORMATION partitionInfo, uint totalSectors, uint volumeID)
{
byte sectorsPerCluster = CalculateSectorsPerCluster((ulong)partitionInfo.PartitionLength, diskGeometry.BytesPerSector);
uint sectorsPerCluster = (uint)CalculateSectorsPerCluster((ulong)partitionInfo.PartitionLength, diskGeometry.BytesPerSector);
uint fatSize = CalculateFATSize(totalSectors, 32, sectorsPerCluster, 2, diskGeometry.BytesPerSector);
uint aligned = (uint)MB / diskGeometry.BytesPerSector;
uint sysAreaSize = ((34 * fatSize + aligned - 1) / aligned) * aligned;
uint reserved = sysAreaSize - 2 * fatSize;
FAT32BootSector bootSector = new FAT32BootSector
{
BytesPerSector = (ushort)diskGeometry.BytesPerSector,
SectorsPerCluster = sectorsPerCluster,
ReservedSectorCount = 32,
SectorsPerCluster = (byte)sectorsPerCluster,
ReservedSectorCount = (ushort)reserved,
NumberOfFATs = 2,
MediaDescriptor = 0xF8,
SectorsPerTrack = (ushort)diskGeometry.SectorsPerTrack,
@@ -186,8 +217,8 @@ namespace BadBuilder.Formatter
{
uint bytesPerSector = diskGeometry.BytesPerSector;
uint totalSectors = (uint)(partitionInfo.PartitionLength / bytesPerSector);
uint systemAreaSize = bootSector.ReservedSectorCount + (bootSector.NumberOfFATs * bootSector.SectorsPerFAT) + bootSector.SectorsPerCluster;
uint userAreaSize = totalSectors - systemAreaSize;
uint userAreaSize = totalSectors - bootSector.ReservedSectorCount - (bootSector.NumberOfFATs * bootSector.SectorsPerFAT);
uint zeroOut = bootSector.ReservedSectorCount + (bootSector.NumberOfFATs * bootSector.SectorsPerFAT) + bootSector.SectorsPerCluster;
uint clusterCount = userAreaSize / bootSector.SectorsPerCluster;
if (clusterCount < 65536 || clusterCount > 0x0FFFFFFF)
@@ -195,7 +226,7 @@ namespace BadBuilder.Formatter
fsInfo.FreeClusterCount = clusterCount - 1;
ZeroOutSectors(driveHandle, 0, systemAreaSize, bytesPerSector);
ZeroOutSectors(driveHandle, 0, zeroOut, bytesPerSector);
for (int i = 0; i < 2; i++)
{
@@ -210,14 +241,6 @@ namespace BadBuilder.Formatter
WriteSector(driveHandle, sectorStart, 1, bytesPerSector, UintArrayToBytes(firstFATSector));
}
if (!isGPT)
{
SET_PARTITION_INFORMATION setPartInfo = new() { PartitionType = 0x0C };
if (!DeviceIoControl(driveHandle, IOCTL_DISK_SET_PARTITION_INFO, &setPartInfo, (uint)sizeof(SET_PARTITION_INFORMATION), null, 0, null, null))
return Error($"Failed to set the drive partition information. GetLastError: {Marshal.GetLastWin32Error()}");
}
return "";
}
}

View File

@@ -5,8 +5,8 @@ DeviceIoControl
CloseHandle
VirtualAlloc
VirtualFree
SetVolumeLabelW
GUID
DISK_GEOMETRY
PARTITION_INFORMATION
PARTITION_INFORMATION_EX
SET_PARTITION_INFORMATION

View File

@@ -39,23 +39,27 @@ namespace BadBuilder.Formatter
return (uint)(low | (hi << 16));
}
internal static uint CalculateFATSize(uint totalSectors, uint reservedSectors, uint sectorsPerCluster, uint numberOfFATs, uint bytesPerSector)
internal static uint CalculateFATSize(uint diskSize, uint reservedSectors, uint sectorsPerCluster, uint numberOfFATs, uint bytesPerSector)
{
const ulong fatElementSize = 4;
const ulong reservedClusters = 2;
ulong numerator = fatElementSize * (totalSectors - reservedSectors);
ulong denominator = (sectorsPerCluster * bytesPerSector) + (fatElementSize * numberOfFATs);
ulong numerator = diskSize - reservedSectors + reservedClusters * sectorsPerCluster;
ulong denominator = (sectorsPerCluster * bytesPerSector / fatElementSize) + numberOfFATs;
return (uint)((numerator / denominator) + 1);
return (uint)(numerator / denominator + 1);
}
internal static byte CalculateSectorsPerCluster(ulong diskSizeBytes, uint bytesPerSector) => (diskSizeBytes / (1024 * 1024)) switch
internal static long CalculateSectorsPerCluster(ulong diskSizeBytes, uint bytesPerSector) => diskSizeBytes switch
{
var size when size > 512 => (byte)((4 * 1024) / bytesPerSector),
var size when size > 8192 => (byte)((8 * 1024) / bytesPerSector),
var size when size > 16384 => (byte)((16 * 1024) / bytesPerSector),
var size when size > 32768 => (byte)((32 * 1024) / bytesPerSector),
_ => 1
< 64 * MB => ((512) / bytesPerSector),
< 128 * MB => ((1 * KB) / bytesPerSector),
< 256 * MB => ((2 * KB) / bytesPerSector),
< 8 * GB => ((4 * KB) / bytesPerSector),
< 16 * GB => ((8 * KB) / bytesPerSector),
< 32 * GB => ((16 * KB) / bytesPerSector),
< 2 * TB => ((32 * KB) / bytesPerSector),
_ => ((64 * KB) / bytesPerSector)
};

View File

@@ -33,18 +33,15 @@ namespace BadBuilder
);
}
static bool FormatDisk(List<DiskInfo> disks, string selectedDisk)
static bool FormatDisk(DiskInfo disk)
{
int diskIndex = disks.FindIndex(disk => $"{disk.DriveLetter} ({disk.SizeFormatted}) - {disk.Type}" == selectedDisk);
bool ret = true;
string output = string.Empty;
AnsiConsole.Status().SpinnerStyle(LightOrangeStyle).Start($"[#76B900]Formatting disk[/] {selectedDisk}", async ctx =>
AnsiConsole.Status().SpinnerStyle(LightOrangeStyle).Start($"[#76B900]Formatting disk[/] {disk.DriveLetter} ({disk.SizeFormatted}) - {disk.Type}", async ctx =>
{
if (diskIndex == -1) return;
ClearConsole();
output = DiskHelper.FormatDisk(disks[diskIndex]);
output = DiskHelper.FormatDisk(disk);
if (output != string.Empty) ret = false;
});

View File

@@ -11,9 +11,11 @@ namespace BadBuilder
{
List<DownloadItem> 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"),
("ABadAvatar", "https://cdn.niklascfw.de/files/ABadAvatar-publicbeta1.0.zip"),
("Aurora Dashboard", "https://cdn.niklascfw.de/files/Aurora%200.7b.2%20-%20Release%20Package.rar"),
("XeXmenu", "https://github.com/Pdawg-bytes/BadBuilder/releases/download/v0.10a/MenuData.7z"),
("Rock Band Blitz", "https://github.com/Pdawg-bytes/BadBuilder/releases/download/v0.10a/GameData.zip"),
("Simple 360 NAND Flasher", "https://github.com/Pdawg-bytes/BadBuilder/releases/download/v0.10a/Flasher.7z"),
};
await DownloadHelper.GetGitHubAssets(items);
@@ -47,11 +49,11 @@ namespace BadBuilder
itemsToDownload.Sort((a, b) => b.name.Length.CompareTo(a.name.Length));
if (!Directory.Exists($"{DOWNLOAD_DIR}"))
Directory.CreateDirectory($"{DOWNLOAD_DIR}");
if (itemsToDownload.Any())
{
if (!Directory.Exists($"{DOWNLOAD_DIR}"))
Directory.CreateDirectory($"{DOWNLOAD_DIR}");
HttpClient downloadClient = new();
await AnsiConsole.Progress()
@@ -90,7 +92,7 @@ namespace BadBuilder
if (File.Exists(destinationPath)) continue;
string existingPath = AnsiConsole.Prompt(
new TextPrompt<string>($"Enter the path for the [bold]{selectedItem}[/] archive:")
new TextPrompt<string>($"Enter the path to the [bold]{selectedItem}[/] archive:")
.PromptStyle(LightOrangeStyle)
.Validate(path =>
{
@@ -100,10 +102,17 @@ namespace BadBuilder
})
).Trim().Trim('"');
File.Copy(existingPath, destinationPath, overwrite: true);
AnsiConsole.MarkupLine($"[italic #76B900]Successfully copied [bold]{selectedItem}[/] to the working directory.[/]\n");
}
try
{
File.Copy(existingPath, destinationPath, overwrite: true);
AnsiConsole.MarkupLine($"[italic #76B900]Successfully copied [bold]{selectedItem}[/] to the working directory.[/]\n");
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[italic red]Failed to copy [bold]{selectedItem}[/] to the working directory. Exception: {ex.Message}[/]\n");
}
}
return items.Select(item => new ArchiveItem(item.name, Path.Combine(DOWNLOAD_DIR, item.url.Split('/').Last()))).ToList();
}

View File

@@ -24,9 +24,6 @@ namespace BadBuilder
await ArchiveHelper.ExtractFileAsync(item.name, item.path, task);
}));
});
string status = "[+]";
AnsiConsole.MarkupInterpolated($"[#76B900]{status}[/] [bold]{filesToExtract.Count()}[/] files extracted.");
}
}
}

View File

@@ -17,7 +17,7 @@ namespace BadBuilder
);
}
public static List<HomebrewApp> ManageHomebrewApps()
internal static List<HomebrewApp> ManageHomebrewApps()
{
var homebrewApps = new List<HomebrewApp>();
@@ -37,11 +37,7 @@ namespace BadBuilder
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":
@@ -85,31 +81,30 @@ namespace BadBuilder
string folderPath = AnsiConsole.Ask<string>("[#FFA500]Enter the folder path for the app:[/]");
if (!Directory.Exists(folderPath))
{
AnsiConsole.MarkupLine("[#ffac4d]Invalid folder path. Please try again.[/]");
AnsiConsole.MarkupLine("[#ffac4d]Invalid folder path. Please try again.\n[/]");
return null;
}
string[] xexFiles = Directory.GetFiles(folderPath, "*.xex");
if (xexFiles.Length == 0)
{
AnsiConsole.MarkupLine("[#ffac4d]No XEX files found in this folder.[/]");
return null;
}
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
string entryPoint = xexFiles.Length switch
{
0 => AnsiConsole.Prompt(
new TextPrompt<string>("[#ffac4d]No .xex files found.[/] Enter entry point:")
.Validate(path => File.Exists(path) ? ValidationResult.Success() : ValidationResult.Error("File not found"))
),
new TextPrompt<string>("[grey]No .xex files found in this folder.[/] [#FFA500]Enter the path to the entry point:[/]")
.Validate(path => File.Exists(path.Trim().Trim('"')) && Path.GetExtension(path.Trim().Trim('"')) == ".xex" ? ValidationResult.Success() : ValidationResult.Error("[#ffac4d]File not found or is not an XEX file.[/]\n"))
).Trim().Trim('"'),
1 => xexFiles[0],
_ => AnsiConsole.Prompt(
new SelectionPrompt<string>()
new SelectionPrompt<string?>()
.Title("[#FFA500]Select entry point:[/]")
.HighlightStyle(PeachStyle)
.AddChoices(xexFiles.Select(Path.GetFileName))
)
};
#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
ClearConsole();

View File

@@ -7,7 +7,7 @@ namespace BadBuilder.Helpers
{
internal static class DiskHelper
{
public static List<DiskInfo> GetDisks()
internal static List<DiskInfo> GetDisks()
{
var disks = new List<DiskInfo>();
@@ -34,7 +34,7 @@ namespace BadBuilder.Helpers
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return "\u001b[38;2;255;114;0m[-]\u001b[0m Formatting is currently only supported on Windows. Please format your drive manually and try again.";
return DiskFormatter.FormatVolume(disk.DriveLetter[0]);
return DiskFormatter.FormatVolume(disk.DriveLetter[0], disk.TotalSize);
}
}
}

View File

@@ -13,7 +13,7 @@ namespace BadBuilder.Helpers
List<string> repos =
[
"grimdoomer/Xbox360BadUpdate",
"FreeMyXe/FreeMyXe"
"Byrom90/XeUnshackle"
];
foreach (var repo in repos)
@@ -28,6 +28,7 @@ namespace BadBuilder.Helpers
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",
var name when name.Contains("XeUnshackle", StringComparison.OrdinalIgnoreCase) => "XeUnshackle",
_ => asset.Name.Substring(0, asset.Name.Length - 4)
};
@@ -40,6 +41,7 @@ namespace BadBuilder.Helpers
{
try
{
byte[] downloadBuffer = new byte[8192];
using (HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
@@ -52,22 +54,22 @@ namespace BadBuilder.Helpers
using (Stream contentStream = await response.Content.ReadAsStreamAsync())
using (FileStream fileStream = new($"{DOWNLOAD_DIR}/{filename}", System.IO.FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
byte[] buffer = new byte[8192];
Array.Clear(downloadBuffer);
while (true)
{
var read = await contentStream.ReadAsync(buffer, 0, buffer.Length);
var read = await contentStream.ReadAsync(downloadBuffer, 0, downloadBuffer.Length);
if (read == 0)
break;
task.Increment(read);
await fileStream.WriteAsync(buffer, 0, read);
await fileStream.WriteAsync(downloadBuffer, 0, read);
}
}
}
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error downloading:[/] {ex}");
AnsiConsole.MarkupLine($"[red]Error downloading file from [bold]{url}[/]: {ex}[/]");
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using Spectre.Console;
using System.Diagnostics;
namespace BadBuilder.Helpers
{
@@ -12,8 +13,8 @@ namespace BadBuilder.Helpers
{
FileName = xexToolPath,
Arguments = $"-m r -r a \"{xexPath}\"",
RedirectStandardOutput = false,
RedirectStandardError = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
@@ -21,6 +22,13 @@ namespace BadBuilder.Helpers
process.Start();
await process.WaitForExitAsync();
if (process.ExitCode != 0)
{
string status = "[-]";
AnsiConsole.MarkupLineInterpolated($"\n[#FF7200]{status}[/] The program {Path.GetFileNameWithoutExtension(xexPath)} was unable to be patched. XexTool output:");
Console.WriteLine(process.StandardError.ReadToEnd());
}
}
}
}

View File

@@ -2,29 +2,32 @@
global using ArchiveItem = (string name, string path);
global using HomebrewApp = (string name, string folder, string entryPoint);
using System.Linq;
using Spectre.Console;
using BadBuilder.Models;
using BadBuilder.Helpers;
using BadBuilder.Utilities;
using static BadBuilder.Utilities.Constants;
using BadBuilder.Utilities;
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 OrangeStyle = new(new Color(255, 114, 0));
static readonly Style LightOrangeStyle = new(new Color(255, 172, 77));
static readonly Style PeachStyle = new(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 readonly Style GreenStyle = new(new Color(118, 185, 0));
static readonly Style GrayStyle = new(new Color(132, 133, 137));
static string XexToolPath = string.Empty;
static string TargetDriveLetter = string.Empty;
static ActionQueue actionQueue = new();
static DiskInfo targetDisk = new("Z:\\", "Fixed", 0, "", 0, int.MaxValue); // Default values just incase.
static void Main(string[] args)
{
ShowWelcomeMessage();
@@ -38,12 +41,15 @@ namespace BadBuilder
List<DiskInfo> disks = DiskHelper.GetDisks();
string selectedDisk = PromptDiskSelection(disks);
TargetDriveLetter = selectedDisk.Substring(0, 3);
TargetDriveLetter = selectedDisk[..3];
int diskIndex = disks.FindIndex(disk => $"{disk.DriveLetter} ({disk.SizeFormatted}) - {disk.Type}" == selectedDisk);
targetDisk = disks[diskIndex];
bool confirmation = PromptFormatConfirmation(selectedDisk);
if (confirmation)
{
if (!FormatDisk(disks, selectedDisk)) continue;
if (!FormatDisk(targetDisk)) continue;
break;
}
}
@@ -51,15 +57,94 @@ namespace BadBuilder
List<ArchiveItem> downloadedFiles = DownloadRequiredFiles().Result;
ExtractFiles(downloadedFiles).Wait();
ClearConsole();
string selectedDefaultApp = "XeUnshackle";
AnsiConsole.MarkupLine("[#76B900]{0}[/] Default payload set to [bold]XeUnshackle[/].", Markup.Escape("[*]"));
AnsiConsole.MarkupLine("\n\n[#76B900]{0}[/] Copying requried files and folders.", Markup.Escape("[*]"));
AnsiConsole.MarkupLine("[#76B900]{0}[/] Copying requried files and folders.", Markup.Escape("[*]"));
foreach (var folder in Directory.GetDirectories($@"{EXTRACTED_DIR}"))
{
var folderName = folder.Split("\\").Last();
switch (folderName)
switch (folder.Split("\\").Last())
{
case "XEXMenu":
case "ABadAvatar":
string avatarPayloadPath = Path.Combine(folder, "BadUpdatePayload");
if (Directory.Exists(avatarPayloadPath))
{
EnqueueMirrorDirectory(
avatarPayloadPath,
Path.Combine(TargetDriveLetter, "BadUpdatePayload"),
5
);
}
string avatarContentPath = Path.Combine(folder, "Content");
if (Directory.Exists(avatarContentPath))
{
EnqueueMirrorDirectory(
avatarContentPath,
Path.Combine(TargetDriveLetter, "Content"),
5
);
}
break;
case "Aurora Dashboard":
string auroraDestination = Path.Combine(TargetDriveLetter, "Apps", "Aurora");
Directory.CreateDirectory(auroraDestination);
bool auroraRootHasXex = Directory.GetFiles(folder, "*.xex", SearchOption.TopDirectoryOnly).Any();
if (auroraRootHasXex)
{
EnqueueMirrorDirectory(
folder,
auroraDestination,
4
);
break;
}
string[] auroraSubDirs = Directory.GetDirectories(folder);
string[] auroraRootFiles = Directory.GetFiles(folder);
string? primaryAuroraDir = auroraSubDirs.FirstOrDefault(dir =>
Directory.GetFiles(dir, "*.xex", SearchOption.TopDirectoryOnly).Any());
if (primaryAuroraDir == null)
{
EnqueueMirrorDirectory(
folder,
auroraDestination,
4
);
break;
}
EnqueueMirrorDirectory(
primaryAuroraDir,
auroraDestination,
4
);
foreach (string subDir in auroraSubDirs.Where(dir => dir != primaryAuroraDir))
{
EnqueueMirrorDirectory(
subDir,
Path.Combine(auroraDestination, Path.GetFileName(subDir)),
4
);
}
foreach (string file in auroraRootFiles)
{
EnqueueFileCopy(
file,
Path.Combine(auroraDestination, Path.GetFileName(file)),
4
);
}
break;
case "XeXmenu":
EnqueueMirrorDirectory(
Path.Combine(folder, $"{ContentFolder}C0DE9999"),
Path.Combine(TargetDriveLetter, $"{ContentFolder}C0DE9999"),
@@ -67,10 +152,12 @@ namespace BadBuilder
);
break;
case "FreeMyXe":
EnqueueFileCopy(
Path.Combine(folder, "FreeMyXe.xex"),
Path.Combine(TargetDriveLetter, "BadUpdatePayload", "default.xex"),
case "XeUnshackle":
string subFolderPath = Directory.GetDirectories(folder).FirstOrDefault();
File.Delete(Path.Combine(subFolderPath, "README - IMPORTANT.txt"));
EnqueueMirrorDirectory(
subFolderPath,
TargetDriveLetter,
9
);
break;
@@ -79,14 +166,10 @@ namespace BadBuilder
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");
}
writer.WriteLine($"This drive was created with BadBuilder by Pdawg.\nFind more info here: https://github.com/Pdawg-bytes/BadBuilder\nConfiguration: \n- BadUpdate target binary: {selectedDefaultApp}");
Directory.CreateDirectory(Path.Combine(TargetDriveLetter, "Apps"));
await FileSystemHelper.MirrorDirectoryAsync(Path.Combine(folder, "Rock Band Blitz"), TargetDriveLetter);
@@ -113,38 +196,48 @@ namespace BadBuilder
}, 6);
break;
default: throw new Exception($"Unexpected directory in working folder: {folder}");
default: throw new Exception($"[-] Unexpected directory in working folder: {folder}");
}
}
actionQueue.ExecuteActionsAsync().Wait();
ClearConsole();
if (!PromptAddHomebrew())
string launchIniPath = Path.Combine(TargetDriveLetter, "launch.ini");
if (File.Exists(launchIniPath))
{
AnsiConsole.MarkupLine("\n[#76B900]{0}[/] Your USB drive is ready to go.", Markup.Escape("[+]"));
Console.Write("\nPress any key to exit...");
Console.ReadKey();
Environment.Exit(0);
List<string> launchIniLines = File.ReadAllLines(launchIniPath).ToList();
for (int i = 0; i < launchIniLines.Count; i++)
{
if (launchIniLines[i].TrimStart().StartsWith("Default =", StringComparison.OrdinalIgnoreCase))
{
launchIniLines[i] = @"Default = Usb:\Apps\Aurora\Aurora.xex";
break;
}
}
const string commentText = "; Hallo von NiklasCFW :D";
int autoContIndex = launchIniLines.FindIndex(line =>
line.Trim().Equals("autocont = false", StringComparison.OrdinalIgnoreCase));
if (autoContIndex >= 0)
{
bool commentAlreadyPresent = autoContIndex > 0 &&
launchIniLines[autoContIndex - 1].Trim().Equals(commentText, StringComparison.OrdinalIgnoreCase);
if (!commentAlreadyPresent)
{
launchIniLines.Insert(autoContIndex, commentText);
}
}
File.WriteAllLines(launchIniPath, launchIniLines);
}
Console.WriteLine();
List<HomebrewApp> homebrewApps = ManageHomebrewApps();
AnsiConsole.Status()
.SpinnerStyle(OrangeStyle)
.StartAsync("Copying and patching homebrew apps.", async ctx =>
{
await Task.WhenAll(homebrewApps.Select(async item =>
{
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();
string status = "[+]";
AnsiConsole.MarkupInterpolated($"\n[#76B900]{status}[/] [bold]{homebrewApps.Count()}[/] apps copied.\n");
File.AppendAllText(Path.Combine(TargetDriveLetter, "info.txt"), $"- Disk formatted using {(targetDisk.TotalSize < 31 * GB ? "Windows \"format.com\"" : "BadBuilder Large FAT32 formatter")}\n");
File.AppendAllText(Path.Combine(TargetDriveLetter, "info.txt"), $"- Disk total size: {targetDisk.TotalSize} bytes\n");
ClearConsole();
WriteHomebrewLog(1);
AnsiConsole.MarkupLine("\n[#76B900]{0}[/] Your USB drive is ready to go.", Markup.Escape("[+]"));
Console.Write("\nPress any key to exit...");
@@ -167,6 +260,13 @@ namespace BadBuilder
}, priority);
}
static void WriteHomebrewLog(int count)
{
string logPath = Path.Combine(TargetDriveLetter, "info.txt");
string logEntry = $"- {count} homebrew app(s) added (including Simple 360 NAND Flasher)\n";
File.AppendAllText(logPath, logEntry);
}
static void ShowWelcomeMessage() => AnsiConsole.Markup(
"""
@@ -177,12 +277,12 @@ namespace BadBuilder
[#CCE388] [/]
[#CCE388] [/]
[#76B900]v0.10a[/]
[#76B900]v0.31[/]
Xbox 360 [#FF7200]BadUpdate[/] USB Builder
[#848589]Created by Pdawg[/]
[#848589]Created by Pdawg | Forked by NiklasCFW[/]
[#76B900][/]
""");
""");
static string PromptForAction() => AnsiConsole.Prompt(
new SelectionPrompt<string>()

View File

@@ -3,9 +3,14 @@
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 DOWNLOAD_DIR = $@"{WORKING_DIR}\Download";
internal const string EXTRACTED_DIR = $@"{WORKING_DIR}\Extract";
internal const string ContentFolder = "Content\\0000000000000000\\";
internal const long KB = 1024L;
internal const long MB = 1048576L;
internal const long GB = 1073741824L;
internal const long TB = 1099511627776L;
}
}

97
README.md Normal file
View File

@@ -0,0 +1,97 @@
# BadBuilder
BadBuilder is a tool for creating a BadUpdate USB drive for the Xbox 360. It automates the process of formatting the USB drive, downloading required files, extracting them, and allowing the addition of homebrew applications.
## Features
### USB Formatting (Windows Only)
- Uses a custom FAT32 formatter that supports large USB drives (≥32GB).
- Ensures compatibility with the Xbox 360.
> [!NOTE]
> Currently, the formatting feature is **Windows-only**. If you compile BadBuilder for another OS, it'll prompt you to manually format your target disk.
### Automatic File Downloading
- Detects and downloads the latest required files automatically.
- Recognizes previously downloaded files and reuses them by default.
- Allows specifying custom paths for required files if they are already on your system.
> [!IMPORTANT]
> BadBuilder does not dynamically locate files inside ZIP archives. If your provided archive has a different folder structure than expected, the process will fail abruptly. Ensure your archive matches the expected format if specifying an existing copy.
### File Extraction & Copying
- Extracts all necessary files automatically.
- Prepares the USB drive for the BadUpdate exploit by copying all required files.
- Includes optional content packs such as **ABadAvatar** and the **Aurora Dashboard**.
## Build
Run these commands from the repository root:
```
dotnet restore
dotnet build BadBuilder.sln
```
To publish a self-contained single-file release for 64-bit Windows:
```
dotnet publish BadBuilder/BadBuilder.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
```
If you want the runtime bundled but with trimming and compression enabled (smaller exe, may require extra testing):
```
dotnet publish BadBuilder/BadBuilder.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true -p:EnableCompressionInSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=false
```
To create framework-dependent single-file publishes (requires .NET 8 runtime on the target machine), omit `--self-contained true` and pick the architecture you need:
```
dotnet publish BadBuilder/BadBuilder.csproj -c Release -r win-x64 -p:SelfContained=false -p:PublishSingleFile=true
dotnet publish BadBuilder/BadBuilder.csproj -c Release -r win-x86 -p:SelfContained=false -p:PublishSingleFile=true
dotnet publish BadBuilder/BadBuilder.csproj -c Release -r win-arm64 -p:SelfContained=false -p:PublishSingleFile=true
```
## How to Use
1. **Launch the executable**. It will open inside of a Terminal window.
2. **Formatting (Windows Only):** BadBuilder will format your USB drive as FAT32, even if its larger than 32GB.
> [!CAUTION]
> Formatting a disk means that all data will be lost. Make sure you have selected the right drive before confirming the format. I am not responsible for any data loss.
3. **Download Files:** BadBuilder will fetch the required exploit files or let you specify an existing location. This includes the ABadAvatar payload, XeUnshackle, Aurora Dashboard, Rock Band Blitz, XeXMenu, and Simple 360 NAND Flasher.
4. **Extract Files:** BadBuilder will automatically extract everything needed.
5. **Configure Payload:** BadBuilder automatically configures [XeUnshackle](https://github.com/Byrom90/XeUnshackle) as the default payload for BadUpdate.
6. **Copy Files:** BadBuilder will copy all of the extracted files to the correct locations, including deploying the Aurora Dashboard to `Apps\Aurora`.
## Example Homebrew Folder Structure
If you want to add Aurora, you would select the **root folder**, like:
```
D:\Aurora 0.7b.2 - Release Package
```
Which contains:
```
Aurora 0.7b.2 - Release Package/
├── Data/
├── Media/
├── Plugins/
├── Skins/
├── User/
├── Aurora.xex
├── live.json
├── nxeart
```
BadBuilder will detect `Aurora.xex` as the entry point and patch it accordingly.
> [!IMPORTANT]
> Homebrew apps which do not contain the entry point in the root folder will require you to manually enter the path of the entry point.
## Reporting Issues
If you encounter any problems, please create a new issue with details about your setup and the problem.
### Credits
- **Grimdoomer:** [BadUpdate](https://github.com/grimdoomer/Xbox360BadUpdate)
- **shutterbug2000:** [ABadAvatar](https://github.com/shutterbug2000/ABadAvatar)
- **Phoenix:** [Aurora Dashboard](https://phoenix.xboxunity.net/#/news)
- **Byrom90:** [XeUnshackle](https://github.com/Byrom90/XeUnshackle)
- **Swizzy:** [Simple 360 NAND Flasher](https://github.com/Swizzy/XDK_Projects)
- **Team XeDEV:** XeXMenu