mirror of
https://github.com/Team-Resurgent/Repackinator.git
synced 2026-08-15 02:18:07 -07:00
1198 lines
51 KiB
C#
1198 lines
51 KiB
C#
using Downloader;
|
|
using Repackinator.Core.Exceptions;
|
|
using Repackinator.Core.Helpers;
|
|
using Repackinator.Core.Logging;
|
|
using Repackinator.Core.Models;
|
|
using Repackinator.Core.Streams;
|
|
using XboxToolkit;
|
|
using XboxToolkit.Interface;
|
|
using XboxToolkit.Models.Xbe;
|
|
using XboxToolkit.Internal;
|
|
using SharpCompress.Archives;
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
|
|
namespace Repackinator.Core.Actions
|
|
{
|
|
public class Repacker
|
|
{
|
|
private Action<LogMessage>? Logger { get; set; }
|
|
|
|
private Action<ProgressInfo>? Progress { get; set; }
|
|
|
|
private ProgressInfo CurrentProgress = new ProgressInfo();
|
|
|
|
private GameData[]? GameDataList { get; set; }
|
|
|
|
private void SendProgress()
|
|
{
|
|
if (Progress == null)
|
|
{
|
|
return;
|
|
}
|
|
Progress(CurrentProgress);
|
|
}
|
|
|
|
private void Log(LogMessageLevel level, string message)
|
|
{
|
|
if (Logger == null)
|
|
{
|
|
return;
|
|
}
|
|
var logMessage = new LogMessage(level, message);
|
|
Logger(logMessage);
|
|
File.AppendAllText("RepackLog.txt", logMessage.ToLogFormat());
|
|
}
|
|
|
|
private int ProcessFile(string inputFile, string outputPath, string unpackPath, string trainerPath, GroupingOptionType grouping, bool hasAllCrcs, bool upperCase, CompressOptionType compressType, ScrubOptionType scrubType, bool noSplit, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(inputFile))
|
|
{
|
|
Log(LogMessageLevel.NotFound, $"Not found as '{Path.GetFileName(inputFile)}' does not exist.");
|
|
return -1;
|
|
}
|
|
|
|
Log(LogMessageLevel.Info, $"Processing '{Path.GetFileName(inputFile)}'...");
|
|
|
|
var extension = Path.GetExtension(inputFile).ToLower();
|
|
if (extension.Equals(".iso") || extension.Equals(".cso") || extension.Equals(".cci"))
|
|
{
|
|
return ProcessIso(inputFile, outputPath, trainerPath, grouping, upperCase, compressType, scrubType, noSplit, cancellationToken);
|
|
}
|
|
|
|
return ProcessArchive(inputFile, outputPath, unpackPath, trainerPath, grouping, hasAllCrcs, upperCase, compressType, scrubType, noSplit, cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Processing '{inputFile}' caused error '{ex}'.");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
public int ProcessArchive(string inputFile, string outputPath, string tempPath, string trainerPath, GroupingOptionType grouping, bool hasAllCrcs, bool upperCase, CompressOptionType compressType, ScrubOptionType scrubType, bool noSplit, CancellationToken cancellationToken)
|
|
{
|
|
if (GameDataList == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "GameData should not be null.");
|
|
return -1;
|
|
}
|
|
|
|
string unpackPath;
|
|
if (string.IsNullOrEmpty(tempPath))
|
|
{
|
|
unpackPath = Path.Combine(outputPath, "Repackinator-Temp");
|
|
}
|
|
else
|
|
{
|
|
unpackPath = Path.Combine(tempPath, "Repackinator-Temp");
|
|
}
|
|
|
|
var processOutput = string.Empty;
|
|
var deleteProcessOutput = false;
|
|
|
|
try
|
|
{
|
|
var processStopwatch = new Stopwatch();
|
|
processStopwatch.Start();
|
|
|
|
if (Directory.Exists(unpackPath))
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(unpackPath, true);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Failed to delete directory '{unpackPath}, close any windows accessing the folder.");
|
|
return -1;
|
|
}
|
|
}
|
|
Directory.CreateDirectory(unpackPath);
|
|
|
|
var needsSecondPass = false;
|
|
|
|
var isoFound = false;
|
|
|
|
try
|
|
{
|
|
using (var archiveStream = File.OpenRead(inputFile))
|
|
using (var archive = ArchiveFactory.Open(archiveStream))
|
|
{
|
|
foreach (var entry in archive.Entries)
|
|
{
|
|
if (!Path.GetExtension(entry.Key)?.Equals(".iso", StringComparison.CurrentCultureIgnoreCase) ?? true)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
isoFound = true;
|
|
|
|
var entryCRC = entry.Crc.ToString("X8");
|
|
|
|
bool processArchive = !hasAllCrcs;
|
|
|
|
GameData? tempGameData = null;
|
|
for (var i = 0; i < GameDataList.Length; i++)
|
|
{
|
|
GameData game = GameDataList[i];
|
|
if (game.ISOChecksum.PadLeft(8, '0').Equals(entryCRC, StringComparison.CurrentCultureIgnoreCase))
|
|
{
|
|
processArchive = game.Process.Equals("Y", StringComparison.CurrentCultureIgnoreCase);
|
|
tempGameData = game;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (processArchive)
|
|
{
|
|
Log(LogMessageLevel.Info, "Extracting ISO...");
|
|
|
|
if (tempGameData == null || compressType != CompressOptionType.None || scrubType != ScrubOptionType.None)
|
|
{
|
|
needsSecondPass = true;
|
|
}
|
|
|
|
using (var fileStream1 = new FileStream(Path.Combine(unpackPath, @"Repackinator.1.temp"), FileMode.Create, FileAccess.Write, FileShare.None, 2048 * 4096))
|
|
using (var fileStream2 = new FileStream(Path.Combine(unpackPath, @"Repackinator.2.temp"), FileMode.Create, FileAccess.Write, FileShare.None, 2048 * 4096))
|
|
{
|
|
var extractProgress = new Action<float>((progress) =>
|
|
{
|
|
CurrentProgress.Progress2 = progress;
|
|
CurrentProgress.Progress2Text = $"Extracting ISO...";
|
|
SendProgress();
|
|
});
|
|
using (var extractSplitStream = new ExtractSplitStream(fileStream1, fileStream2, entry.Size, extractProgress, cancellationToken))
|
|
{
|
|
entry.WriteTo(extractSplitStream);
|
|
}
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
if (tempGameData != null)
|
|
{
|
|
Log(LogMessageLevel.Skipped, $"Skipping '{Path.GetFileName(inputFile)}' as requested to skip in dataset based on user selection.");
|
|
}
|
|
else
|
|
{
|
|
Log(LogMessageLevel.NotFound, $"Not found info for '{Path.GetFileName(inputFile)}' as CRC not found in dataset.");
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (ExtractAbortException)
|
|
{
|
|
return -1;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Failed to extract archive - {ex}");
|
|
return -1;
|
|
}
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if (!isoFound)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Repackinator only accepts archive containing an ISO file.");
|
|
return -1;
|
|
}
|
|
|
|
var xbeData = Array.Empty<byte>();
|
|
var tempIsoPath = Path.Combine(unpackPath, @"Repackinator.1.temp");
|
|
using (var containerReader = new ISOContainerReader(tempIsoPath))
|
|
{
|
|
if (!containerReader.TryMount())
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to mount container.");
|
|
return -1;
|
|
}
|
|
try
|
|
{
|
|
if (!containerReader.TryGetDefault(out xbeData, out _))
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to extract default.xbe.");
|
|
return -1;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
containerReader.Dismount();
|
|
}
|
|
}
|
|
|
|
if (!XboxToolkit.XbeUtility.TryGetXbeCert(xbeData, out var cert) || cert == null)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to get data from default.xbe.");
|
|
return -1;
|
|
}
|
|
|
|
var titleId = cert.Title_Id.ToString("X8");
|
|
var gameRegion = XboxToolkit.Models.Xbe.XbeCertificate.GameRegionToString(cert.Game_Region);
|
|
var version = cert.Version.ToString("X8");
|
|
|
|
bool inDatasetISO = false;
|
|
int gameIndex = -1;
|
|
GameData? gameData = null;
|
|
for (var i = 0; i < GameDataList.Length; i++)
|
|
{
|
|
GameData game = GameDataList[i];
|
|
if (game.TitleID == titleId && game.Region == gameRegion && game.Version == version)
|
|
{
|
|
inDatasetISO = true;
|
|
if (game.Process != null && game.Process.Equals("Y", StringComparison.CurrentCultureIgnoreCase))
|
|
{
|
|
gameData = game;
|
|
gameIndex = i;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!gameData.HasValue)
|
|
{
|
|
if (inDatasetISO)
|
|
{
|
|
Log(LogMessageLevel.Skipped, $"Skipping '{Path.GetFileName(inputFile)}' as requested to skip in dataset based on xbe info.");
|
|
}
|
|
else
|
|
{
|
|
Log(LogMessageLevel.NotFound, $"Not found info for '{Path.GetFileName(inputFile)}' as titleid, region and version not found in dataset.");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.Region == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "Region is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.XBETitle == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "XBE title is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.FolderName == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "Folder name is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.ISOName == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "ISO name is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.Letter == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "Letter is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (grouping == GroupingOptionType.Region)
|
|
{
|
|
outputPath = Path.Combine(outputPath, gameData.Value.Region);
|
|
}
|
|
else if (grouping == GroupingOptionType.Letter)
|
|
{
|
|
outputPath = Path.Combine(outputPath, gameData.Value.Letter);
|
|
}
|
|
else if (grouping == GroupingOptionType.RegionLetter)
|
|
{
|
|
outputPath = Path.Combine(outputPath, gameData.Value.Region, gameData.Value.Letter);
|
|
}
|
|
else if (grouping == GroupingOptionType.LetterRegion)
|
|
{
|
|
outputPath = Path.Combine(outputPath, gameData.Value.Letter, gameData.Value.Region);
|
|
}
|
|
|
|
var scrub = false;
|
|
var trimScrub = false;
|
|
if (scrubType == ScrubOptionType.Scrub)
|
|
{
|
|
scrub = true;
|
|
}
|
|
else if (scrubType == ScrubOptionType.TrimScrub)
|
|
{
|
|
scrub = true;
|
|
trimScrub = true;
|
|
}
|
|
|
|
var xbeTitle = upperCase ? gameData.Value.XBETitle.ToUpper() : gameData.Value.XBETitle;
|
|
var folderName = upperCase ? gameData.Value.FolderName.ToUpper() : gameData.Value.FolderName;
|
|
var isoFileName = upperCase ? gameData.Value.ISOName.ToUpper() : gameData.Value.ISOName;
|
|
|
|
processOutput = Path.Combine(outputPath, folderName);
|
|
|
|
if (Directory.Exists(processOutput))
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(processOutput, true);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Failed to delete directory '{processOutput}, close any windows accessing the folder.");
|
|
return -1;
|
|
}
|
|
}
|
|
Directory.CreateDirectory(processOutput);
|
|
|
|
if (needsSecondPass)
|
|
{
|
|
if (compressType != CompressOptionType.None)
|
|
{
|
|
var message = $"Creating Compressed ISO...";
|
|
|
|
Log(LogMessageLevel.Info, message);
|
|
|
|
var repackProgress = new Action<int, float>((stage, progress) =>
|
|
{
|
|
CurrentProgress.Progress2 = progress;
|
|
|
|
if (stage == 0)
|
|
{
|
|
CurrentProgress.Progress2Text = "Processing Data Sectors...";
|
|
}
|
|
else if (stage == 1)
|
|
{
|
|
CurrentProgress.Progress2Text = "Processing Security Sectors...";
|
|
}
|
|
else
|
|
{
|
|
CurrentProgress.Progress2Text = message;
|
|
}
|
|
|
|
SendProgress();
|
|
});
|
|
|
|
var tempIsoPath1 = Path.Combine(unpackPath, @"Repackinator.1.temp");
|
|
using (var containerReader1 = new ISOContainerReader(tempIsoPath1))
|
|
{
|
|
if (!containerReader1.TryMount())
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to mount container.");
|
|
return -1;
|
|
}
|
|
try
|
|
{
|
|
if (compressType == CompressOptionType.CCI)
|
|
{
|
|
var outputFile = Path.Combine(processOutput, $"{isoFileName}.cci");
|
|
var processingOptions = ProcessingOptions.OneToOneCopy;
|
|
if (scrub) processingOptions |= ProcessingOptions.ScrubSectors;
|
|
if (trimScrub) processingOptions |= ProcessingOptions.TrimSectors;
|
|
|
|
var simpleProgress = new Action<float>((progress) =>
|
|
{
|
|
repackProgress(2, progress);
|
|
});
|
|
|
|
if (!ContainerUtility.ConvertContainerToCCI(containerReader1, processingOptions, outputFile, 0, simpleProgress))
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to process file 'Repackinator.temp'.");
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
containerReader1.Dismount();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var message = $"Creating ISO...";
|
|
|
|
Log(LogMessageLevel.Info, message);
|
|
|
|
var repackProgress = new Action<int, float>((stage, progress) =>
|
|
{
|
|
CurrentProgress.Progress2 = progress;
|
|
|
|
if (stage == 0)
|
|
{
|
|
CurrentProgress.Progress2Text = "Processing Data Sectors...";
|
|
}
|
|
else if (stage == 1)
|
|
{
|
|
CurrentProgress.Progress2Text = "Processing Security Sectors...";
|
|
}
|
|
else
|
|
{
|
|
CurrentProgress.Progress2Text = message;
|
|
}
|
|
|
|
SendProgress();
|
|
});
|
|
|
|
var tempIsoPath2 = Path.Combine(unpackPath, @"Repackinator.1.temp");
|
|
using (var containerReader2 = new ISOContainerReader(tempIsoPath2))
|
|
{
|
|
if (!containerReader2.TryMount())
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to mount container.");
|
|
return -1;
|
|
}
|
|
try
|
|
{
|
|
var outputFile = Path.Combine(processOutput, $"{isoFileName}.iso");
|
|
var processingOptions = ProcessingOptions.OneToOneCopy;
|
|
if (scrub) processingOptions |= ProcessingOptions.ScrubSectors;
|
|
if (trimScrub) processingOptions |= ProcessingOptions.TrimSectors;
|
|
|
|
var splitPoint = noSplit ? 0L : 4L * 1024 * 1024 * 1024; // 4GB
|
|
|
|
var simpleProgress = new Action<float>((progress) =>
|
|
{
|
|
repackProgress(2, progress);
|
|
});
|
|
|
|
if (!ContainerUtility.ConvertContainerToISO(containerReader2, processingOptions, outputFile, splitPoint, simpleProgress))
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to process file 'Repackinator.temp'.");
|
|
return -1;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
containerReader2.Dismount();
|
|
}
|
|
}
|
|
}
|
|
|
|
CurrentProgress.Progress2 = 1.0f;
|
|
SendProgress();
|
|
|
|
File.Delete(Path.Combine(unpackPath, @"Repackinator.1.temp"));
|
|
File.Delete(Path.Combine(unpackPath, @"Repackinator.2.temp"));
|
|
}
|
|
else
|
|
{
|
|
File.Move(Path.Combine(unpackPath, @"Repackinator.1.temp"), Path.Combine(processOutput, $"{isoFileName}.1.iso"));
|
|
File.Move(Path.Combine(unpackPath, @"Repackinator.2.temp"), Path.Combine(processOutput, $"{isoFileName}.2.iso"));
|
|
}
|
|
|
|
var hasEtm = Utility.TryFindMatchingEtm(trainerPath, cert.Title_Id, out var etmData);
|
|
if (hasEtm)
|
|
{
|
|
File.WriteAllBytes(Path.Combine(processOutput, "trainer.etm"), etmData);
|
|
}
|
|
|
|
var attach = ResourceLoader.GetEmbeddedResourceBytes(hasEtm ? "hermes-menu" : "hermes.xbe");
|
|
if (XboxToolkit.XbeUtility.TryGetXbeImage(xbeData, XboxToolkit.XbeUtility.ImageType.TitleImage, out var xprImage))
|
|
{
|
|
if (XboxToolkit.XprUtility.ConvertXprToJpeg(xprImage, out var jpgImage))
|
|
{
|
|
if (jpgImage != null)
|
|
{
|
|
File.WriteAllBytes(Path.Combine(processOutput, "default.tbn"), jpgImage);
|
|
}
|
|
if (!XboxToolkit.XbeUtility.TryReplaceXbeTitleImage(attach, jpgImage))
|
|
{
|
|
deleteProcessOutput = true;
|
|
Log(LogMessageLevel.Error, "Failed to replace image.");
|
|
return -1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
deleteProcessOutput = true;
|
|
Log(LogMessageLevel.Error, "Failed to create jpg.");
|
|
return -1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log(LogMessageLevel.Warning, "Failed to extract xpr as probably missing, will use default image.");
|
|
}
|
|
|
|
if (XboxToolkit.XbeUtility.ReplaceCertInfo(attach, xbeData, xbeTitle, out var patchedAttach) && patchedAttach != null)
|
|
{
|
|
File.WriteAllBytes(Path.Combine(processOutput, "default.xbe"), patchedAttach);
|
|
}
|
|
else
|
|
{
|
|
deleteProcessOutput = true;
|
|
Log(LogMessageLevel.Error, "failed creating attach xbe.");
|
|
return -1;
|
|
}
|
|
|
|
processStopwatch.Stop();
|
|
Log(LogMessageLevel.Completed, $"Completed Processing '{Path.GetFileName(inputFile)}' (Time Taken {processStopwatch.Elapsed.TotalHours:00}:{processStopwatch.Elapsed.Minutes:00}:{processStopwatch.Elapsed.Seconds:00}).");
|
|
return gameIndex;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
deleteProcessOutput = true;
|
|
Log(LogMessageLevel.Error, $"Processing '{inputFile}' caused error '{ex}'.");
|
|
return -1;
|
|
}
|
|
finally
|
|
{
|
|
if (deleteProcessOutput && Directory.Exists(processOutput))
|
|
{
|
|
Directory.Delete(processOutput, true);
|
|
}
|
|
if (Directory.Exists(unpackPath))
|
|
{
|
|
Directory.Delete(unpackPath, true);
|
|
}
|
|
if (cancellationToken.IsCancellationRequested && Directory.Exists(processOutput))
|
|
{
|
|
Directory.Delete(processOutput, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
public int ProcessIso(string inputFile, string outputPath, string trainerPath, GroupingOptionType grouping, bool upperCase, CompressOptionType compressType, ScrubOptionType scrubType, bool noSplit, CancellationToken cancellationToken)
|
|
{
|
|
if (GameDataList == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "GameData should not be null.");
|
|
return -1;
|
|
}
|
|
|
|
var processOutput = string.Empty;
|
|
var deleteProcessOutput = false;
|
|
|
|
try
|
|
{
|
|
var processStopwatch = new Stopwatch();
|
|
processStopwatch.Start();
|
|
|
|
var xbeData = Array.Empty<byte>();
|
|
if (!ContainerUtility.TryAutoDetectContainerType(inputFile, out var containerReader) || containerReader == null)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to detect container type.");
|
|
return -1;
|
|
}
|
|
using (containerReader)
|
|
{
|
|
if (!containerReader.TryMount())
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to mount container.");
|
|
return -1;
|
|
}
|
|
try
|
|
{
|
|
if (!containerReader.TryGetDefault(out xbeData, out _))
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to extract default.xbe.");
|
|
return -1;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
containerReader.Dismount();
|
|
}
|
|
}
|
|
|
|
if (!XboxToolkit.XbeUtility.TryGetXbeCert(xbeData, out var cert) || cert == null)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to get data from default.xbe.");
|
|
return -1;
|
|
}
|
|
|
|
var titleId = cert.Title_Id.ToString("X8");
|
|
var gameRegion = XboxToolkit.Models.Xbe.XbeCertificate.GameRegionToString(cert.Game_Region);
|
|
var version = cert.Version.ToString("X8");
|
|
|
|
bool inDatasetISO = false;
|
|
|
|
int gameIndex = -1;
|
|
GameData? gameData = null;
|
|
for (var i = 0; i < GameDataList.Length; i++)
|
|
{
|
|
GameData game = GameDataList[i];
|
|
if (game.TitleID == titleId && game.Region == gameRegion && game.Version == version)
|
|
{
|
|
inDatasetISO = true;
|
|
if (game.Process != null && game.Process.Equals("Y", StringComparison.CurrentCultureIgnoreCase))
|
|
{
|
|
gameData = game;
|
|
gameIndex = i;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!gameData.HasValue)
|
|
{
|
|
if (inDatasetISO)
|
|
{
|
|
Log(LogMessageLevel.Skipped, $"Skipping '{Path.GetFileName(inputFile)}' as requested to skip in dataset based on xbe info.");
|
|
}
|
|
else
|
|
{
|
|
Log(LogMessageLevel.NotFound, $"Not found info for '{Path.GetFileName(inputFile)}' as titleid, region and version not found in dataset.");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.Region == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "Region is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.XBETitle == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "XBE title is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.FolderName == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "Folder name is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.ISOName == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "ISO name is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (gameData.Value.Letter == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "Letter is null in dataset.");
|
|
return -1;
|
|
}
|
|
|
|
if (grouping == GroupingOptionType.Region)
|
|
{
|
|
outputPath = Path.Combine(outputPath, gameData.Value.Region);
|
|
}
|
|
else if (grouping == GroupingOptionType.Letter)
|
|
{
|
|
outputPath = Path.Combine(outputPath, gameData.Value.Letter);
|
|
}
|
|
else if (grouping == GroupingOptionType.RegionLetter)
|
|
{
|
|
outputPath = Path.Combine(outputPath, gameData.Value.Region, gameData.Value.Letter);
|
|
}
|
|
else if (grouping == GroupingOptionType.LetterRegion)
|
|
{
|
|
outputPath = Path.Combine(outputPath, gameData.Value.Letter, gameData.Value.Region);
|
|
}
|
|
|
|
var scrub = false;
|
|
var trimScrub = false;
|
|
if (scrubType == ScrubOptionType.Scrub)
|
|
{
|
|
scrub = true;
|
|
}
|
|
else if (scrubType == ScrubOptionType.TrimScrub)
|
|
{
|
|
scrub = true;
|
|
trimScrub = true;
|
|
}
|
|
|
|
var xbeTitle = upperCase ? gameData.Value.XBETitle.ToUpper() : gameData.Value.XBETitle;
|
|
var folderName = upperCase ? gameData.Value.FolderName.ToUpper() : gameData.Value.FolderName;
|
|
var isoFileName = upperCase ? gameData.Value.ISOName.ToUpper() : gameData.Value.ISOName;
|
|
|
|
processOutput = Path.Combine(outputPath, folderName);
|
|
|
|
if (Directory.Exists(processOutput))
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(processOutput, true);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Failed to delete directory '{processOutput}, close any windows accessing the folder.");
|
|
return -1;
|
|
}
|
|
}
|
|
Directory.CreateDirectory(processOutput);
|
|
|
|
var hasEtm = Utility.TryFindMatchingEtm(trainerPath, cert.Title_Id, out var etmData);
|
|
if (hasEtm)
|
|
{
|
|
File.WriteAllBytes(Path.Combine(processOutput, "trainer.etm"), etmData);
|
|
}
|
|
|
|
var attach = ResourceLoader.GetEmbeddedResourceBytes(hasEtm ? "hermes-menu" : "hermes.xbe");
|
|
if (XboxToolkit.XbeUtility.TryGetXbeImage(xbeData, XboxToolkit.XbeUtility.ImageType.TitleImage, out var xprImage))
|
|
{
|
|
if (XboxToolkit.XprUtility.ConvertXprToJpeg(xprImage, out var jpgImage))
|
|
{
|
|
if (jpgImage != null)
|
|
{
|
|
File.WriteAllBytes(Path.Combine(processOutput, "default.tbn"), jpgImage);
|
|
}
|
|
if (!XboxToolkit.XbeUtility.TryReplaceXbeTitleImage(attach, jpgImage))
|
|
{
|
|
deleteProcessOutput = true;
|
|
Log(LogMessageLevel.Error, "Failed to replace image.");
|
|
return -1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
deleteProcessOutput = true;
|
|
Log(LogMessageLevel.Error, "Failed to create jpg.");
|
|
return -1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log(LogMessageLevel.Warning, "Failed to extract xpr as probably missing, will use default image.");
|
|
}
|
|
|
|
if (XboxToolkit.XbeUtility.ReplaceCertInfo(attach, xbeData, xbeTitle, out var patchedAttach) && patchedAttach != null)
|
|
{
|
|
File.WriteAllBytes(Path.Combine(processOutput, "default.xbe"), patchedAttach);
|
|
}
|
|
else
|
|
{
|
|
deleteProcessOutput = true;
|
|
Log(LogMessageLevel.Error, "failed creating attach xbe.");
|
|
return -1;
|
|
}
|
|
|
|
if (compressType != CompressOptionType.None)
|
|
{
|
|
var message = $"Creating Compressed ISO...";
|
|
|
|
Log(LogMessageLevel.Info, message);
|
|
|
|
var repackProgress = new Action<int, float>((stage, progress) =>
|
|
{
|
|
CurrentProgress.Progress2 = progress;
|
|
|
|
if (stage == 0)
|
|
{
|
|
CurrentProgress.Progress2Text = "Processing Data Sectors...";
|
|
}
|
|
else if (stage == 1)
|
|
{
|
|
CurrentProgress.Progress2Text = "Processing Security Sectors...";
|
|
}
|
|
else
|
|
{
|
|
CurrentProgress.Progress2Text = message;
|
|
}
|
|
|
|
SendProgress();
|
|
});
|
|
|
|
if (!ContainerUtility.TryAutoDetectContainerType(inputFile, out var containerReader3) || containerReader3 == null)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to detect container type.");
|
|
return -1;
|
|
}
|
|
using (containerReader3)
|
|
{
|
|
if (!containerReader3.TryMount())
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to mount container.");
|
|
return -1;
|
|
}
|
|
try
|
|
{
|
|
if (compressType == CompressOptionType.CCI)
|
|
{
|
|
var outputFile = Path.Combine(processOutput, $"{isoFileName}.cci");
|
|
var processingOptions = ProcessingOptions.OneToOneCopy;
|
|
if (scrub) processingOptions |= ProcessingOptions.ScrubSectors;
|
|
if (trimScrub) processingOptions |= ProcessingOptions.TrimSectors;
|
|
|
|
var simpleProgress = new Action<float>((progress) =>
|
|
{
|
|
repackProgress(2, progress);
|
|
});
|
|
|
|
if (!ContainerUtility.ConvertContainerToCCI(containerReader3, processingOptions, outputFile, 0, simpleProgress))
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to process file '{inputFile}'.");
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
containerReader3.Dismount();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var message = $"Creating ISO...";
|
|
|
|
Log(LogMessageLevel.Info, message);
|
|
|
|
var repackProgress = new Action<int, float>((stage, progress) =>
|
|
{
|
|
CurrentProgress.Progress2 = progress;
|
|
|
|
if (stage == 0)
|
|
{
|
|
CurrentProgress.Progress2Text = "Processing Data Sectors...";
|
|
}
|
|
else if (stage == 1)
|
|
{
|
|
CurrentProgress.Progress2Text = "Processing Security Sectors...";
|
|
}
|
|
else
|
|
{
|
|
CurrentProgress.Progress2Text = message;
|
|
}
|
|
|
|
SendProgress();
|
|
});
|
|
|
|
if (!ContainerUtility.TryAutoDetectContainerType(inputFile, out var containerReader4) || containerReader4 == null)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to detect container type.");
|
|
return -1;
|
|
}
|
|
using (containerReader4)
|
|
{
|
|
if (!containerReader4.TryMount())
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to mount container.");
|
|
return -1;
|
|
}
|
|
try
|
|
{
|
|
var outputFile = Path.Combine(processOutput, $"{isoFileName}.iso");
|
|
var processingOptions = ProcessingOptions.OneToOneCopy;
|
|
if (scrub) processingOptions |= ProcessingOptions.ScrubSectors;
|
|
if (trimScrub) processingOptions |= ProcessingOptions.TrimSectors;
|
|
|
|
var splitPoint = noSplit ? 0L : 0xFF000000L;
|
|
|
|
var simpleProgress = new Action<float>((progress) =>
|
|
{
|
|
repackProgress(2, progress);
|
|
});
|
|
|
|
if (!ContainerUtility.ConvertContainerToISO(containerReader4, processingOptions, outputFile, splitPoint, simpleProgress))
|
|
{
|
|
Log(LogMessageLevel.Error, $"Unable to process file '{inputFile}'.");
|
|
return -1;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
containerReader4.Dismount();
|
|
}
|
|
}
|
|
}
|
|
|
|
CurrentProgress.Progress2 = 1.0f;
|
|
SendProgress();
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
deleteProcessOutput = true;
|
|
return -1;
|
|
}
|
|
|
|
processStopwatch.Stop();
|
|
Log(LogMessageLevel.Completed, $"Completed Processing '{Path.GetFileName(inputFile)}' (Time Taken {processStopwatch.Elapsed.TotalHours:00}:{processStopwatch.Elapsed.Minutes:00}:{processStopwatch.Elapsed.Seconds:00}).");
|
|
return gameIndex;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
deleteProcessOutput = true;
|
|
Log(LogMessageLevel.Error, $"Processing '{inputFile}' caused error '{ex}'.");
|
|
return -1;
|
|
}
|
|
finally
|
|
{
|
|
if (deleteProcessOutput && Directory.Exists(processOutput))
|
|
{
|
|
Directory.Delete(processOutput, true);
|
|
}
|
|
if (cancellationToken.IsCancellationRequested && Directory.Exists(processOutput))
|
|
{
|
|
Directory.Delete(processOutput, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task<bool> DownloadFromUrlToPath(string url, string path, Action<long, long, double> downloaded, CancellationToken cancellationToken)
|
|
{
|
|
var downloadOpt = new DownloadConfiguration()
|
|
{
|
|
ChunkCount = 8,
|
|
ParallelDownload = true,
|
|
MaxTryAgainOnFailure = 5,
|
|
ReserveStorageSpaceBeforeStartingDownload = true
|
|
};
|
|
|
|
var downloader = new DownloadService(downloadOpt);
|
|
downloader.DownloadProgressChanged += (s, e) =>
|
|
{
|
|
downloaded(e.ReceivedBytesSize, e.TotalBytesToReceive, e.BytesPerSecondSpeed);
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
downloader.CancelAsync();
|
|
}
|
|
};
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
|
|
await downloader.DownloadFileTaskAsync(url, path);
|
|
|
|
return downloader.Status == DownloadStatus.Completed;
|
|
}
|
|
|
|
public void StartRepacking(GameData[]? gameData, Config config, Action<ProgressInfo>? progress, Action<LogMessage> logger, Stopwatch stopwatch, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
Logger = logger;
|
|
Progress = progress;
|
|
|
|
if (gameData == null)
|
|
{
|
|
Log(LogMessageLevel.Error, "RepackList.json not found.");
|
|
return;
|
|
}
|
|
|
|
if (!Directory.Exists(config.InputPath))
|
|
{
|
|
Log(LogMessageLevel.Error, "Input path is invalid.");
|
|
return;
|
|
}
|
|
|
|
if (!Directory.Exists(config.OutputPath))
|
|
{
|
|
Log(LogMessageLevel.Error, "Output path is invalid.");
|
|
return;
|
|
}
|
|
|
|
if (!Directory.Exists(config.UnpackPath) && !string.IsNullOrEmpty(config.UnpackPath))
|
|
{
|
|
Log(LogMessageLevel.Error, "Unpack path is invalid.");
|
|
return;
|
|
}
|
|
|
|
if (string.Equals(config.InputPath, config.OutputPath, StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(config.InputPath, config.UnpackPath, StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(config.OutputPath, config.UnpackPath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Log(LogMessageLevel.Error, "Input, Output & Unpack paths must be different.");
|
|
return;
|
|
}
|
|
|
|
GameDataList = gameData;
|
|
|
|
int crcMissingCount = 0;
|
|
foreach (var gameDataItem in GameDataList)
|
|
{
|
|
if (string.IsNullOrEmpty(gameDataItem.ISOChecksum))
|
|
{
|
|
crcMissingCount++;
|
|
}
|
|
}
|
|
|
|
stopwatch.Restart();
|
|
|
|
if (File.Exists("RepackLog.txt"))
|
|
{
|
|
File.Delete("RepackLog.txt");
|
|
}
|
|
|
|
if (crcMissingCount > 0)
|
|
{
|
|
Log(LogMessageLevel.Warning, $"There are {crcMissingCount} ISO CRC's missing this will cause compressed ISO's to take a while longer.");
|
|
Log(LogMessageLevel.None, "");
|
|
}
|
|
|
|
CurrentProgress.Progress1 = 0;
|
|
CurrentProgress.Progress1Text = string.Empty;
|
|
CurrentProgress.Progress2 = 0;
|
|
CurrentProgress.Progress2Text = string.Empty;
|
|
SendProgress();
|
|
|
|
if (config.LeechType > 0)
|
|
{
|
|
var count = 0;
|
|
var leechlistCount = GameDataList.Where(l => l.Process.Equals("Y", StringComparison.CurrentCultureIgnoreCase)).Count();
|
|
|
|
for (int i = 0; i < GameDataList.Length; i++)
|
|
{
|
|
GameData gameDataItem = GameDataList[i];
|
|
|
|
if (!config.Section.Equals("[AllSections]") && gameDataItem.Section.Equals(config.Section, StringComparison.CurrentCultureIgnoreCase) == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (gameDataItem.Process.Equals("Y", StringComparison.CurrentCultureIgnoreCase) == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
count++;
|
|
|
|
var decodedLink = gameDataItem.Link;
|
|
if (!decodedLink.StartsWith("http", StringComparison.CurrentCultureIgnoreCase) && string.IsNullOrEmpty(decodedLink) == false)
|
|
{
|
|
byte[] linkBytes = Convert.FromBase64String(gameDataItem.Link);
|
|
decodedLink = Encoding.ASCII.GetString(linkBytes);
|
|
}
|
|
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"RepackinatorDownload{Path.GetExtension(decodedLink)}");
|
|
if (config.LeechType > 1)
|
|
{
|
|
tempPath = Path.Combine(config.InputPath, $"{gameDataItem.ISOName}{Path.GetExtension(decodedLink)}");
|
|
}
|
|
|
|
if (File.Exists(tempPath))
|
|
{
|
|
File.Delete(tempPath);
|
|
}
|
|
|
|
try
|
|
{
|
|
CurrentProgress.Progress1 = i / (float)GameDataList.Length;
|
|
CurrentProgress.Progress1Text = $"Processing {count} of {leechlistCount}";
|
|
SendProgress();
|
|
|
|
if (string.IsNullOrEmpty(decodedLink) == false)
|
|
{
|
|
Log(LogMessageLevel.Info, $"Downloading '{gameDataItem.ISOName}'.");
|
|
|
|
var result = DownloadFromUrlToPath(decodedLink, tempPath, (downloaded, totalLength, bytesPerSecond) =>
|
|
{
|
|
CurrentProgress.Progress2 = downloaded / (float)totalLength;
|
|
CurrentProgress.Progress2Text = $"Downloaded {Math.Round(downloaded / (1024 * 1024.0f), 2)}MB of {Math.Round(totalLength / (1024 * 1024.0f), 2)}MB ({Math.Round(bytesPerSecond / 1024, 2)}KB/s)";
|
|
SendProgress();
|
|
}, cancellationToken).Result;
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
Log(LogMessageLevel.None, "");
|
|
Log(LogMessageLevel.Info, "Cancelled.");
|
|
break;
|
|
}
|
|
|
|
if (!result)
|
|
{
|
|
Log(LogMessageLevel.Error, "Download Failed.");
|
|
continue;
|
|
}
|
|
|
|
if (config.LeechType == 3)
|
|
{
|
|
gameDataItem.Process = "N";
|
|
GameDataHelper.SaveGameData(gameData);
|
|
continue;
|
|
}
|
|
|
|
var gameIndex = ProcessFile(tempPath, config.OutputPath, config.UnpackPath, config.TrainerPath, config.GroupingOption, crcMissingCount == 0, config.Uppercase, config.CompressOption, config.ScrubOption, config.NoSplit, cancellationToken);
|
|
if (gameIndex >= 0)
|
|
{
|
|
gameData[gameIndex].Process = "N";
|
|
GameDataHelper.SaveGameData(gameData);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log(LogMessageLevel.Warning, $"No download link for '{gameDataItem.ISOName}'.");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
if (config.LeechType == 1 && File.Exists(tempPath))
|
|
{
|
|
File.Delete(tempPath);
|
|
}
|
|
}
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
Log(LogMessageLevel.None, "");
|
|
Log(LogMessageLevel.Info, "Cancelled.");
|
|
break;
|
|
}
|
|
|
|
Log(LogMessageLevel.None, "");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var acceptedFiletypes = new string[] { ".iso", ".cso", ".cci", ".zip", ".rar", ".7z" };
|
|
var tempFiles = Directory.GetFileSystemEntries(config.InputPath, "*", new EnumerationOptions { IgnoreInaccessible = true, RecurseSubdirectories = config.RecurseInput, MatchCasing = MatchCasing.CaseInsensitive })
|
|
.Where(file => acceptedFiletypes.Contains(Path.GetExtension(file), StringComparer.CurrentCultureIgnoreCase))
|
|
.ToList();
|
|
|
|
var files = new List<string>();
|
|
for (int i = 0; i < tempFiles.Count; i++)
|
|
{
|
|
var nameWithoutExtension = Path.GetFileNameWithoutExtension(tempFiles[i]);
|
|
var subExtension = Path.GetExtension(nameWithoutExtension);
|
|
if (subExtension.Equals(".2"))
|
|
{
|
|
continue;
|
|
}
|
|
files.Add(tempFiles[i]);
|
|
}
|
|
|
|
for (int i = 0; i < files.Count; i++)
|
|
{
|
|
string? file = files[i];
|
|
CurrentProgress.Progress1 = i / (float)files.Count;
|
|
CurrentProgress.Progress1Text = $"Processing {i + 1} of {files.Count}";
|
|
SendProgress();
|
|
|
|
var gameIndex = ProcessFile(file, config.OutputPath, config.UnpackPath, config.TrainerPath, config.GroupingOption, crcMissingCount == 0, config.Uppercase, config.CompressOption, config.ScrubOption, config.NoSplit, cancellationToken);
|
|
if (gameIndex >= 0)
|
|
{
|
|
gameData[gameIndex].Process = "N";
|
|
GameDataHelper.SaveGameData(gameData);
|
|
}
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
Log(LogMessageLevel.None, "");
|
|
Log(LogMessageLevel.Info, "Cancelled.");
|
|
break;
|
|
}
|
|
|
|
Log(LogMessageLevel.None, "");
|
|
}
|
|
}
|
|
|
|
CurrentProgress.Progress1 = 1.0f;
|
|
SendProgress();
|
|
|
|
stopwatch.Stop();
|
|
|
|
Log(LogMessageLevel.Done, $"Completed Processing List (Time Taken {stopwatch.Elapsed.TotalHours:00}:{stopwatch.Elapsed.Minutes:00}:{stopwatch.Elapsed.Seconds:00}).");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log(LogMessageLevel.Error, $"Exception occured '{ex}'.");
|
|
}
|
|
}
|
|
}
|
|
}
|