From e0274c268a7bde6b0a43fb093aaecb9385d945cd Mon Sep 17 00:00:00 2001 From: Rupert Avery Date: Sat, 18 Jan 2020 19:22:32 +0800 Subject: [PATCH] Support extract PBP TOC to CUE Refactoring/code cleanup Unit tests added for IndexPosition math --- PSXPackager.sln | 6 + PSXPackager/ApplicationInfo.cs | 13 + PSXPackager/FileExtensionHelper.cs | 37 ++ PSXPackager/MergedBin.cs | 11 + PSXPackager/Options.cs | 27 ++ PSXPackager/Program.cs | 456 +++++++----------- PSXPackager/Properties/launchSettings.json | 2 +- Popstation/CueReader.cs | 10 +- Popstation/CueWriter.cs | 34 ++ Popstation/ExtractIsoInfo.cs | 1 + Popstation/Helper.cs | 71 +++ Popstation/IndexPosition.Operators.cs | 139 ++++++ Popstation/IndexPosition.cs | 56 +-- Popstation/{INDEX.cs => IsoIndexLite.cs} | 2 +- Popstation/PbpStream.cs | 96 +++- Popstation/Popstation.Extract.cs | 63 ++- Popstation/Popstation.cs | 49 +- .../IndexPositionWithIntMathTests.cs | 147 ++++++ .../IndexPositionWithPositionMathTests.cs | 203 ++++++++ UnitTestProject/Properties/AssemblyInfo.cs | 20 + UnitTestProject/UnitTestProject.csproj | 75 +++ UnitTestProject/packages.config | 5 + 22 files changed, 1138 insertions(+), 385 deletions(-) create mode 100644 PSXPackager/ApplicationInfo.cs create mode 100644 PSXPackager/FileExtensionHelper.cs create mode 100644 PSXPackager/MergedBin.cs create mode 100644 PSXPackager/Options.cs create mode 100644 Popstation/CueWriter.cs create mode 100644 Popstation/Helper.cs create mode 100644 Popstation/IndexPosition.Operators.cs rename Popstation/{INDEX.cs => IsoIndexLite.cs} (83%) create mode 100644 UnitTestProject/IndexPositionWithIntMathTests.cs create mode 100644 UnitTestProject/IndexPositionWithPositionMathTests.cs create mode 100644 UnitTestProject/Properties/AssemblyInfo.cs create mode 100644 UnitTestProject/UnitTestProject.csproj create mode 100644 UnitTestProject/packages.config diff --git a/PSXPackager.sln b/PSXPackager.sln index 6bf4d9d..58552e7 100644 --- a/PSXPackager.sln +++ b/PSXPackager.sln @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils.Iso9660", "DiscUt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils.Streams", "DiscUtils.Streams\DiscUtils.Streams.csproj", "{C76270B7-9FF7-400C-9A0C-D6A910C2B21D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestProject", "UnitTestProject\UnitTestProject.csproj", "{477347DA-4397-413B-B3AB-F13200D31543}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {C76270B7-9FF7-400C-9A0C-D6A910C2B21D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C76270B7-9FF7-400C-9A0C-D6A910C2B21D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C76270B7-9FF7-400C-9A0C-D6A910C2B21D}.Release|Any CPU.Build.0 = Release|Any CPU + {477347DA-4397-413B-B3AB-F13200D31543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {477347DA-4397-413B-B3AB-F13200D31543}.Debug|Any CPU.Build.0 = Debug|Any CPU + {477347DA-4397-413B-B3AB-F13200D31543}.Release|Any CPU.ActiveCfg = Release|Any CPU + {477347DA-4397-413B-B3AB-F13200D31543}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PSXPackager/ApplicationInfo.cs b/PSXPackager/ApplicationInfo.cs new file mode 100644 index 0000000..9acbd36 --- /dev/null +++ b/PSXPackager/ApplicationInfo.cs @@ -0,0 +1,13 @@ +namespace PSXPackager +{ + public static class ApplicationInfo + { + public static string AppPath { get; private set; } + static ApplicationInfo() + { + string path = System.Reflection.Assembly.GetExecutingAssembly().CodeBase.Replace("file:\\\\\\", "").Replace("file:///", ""); + AppPath = System.IO.Path.GetDirectoryName(path); + } + + } +} diff --git a/PSXPackager/FileExtensionHelper.cs b/PSXPackager/FileExtensionHelper.cs new file mode 100644 index 0000000..84ea9e3 --- /dev/null +++ b/PSXPackager/FileExtensionHelper.cs @@ -0,0 +1,37 @@ +using System.IO; + +namespace PSXPackager +{ + public static class FileExtensionHelper + { + public static bool IsCue(string filename) + { + return Path.GetExtension(filename).ToLower() == ".cue"; + } + + public static bool IsPbp(string filename) + { + return Path.GetExtension(filename).ToLower() == ".pbp"; + } + + public static bool IsArchive(string filename) + { + return Path.GetExtension(filename).ToLower() == ".7z" || + Path.GetExtension(filename).ToLower() == ".rar" || + Path.GetExtension(filename).ToLower() == ".zip"; + } + + public static bool IsBin(string filename) + { + return Path.GetExtension(filename).ToLower() == ".bin"; + } + + public static bool IsImageFile(string filename) + { + return Path.GetExtension(filename).ToLower() == ".bin" || + Path.GetExtension(filename).ToLower() == ".img" || + Path.GetExtension(filename).ToLower() == ".iso"; + } + + } +} diff --git a/PSXPackager/MergedBin.cs b/PSXPackager/MergedBin.cs new file mode 100644 index 0000000..c406677 --- /dev/null +++ b/PSXPackager/MergedBin.cs @@ -0,0 +1,11 @@ +using Popstation; +using System.Collections.Generic; + +namespace PSXPackager +{ + public class MergedBin + { + public string Path { get; set; } + public List CueFiles { get; set; } + } +} diff --git a/PSXPackager/Options.cs b/PSXPackager/Options.cs new file mode 100644 index 0000000..77eb131 --- /dev/null +++ b/PSXPackager/Options.cs @@ -0,0 +1,27 @@ +using CommandLine; + +namespace PSXPackager +{ + public class Options + { + [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")] + public bool Verbose { get; set; } + + [Option('l', "level", Required = false, HelpText = "Set compression level 0-9, default 5", Default = 5)] + public int CompressionLevel { get; set; } + + [Option('o', "output", Required = false + , HelpText = "The output path where the converted file will be written")] + public string OutputPath { get; set; } + + [Option('i', "input", Group = "input", HelpText = "The input file to convert")] + public string InputPath { get; set; } + + [Option('b', "batch", Group = "input", HelpText = "The path to batch process a set of files")] + public string Batch { get; set; } + + [Option('e', "ext", Required = false, HelpText = "The extension of the files to process in the batch folder, e.g. .7z")] + public string BatchExtension { get; set; } + + } +} diff --git a/PSXPackager/Program.cs b/PSXPackager/Program.cs index a292d3f..3a5b744 100644 --- a/PSXPackager/Program.cs +++ b/PSXPackager/Program.cs @@ -3,57 +3,199 @@ using Popstation; using SevenZipExtractor; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace PSXPackager { - - public class MergedBin - { - public string Path { get; set; } - public List CueFiles { get; set; } - } - class Program { + static CancellationTokenSource cancelToken; - static bool IsCue(string filename) + static void Main(string[] args) { - return Path.GetExtension(filename).ToLower() == ".cue"; - } - static bool IsPbp(string filename) - { - return Path.GetExtension(filename).ToLower() == ".pbp"; - } + var tempPath = Path.Combine(Path.GetTempPath(), "PSXPackager"); - static bool IsArchive(string filename) - { - return Path.GetExtension(filename).ToLower() == ".7z" || - Path.GetExtension(filename).ToLower() == ".rar" || - Path.GetExtension(filename).ToLower() == ".zip"; + if (!Directory.Exists(tempPath)) + { + Directory.CreateDirectory(tempPath); + } + + cancelToken = new CancellationTokenSource(); + + Parser.Default.ParseArguments(args) + .WithParsed(async o => + { + + if (!string.IsNullOrEmpty(o.InputPath)) + { + if (o.CompressionLevel < 0 || o.CompressionLevel > 9) + { + Console.WriteLine($"Invalid compression level, please enter a value from 0 to 9"); + return; + } + Console.WriteLine($"Input: {o.InputPath}"); + } + else if (!string.IsNullOrEmpty(o.Batch)) + { + Console.WriteLine($"Batch: {o.Batch}"); + Console.WriteLine($"Extension: {o.BatchExtension}"); + } + + if (string.IsNullOrEmpty(o.OutputPath)) + { + if (!string.IsNullOrEmpty(o.InputPath)) + { + o.OutputPath = Path.GetDirectoryName(o.InputPath); + } + else if (!string.IsNullOrEmpty(o.Batch)) + { + o.OutputPath = o.Batch; + } + } + + Console.WriteLine($"Output: {o.OutputPath}"); + Console.WriteLine($"Compression Level: {o.CompressionLevel}"); + Console.WriteLine(); + + if (!string.IsNullOrEmpty(o.InputPath)) + { + ProcessFile(o.InputPath, o.OutputPath, tempPath, o.CompressionLevel, cancelToken.Token).GetAwaiter().GetResult(); + } + else if (!string.IsNullOrEmpty(o.Batch)) + { + var files = Directory.GetFiles(o.Batch, $"*{o.BatchExtension}"); + + foreach (var file in files) + { + ProcessFile(file, o.OutputPath, tempPath, o.CompressionLevel, cancelToken.Token).GetAwaiter().GetResult(); + if (cancelToken.Token.IsCancellationRequested) + { + break; + } + } + } + }); } - static bool IsBin(string filename) + protected static void myHandler(object sender, ConsoleCancelEventArgs args) { - return Path.GetExtension(filename).ToLower() == ".bin"; + if (!cancelToken.IsCancellationRequested) + { + Console.WriteLine("Stopping conversion..."); + cancelToken.Cancel(); + } + args.Cancel = true; } - static bool IsImageFile(string filename) + static async Task ProcessFile(string file, string outPath, string tempPath, int compressionLevel, CancellationToken cancellationToken) { - return Path.GetExtension(filename).ToLower() == ".bin" || - Path.GetExtension(filename).ToLower() == ".img" || - Path.GetExtension(filename).ToLower() == ".iso"; - } + Console.WriteLine($"Converting {file}..."); + + List tempFiles = null; + string srcToc = null; + + Console.CancelKeyPress += new ConsoleCancelEventHandler(myHandler); + try + { + + if (FileExtensionHelper.IsArchive(file)) + { + tempFiles = Unpack(file, tempPath, cancellationToken); + + if (cancellationToken.IsCancellationRequested) return; + + file = ""; + + if (tempFiles.Count(FileExtensionHelper.IsImageFile) == 0) + { + Console.WriteLine("No image files found!"); + } + else if (tempFiles.Count(FileExtensionHelper.IsImageFile) == 1) + { + file = tempFiles.FirstOrDefault(FileExtensionHelper.IsImageFile); + } + else if (tempFiles.Count(FileExtensionHelper.IsBin) > 1) + { + Console.WriteLine($"Multi-bin image was found!"); + + var cue = tempFiles.FirstOrDefault(FileExtensionHelper.IsCue); + if (cue != null) + { + file = cue; + } + else + { + Console.WriteLine($"No cue sheet found!"); + } + } + } + + if (!string.IsNullOrEmpty(file)) + { + + if (FileExtensionHelper.IsPbp(file)) + { + await ExtractPbp(file, outPath, cancellationToken); + } + else + { + if (FileExtensionHelper.IsCue(file)) + { + var filePath = Path.GetDirectoryName(file); + + var cueFiles = CueReader.Read(file); + if (cueFiles.Count > 1) + { + var mergedBin = MergeBins(file, cueFiles, tempPath); + var cueFile = Path.Combine(tempPath, Path.GetFileNameWithoutExtension(mergedBin.Path) + ".cue"); + CueWriter.Write(cueFile, mergedBin.CueFiles); + srcToc = cueFile; + file = mergedBin.Path; + + tempFiles.Add(mergedBin.Path); + tempFiles.Add(cueFile); + } + else + { + srcToc = file; + file = Path.Combine(filePath, cueFiles.First().FileName); + } + } + + await ConvertIso(file, srcToc, outPath, compressionLevel, cancellationToken); + } + } + } + finally + { + Console.CursorVisible = true; + if (cancellationToken.IsCancellationRequested) + { + Console.WriteLine("Conversion cancelled"); + } + else + { + Console.WriteLine("Conversion completed!"); + } + + if (tempFiles != null) + { + foreach (var tempFile in tempFiles) + { + File.Delete(tempFile); + } + } + } + } + static MergedBin MergeBins(string file, IEnumerable cueFiles, string tempPath) { @@ -122,7 +264,7 @@ static List Unpack(string file, string tempPath, CancellationToken cance var unpackTasks = new List(); foreach (Entry entry in archiveFile.Entries) { - if (IsImageFile(entry.FileName) || IsCue(entry.FileName)) + if (FileExtensionHelper.IsImageFile(entry.FileName) || FileExtensionHelper.IsCue(entry.FileName)) { Console.WriteLine($"Decompressing {entry.FileName}..."); var path = Path.Combine(tempPath, entry.FileName); @@ -142,16 +284,12 @@ static List Unpack(string file, string tempPath, CancellationToken cance return files; } - static Task ConvertIso(string srcIso, string srcToc, string outpath, int compressionLevel, CancellationToken cancellationToken) + static GameEntry FindGameInfo(string srcIso) { - string path = System.Reflection.Assembly.GetExecutingAssembly().CodeBase.Replace("file:\\\\\\", "").Replace("file:///", ""); - var appPath = System.IO.Path.GetDirectoryName(path); - - var regex = new Regex("(S[LC]\\w{2})[_-](\\d{3})\\.(\\d{2})"); var bootRegex = new Regex("BOOT\\s*=\\s*cdrom:\\\\?(?:.*?\\\\)?(S[LC]\\w{2}[_-]?\\d{3}\\.\\d{2});1"); - GameEntry game = null; + GameEntry game = null; using (var stream = new FileStream(srcIso, FileMode.Open)) { @@ -198,7 +336,7 @@ static Task ConvertIso(string srcIso, string srcToc, string outpath, int compres if (syscnfFound) { - var gameDB = new GameDB(Path.Combine(appPath, "Resources", "gameinfo.db")); + var gameDB = new GameDB(Path.Combine(ApplicationInfo.AppPath, "Resources", "gameinfo.db")); game = gameDB.GetEntryByScannerID(gameId); @@ -209,16 +347,24 @@ static Task ConvertIso(string srcIso, string srcToc, string outpath, int compres else { Console.WriteLine($"Could not find gameId {gameId}!"); + return null; } } else { Console.WriteLine($"Could not find SYSTEM.CNF!"); + return null; } } - if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); + return game; + } + + static Task ConvertIso(string srcIso, string srcToc, string outpath, int compressionLevel, CancellationToken cancellationToken) + { + var game = FindGameInfo(srcIso); + var appPath = ApplicationInfo.AppPath; var info = new ConvertIsoInfo() { @@ -255,10 +401,12 @@ static Task ConvertIso(string srcIso, string srcToc, string outpath, int compres static Task ExtractPbp(string srcPbp, string outpath, CancellationToken cancellationToken) { var filename = Path.GetFileNameWithoutExtension(srcPbp) + ".bin"; + var info = new ExtractIsoInfo() { SourcePbp = srcPbp, - DestinationIso = Path.Combine(outpath, filename) + DestinationIso = Path.Combine(outpath, filename), + CreateCuesheet = true }; var popstation = new Popstation.Popstation(); @@ -269,238 +417,6 @@ static Task ExtractPbp(string srcPbp, string outpath, CancellationToken cancella return popstation.Extract(info, cancellationToken); } - public class Options - { - [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")] - public bool Verbose { get; set; } - - [Option('l', "level", Required = false, HelpText = "Set compression level 0-9, default 5", Default = 5)] - public int CompressionLevel { get; set; } - - [Option('o', "output", Required = false - , HelpText = "The output path where the converted file will be written")] - public string OutputPath { get; set; } - - [Option('i', "input", Group = "input", HelpText = "The input file to convert")] - public string InputPath { get; set; } - - [Option('b', "batch", Group = "input", HelpText = "The path to batch process a set of files")] - public string Batch { get; set; } - - [Option('e', "ext", Required = false, HelpText = "The extension of the files to process in the batch folder, e.g. .7z")] - public string BatchExtension { get; set; } - - } - - static void Main(string[] args) - { - var tempPath = Path.Combine(Path.GetTempPath(), "PSXPackager"); - - if (!Directory.Exists(tempPath)) - { - Directory.CreateDirectory(tempPath); - } - - cancelToken = new CancellationTokenSource(); - - - Parser.Default.ParseArguments(args) - .WithParsed(async o => - { - - if (!string.IsNullOrEmpty(o.InputPath)) - { - if (o.CompressionLevel < 0 || o.CompressionLevel > 9) - { - Console.WriteLine($"Invalid compression level, please enter a value from 0 to 9"); - return; - } - Console.WriteLine($"Input: {o.InputPath}"); - } - else if (!string.IsNullOrEmpty(o.Batch)) - { - Console.WriteLine($"Batch: {o.Batch}"); - Console.WriteLine($"Extension: {o.BatchExtension}"); - } - - if (string.IsNullOrEmpty(o.OutputPath)) - { - if (!string.IsNullOrEmpty(o.InputPath)) - { - o.OutputPath = Path.GetDirectoryName(o.InputPath); - } - else if (!string.IsNullOrEmpty(o.Batch)) - { - o.OutputPath = o.Batch; - } - } - - Console.WriteLine($"Output: {o.OutputPath}"); - Console.WriteLine($"Compression Level: {o.CompressionLevel}"); - Console.WriteLine(); - - - - if (!string.IsNullOrEmpty(o.InputPath)) - { - ProcessFile(o.InputPath, o.OutputPath, tempPath, o.CompressionLevel, cancelToken.Token).GetAwaiter().GetResult(); - } - else if (!string.IsNullOrEmpty(o.Batch)) - { - var files = Directory.GetFiles(o.Batch, $"*{o.BatchExtension}"); - - foreach (var file in files) - { - ProcessFile(file, o.OutputPath, tempPath, o.CompressionLevel, cancelToken.Token).GetAwaiter().GetResult(); - if (cancelToken.Token.IsCancellationRequested) - { - break; - } - } - } - }); - } - - static void WriteCue(string path, IEnumerable cueFiles) - { - using (var file = new FileStream(path, FileMode.Create)) - { - var writer = new StreamWriter(file); - foreach (var cueFile in cueFiles) - { - writer.WriteLine($"FILE \"{cueFile.FileName}\" {cueFile.FileType}"); - foreach (var cueTrack in cueFile.Tracks) - { - writer.WriteLine($" TRACK {cueTrack.Number:00} {cueTrack.DataType}"); - - foreach (var cueIndex in cueTrack.Indexes) - { - writer.WriteLine($" INDEX {cueIndex.Number:00} {cueIndex.Position.ToString()}"); - - } - } - } - writer.Flush(); - writer.Close(); - } - } - - static CancellationTokenSource cancelToken; - - protected static void myHandler(object sender, ConsoleCancelEventArgs args) - { - if (!cancelToken.IsCancellationRequested) - { - Console.WriteLine("Stopping conversion..."); - cancelToken.Cancel(); - } - args.Cancel = true; - } - - static async Task ProcessFile(string file, string outPath, string tempPath, int compressionLevel, CancellationToken cancellationToken) - { - Console.WriteLine($"Converting {file}..."); - - List tempFiles = null; - string srcToc = null; - - Console.CancelKeyPress += new ConsoleCancelEventHandler(myHandler); - try - { - - if (IsArchive(file)) - { - tempFiles = Unpack(file, tempPath, cancellationToken); - - if (cancellationToken.IsCancellationRequested) return; - - file = ""; - - if (tempFiles.Count(IsImageFile) == 0) - { - Console.WriteLine("No image files found!"); - } - else if (tempFiles.Count(IsImageFile) == 1) - { - file = tempFiles.FirstOrDefault(IsImageFile); - } - else if (tempFiles.Count(IsBin) > 1) - { - Console.WriteLine($"Multi-bin image was found!"); - - var cue = tempFiles.FirstOrDefault(IsCue); - if (cue != null) - { - file = cue; - } - else - { - Console.WriteLine($"No cue sheet found!"); - } - } - } - - if (!string.IsNullOrEmpty(file)) - { - - if (IsPbp(file)) - { - await ExtractPbp(file, outPath, cancellationToken); - } - else - { - if (IsCue(file)) - { - var filePath = Path.GetDirectoryName(file); - - var cueReader = new CueReader(); - var cueFiles = cueReader.Read(file); - if (cueFiles.Count > 1) - { - var mergedBin = MergeBins(file, cueFiles, tempPath); - var cueFile = Path.Combine(tempPath, Path.GetFileNameWithoutExtension(mergedBin.Path) + ".cue"); - WriteCue(cueFile, mergedBin.CueFiles); - srcToc = cueFile; - file = mergedBin.Path; - - tempFiles.Add(mergedBin.Path); - tempFiles.Add(cueFile); - } - else - { - srcToc = file; - file = Path.Combine(filePath, cueFiles.First().FileName); - } - } - - await ConvertIso(file, srcToc, outPath, compressionLevel, cancellationToken); - } - - - } - } - finally - { - Console.CursorVisible = true; - if (cancellationToken.IsCancellationRequested) - { - Console.WriteLine("Conversion cancelled"); - } - else - { - Console.WriteLine("Conversion completed!"); - } - - if (tempFiles != null) - { - foreach (var tempFile in tempFiles) - { - File.Delete(tempFile); - } - } - } - } - static int y; static long total; static long lastTicks; diff --git a/PSXPackager/Properties/launchSettings.json b/PSXPackager/Properties/launchSettings.json index 2bd5bdc..018c4f3 100644 --- a/PSXPackager/Properties/launchSettings.json +++ b/PSXPackager/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "PSXPackager": { "commandName": "Project", - "commandLineArgs": "-o C:\\ROMS\\PSX -b C:\\ROMS\\PSX -e .7z" + "commandLineArgs": "-i \"C:\\ROMS\\PSX\\Twisted Metal III.PBP\"" } } } \ No newline at end of file diff --git a/Popstation/CueReader.cs b/Popstation/CueReader.cs index 60cd49f..12621d6 100644 --- a/Popstation/CueReader.cs +++ b/Popstation/CueReader.cs @@ -6,13 +6,13 @@ namespace Popstation { - public class CueReader + public static class CueReader { - Regex fileRegex = new Regex("^FILE \"(.*?)\" (.*?)\\s*$"); - Regex trackRegex = new Regex("^\\s*TRACK (\\d+) (.*?)\\s*$"); - Regex indexRegex = new Regex("^\\s*INDEX (\\d+) (\\d+:\\d+:\\d+)\\s*$"); + static Regex fileRegex = new Regex("^FILE \"(.*?)\" (.*?)\\s*$"); + static Regex trackRegex = new Regex("^\\s*TRACK (\\d+) (.*?)\\s*$"); + static Regex indexRegex = new Regex("^\\s*INDEX (\\d+) (\\d+:\\d+:\\d+)\\s*$"); - public List Read(string file) + public static List Read(string file) { var cueFiles = new List(); CueFile cueFile = null; diff --git a/Popstation/CueWriter.cs b/Popstation/CueWriter.cs new file mode 100644 index 0000000..9c57fcf --- /dev/null +++ b/Popstation/CueWriter.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.IO; + +namespace Popstation +{ + public static class CueWriter + { + public static void Write(string file, IEnumerable cueFiles) + { + using (var stream = new FileStream(file, FileMode.Create)) + { + using (var writer = new StreamWriter(stream)) + { + foreach (var cueFile in cueFiles) + { + writer.WriteLine($"FILE \"{cueFile.FileName}\" {cueFile.FileType}"); + foreach (var cueTrack in cueFile.Tracks) + { + writer.WriteLine($" TRACK {cueTrack.Number:00} {cueTrack.DataType}"); + + foreach (var cueIndex in cueTrack.Indexes) + { + writer.WriteLine($" INDEX {cueIndex.Number:00} {cueIndex.Position.ToString()}"); + + } + } + } + writer.Flush(); + } + } + } + + } +} diff --git a/Popstation/ExtractIsoInfo.cs b/Popstation/ExtractIsoInfo.cs index 40d0995..8e3b6a7 100644 --- a/Popstation/ExtractIsoInfo.cs +++ b/Popstation/ExtractIsoInfo.cs @@ -4,5 +4,6 @@ public class ExtractIsoInfo { public string SourcePbp { get; set; } public string DestinationIso { get; set; } + public bool CreateCuesheet { get; set; } } } diff --git a/Popstation/Helper.cs b/Popstation/Helper.cs new file mode 100644 index 0000000..1237b33 --- /dev/null +++ b/Popstation/Helper.cs @@ -0,0 +1,71 @@ +using System; + +namespace Popstation +{ + public static class CueTrackType + { + public const string Data = "MODE2/2352"; + public const string Audio = "AUDIO"; + } + + + public static class Helper + { + public static string GetDataType(TrackTypeEnum trackType) + { + switch (trackType) + { + case TrackTypeEnum.Data: + return CueTrackType.Data; + case TrackTypeEnum.Audio: + return CueTrackType.Audio; + } + throw new ArgumentOutOfRangeException(); + } + + + public static TrackTypeEnum GetTrackType(string dataType) + { + switch (dataType) + { + case CueTrackType.Data: + return TrackTypeEnum.Data; + case CueTrackType.Audio: + return TrackTypeEnum.Audio; + } + throw new ArgumentOutOfRangeException(); + } + + + public static byte ToBinaryDecimal(int value) + { + var ones = value % 10; + var tens = value / 10; + return (byte)(tens * 0x10 + ones); + } + + public static int FromBinaryDecimal(byte value) + { + var ones = value % 16; + var tens = value / 16; + return (byte)(tens * 10 + ones); + } + + public static IndexPosition PositionFromFrames(long frames) + { + int totalSeconds = (int)(frames / 75); + int minutes = totalSeconds / 60; + int seconds = totalSeconds % 60; + frames = frames % 75; + + var position = new IndexPosition() + { + Minutes = minutes, + Seconds = seconds, + Frames = (int)frames, + }; + + return position; + } + } +} \ No newline at end of file diff --git a/Popstation/IndexPosition.Operators.cs b/Popstation/IndexPosition.Operators.cs new file mode 100644 index 0000000..c6cd096 --- /dev/null +++ b/Popstation/IndexPosition.Operators.cs @@ -0,0 +1,139 @@ +namespace Popstation +{ + public partial class IndexPosition + { + public static IndexPosition operator +(IndexPosition positionA, IndexPosition positionB) + { + var frames = positionA.Frames + positionB.Frames; + var framesCarry = 0; + + if (frames >= 75) + { + framesCarry = frames / 75; + frames = frames % 75; + } + + var secondsCarry = 0; + + var seconds = positionA.Seconds + positionB.Seconds + framesCarry; + + if (seconds >= 60) + { + secondsCarry = seconds / 60; + seconds = seconds % 60; + } + + var minutes = positionA.Minutes + positionB.Minutes + secondsCarry; + + return new IndexPosition() + { + Minutes = minutes, + Seconds = seconds, + Frames = frames, + }; + } + + public static IndexPosition operator -(IndexPosition positionA, IndexPosition positionB) + { + var frames = positionA.Frames - positionB.Frames; + + var secondsBorrow = 0; + + if (frames < 0) + { + secondsBorrow = 1; + frames = 75 + frames; + } + + var minutesBorrow = 0; + + var seconds = positionA.Seconds - positionB.Seconds - secondsBorrow; + + if (seconds < 0) + { + minutesBorrow = 1; + seconds = 60 + seconds; + } + + var minutes = positionA.Minutes - positionB.Minutes - minutesBorrow; + + return new IndexPosition() + { + Minutes = minutes, + Seconds = seconds, + Frames = frames, + }; + } + + public static IndexPosition operator -(IndexPosition positionA, int framesB) + { + var temp = framesB; + var mm = temp / (60 * 75); + temp = temp - mm * (60 * 75); + var ss = temp / 75; + temp = temp - ss * 75; + var ff = temp; + + var frames = positionA.Frames - ff; + + var secondsBorrow = 0; + + if (frames < 0) + { + secondsBorrow = 1; + frames = 75 + frames; + } + + var minutesBorrow = 0; + + var seconds = positionA.Seconds - ss - secondsBorrow; + + if (seconds < 0) + { + minutesBorrow = 1; + seconds = 60 + seconds; + } + + var minutes = positionA.Minutes - mm - minutesBorrow; + + return new IndexPosition() + { + Minutes = minutes, + Seconds = seconds, + Frames = frames, + }; + } + + public static IndexPosition operator +(IndexPosition positionA, int framesB) + { + var frames = positionA.Frames + framesB; + + var framesCarry = 0; + + if (frames >= 75) + { + framesCarry = frames / 75; + frames = frames % 75; + } + + var secondsCarry = 0; + + var seconds = positionA.Seconds + framesCarry; + + if (seconds >= 60) + { + secondsCarry = seconds / 60; + seconds = seconds % 60; + } + + var minutes = positionA.Minutes + secondsCarry; + + return new IndexPosition() + { + Minutes = minutes, + Seconds = seconds, + Frames = frames, + }; + } + } +} diff --git a/Popstation/IndexPosition.cs b/Popstation/IndexPosition.cs index 11cce58..3e97064 100644 --- a/Popstation/IndexPosition.cs +++ b/Popstation/IndexPosition.cs @@ -3,66 +3,12 @@ namespace Popstation { [DebuggerDisplay("{Minutes}:{Seconds}:{Frames}")] - public class IndexPosition + public partial class IndexPosition { public int Minutes { get; set; } public int Seconds { get; set; } public int Frames { get; set; } - public static IndexPosition operator +(IndexPosition positionA, IndexPosition positionB) - { - var frames = positionA.Frames + positionB.Frames; - var framesCarry = 0; - if(frames >= 75) - { - framesCarry = frames / 75; - frames = frames % 75; - } - var secondsCarry = 0; - var seconds = positionA.Seconds + positionB.Seconds + framesCarry; - if (seconds >= 60) - { - secondsCarry = seconds / 60; - seconds = seconds % 60; - } - - var minutes = positionA.Minutes + positionB.Minutes + secondsCarry; - - return new IndexPosition() - { - Minutes = minutes, - Seconds = seconds, - Frames = frames, - }; - } - - public static IndexPosition operator +(IndexPosition positionA, int framesB) - { - var frames = positionA.Frames + framesB; - var framesCarry = 0; - if (frames >= 75) - { - framesCarry = frames / 75; - frames = frames % 75; - } - var secondsCarry = 0; - var seconds = positionA.Seconds + framesCarry; - if (seconds >= 60) - { - secondsCarry = seconds / 60; - seconds = seconds % 60; - } - - var minutes = positionA.Minutes + secondsCarry; - - return new IndexPosition() - { - Minutes = minutes, - Seconds = seconds, - Frames = frames, - }; - } - public override string ToString() { return $"{Minutes:00}:{Seconds:00}:{Frames:00}"; diff --git a/Popstation/INDEX.cs b/Popstation/IsoIndexLite.cs similarity index 83% rename from Popstation/INDEX.cs rename to Popstation/IsoIndexLite.cs index 89f272c..494b964 100644 --- a/Popstation/INDEX.cs +++ b/Popstation/IsoIndexLite.cs @@ -1,7 +1,7 @@ namespace Popstation { // Struct to store an ISO index - public class INDEX + public class IsoIndexLite { public int Offset { get; set; } public int Length { get; set; } diff --git a/Popstation/PbpStream.cs b/Popstation/PbpStream.cs index fc7563c..f122380 100644 --- a/Popstation/PbpStream.cs +++ b/Popstation/PbpStream.cs @@ -4,6 +4,20 @@ namespace Popstation { + public enum TrackTypeEnum + { + Data = 0x41, + Audio = 0x01 + } + + public class TOCEntry + { + public TrackTypeEnum TrackType { get; set; } + public int TrackNo { get; set; } + public int Minutes { get; set; } + public int Seconds { get; set; } + public int Frames { get; set; } + } public class PbpStream : IDisposable { // The maximum possible number of ISO indexes @@ -11,6 +25,9 @@ public class PbpStream : IDisposable //The location of the PSAR offset in the PBP header const int HEADER_PSAR_OFFSET = 0x24; // The location of the ISO indexes in the PSAR + const int PSAR_GAMEID_OFFSET = 0x400; + const int PSAR_TOC_OFFSET = 0x800; + const int PSAR_INDEX_OFFSET = 0x4000; // The location of the ISO data in the PSAR const int PSAR_ISO_OFFSET = 0x100000; @@ -21,11 +38,13 @@ public class PbpStream : IDisposable public uint IsoSize { get; } - public List IsoIndex { get; private set; } + public List IsoIndex { get; private set; } + public List TOC { get; private set; } public PbpStream(string path, FileMode mode, FileAccess access) { stream = new FileStream(path, mode, access); + TOC = ReadTOC(); IsoIndex = ReadIsoIndexes(); if (IsoIndex.Count == 0) throw new Exception("No iso index was found."); @@ -33,7 +52,60 @@ public PbpStream(string path, FileMode mode, FileAccess access) IsoSize = GetIsoSize(); } - private List ReadIsoIndexes() + private List ReadTOC() + { + byte[] buffer = new byte[0xA]; + + var entries = new List(); + + // Read in the offset of the PSAR file + stream.Seek(HEADER_PSAR_OFFSET, SeekOrigin.Begin); + var psar_offset = stream.ReadInteger(); + + if (psar_offset == 0 || stream.Position != HEADER_PSAR_OFFSET + sizeof(int)) + { + throw new Exception("Invalid PSAR offset or corrupted file"); + } + + stream.Seek(psar_offset + PSAR_TOC_OFFSET, SeekOrigin.Begin); + + stream.Read(buffer, 0, 0xA); + if (buffer[2] != 0xA0) throw new Exception("Invalid TOC!"); + int startTrack = Helper.FromBinaryDecimal(buffer[7]); + stream.Read(buffer, 0, 0xA); + if (buffer[2] != 0xA1) throw new Exception("Invalid TOC!"); + int endTrack = Helper.FromBinaryDecimal(buffer[7]); + stream.Read(buffer, 0, 0xA); + if (buffer[2] != 0xA2) throw new Exception("Invalid TOC!"); + int mm = Helper.FromBinaryDecimal(buffer[7]); + int ss = Helper.FromBinaryDecimal(buffer[8]); + int ff = Helper.FromBinaryDecimal(buffer[9]); + //var frames = mm * 60 * 75 + ss * 75 + ff; + //var size = 2352 * frames; + + for(var c = startTrack; c <= endTrack; c++) + { + stream.Read(buffer, 0, 0xA); + var trackNo = Helper.FromBinaryDecimal(buffer[2]); + if (trackNo != c) throw new Exception("Invalid TOC!"); + + var entry = new TOCEntry + { + TrackType = (TrackTypeEnum)buffer[0], + TrackNo = trackNo, + Minutes = Helper.FromBinaryDecimal(buffer[3]), + Seconds = Helper.FromBinaryDecimal(buffer[4]), + Frames = Helper.FromBinaryDecimal(buffer[5]) + }; + + entries.Add(entry); + + } + + return entries; + } + + private List ReadIsoIndexes() { int psar_offset; int this_offset; @@ -42,12 +114,17 @@ private List ReadIsoIndexes() int length; int[] dummy = new int[6]; - var iso_index = new List(); + var iso_index = new List(); // Read in the offset of the PSAR file stream.Seek(HEADER_PSAR_OFFSET, SeekOrigin.Begin); psar_offset = stream.ReadInteger(); + if(psar_offset == 0 || stream.Position != HEADER_PSAR_OFFSET + sizeof(int)) + { + throw new Exception("Invalid PSAR offset or corrupted file"); + } + // Go to the location of the ISO indexes in the PSAR stream.Seek(psar_offset + PSAR_INDEX_OFFSET, SeekOrigin.Begin); @@ -70,13 +147,16 @@ private List ReadIsoIndexes() // Check if this looks like a valid offset if (offset != 0 || length != 0) { - var index = new INDEX(); - // Store the block offset - index.Offset = offset; - // Store the block length - index.Length = length; + var index = new IsoIndexLite + { + Offset = offset, + Length = length + }; + iso_index.Add(index); + count++; + if (count >= MAX_INDEXES) { throw new Exception("Number of indexes exceeds maximum allowed"); diff --git a/Popstation/Popstation.Extract.cs b/Popstation/Popstation.Extract.cs index 9307104..de0d10e 100644 --- a/Popstation/Popstation.Extract.cs +++ b/Popstation/Popstation.Extract.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; namespace Popstation { @@ -11,9 +12,59 @@ public Task Extract(ExtractIsoInfo extractInfo, CancellationToken cancellationTo return Task.Run(() => ExtractIso(extractInfo, cancellationToken)); } - private void ExtractIso(ExtractIsoInfo extractInfo, CancellationToken cancellationToken) + private CueFile ExtractTOC(ExtractIsoInfo extractInfo, PbpStream stream) { + var cueFile = new CueFile() + { + FileName = Path.GetFileName(extractInfo.DestinationIso), + Tracks = new List(), + FileType = "BINARY" + }; + + var audioLeadin = new IndexPosition { Seconds = 2 }; + + foreach (var track in stream.TOC) + { + var position = new IndexPosition + { + Minutes = track.Minutes, + Seconds = track.Seconds, + Frames = track.Frames, + }; + + var indexes = new List(); + + if (track.TrackType == TrackTypeEnum.Audio) + { + indexes.Add(new CueIndex() + { + Number = 0, + Position = position - audioLeadin, + }); + } + + indexes.Add(new CueIndex() + { + Number = 1, + Position = position, + }); + + var cueTrack = new CueTrack() + { + DataType = Helper.GetDataType(track.TrackType), + Indexes = indexes, + Number = track.TrackNo + }; + + + cueFile.Tracks.Add(cueTrack); + } + return cueFile; + } + + private void ExtractIso(ExtractIsoInfo extractInfo, CancellationToken cancellationToken) + { using (var iso_stream = new FileStream(extractInfo.DestinationIso, FileMode.Create, FileAccess.Write)) { using (var stream = new PbpStream(extractInfo.SourcePbp, FileMode.Open, FileAccess.Read)) @@ -48,6 +99,16 @@ private void ExtractIso(ExtractIsoInfo extractInfo, CancellationToken cancellati break; } } + + if (extractInfo.CreateCuesheet) + { + var filename = Path.GetFileNameWithoutExtension(extractInfo.DestinationIso) + ".cue"; + + var path = Path.GetDirectoryName(extractInfo.DestinationIso); + + CueWriter.Write(Path.Combine(path, filename), new CueFile[] { ExtractTOC(extractInfo, stream) }); + } + } OnEvent?.Invoke(PopstationEventEnum.ExtractComplete, null); diff --git a/Popstation/Popstation.cs b/Popstation/Popstation.cs index dd74bf6..dd811d5 100644 --- a/Popstation/Popstation.cs +++ b/Popstation/Popstation.cs @@ -8,32 +8,6 @@ namespace Popstation { - public static class Helper - { - public static byte ToBinaryDecimal(int value) - { - var ones = value % 10; - var tens = value / 10; - return (byte)(tens * 0x10 + ones); - } - - public static IndexPosition PositionFromFrames(long frames) - { - int totalSeconds = (int)(frames / 75); - int minutes = totalSeconds / 60; - int seconds = totalSeconds % 60; - frames = frames % 75; - - var position = new IndexPosition() - { - Minutes = minutes, - Seconds = seconds, - Frames = (int)frames, - }; - - return position; - } - } public partial class Popstation { @@ -693,17 +667,6 @@ private void ConvertMultiIso(ConvertIsoInfo convertInfo, CancellationToken cance } - private byte GetTrackType(string trackType) - { - switch (trackType) - { - case "MODE2/2352": - return 0x41; - case "AUDIO": - return 0x01; - } - throw new ArgumentOutOfRangeException(); - } private void ConvertIso(ConvertIsoInfo convertInfo, CancellationToken cancellationToken) @@ -734,7 +697,7 @@ private void ConvertIso(ConvertIsoInfo convertInfo, CancellationToken cancellati var disc = convertInfo.DiscInfos[0]; - var iso_index = new List(); + var iso_index = new List(); using (var _in = new FileStream(disc.SourceIso, FileMode.Open, FileAccess.Read)) { @@ -758,9 +721,7 @@ private void ConvertIso(ConvertIsoInfo convertInfo, CancellationToken cancellati if (!string.IsNullOrEmpty(disc.SourceToc)) { - - var reader = new CueReader(); - var cueFiles = reader.Read(disc.SourceToc); + var cueFiles = CueReader.Read(disc.SourceToc); var tracks = cueFiles.SelectMany(cf => cf.Tracks).ToList(); convertInfo.TocData = new byte[0xA * (tracks.Count + 3)]; @@ -772,7 +733,7 @@ private void ConvertIso(ConvertIsoInfo convertInfo, CancellationToken cancellati var ctr = 0; - trackBuffer[0] = GetTrackType(tracks.First().DataType); + trackBuffer[0] = (byte)Helper.GetTrackType(tracks.First().DataType); trackBuffer[1] = 0x00; trackBuffer[2] = 0xA0; trackBuffer[3] = 0x00; @@ -786,7 +747,7 @@ private void ConvertIso(ConvertIsoInfo convertInfo, CancellationToken cancellati Array.Copy(trackBuffer, 0, convertInfo.TocData, ctr, 0xA); ctr += 0xA; - trackBuffer[0] = GetTrackType(tracks.Last().DataType); + trackBuffer[0] = (byte)Helper.GetTrackType(tracks.Last().DataType); trackBuffer[2] = 0xA1; trackBuffer[7] = Helper.ToBinaryDecimal(tracks.Last().Number); trackBuffer[8] = 0x00; @@ -805,7 +766,7 @@ private void ConvertIso(ConvertIsoInfo convertInfo, CancellationToken cancellati foreach (var track in tracks) { - trackBuffer[0] = GetTrackType(track.DataType); + trackBuffer[0] = (byte)Helper.GetTrackType(track.DataType); trackBuffer[1] = 0x00; trackBuffer[2] = Helper.ToBinaryDecimal(track.Number); var pos = track.Indexes.First(idx => idx.Number == 1).Position; diff --git a/UnitTestProject/IndexPositionWithIntMathTests.cs b/UnitTestProject/IndexPositionWithIntMathTests.cs new file mode 100644 index 0000000..da4a18c --- /dev/null +++ b/UnitTestProject/IndexPositionWithIntMathTests.cs @@ -0,0 +1,147 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Popstation; + +namespace UnitTestProject +{ + [TestClass] + public class IndexPositionWithIntMathTests + { + [TestMethod] + public void AddIndexPositionWithInt() + { + var indexA = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexC = indexA + 5; + + Assert.AreEqual(10, indexC.Frames); + Assert.AreEqual(5, indexC.Seconds); + Assert.AreEqual(5, indexC.Minutes); + } + + [TestMethod] + public void AddIndexPositionWithIntCarryFrames() + { + var indexA = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexC = indexA + 70; + + Assert.AreEqual(0, indexC.Frames); + Assert.AreEqual(6, indexC.Seconds); + Assert.AreEqual(5, indexC.Minutes); + } + + [TestMethod] + public void AddIndexPositionWithIntCarrySeconds() + { + var indexA = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexC = indexA + 4125; // 55 seconds + + Assert.AreEqual(5, indexC.Frames); + Assert.AreEqual(0, indexC.Seconds); + Assert.AreEqual(6, indexC.Minutes); + } + + [TestMethod] + public void AddIndexPositionWithIntCarryFramesAndSeconds() + { + var indexA = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexC = indexA + 4120; // 70 frames + 54 seconds + + Assert.AreEqual(0, indexC.Frames); + Assert.AreEqual(0, indexC.Seconds); + Assert.AreEqual(6, indexC.Minutes); + } + + [TestMethod] + public void SubtractIndexPositionWithFrame() + { + var indexA = new IndexPosition() + { + Frames = 10, + Seconds = 10, + Minutes = 10, + }; + + var indexC = indexA - 5; + + Assert.AreEqual(5, indexC.Frames); + Assert.AreEqual(10, indexC.Seconds); + Assert.AreEqual(10, indexC.Minutes); + } + + [TestMethod] + public void SubtractIndexPositionWithFrameBorrowSeconds() + { + var indexA = new IndexPosition() + { + Frames = 10, + Seconds = 10, + Minutes = 10, + }; + + var indexC = indexA - 11; + + Assert.AreEqual(74, indexC.Frames); + Assert.AreEqual(9, indexC.Seconds); + Assert.AreEqual(10, indexC.Minutes); + } + + [TestMethod] + public void SubtractIndexPositionWithFrameBorrowMinutes() + { + var indexA = new IndexPosition() + { + Frames = 10, + Seconds = 10, + Minutes = 10, + }; + + var indexC = indexA - 825; // 11 seconds + + Assert.AreEqual(10, indexC.Frames); + Assert.AreEqual(59, indexC.Seconds); + Assert.AreEqual(9, indexC.Minutes); + } + + [TestMethod] + public void SubtractIndexPositionWithFrameBorrowSecondsAndMinutes() + { + var indexA = new IndexPosition() + { + Frames = 10, + Seconds = 10, + Minutes = 10, + }; + + var indexC = indexA - 761; // 10 seconds + 11 frames + + Assert.AreEqual(74, indexC.Frames); + Assert.AreEqual(59, indexC.Seconds); + Assert.AreEqual(9, indexC.Minutes); + } + + } +} diff --git a/UnitTestProject/IndexPositionWithPositionMathTests.cs b/UnitTestProject/IndexPositionWithPositionMathTests.cs new file mode 100644 index 0000000..a9ae0e3 --- /dev/null +++ b/UnitTestProject/IndexPositionWithPositionMathTests.cs @@ -0,0 +1,203 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Popstation; + +namespace UnitTestProject +{ + [TestClass] + public class IndexPositionWithPositionMathTests + { + [TestMethod] + public void AddIndexPositions() + { + var indexA = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexB = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexC = indexA + indexB; + + Assert.AreEqual(10, indexC.Frames); + Assert.AreEqual(10, indexC.Seconds); + Assert.AreEqual(10, indexC.Minutes); + } + + [TestMethod] + public void AddIndexPositionsCarryFrames() + { + var indexA = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexB = new IndexPosition() + { + Frames = 70, + Seconds = 5, + Minutes = 5, + }; + + var indexC = indexA + indexB; + + Assert.AreEqual(0, indexC.Frames); + Assert.AreEqual(11, indexC.Seconds); + Assert.AreEqual(10, indexC.Minutes); + } + + [TestMethod] + public void AddIndexPositionsCarrySeconds() + { + var indexA = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexB = new IndexPosition() + { + Frames = 5, + Seconds = 55, + Minutes = 5, + }; + + var indexC = indexA + indexB; + + Assert.AreEqual(10, indexC.Frames); + Assert.AreEqual(0, indexC.Seconds); + Assert.AreEqual(11, indexC.Minutes); + } + + [TestMethod] + public void AddIndexPositionsCarryFramesAndSeconds() + { + var indexA = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexB = new IndexPosition() + { + Frames = 70, + Seconds = 54, + Minutes = 5, + }; + + var indexC = indexA + indexB; + + Assert.AreEqual(0, indexC.Frames); + Assert.AreEqual(0, indexC.Seconds); + Assert.AreEqual(11, indexC.Minutes); + } + + [TestMethod] + public void SubtractIndexPositions() + { + var indexA = new IndexPosition() + { + Frames = 10, + Seconds = 10, + Minutes = 10, + }; + + var indexB = new IndexPosition() + { + Frames = 5, + Seconds = 5, + Minutes = 5, + }; + + var indexC = indexA - indexB; + + Assert.AreEqual(5, indexC.Frames); + Assert.AreEqual(5, indexC.Seconds); + Assert.AreEqual(5, indexC.Minutes); + } + + [TestMethod] + public void SubtractIndexPositionsBorrowSeconds() + { + var indexA = new IndexPosition() + { + Frames = 10, + Seconds = 10, + Minutes = 10, + }; + + var indexB = new IndexPosition() + { + Frames = 11, + Seconds = 5, + Minutes = 5, + }; + + var indexC = indexA - indexB; + + Assert.AreEqual(74, indexC.Frames); + Assert.AreEqual(4, indexC.Seconds); + Assert.AreEqual(5, indexC.Minutes); + } + + [TestMethod] + public void SubtractIndexPositionsBorrowMinutes() + { + var indexA = new IndexPosition() + { + Frames = 10, + Seconds = 10, + Minutes = 10, + }; + + var indexB = new IndexPosition() + { + Frames = 10, + Seconds = 11, + Minutes = 5, + }; + + var indexC = indexA - indexB; + + Assert.AreEqual(0, indexC.Frames); + Assert.AreEqual(59, indexC.Seconds); + Assert.AreEqual(4, indexC.Minutes); + } + + [TestMethod] + public void SubtractIndexPositionsBorrowSecondsAndMinutes() + { + var indexA = new IndexPosition() + { + Frames = 10, + Seconds = 10, + Minutes = 10, + }; + + var indexB = new IndexPosition() + { + Frames = 11, + Seconds = 10, + Minutes = 5, + }; + + var indexC = indexA - indexB; + + Assert.AreEqual(74, indexC.Frames); + Assert.AreEqual(59, indexC.Seconds); + Assert.AreEqual(4, indexC.Minutes); + } + + } +} diff --git a/UnitTestProject/Properties/AssemblyInfo.cs b/UnitTestProject/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a18b1bd --- /dev/null +++ b/UnitTestProject/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("UnitTestProject")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("UnitTestProject")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("477347da-4397-413b-b3ab-f13200d31543")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/UnitTestProject/UnitTestProject.csproj b/UnitTestProject/UnitTestProject.csproj new file mode 100644 index 0000000..09e4d75 --- /dev/null +++ b/UnitTestProject/UnitTestProject.csproj @@ -0,0 +1,75 @@ + + + + + + Debug + AnyCPU + {477347DA-4397-413B-B3AB-F13200D31543} + Library + Properties + UnitTestProject + UnitTestProject + v4.7.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + {28756239-6D08-4ED2-80FF-7F389858C782} + Popstation + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/UnitTestProject/packages.config b/UnitTestProject/packages.config new file mode 100644 index 0000000..2f7c5a1 --- /dev/null +++ b/UnitTestProject/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file