diff --git a/SabreTools.Helper/Dats/DatFile.cs b/SabreTools.Helper/Dats/DatFile.cs index 6477dbea..a5ddad70 100644 --- a/SabreTools.Helper/Dats/DatFile.cs +++ b/SabreTools.Helper/Dats/DatFile.cs @@ -1,36 +1,11 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Web; -using System.Xml; using SabreTools.Helper.Data; -using SabreTools.Helper.Skippers; -using SabreTools.Helper.Tools; - -#if MONO -using System.IO; -#else -using Alphaleonis.Win32.Filesystem; - -using FileAccess = System.IO.FileAccess; -using FileMode = System.IO.FileMode; -using FileStream = System.IO.FileStream; -using IOException = System.IO.IOException; -using MemoryStream = System.IO.MemoryStream; -using SearchOption = System.IO.SearchOption; -using StreamReader = System.IO.StreamReader; -using StreamWriter = System.IO.StreamWriter; -#endif -using NaturalSort; -using SharpCompress.Common; namespace SabreTools.Helper.Dats { - public class DatFile : ICloneable + public partial class DatFile : ICloneable { #region Private instance variables @@ -298,416 +273,6 @@ namespace SabreTools.Helper.Dats #region Instance Methods - #region Bucketing [MODULAR DONE] - - /// - /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by CRC - /// - /// True if roms should be deduped, false otherwise - /// Logger object for file and console output - /// True if the number of hashes counted is to be output (default), false otherwise - public void BucketByCRC(bool mergeroms, Logger logger, bool output = true) - { - // If we already have the right sorting, trust it - if (_sortedBy == SortedBy.CRC) - { - return; - } - - // Set the sorted type - _sortedBy = SortedBy.CRC; - - SortedDictionary> sortable = new SortedDictionary>(); - long count = 0; - - // If we have a null dict or an empty one, output a new dictionary - if (Files == null || Files.Count == 0) - { - Files = sortable; - } - - logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by CRC"); - - // Process each all of the roms - List keys = Files.Keys.ToList(); - foreach (string key in keys) - { - List roms = Files[key]; - - // If we're merging the roms, do so - if (mergeroms) - { - roms = DatItem.Merge(roms, logger); - } - - // Now add each of the roms to their respective games - foreach (DatItem rom in roms) - { - count++; - string newkey = (rom.Type == ItemType.Rom ? ((Rom)rom).CRC : Constants.CRCZero); - if (sortable.ContainsKey(newkey)) - { - sortable[newkey].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - sortable.Add(newkey, temp); - } - } - } - - // Now go through and sort all of the lists - keys = sortable.Keys.ToList(); - foreach (string key in keys) - { - List sortedlist = sortable[key]; - DatItem.Sort(ref sortedlist, false); - sortable[key] = sortedlist; - } - - // Output the count if told to - if (output) - { - logger.User("A total of " + count + " file hashes will be written out to file"); - } - - // Now assign the dictionary back - Files = sortable; - } - - /// - /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by Game - /// - /// True if roms should be deduped, false otherwise - /// True if games should only be compared on game and file name, false if system and source are counted - /// Logger object for file and console output - /// True if the number of hashes counted is to be output (default), false otherwise - /// True if the game should be lowercased (default), false otherwise - public void BucketByGame(bool mergeroms, bool norename, Logger logger, bool output = true, bool lower = true) - { - // If we already have the right sorting, trust it - if (_sortedBy == SortedBy.Game) - { - return; - } - - // Set the sorted type - _sortedBy = SortedBy.Game; - - SortedDictionary> sortable = new SortedDictionary>(); - long count = 0; - - // If we have a null dict or an empty one, output a new dictionary - if (Files == null || Files.Count == 0) - { - Files = sortable; - } - - logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by game"); - - // Process each all of the roms - List keys = Files.Keys.ToList(); - foreach (string key in keys) - { - List roms = Files[key]; - - // If we're merging the roms, do so - if (mergeroms) - { - roms = DatItem.Merge(roms, logger); - } - - // Now add each of the roms to their respective games - foreach (DatItem rom in roms) - { - count++; - string newkey = (norename ? "" - : rom.SystemID.ToString().PadLeft(10, '0') - + "-" - + rom.SourceID.ToString().PadLeft(10, '0') + "-") - + (String.IsNullOrEmpty(rom.Machine.Name) - ? "Default" - : rom.Machine.Name); - if (lower) - { - newkey = newkey.ToLowerInvariant(); - } - newkey = HttpUtility.HtmlEncode(newkey); - if (sortable.ContainsKey(newkey)) - { - sortable[newkey].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - sortable.Add(newkey, temp); - } - } - } - - // Now go through and sort all of the lists - keys = sortable.Keys.ToList(); - foreach (string key in keys) - { - List sortedlist = sortable[key]; - DatItem.Sort(ref sortedlist, norename); - sortable[key] = sortedlist; - } - - // Output the count if told to - if (output) - { - logger.User("A total of " + count + " file hashes will be written out to file"); - } - - // Now assign the dictionary back - Files = sortable; - } - - /// - /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by MD5 - /// - /// True if roms should be deduped, false otherwise - /// Logger object for file and console output - /// True if the number of hashes counted is to be output (default), false otherwise - public void BucketByMD5(bool mergeroms, Logger logger, bool output = true) - { - // If we already have the right sorting, trust it - if (_sortedBy == SortedBy.MD5) - { - return; - } - - // Set the sorted type - _sortedBy = SortedBy.MD5; - - SortedDictionary> sortable = new SortedDictionary>(); - long count = 0; - - // If we have a null dict or an empty one, output a new dictionary - if (Files == null || Files.Count == 0) - { - Files = sortable; - } - - logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by MD5"); - - // Process each all of the roms - List keys = Files.Keys.ToList(); - foreach (string key in keys) - { - List roms = Files[key]; - - // If we're merging the roms, do so - if (mergeroms) - { - roms = DatItem.Merge(roms, logger); - } - - // Now add each of the roms to their respective games - foreach (DatItem rom in roms) - { - count++; - string newkey = (rom.Type == ItemType.Rom - ? ((Rom)rom).MD5 - : (rom.Type == ItemType.Disk - ? ((Disk)rom).MD5 - : Constants.MD5Zero)); - if (sortable.ContainsKey(newkey)) - { - sortable[newkey].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - sortable.Add(newkey, temp); - } - } - } - - // Now go through and sort all of the lists - keys = sortable.Keys.ToList(); - foreach (string key in keys) - { - List sortedlist = sortable[key]; - DatItem.Sort(ref sortedlist, false); - sortable[key] = sortedlist; - } - - // Output the count if told to - if (output) - { - logger.User("A total of " + count + " file hashes will be written out to file"); - } - - // Now assign the dictionary back - Files = sortable; - } - - /// - /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by SHA1 - /// - /// True if roms should be deduped, false otherwise - /// Logger object for file and console output - /// True if the number of hashes counted is to be output (default), false otherwise - public void BucketBySHA1(bool mergeroms, Logger logger, bool output = true) - { - // If we already have the right sorting, trust it - if (_sortedBy == SortedBy.SHA1) - { - return; - } - - // Set the sorted type - _sortedBy = SortedBy.SHA1; - - SortedDictionary> sortable = new SortedDictionary>(); - long count = 0; - - // If we have a null dict or an empty one, output a new dictionary - if (Files == null || Files.Count == 0) - { - Files = sortable; - } - - logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by SHA-1"); - - // Process each all of the roms - List keys = Files.Keys.ToList(); - foreach (string key in keys) - { - List roms = Files[key]; - - // If we're merging the roms, do so - if (mergeroms) - { - roms = DatItem.Merge(roms, logger); - } - - // Now add each of the roms to their respective games - foreach (DatItem rom in roms) - { - count++; - string newkey = (rom.Type == ItemType.Rom - ? ((Rom)rom).SHA1 - : (rom.Type == ItemType.Disk - ? ((Disk)rom).SHA1 - : Constants.MD5Zero)); - if (sortable.ContainsKey(newkey)) - { - sortable[newkey].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - sortable.Add(newkey, temp); - } - } - } - - // Now go through and sort all of the lists - keys = sortable.Keys.ToList(); - foreach (string key in keys) - { - List sortedlist = sortable[key]; - DatItem.Sort(ref sortedlist, false); - sortable[key] = sortedlist; - } - - // Output the count if told to - if (output) - { - logger.User("A total of " + count + " file hashes will be written out to file"); - } - - // Now assign the dictionary back - Files = sortable; - } - - /// - /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by Size - /// - /// True if roms should be deduped, false otherwise - /// Logger object for file and console output - /// True if the number of hashes counted is to be output (default), false otherwise - public void BucketBySize(bool mergeroms, Logger logger, bool output = true) - { - // If we already have the right sorting, trust it - if (_sortedBy == SortedBy.Size) - { - return; - } - - // Set the sorted type - _sortedBy = SortedBy.Size; - - SortedDictionary> sortable = new SortedDictionary>(); - long count = 0; - - // If we have a null dict or an empty one, output a new dictionary - if (Files == null || Files.Count == 0) - { - Files = sortable; - } - - logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by size"); - - // Process each all of the roms - List keys = Files.Keys.ToList(); - foreach (string key in keys) - { - List roms = Files[key]; - - // If we're merging the roms, do so - if (mergeroms) - { - roms = DatItem.Merge(roms, logger); - } - - // Now add each of the roms to their respective games - foreach (DatItem rom in roms) - { - count++; - string newkey = (rom.Type == ItemType.Rom ? ((Rom)rom).Size.ToString() : "-1"); - if (sortable.ContainsKey(newkey)) - { - sortable[newkey].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - sortable.Add(newkey, temp); - } - } - } - - // Now go through and sort all of the lists - keys = sortable.Keys.ToList(); - foreach (string key in keys) - { - List sortedlist = sortable[key]; - DatItem.Sort(ref sortedlist, false); - sortable[key] = sortedlist; - } - - // Output the count if told to - if (output) - { - logger.User("A total of " + count + " file hashes will be written out to file"); - } - - // Now assign the dictionary back - Files = sortable; - } - - #endregion - #region Cloning Methods [MODULAR DONE] public object Clone() @@ -796,6679 +361,6 @@ namespace SabreTools.Helper.Dats #endregion - #region Converting and Updating [MODULAR DONE] - - /// - /// Determine if input files should be merged, diffed, or processed invidually - /// - /// Names of the input files and/or folders - /// Optional param for output directory - /// True if input files should be merged into a single file, false otherwise - /// Non-zero flag for diffing mode, zero otherwise - /// True if the cascade-diffed files should overwrite their inputs, false otherwise - /// True if the first cascaded diff file should be skipped on output, false otherwise - /// True if the date should not be appended to the default name, false otherwise [OBSOLETE] - /// True to clean the game names to WoD standard, false otherwise (default) - /// True to allow SL DATs to have game names used instead of descriptions, false otherwise (default) - /// Filter object to be passed to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Integer representing the maximum amount of parallelization to be used - /// Logging object for console and file output - public void DetermineUpdateType(List inputPaths, string outDir, bool merge, DiffMode diff, bool inplace, bool skip, - bool bare, bool clean, bool softlist, Filter filter, bool trim, bool single, string root, int maxDegreeOfParallelism, Logger logger) - { - // If we're in merging or diffing mode, use the full list of inputs - if (merge || diff != 0) - { - // Make sure there are no folders in inputs - List newInputFileNames = FileTools.GetOnlyFilesFromInputs(inputPaths, maxDegreeOfParallelism, logger, appendparent: true); - - // If we're in inverse cascade, reverse the list - if ((diff & DiffMode.ReverseCascade) != 0) - { - newInputFileNames.Reverse(); - } - - // Create a dictionary of all ROMs from the input DATs - List datHeaders = PopulateUserData(newInputFileNames, inplace, clean, softlist, - outDir, filter, trim, single, root, maxDegreeOfParallelism, logger); - - // Modify the Dictionary if necessary and output the results - if (diff != 0 && diff < DiffMode.Cascade) - { - DiffNoCascade(diff, outDir, newInputFileNames, logger); - } - // If we're in cascade and diff, output only cascaded diffs - else if (diff != 0 && diff >= DiffMode.Cascade) - { - DiffCascade(outDir, inplace, newInputFileNames, datHeaders, skip, logger); - } - // Output all entries with user-defined merge - else - { - MergeNoDiff(outDir, newInputFileNames, datHeaders, logger); - } - } - // Otherwise, loop through all of the inputs individually - else - { - Update(inputPaths, outDir, clean, softlist, filter, trim, single, root, maxDegreeOfParallelism, logger); - } - return; - } - - /// - /// Populate the user DatData object from the input files - /// - /// Filter object to be passed to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Integer representing the maximum amount of parallelization to be used - /// Logging object for console and file output - /// List of DatData objects representing headers - private List PopulateUserData(List inputs, bool inplace, bool clean, bool softlist, string outDir, - Filter filter, bool trim, bool single, string root, int maxDegreeOfParallelism, Logger logger) - { - DatFile[] datHeaders = new DatFile[inputs.Count]; - DateTime start = DateTime.Now; - logger.User("Processing individual DATs"); - - /// BEGIN - Parallel.For(0, - inputs.Count, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - i => - { - string input = inputs[i]; - logger.User("Adding DAT: " + input.Split('¬')[0]); - datHeaders[i] = new DatFile - { - DatFormat = (DatFormat != 0 ? DatFormat : 0), - Files = new SortedDictionary>(), - MergeRoms = MergeRoms, - }; - - datHeaders[i].Parse(input.Split('¬')[0], i, 0, filter, trim, single, root, logger, true, clean, softlist); - }); - - logger.User("Processing complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - logger.User("Populating internal DAT"); - Files = new SortedDictionary>(); - for (int i = 0; i < inputs.Count; i++) - { - List keys = datHeaders[i].Files.Keys.ToList(); - foreach (string key in keys) - { - if (Files.ContainsKey(key)) - { - Files[key].AddRange(datHeaders[i].Files[key]); - } - else - { - Files.Add(key, datHeaders[i].Files[key]); - } - datHeaders[i].Files.Remove(key); - } - datHeaders[i].Files = null; - } - /// END - - logger.User("Processing and populating complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - return datHeaders.ToList(); - } - - /// - /// Output non-cascading diffs - /// - /// Non-zero flag for diffing mode, zero otherwise - /// Output directory to write the DATs to - /// List of inputs to write out from - /// Logging object for console and file output - public void DiffNoCascade(DiffMode diff, string outDir, List inputs, Logger logger) - { - DateTime start = DateTime.Now; - logger.User("Initializing all output DATs"); - - // Default vars for use - string post = ""; - DatFile outerDiffData = new DatFile(); - DatFile dupeData = new DatFile(); - - // Don't have External dupes - if ((diff & DiffMode.NoDupes) != 0) - { - post = " (No Duplicates)"; - outerDiffData = (DatFile)CloneHeader(); - outerDiffData.FileName += post; - outerDiffData.Name += post; - outerDiffData.Description += post; - outerDiffData.Files = new SortedDictionary>(); - } - - // Have External dupes - if ((diff & DiffMode.Dupes) != 0) - { - post = " (Duplicates)"; - dupeData = (DatFile)CloneHeader(); - dupeData.FileName += post; - dupeData.Name += post; - dupeData.Description += post; - dupeData.Files = new SortedDictionary>(); - } - - // Create a list of DatData objects representing individual output files - List outDats = new List(); - - // Loop through each of the inputs and get or create a new DatData object - if ((diff & DiffMode.Individuals) != 0) - { - DatFile[] outDatsArray = new DatFile[inputs.Count]; - - Parallel.For(0, inputs.Count, j => - { - string innerpost = " (" + Path.GetFileNameWithoutExtension(inputs[j].Split('¬')[0]) + " Only)"; - DatFile diffData = (DatFile)CloneHeader(); - diffData.FileName += innerpost; - diffData.Name += innerpost; - diffData.Description += innerpost; - diffData.Files = new SortedDictionary>(); - outDatsArray[j] = diffData; - }); - - outDats = outDatsArray.ToList(); - } - logger.User("Initializing complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - // Now, loop through the dictionary and populate the correct DATs - start = DateTime.Now; - logger.User("Populating all output DATs"); - List keys = Files.Keys.ToList(); - foreach (string key in keys) - { - List roms = DatItem.Merge(Files[key], logger); - - if (roms != null && roms.Count > 0) - { - foreach (DatItem rom in roms) - { - // No duplicates - if ((diff & DiffMode.NoDupes) != 0 || (diff & DiffMode.Individuals) != 0) - { - if ((rom.Dupe & DupeType.Internal) != 0) - { - // Individual DATs that are output - if ((diff & DiffMode.Individuals) != 0) - { - if (outDats[rom.SystemID].Files.ContainsKey(key)) - { - outDats[rom.SystemID].Files[key].Add(rom); - } - else - { - List tl = new List(); - tl.Add(rom); - outDats[rom.SystemID].Files.Add(key, tl); - } - } - - // Merged no-duplicates DAT - if ((diff & DiffMode.NoDupes) != 0) - { - DatItem newrom = rom; - newrom.Machine.Name += " (" + Path.GetFileNameWithoutExtension(inputs[newrom.SystemID].Split('¬')[0]) + ")"; - - if (outerDiffData.Files.ContainsKey(key)) - { - outerDiffData.Files[key].Add(newrom); - } - else - { - List tl = new List(); - tl.Add(rom); - outerDiffData.Files.Add(key, tl); - } - } - } - } - - // Duplicates only - if ((diff & DiffMode.Dupes) != 0) - { - if ((rom.Dupe & DupeType.External) != 0) - { - DatItem newrom = rom; - newrom.Machine.Name += " (" + Path.GetFileNameWithoutExtension(inputs[newrom.SystemID].Split('¬')[0]) + ")"; - - if (dupeData.Files.ContainsKey(key)) - { - dupeData.Files[key].Add(newrom); - } - else - { - List tl = new List(); - tl.Add(rom); - dupeData.Files.Add(key, tl); - } - } - } - } - } - } - logger.User("Populating complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - // Finally, loop through and output each of the DATs - start = DateTime.Now; - logger.User("Outputting all created DATs"); - - // Output the difflist (a-b)+(b-a) diff - if ((diff & DiffMode.NoDupes) != 0) - { - outerDiffData.WriteToFile(outDir, logger); - } - - // Output the (ab) diff - if ((diff & DiffMode.Dupes) != 0) - { - dupeData.WriteToFile(outDir, logger); - } - - // Output the individual (a-b) DATs - if ((diff & DiffMode.Individuals) != 0) - { - for (int j = 0; j < inputs.Count; j++) - { - // If we have an output directory set, replace the path - string[] split = inputs[j].Split('¬'); - string path = outDir + (split[0] == split[1] - ? Path.GetFileName(split[0]) - : (Path.GetDirectoryName(split[0]).Remove(0, split[1].Length))); - - // If we have more than 0 roms, output - if (outDats[j].Files.Count > 0) - { - outDats[j].WriteToFile(path, logger); - } - } - } - logger.User("Outputting complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - } - - /// - /// Output cascading diffs - /// - /// Output directory to write the DATs to - /// True if cascaded diffs are outputted in-place, false otherwise - /// List of inputs to write out from - /// Dat headers used optionally - /// True if the first cascaded diff file should be skipped on output, false otherwise - /// Logging object for console and file output - public void DiffCascade(string outDir, bool inplace, List inputs, List datHeaders, bool skip, Logger logger) - { - string post = ""; - - // Create a list of DatData objects representing output files - List outDats = new List(); - - // Loop through each of the inputs and get or create a new DatData object - DateTime start = DateTime.Now; - logger.User("Initializing all output DATs"); - - DatFile[] outDatsArray = new DatFile[inputs.Count]; - - Parallel.For(0, inputs.Count, j => - { - string innerpost = " (" + Path.GetFileNameWithoutExtension(inputs[j].Split('¬')[0]) + " Only)"; - DatFile diffData; - - // If we're in inplace mode, take the appropriate DatData object already stored - if (inplace || !String.IsNullOrEmpty(outDir)) - { - diffData = datHeaders[j]; - } - else - { - diffData = (DatFile)CloneHeader(); - diffData.FileName += post; - diffData.Name += post; - diffData.Description += post; - } - diffData.Files = new SortedDictionary>(); - - outDatsArray[j] = diffData; - }); - - outDats = outDatsArray.ToList(); - logger.User("Initializing complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - // Now, loop through the dictionary and populate the correct DATs - start = DateTime.Now; - logger.User("Populating all output DATs"); - List keys = Files.Keys.ToList(); - - foreach (string key in keys) - { - List roms = DatItem.Merge(Files[key], logger); - - if (roms != null && roms.Count > 0) - { - foreach (DatItem rom in roms) - { - // There's odd cases where there are items with System ID < 0. Skip them for now - if (rom.SystemID < 0) - { - logger.Warning("Item found with a <0 SystemID: " + rom.Name); - continue; - } - - if (outDats[rom.SystemID].Files.ContainsKey(key)) - { - outDats[rom.SystemID].Files[key].Add(rom); - } - else - { - List tl = new List(); - tl.Add(rom); - outDats[rom.SystemID].Files.Add(key, tl); - } - } - } - } - logger.User("Populating complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - // Finally, loop through and output each of the DATs - start = DateTime.Now; - logger.User("Outputting all created DATs"); - for (int j = (skip ? 1 : 0); j < inputs.Count; j++) - { - // If we have an output directory set, replace the path - string path = ""; - if (inplace) - { - path = Path.GetDirectoryName(inputs[j].Split('¬')[0]); - } - else if (!String.IsNullOrEmpty(outDir)) - { - string[] split = inputs[j].Split('¬'); - path = outDir + (split[0] == split[1] - ? Path.GetFileName(split[0]) - : (Path.GetDirectoryName(split[0]).Remove(0, split[1].Length))); ; - } - - // If we have more than 0 roms, output - if (outDats[j].Files.Count > 0) - { - outDats[j].WriteToFile(path, logger); - } - } - logger.User("Outputting complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - } - - /// - /// Output user defined merge - /// - /// Output directory to write the DATs to - /// List of inputs to write out from - /// Dat headers used optionally - /// Logging object for console and file output - public void MergeNoDiff(string outDir, List inputs, List datHeaders, Logger logger) - { - // If we're in SuperDAT mode, prefix all games with their respective DATs - if (Type == "SuperDAT") - { - List keys = Files.Keys.ToList(); - foreach (string key in keys) - { - List newroms = new List(); - foreach (DatItem rom in Files[key]) - { - DatItem newrom = rom; - string filename = inputs[newrom.SystemID].Split('¬')[0]; - string rootpath = inputs[newrom.SystemID].Split('¬')[1]; - - rootpath += (rootpath == "" ? "" : Path.DirectorySeparatorChar.ToString()); - filename = filename.Remove(0, rootpath.Length); - newrom.Machine.Name = Path.GetDirectoryName(filename) + Path.DirectorySeparatorChar - + Path.GetFileNameWithoutExtension(filename) + Path.DirectorySeparatorChar - + newrom.Machine.Name; - newroms.Add(newrom); - } - Files[key] = newroms; - } - } - - // Output a DAT only if there are roms - if (Files.Count != 0) - { - WriteToFile(outDir, logger); - } - } - - /// - /// Convert, update, and filter a DAT file or set of files using a base - /// - /// Names of the input files and/or folders - /// Optional param for output directory - /// True if input files should be merged into a single file, false otherwise - /// Non-zero flag for diffing mode, zero otherwise - /// True if the cascade-diffed files should overwrite their inputs, false otherwise - /// True if the first cascaded diff file should be skipped on output, false otherwise - /// True if the date should not be appended to the default name, false otherwise [OBSOLETE] - /// True to clean the game names to WoD standard, false otherwise (default) - /// True to allow SL DATs to have game names used instead of descriptions, false otherwise (default) - /// Filter object to be passed to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Integer representing the maximum amount of parallelization to be used - /// Logging object for console and file output - public void Update(List inputFileNames, string outDir, bool clean, bool softlist, Filter filter, - bool trim, bool single, string root, int maxDegreeOfParallelism, Logger logger) - { - // Sort the input filenames - inputFileNames.Sort(new NaturalComparer()); - - Parallel.ForEach(inputFileNames, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - inputFileName => - { - // Clean the input string - if (inputFileName != "") - { - inputFileName = Path.GetFullPath(inputFileName); - } - - if (File.Exists(inputFileName)) - { - DatFile innerDatdata = (DatFile)CloneHeader(); - logger.User("Processing \"" + Path.GetFileName(inputFileName) + "\""); - innerDatdata.Parse(inputFileName, 0, 0, filter, trim, single, - root, logger, true, clean, softlist, - keepext: ((innerDatdata.DatFormat & DatFormat.TSV) != 0 || (innerDatdata.DatFormat & DatFormat.CSV) != 0)); - - // If we have roms, output them - if (innerDatdata.Files.Count != 0) - { - innerDatdata.WriteToFile((outDir == "" ? Path.GetDirectoryName(inputFileName) : outDir), logger, overwrite: (outDir != "")); - } - } - else if (Directory.Exists(inputFileName)) - { - inputFileName = Path.GetFullPath(inputFileName) + Path.DirectorySeparatorChar; - - Parallel.ForEach(Directory.EnumerateFiles(inputFileName, "*", SearchOption.AllDirectories), - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - file => - { - logger.User("Processing \"" + Path.GetFullPath(file).Remove(0, inputFileName.Length) + "\""); - DatFile innerDatdata = (DatFile)Clone(); - innerDatdata.Files = null; - innerDatdata.Parse(file, 0, 0, filter, - trim, single, root, logger, true, clean, softlist, - keepext: ((innerDatdata.DatFormat & DatFormat.TSV) != 0 || (innerDatdata.DatFormat & DatFormat.CSV) != 0)); - - // If we have roms, output them - if (innerDatdata.Files != null && innerDatdata.Files.Count != 0) - { - innerDatdata.WriteToFile((outDir == "" ? Path.GetDirectoryName(file) : outDir + Path.GetDirectoryName(file).Remove(0, inputFileName.Length - 1)), logger, overwrite: (outDir != "")); - } - }); - } - else - { - logger.Error("I'm sorry but " + inputFileName + " doesn't exist!"); - } - }); - } - - #endregion - - #region Parsing [MODULAR DONE, FOR NOW] - - /// - /// Parse a DAT and return all found games and roms within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// The DatData object representing found roms to this point - /// Logger object for console and/or file output - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if SL XML names should be kept, false otherwise (default) - /// True if original extension should be kept, false otherwise (default) - public void Parse(string filename, int sysid, int srcid, Logger logger, bool keep = false, bool clean = false, bool softlist = false, bool keepext = false) - { - Parse(filename, sysid, srcid, new Filter(), false, false, "", logger, keep, clean, softlist, keepext); - } - - /// - /// Parse a DAT and return all found games and roms within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if SL XML names should be kept, false otherwise (default) - /// True if original extension should be kept, false otherwise (default) - public void Parse( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom filtering - Filter filter, - - // Rom renaming - bool trim, - bool single, - string root, - - // Miscellaneous - Logger logger, - bool keep = false, - bool clean = false, - bool softlist = false, - bool keepext = false) - { - // Check the file extension first as a safeguard - string ext = Path.GetExtension(filename).ToLowerInvariant(); - if (ext.StartsWith(".")) - { - ext = ext.Substring(1); - } - if (ext != "dat" && ext != "md5" && ext != "sfv" && ext != "sha1" && ext != "txt" && ext != "xml") - { - return; - } - - // If the output filename isn't set already, get the internal filename - FileName = (String.IsNullOrEmpty(FileName) ? (keepext ? Path.GetFileName(filename) : Path.GetFileNameWithoutExtension(filename)) : FileName); - - // If the output type isn't set already, get the internal output type - DatFormat = (DatFormat == 0 ? FileTools.GetDatFormat(filename, logger) : DatFormat); - - // Make sure there's a dictionary to read to - if (Files == null) - { - Files = new SortedDictionary>(); - } - - // Now parse the correct type of DAT - switch (FileTools.GetDatFormat(filename, logger)) - { - case DatFormat.AttractMode: - ParseAttractMode(filename, sysid, srcid, filter, trim, single, root, logger, keep, clean); - break; - case DatFormat.ClrMamePro: - case DatFormat.DOSCenter: - ParseCMP(filename, sysid, srcid, filter, trim, single, root, logger, keep, clean); - break; - case DatFormat.Logiqx: - case DatFormat.OfflineList: - case DatFormat.SabreDat: - case DatFormat.SoftwareList: - ParseGenericXML(filename, sysid, srcid, filter, trim, single, root, logger, keep, clean, softlist); - break; - case DatFormat.RedumpMD5: - ParseRedumpMD5(filename, sysid, srcid, filter, trim, single, root, logger, clean); - break; - case DatFormat.RedumpSFV: - ParseRedumpSFV(filename, sysid, srcid, filter, trim, single, root, logger, clean); - break; - case DatFormat.RedumpSHA1: - ParseRedumpSHA1(filename, sysid, srcid, filter, trim, single, root, logger, clean); - break; - case DatFormat.RomCenter: - ParseRC(filename, sysid, srcid, filter, trim, single, root, logger, clean); - break; - default: - return; - } - } - - /// - /// Parse an AttractMode DAT and return all found games within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - private void ParseAttractMode( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom filtering - Filter filter, - - // Rom renaming - bool trim, - bool single, - string root, - - // Miscellaneous - Logger logger, - bool keep, - bool clean) - { - // Open a file reader - Encoding enc = Style.GetEncoding(filename); - StreamReader sr = new StreamReader(File.OpenRead(filename), enc); - - sr.ReadLine(); // Skip the first line since it's the header - while (!sr.EndOfStream) - { - string line = sr.ReadLine(); - - /* - The gameinfo order is as follows - 0 - game name - 1 - game description - 2 - emulator name (filename) - 3 - cloneof - 4 - year - 5 - manufacturer - 6 - category - 7 - players - 8 - rotation - 9 - control - 10 - status - 11 - displaycount - 12 - displaytype - 13 - alt romname - 14 - alt title - 15 - extra - 16 - buttons - */ - - string[] gameinfo = line.Split(';'); - - Rom rom = new Rom - { - Name = "-", - Size = Constants.SizeZero, - CRC = Constants.CRCZero, - MD5 = Constants.MD5Zero, - SHA1 = Constants.SHA1Zero, - ItemStatus = ItemStatus.None, - - Machine = new Machine - { - Name = gameinfo[0], - Description = gameinfo[1], - CloneOf = gameinfo[3], - Year = gameinfo[4], - Manufacturer = gameinfo[5], - Comment = gameinfo[15], - } - }; - - // Now process and add the rom - string key = ""; - ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); - } - - sr.Dispose(); - } - - /// - /// Parse a ClrMamePro DAT and return all found games and roms within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - private void ParseCMP( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom filtering - Filter filter, - - // Rom renaming - bool trim, - bool single, - string root, - - // Miscellaneous - Logger logger, - bool keep, - bool clean) - { - // Open a file reader - Encoding enc = Style.GetEncoding(filename); - StreamReader sr = new StreamReader(File.OpenRead(filename), enc); - - bool block = false, superdat = false; - string blockname = "", tempgamename = "", gamedesc = "", cloneof = "", - romof = "", sampleof = "", year = "", manufacturer = ""; - while (!sr.EndOfStream) - { - string line = sr.ReadLine(); - - // Comments in CMP DATs start with a # - if (line.Trim().StartsWith("#")) - { - continue; - } - - // If the line is the header or a game - if (Regex.IsMatch(line, Constants.HeaderPatternCMP)) - { - GroupCollection gc = Regex.Match(line, Constants.HeaderPatternCMP).Groups; - - if (gc[1].Value == "clrmamepro" || gc[1].Value == "romvault" || gc[1].Value.ToLowerInvariant() == "doscenter") - { - blockname = "header"; - } - - block = true; - } - - // If the line is a rom-like item and we're in a block - else if ((line.Trim().StartsWith("rom (") - || line.Trim().StartsWith("disk (") - || line.Trim().StartsWith("file (") - || (line.Trim().StartsWith("sample") && !line.Trim().StartsWith("sampleof")) - ) && block) - { - ItemType temptype = ItemType.Rom; - if (line.Trim().StartsWith("rom (")) - { - temptype = ItemType.Rom; - } - else if (line.Trim().StartsWith("disk (")) - { - temptype = ItemType.Disk; - } - else if (line.Trim().StartsWith("file (")) - { - temptype = ItemType.Rom; - } - else if (line.Trim().StartsWith("sample")) - { - temptype = ItemType.Sample; - } - - // Create the proper DatItem based on the type - DatItem item; - switch (temptype) - { - case ItemType.Archive: - item = new Archive(); - break; - case ItemType.BiosSet: - item = new BiosSet(); - break; - case ItemType.Disk: - item = new Disk(); - break; - case ItemType.Release: - item = new Release(); - break; - case ItemType.Sample: - item = new Sample(); - break; - case ItemType.Rom: - default: - item = new Rom(); - break; - } - - // Then populate it with information - item.Machine = new Machine - { - Name = tempgamename, - Description = gamedesc, - CloneOf = cloneof, - RomOf = romof, - SampleOf = sampleof, - Manufacturer = manufacturer, - Year = year, - }; - - item.SystemID = sysid; - item.SourceID = srcid; - - // Get the blank key to write to - string key = ""; - - // If we have a sample, treat it special - if (temptype == ItemType.Sample) - { - line = line.Trim().Remove(0, 6).Trim().Replace("\"", ""); // Remove "sample" from the input string - item.Name = line; - - // Now process and add the sample - key = ""; - ParseAddHelper(item, filter, trim, single, root, clean, logger, out key); - - continue; - } - - // Get the line split by spaces and quotes - string[] gc = Style.SplitLineAsCMP(line); - - // Special cases for DOSCenter DATs only because of how the lines are arranged - if (line.Trim().StartsWith("file (")) - { - // Loop over the specifics - for (int i = 0; i < gc.Length; i++) - { - // Names are not quoted, for some stupid reason - if (gc[i] == "name") - { - // Advance to the first part of the name - i++; - item.Name = gc[i]; - - // Advance to the next item, adding until we find "size" - i++; - while (i < gc.Length && gc[i] != "size" && gc[i] != "date" && gc[i] != "crc") - { - item.Name += " " + gc[i]; - i++; - } - } - - // Get the size from the next part - else if (gc[i] == "size") - { - i++; - long tempsize = -1; - if (!Int64.TryParse(gc[i], out tempsize)) - { - tempsize = 0; - } - ((Rom)item).Size = tempsize; - i++; - } - - // Get the date from the next part - else if (gc[i] == "date") - { - i++; - ((Rom)item).Date = gc[i].Replace("\"", "") + " " + gc[i + 1].Replace("\"", ""); - i += 3; - } - - // Get the CRC from the next part - else if (gc[i] == "crc") - { - i++; - ((Rom)item).CRC = gc[i].Replace("\"", "").ToLowerInvariant(); - } - } - - // Now process and add the rom - key = ""; - ParseAddHelper(item, filter, trim, single, root, clean, logger, out key); - continue; - } - - // Loop over all attributes normally and add them if possible - for (int i = 0; i < gc.Length; i++) - { - // Look at the current item and use it if possible - string quoteless = gc[i].Replace("\"", ""); - switch (quoteless) - { - //If the item is empty, we automatically skip it because it's a fluke - case "": - continue; - - // Special cases for standalone item statuses - case "baddump": - if (item.Type == ItemType.Rom) - { - ((Rom)item).ItemStatus = ItemStatus.BadDump; - } - else if (item.Type == ItemType.Disk) - { - ((Disk)item).ItemStatus = ItemStatus.BadDump; - } - break; - case "good": - if (item.Type == ItemType.Rom) - { - ((Rom)item).ItemStatus = ItemStatus.Good; - } - else if (item.Type == ItemType.Disk) - { - ((Disk)item).ItemStatus = ItemStatus.Good; - } - break; - case "nodump": - if (item.Type == ItemType.Rom) - { - ((Rom)item).ItemStatus = ItemStatus.Nodump; - } - else if (item.Type == ItemType.Disk) - { - ((Disk)item).ItemStatus = ItemStatus.Nodump; - } - break; - case "verified": - if (item.Type == ItemType.Rom) - { - ((Rom)item).ItemStatus = ItemStatus.Verified; - } - else if (item.Type == ItemType.Disk) - { - ((Disk)item).ItemStatus = ItemStatus.Verified; - } - break; - - // Regular attributes - case "name": - i++; - quoteless = gc[i].Replace("\"", ""); - item.Name = quoteless; - break; - case "size": - if (item.Type == ItemType.Rom) - { - i++; - quoteless = gc[i].Replace("\"", ""); - long size = -1; - if (Int64.TryParse(quoteless, out size)) - { - ((Rom)item).Size = size; - } - } - - break; - case "crc": - if (item.Type == ItemType.Rom) - { - i++; - quoteless = gc[i].Replace("\"", ""); - ((Rom)item).CRC = quoteless.ToLowerInvariant(); - } - break; - case "md5": - if (item.Type == ItemType.Rom) - { - i++; - quoteless = gc[i].Replace("\"", ""); - ((Rom)item).MD5 = quoteless.ToLowerInvariant(); - } - else if (item.Type == ItemType.Disk) - { - i++; - quoteless = gc[i].Replace("\"", ""); - ((Disk)item).MD5 = quoteless.ToLowerInvariant(); - } - break; - case "sha1": - if (item.Type == ItemType.Rom) - { - i++; - quoteless = gc[i].Replace("\"", ""); - ((Rom)item).SHA1 = quoteless.ToLowerInvariant(); - } - else if (item.Type == ItemType.Disk) - { - i++; - quoteless = gc[i].Replace("\"", ""); - ((Disk)item).SHA1 = quoteless.ToLowerInvariant(); - } - break; - case "status": - case "flags": - i++; - quoteless = gc[i].Replace("\"", ""); - if (quoteless.ToLowerInvariant() == "good") - { - if (item.Type == ItemType.Rom) - { - ((Rom)item).ItemStatus = ItemStatus.Good; - } - else if (item.Type == ItemType.Disk) - { - ((Disk)item).ItemStatus = ItemStatus.Good; - } - } - else if (quoteless.ToLowerInvariant() == "baddump") - { - if (item.Type == ItemType.Rom) - { - ((Rom)item).ItemStatus = ItemStatus.BadDump; - } - else if (item.Type == ItemType.Disk) - { - ((Disk)item).ItemStatus = ItemStatus.BadDump; - } - } - else if (quoteless.ToLowerInvariant() == "nodump") - { - if (item.Type == ItemType.Rom) - { - ((Rom)item).ItemStatus = ItemStatus.Nodump; - } - else if (item.Type == ItemType.Disk) - { - ((Disk)item).ItemStatus = ItemStatus.Nodump; - } - } - else if (quoteless.ToLowerInvariant() == "verified") - { - if (item.Type == ItemType.Rom) - { - ((Rom)item).ItemStatus = ItemStatus.Verified; - } - else if (item.Type == ItemType.Disk) - { - ((Disk)item).ItemStatus = ItemStatus.Verified; - } - } - break; - case "date": - if (item.Type == ItemType.Rom) - { - i++; - quoteless = gc[i].Replace("\"", "") + " " + gc[i + 1].Replace("\"", ""); - ((Rom)item).Date = quoteless; - } - i++; - break; - } - } - - // Now process and add the rom - key = ""; - ParseAddHelper(item, filter, trim, single, root, clean, logger, out key); - } - - // If the line is anything but a rom or disk and we're in a block - else if (Regex.IsMatch(line, Constants.ItemPatternCMP) && block) - { - GroupCollection gc = Regex.Match(line, Constants.ItemPatternCMP).Groups; - - if (blockname != "header") - { - string itemval = gc[2].Value.Replace("\"", ""); - switch (gc[1].Value) - { - case "name": - tempgamename = (itemval.ToLowerInvariant().EndsWith(".zip") ? itemval.Remove(itemval.Length - 4) : itemval); - break; - case "description": - gamedesc = itemval; - break; - case "romof": - romof = itemval; - break; - case "cloneof": - cloneof = itemval; - break; - case "year": - year = itemval; - break; - case "manufacturer": - manufacturer = itemval; - break; - case "sampleof": - sampleof = itemval; - break; - } - } - else - { - string itemval = gc[2].Value.Replace("\"", ""); - - if (line.Trim().StartsWith("Name:")) - { - Name = (String.IsNullOrEmpty(Name) ? line.Substring(6) : Name); - superdat = superdat || itemval.Contains(" - SuperDAT"); - if (keep && superdat) - { - Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); - } - continue; - } - - switch (gc[1].Value) - { - case "name": - case "Name:": - Name = (String.IsNullOrEmpty(Name) ? itemval : Name); - superdat = superdat || itemval.Contains(" - SuperDAT"); - if (keep && superdat) - { - Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); - } - break; - case "description": - case "Description:": - Description = (String.IsNullOrEmpty(Description) ? itemval : Description); - break; - case "rootdir": - RootDir = (String.IsNullOrEmpty(RootDir) ? itemval : RootDir); - break; - case "category": - Category = (String.IsNullOrEmpty(Category) ? itemval : Category); - break; - case "version": - case "Version:": - Version = (String.IsNullOrEmpty(Version) ? itemval : Version); - break; - case "date": - case "Date:": - Date = (String.IsNullOrEmpty(Date) ? itemval : Date); - break; - case "author": - case "Author:": - Author = (String.IsNullOrEmpty(Author) ? itemval : Author); - break; - case "email": - Email = (String.IsNullOrEmpty(Email) ? itemval : Email); - break; - case "homepage": - case "Homepage:": - Homepage = (String.IsNullOrEmpty(Homepage) ? itemval : Homepage); - break; - case "url": - Url = (String.IsNullOrEmpty(Url) ? itemval : Url); - break; - case "comment": - case "Comment:": - Comment = (String.IsNullOrEmpty(Comment) ? itemval : Comment); - break; - case "header": - Header = (String.IsNullOrEmpty(Header) ? itemval : Header); - break; - case "type": - Type = (String.IsNullOrEmpty(Type) ? itemval : Type); - superdat = superdat || itemval.Contains("SuperDAT"); - break; - case "forcemerging": - switch (itemval) - { - case "none": - ForceMerging = ForceMerging.None; - break; - case "split": - ForceMerging = ForceMerging.Split; - break; - case "full": - ForceMerging = ForceMerging.Full; - break; - } - break; - case "forcezipping": - switch (itemval) - { - case "yes": - ForcePacking = ForcePacking.Zip; - break; - case "no": - ForcePacking = ForcePacking.Unzip; - break; - } - break; - case "forcepacking": - switch (itemval) - { - case "zip": - ForcePacking = ForcePacking.Zip; - break; - case "unzip": - ForcePacking = ForcePacking.Unzip; - break; - } - break; - } - } - } - - // If we find an end bracket that's not associated with anything else, the block is done - else if (Regex.IsMatch(line, Constants.EndPatternCMP) && block) - { - block = false; - blockname = ""; tempgamename = ""; gamedesc = ""; cloneof = ""; - romof = ""; sampleof = ""; year = ""; manufacturer = ""; - } - } - - sr.Dispose(); - } - - /// - /// Parse an XML DAT (Logiqx, OfflineList, SabreDAT, and Software List) and return all found games and roms within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if SL XML names should be kept, false otherwise (default) - private void ParseGenericXML( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom filtering - Filter filter, - - // Rom renaming - bool trim, - bool single, - string root, - - // Miscellaneous - Logger logger, - bool keep, - bool clean, - bool softlist) - { - // Prepare all internal variables - XmlReader subreader, headreader, flagreader; - bool superdat = false, empty = true; - string key = "", date = ""; - long size = -1; - ItemStatus its = ItemStatus.None; - List parent = new List(); - - Encoding enc = Style.GetEncoding(filename); - XmlReader xtr = FileTools.GetXmlTextReader(filename, logger); - - // If we got a null reader, just return - if (xtr == null) - { - return; - } - - // Otherwise, read the file to the end - try - { - xtr.MoveToContent(); - while (!xtr.EOF) - { - // If we're ending a folder or game, take care of possibly empty games and removing from the parent - if (xtr.NodeType == XmlNodeType.EndElement && (xtr.Name == "directory" || xtr.Name == "dir")) - { - // If we didn't find any items in the folder, make sure to add the blank rom - if (empty) - { - string tempgame = String.Join("\\", parent); - Rom rom = new Rom("null", tempgame); - - // Now process and add the rom - ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); - } - - // Regardless, end the current folder - int parentcount = parent.Count; - if (parentcount == 0) - { - logger.Verbose("Empty parent: " + String.Join("\\", parent)); - empty = true; - } - - // If we have an end folder element, remove one item from the parent, if possible - if (parentcount > 0) - { - parent.RemoveAt(parent.Count - 1); - if (keep && parentcount > 1) - { - Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); - superdat = true; - } - } - } - - // We only want elements - if (xtr.NodeType != XmlNodeType.Element) - { - xtr.Read(); - continue; - } - - switch (xtr.Name) - { - // Handle MAME listxml since they're halfway between a SL and a Logiqx XML - case "mame": - if (xtr.GetAttribute("build") != null) - { - Name = (String.IsNullOrEmpty(Name) ? xtr.GetAttribute("build") : Name); - Description = (String.IsNullOrEmpty(Description) ? Name : Name); - } - xtr.Read(); - break; - // New software lists have this behavior - case "softwarelist": - if (xtr.GetAttribute("name") != null) - { - Name = (String.IsNullOrEmpty(Name) ? xtr.GetAttribute("name") : Name); - } - if (xtr.GetAttribute("description") != null) - { - Description = (String.IsNullOrEmpty(Description) ? xtr.GetAttribute("description") : Description); - } - if (xtr.GetAttribute("forcemerging") != null) - { - switch (xtr.GetAttribute("forcemerging")) - { - case "split": - ForceMerging = ForceMerging.Split; - break; - case "none": - ForceMerging = ForceMerging.None; - break; - case "full": - ForceMerging = ForceMerging.Full; - break; - } - } - if (xtr.GetAttribute("forceitemStatus") != null) - { - switch (xtr.GetAttribute("forceitemStatus")) - { - case "obsolete": - ForceNodump = ForceNodump.Obsolete; - break; - case "required": - ForceNodump = ForceNodump.Required; - break; - case "ignore": - ForceNodump = ForceNodump.Ignore; - break; - } - } - if (xtr.GetAttribute("forcepacking") != null) - { - switch (xtr.GetAttribute("forcepacking")) - { - case "zip": - ForcePacking = ForcePacking.Zip; - break; - case "unzip": - ForcePacking = ForcePacking.Unzip; - break; - } - } - xtr.Read(); - break; - // Handle M1 DATs since they're 99% the same as a SL DAT - case "m1": - Name = (String.IsNullOrEmpty(Name) ? "M1" : Name); - Description = (String.IsNullOrEmpty(Description) ? "M1" : Description); - if (xtr.GetAttribute("version") != null) - { - Version = (String.IsNullOrEmpty(Version) ? xtr.GetAttribute("version") : Version); - } - xtr.Read(); - break; - // OfflineList has a different header format - case "configuration": - headreader = xtr.ReadSubtree(); - - // If there's no subtree to the header, skip it - if (headreader == null) - { - xtr.Skip(); - continue; - } - - // Otherwise, read what we can from the header - while (!headreader.EOF) - { - // We only want elements - if (headreader.NodeType != XmlNodeType.Element || headreader.Name == "configuration") - { - headreader.Read(); - continue; - } - - // Get all header items (ONLY OVERWRITE IF THERE'S NO DATA) - string content = ""; - switch (headreader.Name.ToLowerInvariant()) - { - case "datname": - content = headreader.ReadElementContentAsString(); ; - Name = (String.IsNullOrEmpty(Name) ? content : Name); - superdat = superdat || content.Contains(" - SuperDAT"); - if (keep && superdat) - { - Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); - } - break; - case "datversionurl": - content = headreader.ReadElementContentAsString(); ; - Url = (String.IsNullOrEmpty(Name) ? content : Url); - break; - default: - headreader.Read(); - break; - } - } - - break; - // We want to process the entire subtree of the header - case "header": - headreader = xtr.ReadSubtree(); - - // If there's no subtree to the header, skip it - if (headreader == null) - { - xtr.Skip(); - continue; - } - - // Otherwise, read what we can from the header - while (!headreader.EOF) - { - // We only want elements - if (headreader.NodeType != XmlNodeType.Element || headreader.Name == "header") - { - headreader.Read(); - continue; - } - - // Get all header items (ONLY OVERWRITE IF THERE'S NO DATA) - string content = ""; - switch (headreader.Name) - { - case "name": - content = headreader.ReadElementContentAsString(); ; - Name = (String.IsNullOrEmpty(Name) ? content : Name); - superdat = superdat || content.Contains(" - SuperDAT"); - if (keep && superdat) - { - Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); - } - break; - case "description": - content = headreader.ReadElementContentAsString(); - Description = (String.IsNullOrEmpty(Description) ? content : Description); - break; - case "rootdir": - content = headreader.ReadElementContentAsString(); - RootDir = (String.IsNullOrEmpty(RootDir) ? content : RootDir); - break; - case "category": - content = headreader.ReadElementContentAsString(); - Category = (String.IsNullOrEmpty(Category) ? content : Category); - break; - case "version": - content = headreader.ReadElementContentAsString(); - Version = (String.IsNullOrEmpty(Version) ? content : Version); - break; - case "date": - content = headreader.ReadElementContentAsString(); - Date = (String.IsNullOrEmpty(Date) ? content.Replace(".", "/") : Date); - break; - case "author": - content = headreader.ReadElementContentAsString(); - Author = (String.IsNullOrEmpty(Author) ? content : Author); - - // Special cases for SabreDAT - Email = (String.IsNullOrEmpty(Email) && !String.IsNullOrEmpty(headreader.GetAttribute("email")) ? - headreader.GetAttribute("email") : Email); - Homepage = (String.IsNullOrEmpty(Homepage) && !String.IsNullOrEmpty(headreader.GetAttribute("homepage")) ? - headreader.GetAttribute("homepage") : Email); - Url = (String.IsNullOrEmpty(Url) && !String.IsNullOrEmpty(headreader.GetAttribute("url")) ? - headreader.GetAttribute("url") : Email); - break; - case "email": - content = headreader.ReadElementContentAsString(); - Email = (String.IsNullOrEmpty(Email) ? content : Email); - break; - case "homepage": - content = headreader.ReadElementContentAsString(); - Homepage = (String.IsNullOrEmpty(Homepage) ? content : Homepage); - break; - case "url": - content = headreader.ReadElementContentAsString(); - Url = (String.IsNullOrEmpty(Url) ? content : Url); - break; - case "comment": - content = headreader.ReadElementContentAsString(); - Comment = (String.IsNullOrEmpty(Comment) ? content : Comment); - break; - case "type": - content = headreader.ReadElementContentAsString(); - Type = (String.IsNullOrEmpty(Type) ? content : Type); - superdat = superdat || content.Contains("SuperDAT"); - break; - case "clrmamepro": - case "romcenter": - if (headreader.GetAttribute("header") != null) - { - Header = (String.IsNullOrEmpty(Header) ? headreader.GetAttribute("header") : Header); - } - if (headreader.GetAttribute("plugin") != null) - { - Header = (String.IsNullOrEmpty(Header) ? headreader.GetAttribute("plugin") : Header); - } - if (headreader.GetAttribute("forcemerging") != null) - { - switch (headreader.GetAttribute("forcemerging")) - { - case "split": - ForceMerging = ForceMerging.Split; - break; - case "none": - ForceMerging = ForceMerging.None; - break; - case "full": - ForceMerging = ForceMerging.Full; - break; - } - } - if (headreader.GetAttribute("forceitemStatus") != null) - { - switch (headreader.GetAttribute("forceitemStatus")) - { - case "obsolete": - ForceNodump = ForceNodump.Obsolete; - break; - case "required": - ForceNodump = ForceNodump.Required; - break; - case "ignore": - ForceNodump = ForceNodump.Ignore; - break; - } - } - if (headreader.GetAttribute("forcepacking") != null) - { - switch (headreader.GetAttribute("forcepacking")) - { - case "zip": - ForcePacking = ForcePacking.Zip; - break; - case "unzip": - ForcePacking = ForcePacking.Unzip; - break; - } - } - headreader.Read(); - break; - case "flags": - flagreader = xtr.ReadSubtree(); - - // If we somehow have a null flag section, skip it - if (flagreader == null) - { - xtr.Skip(); - continue; - } - - while (!flagreader.EOF) - { - // We only want elements - if (flagreader.NodeType != XmlNodeType.Element || flagreader.Name == "flags") - { - flagreader.Read(); - continue; - } - - switch (flagreader.Name) - { - case "flag": - if (flagreader.GetAttribute("name") != null && flagreader.GetAttribute("value") != null) - { - content = flagreader.GetAttribute("value"); - switch (flagreader.GetAttribute("name")) - { - case "type": - Type = (String.IsNullOrEmpty(Type) ? content : Type); - superdat = superdat || content.Contains("SuperDAT"); - break; - case "forcemerging": - switch (content) - { - case "split": - ForceMerging = ForceMerging.Split; - break; - case "none": - ForceMerging = ForceMerging.None; - break; - case "full": - ForceMerging = ForceMerging.Full; - break; - } - break; - case "forceitemStatus": - switch (content) - { - case "obsolete": - ForceNodump = ForceNodump.Obsolete; - break; - case "required": - ForceNodump = ForceNodump.Required; - break; - case "ignore": - ForceNodump = ForceNodump.Ignore; - break; - } - break; - case "forcepacking": - switch (content) - { - case "zip": - ForcePacking = ForcePacking.Zip; - break; - case "unzip": - ForcePacking = ForcePacking.Unzip; - break; - } - break; - } - } - flagreader.Read(); - break; - default: - flagreader.Read(); - break; - } - } - headreader.Skip(); - break; - default: - headreader.Read(); - break; - } - } - - // Skip the header node now that we've processed it - xtr.Skip(); - break; - case "machine": - case "game": - case "software": - string temptype = xtr.Name, publisher = "", partname = "", partinterface = "", areaname = ""; - bool? supported = null; - long? areasize = null; - List> infos = new List>(); - List> features = new List>(); - - // We want to process the entire subtree of the game - subreader = xtr.ReadSubtree(); - - // Safeguard for interesting case of "software" without anything except roms - bool software = false; - - // If we have an empty machine, skip it - if (subreader == null) - { - xtr.Skip(); - continue; - } - - // Otherwise, add what is possible - subreader.MoveToContent(); - - // Create a new machine - Machine machine = new Machine - { - Name = xtr.GetAttribute("name"), - Description = xtr.GetAttribute("name"), - RomOf = xtr.GetAttribute("romof") ?? "", - CloneOf = xtr.GetAttribute("cloneof") ?? "", - SampleOf = xtr.GetAttribute("sampleof") ?? "", - }; - - if (subreader.GetAttribute("supported") != null) - { - switch (subreader.GetAttribute("supported")) - { - case "no": - supported = false; - break; - case "yes": - supported = true; - break; - } - } - - if (superdat && !keep) - { - string tempout = Regex.Match(machine.Name, @".*?\\(.*)").Groups[1].Value; - if (tempout != "") - { - machine.Name = tempout; - } - } - // Get the name of the game from the parent - else if (superdat && keep && parent.Count > 0) - { - machine.Name = String.Join("\\", parent) + "\\" + machine.Name; - } - - // Special offline list parts - string ext = ""; - string releaseNumber = ""; - - while (software || !subreader.EOF) - { - software = false; - - // We only want elements - if (subreader.NodeType != XmlNodeType.Element) - { - if (subreader.NodeType == XmlNodeType.EndElement && subreader.Name == "part") - { - partname = ""; - partinterface = ""; - features = new List>(); - } - if (subreader.NodeType == XmlNodeType.EndElement && (subreader.Name == "dataarea" || subreader.Name == "diskarea")) - { - areaname = ""; - areasize = null; - } - - subreader.Read(); - continue; - } - - // Get the roms from the machine - switch (subreader.Name) - { - // For OfflineList only - case "title": - machine.Name = subreader.ReadElementContentAsString(); - break; - case "releaseNumber": - releaseNumber = subreader.ReadElementContentAsString(); - break; - case "romSize": - if (!Int64.TryParse(subreader.ReadElementContentAsString(), out size)) - { - size = -1; - } - break; - case "romCRC": - empty = false; - - ext = (subreader.GetAttribute("extension") != null ? subreader.GetAttribute("extension") : ""); - - DatItem olrom = new Rom - { - Name = releaseNumber + " - " + machine.Name + ext, - Size = size, - CRC = subreader.ReadElementContentAsString(), - ItemStatus = ItemStatus.None, - - Machine = machine, - }; - - // Now process and add the rom - ParseAddHelper(olrom, filter, trim, single, root, clean, logger, out key); - break; - - // For Software List only - case "publisher": - publisher = subreader.ReadElementContentAsString(); - break; - case "info": - infos.Add(Tuple.Create(subreader.GetAttribute("name"), subreader.GetAttribute("value"))); - subreader.Read(); - break; - case "part": - partname = subreader.GetAttribute("name"); - partinterface = subreader.GetAttribute("interface"); - subreader.Read(); - break; - case "feature": - features.Add(Tuple.Create(subreader.GetAttribute("name"), subreader.GetAttribute("value"))); - subreader.Read(); - break; - case "dataarea": - case "diskarea": - areaname = subreader.GetAttribute("name"); - long areasizetemp = -1; - if (Int64.TryParse(subreader.GetAttribute("size"), out areasizetemp)) - { - areasize = areasizetemp; - } - subreader.Read(); - break; - - // For Logiqx, SabreDAT, and Software List - case "description": - machine.Description = subreader.ReadElementContentAsString(); - if (!softlist && temptype == "software") - { - machine.Name = machine.Description.Replace('/', '_').Replace("\"", "''"); - } - break; - case "year": - machine.Year = subreader.ReadElementContentAsString(); - break; - case "manufacturer": - machine.Manufacturer = subreader.ReadElementContentAsString(); - break; - case "release": - empty = false; - - bool? defaultrel = null; - if (subreader.GetAttribute("default") != null) - { - if (subreader.GetAttribute("default") == "yes") - { - defaultrel = true; - } - else if (subreader.GetAttribute("default") == "no") - { - defaultrel = false; - } - } - - DatItem relrom = new Release - { - Name = subreader.GetAttribute("name"), - Region = subreader.GetAttribute("region"), - Language = subreader.GetAttribute("language"), - Date = date, - Default = defaultrel, - - Machine = machine, - - Supported = supported, - Publisher = publisher, - Infos = infos, - PartName = partname, - PartInterface = partinterface, - Features = features, - AreaName = areaname, - AreaSize = areasize, - }; - - // Now process and add the rom - ParseAddHelper(relrom, filter, trim, single, root, clean, logger, out key); - - subreader.Read(); - break; - case "biosset": - empty = false; - - bool? defaultbios = null; - if (subreader.GetAttribute("default") != null) - { - if (subreader.GetAttribute("default") == "yes") - { - defaultbios = true; - } - else if (subreader.GetAttribute("default") == "no") - { - defaultbios = false; - } - } - - DatItem biosrom = new BiosSet - { - Name = subreader.GetAttribute("name"), - Description = subreader.GetAttribute("description"), - Default = defaultbios, - - Machine = machine, - - Supported = supported, - Publisher = publisher, - Infos = infos, - PartName = partname, - PartInterface = partinterface, - Features = features, - AreaName = areaname, - AreaSize = areasize, - - SystemID = sysid, - System = filename, - SourceID = srcid, - }; - - // Now process and add the rom - ParseAddHelper(biosrom, filter, trim, single, root, clean, logger, out key); - - subreader.Read(); - break; - case "archive": - empty = false; - - DatItem archiverom = new Archive - { - Name = subreader.GetAttribute("name"), - - Machine = machine, - - Supported = supported, - Publisher = publisher, - Infos = infos, - PartName = partname, - PartInterface = partinterface, - Features = features, - AreaName = areaname, - AreaSize = areasize, - - SystemID = sysid, - System = filename, - SourceID = srcid, - }; - - // Now process and add the rom - ParseAddHelper(archiverom, filter, trim, single, root, clean, logger, out key); - - subreader.Read(); - break; - case "sample": - empty = false; - - DatItem samplerom = new Sample - { - Name = subreader.GetAttribute("name"), - - Machine = machine, - - Supported = supported, - Publisher = publisher, - Infos = infos, - PartName = partname, - PartInterface = partinterface, - Features = features, - AreaName = areaname, - AreaSize = areasize, - - SystemID = sysid, - System = filename, - SourceID = srcid, - }; - - // Now process and add the rom - ParseAddHelper(samplerom, filter, trim, single, root, clean, logger, out key); - - subreader.Read(); - break; - case "rom": - case "disk": - empty = false; - - // If the rom has a status, flag it - its = ItemStatus.None; - if (subreader.GetAttribute("flags") == "good" || subreader.GetAttribute("status") == "good") - { - its = ItemStatus.Good; - } - if (subreader.GetAttribute("flags") == "baddump" || subreader.GetAttribute("status") == "baddump") - { - logger.Verbose("Bad dump detected: " + - (subreader.GetAttribute("name") != null && subreader.GetAttribute("name") != "" ? "\"" + xtr.GetAttribute("name") + "\"" : "ROM NAME NOT FOUND")); - its = ItemStatus.BadDump; - } - if (subreader.GetAttribute("flags") == "nodump" || subreader.GetAttribute("status") == "nodump") - { - logger.Verbose("Nodump detected: " + - (subreader.GetAttribute("name") != null && subreader.GetAttribute("name") != "" ? "\"" + xtr.GetAttribute("name") + "\"" : "ROM NAME NOT FOUND")); - its = ItemStatus.Nodump; - } - if (subreader.GetAttribute("flags") == "verified" || subreader.GetAttribute("status") == "verified") - { - its = ItemStatus.Verified; - } - - // If the rom has a Date attached, read it in and then sanitize it - date = ""; - if (subreader.GetAttribute("date") != null) - { - DateTime dateTime = DateTime.Now; - if (DateTime.TryParse(subreader.GetAttribute("date"), out dateTime)) - { - date = dateTime.ToString(); - } - else - { - date = subreader.GetAttribute("date"); - } - } - - // Take care of hex-sized files - size = -1; - if (subreader.GetAttribute("size") != null && subreader.GetAttribute("size").Contains("0x")) - { - size = Convert.ToInt64(subreader.GetAttribute("size"), 16); - } - else if (subreader.GetAttribute("size") != null) - { - Int64.TryParse(subreader.GetAttribute("size"), out size); - } - - // If the rom is continue or ignore, add the size to the previous rom - if (subreader.GetAttribute("loadflag") == "continue" || subreader.GetAttribute("loadflag") == "ignore") - { - int index = Files[key].Count() - 1; - DatItem lastrom = Files[key][index]; - if (lastrom.Type == ItemType.Rom) - { - ((Rom)lastrom).Size += size; - } - Files[key].RemoveAt(index); - Files[key].Add(lastrom); - subreader.Read(); - continue; - } - - // If we're in clean mode, sanitize the game name - if (clean) - { - machine.Name = Style.CleanGameName(machine.Name.Split(Path.DirectorySeparatorChar)); - } - - DatItem inrom; - switch (subreader.Name.ToLowerInvariant()) - { - case "disk": - inrom = new Disk - { - Name = subreader.GetAttribute("name"), - MD5 = subreader.GetAttribute("md5")?.ToLowerInvariant(), - SHA1 = subreader.GetAttribute("sha1")?.ToLowerInvariant(), - ItemStatus = its, - - Machine = machine, - - Supported = supported, - Publisher = publisher, - Infos = infos, - PartName = partname, - PartInterface = partinterface, - Features = features, - AreaName = areaname, - AreaSize = areasize, - - SystemID = sysid, - System = filename, - SourceID = srcid, - }; - break; - case "rom": - default: - inrom = new Rom - { - Name = subreader.GetAttribute("name"), - Size = size, - CRC = subreader.GetAttribute("crc"), - MD5 = subreader.GetAttribute("md5")?.ToLowerInvariant(), - SHA1 = subreader.GetAttribute("sha1")?.ToLowerInvariant(), - ItemStatus = its, - Date = date, - - Machine = machine, - - Supported = supported, - Publisher = publisher, - Infos = infos, - PartName = partname, - PartInterface = partinterface, - Features = features, - AreaName = areaname, - AreaSize = areasize, - - SystemID = sysid, - System = filename, - SourceID = srcid, - }; - break; - } - - // Now process and add the rom - ParseAddHelper(inrom, filter, trim, single, root, clean, logger, out key); - - subreader.Read(); - break; - default: - subreader.Read(); - break; - } - } - xtr.Skip(); - break; - case "dir": - case "directory": - // Set SuperDAT flag for all SabreDAT inputs, regardless of depth - superdat = true; - if (keep) - { - Type = (Type == "" ? "SuperDAT" : Type); - } - - string foldername = (xtr.GetAttribute("name") == null ? "" : xtr.GetAttribute("name")); - if (foldername != "") - { - parent.Add(foldername); - } - - xtr.Read(); - break; - case "file": - empty = false; - - // If the rom is itemStatus, flag it - its = ItemStatus.None; - flagreader = xtr.ReadSubtree(); - - // If the subtree is empty, skip it - if (flagreader == null) - { - xtr.Skip(); - continue; - } - - while (!flagreader.EOF) - { - // We only want elements - if (flagreader.NodeType != XmlNodeType.Element || flagreader.Name == "flags") - { - flagreader.Read(); - continue; - } - - switch (flagreader.Name) - { - case "flag": - case "status": - if (flagreader.GetAttribute("name") != null && flagreader.GetAttribute("value") != null) - { - string content = flagreader.GetAttribute("value"); - switch (flagreader.GetAttribute("name")) - { - case "good": - its = ItemStatus.Good; - break; - case "baddump": - logger.Verbose("Bad dump detected: " + (xtr.GetAttribute("name") != null && xtr.GetAttribute("name") != "" ? - "\"" + xtr.GetAttribute("name") + "\"" : "ROM NAME NOT FOUND")); - its = ItemStatus.BadDump; - break; - case "nodump": - logger.Verbose("Nodump detected: " + (xtr.GetAttribute("name") != null && xtr.GetAttribute("name") != "" ? - "\"" + xtr.GetAttribute("name") + "\"" : "ROM NAME NOT FOUND")); - its = ItemStatus.Nodump; - break; - case "verified": - its = ItemStatus.Verified; - break; - } - } - break; - } - - flagreader.Read(); - } - - // If the rom has a Date attached, read it in and then sanitize it - date = ""; - if (xtr.GetAttribute("date") != null) - { - date = DateTime.Parse(xtr.GetAttribute("date")).ToString(); - } - - // Take care of hex-sized files - size = -1; - if (xtr.GetAttribute("size") != null && xtr.GetAttribute("size").Contains("0x")) - { - size = Convert.ToInt64(xtr.GetAttribute("size"), 16); - } - else if (xtr.GetAttribute("size") != null) - { - Int64.TryParse(xtr.GetAttribute("size"), out size); - } - - // If the rom is continue or ignore, add the size to the previous rom - if (xtr.GetAttribute("loadflag") == "continue" || xtr.GetAttribute("loadflag") == "ignore") - { - int index = Files[key].Count() - 1; - DatItem lastrom = Files[key][index]; - if (lastrom.Type == ItemType.Rom) - { - ((Rom)lastrom).Size += size; - } - Files[key].RemoveAt(index); - Files[key].Add(lastrom); - continue; - } - - Machine dir = new Machine(); - - // Get the name of the game from the parent - dir.Name = String.Join("\\", parent); - dir.Description = dir.Name; - - // If we aren't keeping names, trim out the path - if (!keep || !superdat) - { - string tempout = Regex.Match(dir.Name, @".*?\\(.*)").Groups[1].Value; - if (tempout != "") - { - dir.Name = tempout; - } - } - - DatItem rom; - switch (xtr.GetAttribute("type").ToLowerInvariant()) - { - case "disk": - rom = new Disk - { - Name = xtr.GetAttribute("name"), - MD5 = xtr.GetAttribute("md5")?.ToLowerInvariant(), - SHA1 = xtr.GetAttribute("sha1")?.ToLowerInvariant(), - ItemStatus = its, - - Machine = dir, - - SystemID = sysid, - System = filename, - SourceID = srcid, - }; - break; - case "rom": - default: - rom = new Rom - { - Name = xtr.GetAttribute("name"), - Size = size, - CRC = xtr.GetAttribute("crc")?.ToLowerInvariant(), - MD5 = xtr.GetAttribute("md5")?.ToLowerInvariant(), - SHA1 = xtr.GetAttribute("sha1")?.ToLowerInvariant(), - ItemStatus = its, - Date = date, - - Machine = dir, - - SystemID = sysid, - System = filename, - SourceID = srcid, - }; - break; - } - - // Now process and add the rom - ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); - - xtr.Read(); - break; - default: - xtr.Read(); - break; - } - } - } - catch (Exception ex) - { - logger.Warning(ex.ToString()); - - // For XML errors, just skip the affected node - xtr?.Read(); - } - - xtr.Dispose(); - } - - /// - /// Parse a Redump MD5 and return all found games and roms within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - /// True if game names are sanitized, false otherwise (default) - private void ParseRedumpMD5( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom filtering - Filter filter, - - // Rom renaming - bool trim, - bool single, - string root, - - // Miscellaneous - Logger logger, - bool clean) - { - // Open a file reader - Encoding enc = Style.GetEncoding(filename); - StreamReader sr = new StreamReader(File.OpenRead(filename), enc); - - while (!sr.EndOfStream) - { - string line = sr.ReadLine(); - - Rom rom = new Rom - { - Name = line.Split(' ')[1].Replace("*", String.Empty), - Size = -1, - MD5 = line.Split(' ')[0], - ItemStatus = ItemStatus.None, - - Machine = new Machine - { - Name = Path.GetFileNameWithoutExtension(filename), - }, - - SystemID = sysid, - SourceID = srcid, - }; - - // Now process and add the rom - string key = ""; - ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); - } - - sr.Dispose(); - } - - /// - /// Parse a Redump SFV and return all found games and roms within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - /// True if game names are sanitized, false otherwise (default) - private void ParseRedumpSFV( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom filtering - Filter filter, - - // Rom renaming - bool trim, - bool single, - string root, - - // Miscellaneous - Logger logger, - bool clean) - { - // Open a file reader - Encoding enc = Style.GetEncoding(filename); - StreamReader sr = new StreamReader(File.OpenRead(filename), enc); - - while (!sr.EndOfStream) - { - string line = sr.ReadLine(); - - Rom rom = new Rom - { - Name = line.Split(' ')[0].Replace("*", String.Empty), - Size = -1, - CRC = line.Split(' ')[1], - ItemStatus = ItemStatus.None, - - Machine = new Machine - { - Name = Path.GetFileNameWithoutExtension(filename), - }, - - SystemID = sysid, - SourceID = srcid, - }; - - // Now process and add the rom - string key = ""; - ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); - } - - sr.Dispose(); - } - - /// - /// Parse a Redump SHA-1 and return all found games and roms within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - /// True if game names are sanitized, false otherwise (default) - private void ParseRedumpSHA1( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom filtering - Filter filter, - - // Rom renaming - bool trim, - bool single, - string root, - - // Miscellaneous - Logger logger, - bool clean) - { - // Open a file reader - Encoding enc = Style.GetEncoding(filename); - StreamReader sr = new StreamReader(File.OpenRead(filename), enc); - - while (!sr.EndOfStream) - { - string line = sr.ReadLine(); - - Rom rom = new Rom - { - Name = line.Split(' ')[1].Replace("*", String.Empty), - Size = -1, - SHA1 = line.Split(' ')[0], - ItemStatus = ItemStatus.None, - - Machine = new Machine - { - Name = Path.GetFileNameWithoutExtension(filename), - }, - - SystemID = sysid, - SourceID = srcid, - }; - - // Now process and add the rom - string key = ""; - ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); - } - - sr.Dispose(); - } - - /// - /// Parse a RomCenter DAT and return all found games and roms within - /// - /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - /// True if game names are sanitized, false otherwise (default) - private void ParseRC( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom filtering - Filter filter, - - // Rom renaming - bool trim, - bool single, - string root, - - // Miscellaneous - Logger logger, - bool clean) - { - // Open a file reader - Encoding enc = Style.GetEncoding(filename); - StreamReader sr = new StreamReader(File.OpenRead(filename), enc); - - string blocktype = ""; - while (!sr.EndOfStream) - { - string line = sr.ReadLine(); - - // If the line is the start of the credits section - if (line.ToLowerInvariant().StartsWith("[credits]")) - { - blocktype = "credits"; - } - // If the line is the start of the dat section - else if (line.ToLowerInvariant().StartsWith("[dat]")) - { - blocktype = "dat"; - } - // If the line is the start of the emulator section - else if (line.ToLowerInvariant().StartsWith("[emulator]")) - { - blocktype = "emulator"; - } - // If the line is the start of the game section - else if (line.ToLowerInvariant().StartsWith("[games]")) - { - blocktype = "games"; - } - // Otherwise, it's not a section and it's data, so get out all data - else - { - // If we have an author - if (line.ToLowerInvariant().StartsWith("author=")) - { - Author = (String.IsNullOrEmpty(Author) ? line.Split('=')[1] : Author); - } - // If we have one of the three version tags - else if (line.ToLowerInvariant().StartsWith("version=")) - { - switch (blocktype) - { - case "credits": - Version = (String.IsNullOrEmpty(Version) ? line.Split('=')[1] : Version); - break; - case "emulator": - Description = (String.IsNullOrEmpty(Description) ? line.Split('=')[1] : Description); - break; - } - } - // If we have a URL - else if (line.ToLowerInvariant().StartsWith("url=")) - { - Url = (String.IsNullOrEmpty(Url) ? line.Split('=')[1] : Url); - } - // If we have a comment - else if (line.ToLowerInvariant().StartsWith("comment=")) - { - Comment = (String.IsNullOrEmpty(Comment) ? line.Split('=')[1] : Comment); - } - // If we have the split flag - else if (line.ToLowerInvariant().StartsWith("split=")) - { - int split = 0; - if (Int32.TryParse(line.Split('=')[1], out split)) - { - if (split == 1) - { - ForceMerging = ForceMerging.Split; - } - } - } - // If we have the merge tag - else if (line.ToLowerInvariant().StartsWith("merge=")) - { - int merge = 0; - if (Int32.TryParse(line.Split('=')[1], out merge)) - { - if (merge == 1) - { - ForceMerging = ForceMerging.Full; - } - } - } - // If we have the refname tag - else if (line.ToLowerInvariant().StartsWith("refname=")) - { - Name = (String.IsNullOrEmpty(Name) ? line.Split('=')[1] : Name); - } - // If we have a rom - else if (line.StartsWith("¬")) - { - // Some old RC DATs have this behavior - if (line.Contains("¬N¬O")) - { - line = line.Replace("¬N¬O", "") + "¬¬"; - } - - /* - The rominfo order is as follows: - 1 - parent name - 2 - parent description - 3 - game name - 4 - game description - 5 - rom name - 6 - rom crc - 7 - rom size - 8 - romof name - 9 - merge name - */ - string[] rominfo = line.Split('¬'); - - // Try getting the size separately - long size = 0; - if (!Int64.TryParse(rominfo[7], out size)) - { - size = 0; - } - - Rom rom = new Rom - { - Name = rominfo[5], - Size = size, - CRC = rominfo[6], - ItemStatus = ItemStatus.None, - - Machine = new Machine - { - Name = rominfo[3], - Description = rominfo[4], - CloneOf = rominfo[1], - RomOf = rominfo[8], - }, - - SystemID = sysid, - SourceID = srcid, - }; - - // Now process and add the rom - string key = ""; - ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); - } - } - } - - sr.Dispose(); - } - - /// - /// Add a rom to the Dat after checking - /// - /// Item data to check against - /// Filter object for passing to the DatItem level - /// True if we are supposed to trim names to NTFS length, false otherwise - /// True if all games should be replaced by '!', false otherwise - /// String representing root directory to compare against for length calculation - /// Logger object for console and/or file output - private void ParseAddHelper(DatItem item, Filter filter, bool trim, bool single, string root, bool clean, Logger logger, out string key) - { - key = ""; - - // If there's no name in the rom, we log and skip it - if (item.Name == null) - { - logger.Warning("Rom with no name found! Skipping..."); - return; - } - - // If we're in cleaning mode, sanitize the game name - item.Machine.Name = (clean ? Style.CleanGameName(item.Machine.Name) : item.Machine.Name); - - // If we have a Rom or a Disk, clean the hash data - if (item.Type == ItemType.Rom) - { - Rom itemRom = (Rom)item; - - // Sanitize the hashes from null, hex sizes, and "true blank" strings - itemRom.CRC = Style.CleanHashData(itemRom.CRC, Constants.CRCLength); - itemRom.MD5 = Style.CleanHashData(itemRom.MD5, Constants.MD5Length); - itemRom.SHA1 = Style.CleanHashData(itemRom.SHA1, Constants.SHA1Length); - - // If we have a rom and it's missing size AND the hashes match a 0-byte file, fill in the rest of the info - if ((itemRom.Size == 0 || itemRom.Size == -1) - && ((itemRom.CRC == Constants.CRCZero || String.IsNullOrEmpty(itemRom.CRC)) - || itemRom.MD5 == Constants.MD5Zero - || itemRom.SHA1 == Constants.SHA1Zero)) - { - itemRom.Size = Constants.SizeZero; - itemRom.CRC = Constants.CRCZero; - itemRom.MD5 = Constants.MD5Zero; - itemRom.SHA1 = Constants.SHA1Zero; - } - // If the file has no size and it's not the above case, skip and log - else if (itemRom.ItemStatus != ItemStatus.Nodump && (itemRom.Size == 0 || itemRom.Size == -1)) - { - logger.Verbose("Incomplete entry for \"" + itemRom.Name + "\" will be output as nodump"); - itemRom.ItemStatus = ItemStatus.Nodump; - } - // If the file has a size but aboslutely no hashes, skip and log - else if (itemRom.ItemStatus != ItemStatus.Nodump - && itemRom.Size > 0 - && String.IsNullOrEmpty(itemRom.CRC) - && String.IsNullOrEmpty(itemRom.MD5) - && String.IsNullOrEmpty(itemRom.SHA1)) - { - logger.Verbose("Incomplete entry for \"" + itemRom.Name + "\" will be output as nodump"); - itemRom.ItemStatus = ItemStatus.Nodump; - } - - item = itemRom; - } - else if (item.Type == ItemType.Disk) - { - Disk itemDisk = (Disk)item; - - // Sanitize the hashes from null, hex sizes, and "true blank" strings - itemDisk.MD5 = Style.CleanHashData(itemDisk.MD5, Constants.MD5Length); - itemDisk.SHA1 = Style.CleanHashData(itemDisk.SHA1, Constants.SHA1Length); - - // If the file has aboslutely no hashes, skip and log - if (itemDisk.ItemStatus != ItemStatus.Nodump - && String.IsNullOrEmpty(itemDisk.MD5) - && String.IsNullOrEmpty(itemDisk.SHA1)) - { - logger.Verbose("Incomplete entry for \"" + itemDisk.Name + "\" will be output as nodump"); - itemDisk.ItemStatus = ItemStatus.Nodump; - } - - item = itemDisk; - } - - // If the rom passes the filter, include it - if (filter.ItemPasses(item, logger)) - { - // If we are in single game mode, rename all games - if (single) - { - item.Machine.Name = "!"; - } - - // If we are in NTFS trim mode, trim the game name - if (trim) - { - // Windows max name length is 260 - int usableLength = 260 - item.Machine.Name.Length - root.Length; - if (item.Name.Length > usableLength) - { - string ext = Path.GetExtension(item.Name); - item.Name = item.Name.Substring(0, usableLength - ext.Length); - item.Name += ext; - } - } - - lock (Files) - { - // Get the key and add statistical data - switch (item.Type) - { - case ItemType.Archive: - case ItemType.BiosSet: - case ItemType.Release: - case ItemType.Sample: - key = item.Type.ToString(); - break; - case ItemType.Disk: - key = ((Disk)item).MD5; - - // Add statistical data - DiskCount += 1; - TotalSize += 0; - MD5Count += (String.IsNullOrEmpty(((Disk)item).MD5) ? 0 : 1); - SHA1Count += (String.IsNullOrEmpty(((Disk)item).SHA1) ? 0 : 1); - BaddumpCount += (((Disk)item).ItemStatus == ItemStatus.BadDump ? 1 : 0); - NodumpCount += (((Disk)item).ItemStatus == ItemStatus.Nodump ? 1 : 0); - break; - case ItemType.Rom: - key = ((Rom)item).Size + "-" + ((Rom)item).CRC; - - // Add statistical data - RomCount += 1; - TotalSize += (((Rom)item).ItemStatus == ItemStatus.Nodump ? 0 : ((Rom)item).Size); - CRCCount += (String.IsNullOrEmpty(((Rom)item).CRC) ? 0 : 1); - MD5Count += (String.IsNullOrEmpty(((Rom)item).MD5) ? 0 : 1); - SHA1Count += (String.IsNullOrEmpty(((Rom)item).SHA1) ? 0 : 1); - BaddumpCount += (((Rom)item).ItemStatus == ItemStatus.BadDump ? 1 : 0); - NodumpCount += (((Rom)item).ItemStatus == ItemStatus.Nodump ? 1 : 0); - break; - default: - key = "default"; - break; - } - - // Add the item to the DAT - if (Files.ContainsKey(key)) - { - Files[key].Add(item); - } - else - { - List newvalue = new List(); - newvalue.Add(item); - Files.Add(key, newvalue); - } - } - } - } - - #endregion - - #region Populate DAT from Directory [MODULAR DONE, FOR NOW] - - /// - /// Create a new Dat from a directory - /// - /// Base folder to be used in creating the DAT - /// True if MD5 hashes should be skipped over, false otherwise - /// True if SHA-1 hashes should be skipped over, false otherwise - /// True if the date should be omitted from the DAT, false otherwise - /// True if archives should be treated as files, false otherwise - /// True if GZIP archives should be treated as files, false otherwise - /// True if blank items should be created for empty folders, false otherwise - /// True if dates should be archived for all files, false otherwise - /// Name of the directory to create a temp folder in (blank is current directory) - /// Output directory to - /// True if files should be copied to the temp directory before hashing, false otherwise - /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise - /// Integer representing the maximum amount of parallelization to be used - /// Logger object for console and file output - public bool PopulateFromDir(string basePath, bool noMD5, bool noSHA1, bool bare, bool archivesAsFiles, - bool enableGzip, bool addBlanks, bool addDate, string tempDir, bool copyFiles, string headerToCheckAgainst, - int maxDegreeOfParallelism, Logger logger) - { - // If the description is defined but not the name, set the name from the description - if (String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) - { - Name = Description; - } - - // If the name is defined but not the description, set the description from the name - else if (!String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) - { - Description = Name + (bare ? "" : " (" + Date + ")"); - } - - // If neither the name or description are defined, set them from the automatic values - else if (String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) - { - Name = basePath.Split(Path.DirectorySeparatorChar).Last(); - Description = Name + (bare ? "" : " (" + Date + ")"); - } - - // Make sure the dictionary is defined - if (Files == null || Files.Keys.Count == 0) - { - Files = new SortedDictionary>(); - } - - // Process the input - if (Directory.Exists(basePath)) - { - logger.Verbose("Folder found: " + basePath); - - // Process the files in the main folder - List files = Directory.EnumerateFiles(basePath, "*", SearchOption.TopDirectoryOnly).ToList(); - Parallel.ForEach(files, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - item => - { - PopulateFromDirCheckFile(item, basePath, noMD5, noSHA1, bare, archivesAsFiles, enableGzip, addBlanks, addDate, - tempDir, copyFiles, headerToCheckAgainst, maxDegreeOfParallelism, logger); - }); - - // Find all top-level subfolders - files = Directory.EnumerateDirectories(basePath, "*", SearchOption.TopDirectoryOnly).ToList(); - Parallel.ForEach(files, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - item => - { - List subfiles = Directory.EnumerateFiles(item, "*", SearchOption.AllDirectories).ToList(); - Parallel.ForEach(subfiles, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - subitem => - { - PopulateFromDirCheckFile(subitem, basePath, noMD5, noSHA1, bare, archivesAsFiles, enableGzip, addBlanks, addDate, - tempDir, copyFiles, headerToCheckAgainst, maxDegreeOfParallelism, logger); - }); - }); - - // Process the files in all subfolders - files = Directory.EnumerateFiles(basePath, "*", SearchOption.AllDirectories).ToList(); - Parallel.ForEach(files, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - item => - { - PopulateFromDirCheckFile(item, basePath, noMD5, noSHA1, bare, archivesAsFiles, enableGzip, addBlanks, addDate, - tempDir, copyFiles, headerToCheckAgainst, maxDegreeOfParallelism, logger); - }); - - // Now find all folders that are empty, if we are supposed to - if (!Romba && addBlanks) - { - List empties = Directory.EnumerateDirectories(basePath, "*", SearchOption.AllDirectories).ToList(); - Parallel.ForEach(empties, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - dir => - { - if (Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly).Count() == 0) - { - // Get the full path for the directory - string fulldir = Path.GetFullPath(dir); - - // Set the temporary variables - string gamename = ""; - string romname = ""; - - // If we have a SuperDAT, we want anything that's not the base path as the game, and the file as the rom - if (Type == "SuperDAT") - { - gamename = fulldir.Remove(0, basePath.Length + 1); - romname = "-"; - } - - // Otherwise, we want just the top level folder as the game, and the file as everything else - else - { - gamename = fulldir.Remove(0, basePath.Length + 1).Split(Path.DirectorySeparatorChar)[0]; - romname = Path.Combine(fulldir.Remove(0, basePath.Length + 1 + gamename.Length), "-"); - } - - // Sanitize the names - if (gamename.StartsWith(Path.DirectorySeparatorChar.ToString())) - { - gamename = gamename.Substring(1); - } - if (gamename.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - gamename = gamename.Substring(0, gamename.Length - 1); - } - if (romname.StartsWith(Path.DirectorySeparatorChar.ToString())) - { - romname = romname.Substring(1); - } - if (romname.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - romname = romname.Substring(0, romname.Length - 1); - } - - logger.Verbose("Adding blank empty folder: " + gamename); - Files["null"].Add(new Rom(romname, gamename)); - } - }); - } - } - else if (File.Exists(basePath)) - { - PopulateFromDirCheckFile(basePath, Path.GetDirectoryName(Path.GetDirectoryName(basePath)), noMD5, noSHA1, bare, archivesAsFiles, enableGzip, addBlanks, addDate, - tempDir, copyFiles, headerToCheckAgainst, maxDegreeOfParallelism, logger); - } - - // Now that we're done, delete the temp folder (if it's not the default) - logger.User("Cleaning temp folder"); - try - { - if (tempDir != Path.GetTempPath()) - { - Directory.Delete(tempDir, true); - } - } - catch - { - // Just absorb the error for now - } - - return true; - } - - /// - /// Check a given file for hashes, based on current settings - /// - /// Filename of the item to be checked - /// Base folder to be used in creating the DAT - /// True if MD5 hashes should be skipped over, false otherwise - /// True if SHA-1 hashes should be skipped over, false otherwise - /// True if the date should be omitted from the DAT, false otherwise - /// True if archives should be treated as files, false otherwise - /// True if GZIP archives should be treated as files, false otherwise - /// True if blank items should be created for empty folders, false otherwise - /// True if dates should be archived for all files, false otherwise - /// Name of the directory to create a temp folder in (blank is current directory) - /// True if files should be copied to the temp directory before hashing, false otherwise - /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise - /// Integer representing the maximum amount of parallelization to be used - /// Logger object for console and file output - private void PopulateFromDirCheckFile(string item, string basePath, bool noMD5, bool noSHA1, bool bare, bool archivesAsFiles, - bool enableGzip, bool addBlanks, bool addDate, string tempDir, bool copyFiles, string headerToCheckAgainst, - int maxDegreeOfParallelism, Logger logger) - { - // Define the temporary directory - string tempSubDir = Path.GetFullPath(Path.Combine(tempDir, Path.GetRandomFileName())) + Path.DirectorySeparatorChar; - - // Special case for if we are in Romba mode (all names are supposed to be SHA-1 hashes) - if (Romba) - { - Rom rom = ArchiveTools.GetTorrentGZFileInfo(item, logger); - - // If the rom is valid, write it out - if (rom != null && rom.Name != null) - { - // Add the list if it doesn't exist already - string key = rom.Size + "-" + rom.CRC; - - lock (Files) - { - if (!Files.ContainsKey(key)) - { - Files.Add(key, new List()); - } - - Files[key].Add(rom); - logger.User("File added: " + Path.GetFileNameWithoutExtension(item) + Environment.NewLine); - } - } - else - { - logger.User("File not added: " + Path.GetFileNameWithoutExtension(item) + Environment.NewLine); - return; - } - - return; - } - - // If we're copying files, copy it first and get the new filename - string newItem = item; - string newBasePath = basePath; - if (copyFiles) - { - newBasePath = Path.Combine(tempDir, Path.GetRandomFileName()); - newItem = Path.GetFullPath(Path.Combine(newBasePath, Path.GetFullPath(item).Remove(0, basePath.Length + 1))); - Directory.CreateDirectory(Path.GetDirectoryName(newItem)); - File.Copy(item, newItem, true); - } - - // If both deep hash skip flags are set, do a quickscan - if (noMD5 && noSHA1) - { - ArchiveType? type = ArchiveTools.GetCurrentArchiveType(newItem, logger); - - // If we have an archive, scan it - if (type != null && !archivesAsFiles) - { - List extracted = ArchiveTools.GetArchiveFileInfo(newItem, logger); - - foreach (Rom rom in extracted) - { - PopulateFromDirProcessFileHelper(newItem, - rom, - basePath, - (Path.GetDirectoryName(Path.GetFullPath(item)) + Path.DirectorySeparatorChar).Remove(0, basePath.Length) + Path.GetFileNameWithoutExtension(item), - logger); - } - } - // Otherwise, just get the info on the file itself - else if (File.Exists(newItem)) - { - PopulateFromDirProcessFile(newItem, "", newBasePath, noMD5, noSHA1, addDate, headerToCheckAgainst, logger); - } - } - // Otherwise, attempt to extract the files to the temporary directory - else - { - ArchiveScanLevel asl = (archivesAsFiles ? ArchiveScanLevel.SevenZipExternal : ArchiveScanLevel.SevenZipInternal) - | (!archivesAsFiles && enableGzip ? ArchiveScanLevel.GZipInternal : ArchiveScanLevel.GZipExternal) - | (archivesAsFiles ? ArchiveScanLevel.RarExternal : ArchiveScanLevel.RarInternal) - | (archivesAsFiles ? ArchiveScanLevel.ZipExternal : ArchiveScanLevel.ZipInternal); - - bool encounteredErrors = ArchiveTools.ExtractArchive(newItem, tempSubDir, asl, logger); - - // If the file was an archive and was extracted successfully, check it - if (!encounteredErrors) - { - logger.Verbose(Path.GetFileName(item) + " treated like an archive"); - List extracted = Directory.EnumerateFiles(tempSubDir, "*", SearchOption.AllDirectories).ToList(); - Parallel.ForEach(extracted, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - entry => - { - PopulateFromDirProcessFile(entry, - Path.Combine((Type == "SuperDAT" - ? (Path.GetDirectoryName(Path.GetFullPath(item)) + Path.DirectorySeparatorChar).Remove(0, basePath.Length) - : ""), - Path.GetFileNameWithoutExtension(item)), - tempSubDir, - noMD5, - noSHA1, - addDate, - headerToCheckAgainst, - logger); - }); - } - // Otherwise, just get the info on the file itself - else if (File.Exists(newItem)) - { - PopulateFromDirProcessFile(newItem, "", newBasePath, noMD5, noSHA1, addDate, headerToCheckAgainst, logger); - } - } - - // Cue to delete the file if it's a copy - if (copyFiles && item != newItem) - { - try - { - Directory.Delete(newBasePath, true); - } - catch { } - } - - // Delete the sub temp directory - if (Directory.Exists(tempSubDir)) - { - Directory.Delete(tempSubDir, true); - } - } - - /// - /// Process a single file as a file - /// - /// File to be added - /// Parent game to be used - /// Path the represents the parent directory - /// True if MD5 hashes should be skipped over, false otherwise - /// True if SHA-1 hashes should be skipped over, false otherwise - /// True if dates should be archived for all files, false otherwise - /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise - /// Logger object for console and file output - private void PopulateFromDirProcessFile(string item, string parent, string basePath, bool noMD5, bool noSHA1, bool addDate, string headerToCheckAgainst, Logger logger) - { - logger.Verbose(Path.GetFileName(item) + " treated like a file"); - Rom rom = FileTools.GetFileInfo(item, logger, noMD5: noMD5, noSHA1: noSHA1, date: addDate, header: headerToCheckAgainst); - - PopulateFromDirProcessFileHelper(item, rom, basePath, parent, logger); - } - - /// - /// Process a single file as a file (with found Rom data) - /// - /// File to be added - /// Rom data to be used to write to file - /// Path the represents the parent directory - /// Parent game to be used - private void PopulateFromDirProcessFileHelper(string item, DatItem datItem, string basepath, string parent, Logger logger) - { - // If the datItem isn't a Rom or Disk, return - if (datItem.Type != ItemType.Rom && datItem.Type != ItemType.Disk) - { - return; - } - - string key = ""; - if (datItem.Type == ItemType.Rom) - { - key = ((Rom)datItem).Size + "-" + ((Rom)datItem).CRC; - } - else - { - key = ((Disk)datItem).MD5; - } - - // Add the list if it doesn't exist already - lock (Files) - { - if (!Files.ContainsKey(key)) - { - Files.Add(key, new List()); - } - } - - try - { - // If the basepath ends with a directory separator, remove it - if (!basepath.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - basepath += Path.DirectorySeparatorChar.ToString(); - } - - // Make sure we have the full item path - item = Path.GetFullPath(item); - - // Get the data to be added as game and item names - string gamename = ""; - string romname = ""; - - // If the parent is blank, then we have a non-archive file - if (parent == "") - { - // If we have a SuperDAT, we want anything that's not the base path as the game, and the file as the rom - if (Type == "SuperDAT") - { - gamename = Path.GetDirectoryName(item.Remove(0, basepath.Length)); - romname = Path.GetFileName(item); - } - - // Otherwise, we want just the top level folder as the game, and the file as everything else - else - { - gamename = item.Remove(0, basepath.Length).Split(Path.DirectorySeparatorChar)[0]; - romname = item.Remove(0, (Path.Combine(basepath, gamename).Length)); - } - } - - // Otherwise, we assume that we have an archive - else - { - // If we have a SuperDAT, we want the archive name as the game, and the file as everything else (?) - if (Type == "SuperDAT") - { - gamename = parent; - romname = item.Remove(0, basepath.Length); - } - - // Otherwise, we want the archive name as the game, and the file as everything else - else - { - gamename = parent; - romname = item.Remove(0, basepath.Length); - } - } - - // Sanitize the names - if (gamename.StartsWith(Path.DirectorySeparatorChar.ToString())) - { - gamename = gamename.Substring(1); - } - if (gamename.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - gamename = gamename.Substring(0, gamename.Length - 1); - } - if (romname.StartsWith(Path.DirectorySeparatorChar.ToString())) - { - romname = romname.Substring(1); - } - if (romname.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - romname = romname.Substring(0, romname.Length - 1); - } - if (!String.IsNullOrEmpty(gamename) && String.IsNullOrEmpty(romname)) - { - romname = gamename; - gamename = "Default"; - } - - // Update rom information - datItem.Name = romname; - if (datItem.Machine == null) - { - datItem.Machine = new Machine - { - Name = gamename, - Description = gamename, - }; - } - else - { - datItem.Machine.Name = gamename; - datItem.Machine.Description = gamename; - } - - // Add the file information to the DAT - lock (Files) - { - if (Files.ContainsKey(key)) - { - Files[key].Add(datItem); - } - else - { - List temp = new List(); - temp.Add(datItem); - Files.Add(key, temp); - } - } - - logger.User("File added: " + romname + Environment.NewLine); - } - catch (IOException ex) - { - logger.Error(ex.ToString()); - return; - } - } - - #endregion - - #region Rebuilding and Verifying [MODULAR DONE, FOR NOW] - - /// - /// Process the DAT and find all matches in input files and folders - /// - /// List of input files/folders to check - /// Output directory to use to build to - /// Temporary directory for archive extraction - /// True to enable external scanning of archives, false otherwise - /// True if the date from the DAT should be used if available, false otherwise - /// True if input files should be deleted, false otherwise - /// True if the DAT should be used as a filter instead of a template, false otherwise - /// Output format that files should be written to - /// True if files should be output in Romba depot folders, false otherwise - /// ArchiveScanLevel representing the archive handling levels - /// True if the updated DAT should be output, false otherwise - /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise - /// Logger object for file and console output - /// True if rebuilding was a success, false otherwise - public bool RebuildToOutput(List inputs, string outDir, string tempDir, bool quickScan, bool date, - bool delete, bool inverse, OutputFormat outputFormat, bool romba, ArchiveScanLevel archiveScanLevel, bool updateDat, - string headerToCheckAgainst, int maxDegreeOfParallelism, Logger logger) - { - #region Perform setup - - // If the DAT is not populated and inverse is not set, inform the user and quit - if ((Files == null || Files.Count == 0) && !inverse) - { - logger.User("No entries were found to rebuild, exiting..."); - return false; - } - - // Check that the output directory exists - if (!Directory.Exists(outDir)) - { - Directory.CreateDirectory(outDir); - outDir = Path.GetFullPath(outDir); - } - - // Check the temp directory - if (String.IsNullOrEmpty(tempDir)) - { - tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - } - - // Then create or clean the temp directory - if (!Directory.Exists(tempDir)) - { - Directory.CreateDirectory(tempDir); - } - else - { - FileTools.CleanDirectory(tempDir); - } - - // Preload the Skipper list - int listcount = Skipper.List.Count; - - #endregion - - bool success = true; - DatFile matched = new DatFile(); - List files = new List(); - - #region Retrieve a list of all files - - logger.User("Retrieving list all files from input"); - DateTime start = DateTime.Now; - - // Create a list of just files from inputs - Parallel.ForEach(inputs, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism, }, - input => { - if (File.Exists(input)) - { - logger.Verbose("File found: '" + input + "'"); - files.Add(Path.GetFullPath(input)); - } - else if (Directory.Exists(input)) - { - logger.Verbose("Directory found: '" + input + "'"); - Parallel.ForEach(Directory.EnumerateFiles(input, "*", SearchOption.AllDirectories), - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism, }, - file => - { - logger.Verbose("File found: '" + file + "'"); - files.Add(Path.GetFullPath(file)); - }); - } - else - { - logger.Error("'" + input + "' is not a file or directory!"); - } - }); - logger.User("Retrieving complete in: " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - #endregion - - DatFile current = new DatFile(); - Dictionary fileToSkipperRule = new Dictionary(); - - #region Create a dat from input files - - logger.User("Getting hash information for all input files"); - start = DateTime.Now; - - // Now that we have a list of just files, we get a DAT from the input files - Parallel.ForEach(files, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - file => - { - // If we somehow have a null filename, return - if (file == null) - { - return; - } - - // Define the temporary directory - string tempSubDir = Path.GetFullPath(Path.Combine(tempDir, Path.GetRandomFileName())) + Path.DirectorySeparatorChar; - - // Get the required scanning level for the file - bool shouldExternalProcess = false; - bool shouldInternalProcess = false; - ArchiveTools.GetInternalExternalProcess(file, archiveScanLevel, logger, out shouldExternalProcess, out shouldInternalProcess); - - // If we're supposed to scan the file externally - if (shouldExternalProcess) - { - Rom rom = FileTools.GetFileInfo(file, logger, noMD5: quickScan, noSHA1: quickScan, header: headerToCheckAgainst); - rom.Name = Path.GetFullPath(file); - - lock (Files) - { - string key = rom.Size + "-" + rom.CRC; - if (current.Files.ContainsKey(key)) - { - current.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - current.Files.Add(key, temp); - } - } - - // If we had a header, we want the full file information too - if (headerToCheckAgainst != null) - { - rom = FileTools.GetFileInfo(file, logger, noMD5: quickScan, noSHA1: quickScan); - rom.Name = Path.GetFullPath(file); - - lock (Files) - { - string key = rom.Size + "-" + rom.CRC; - if (current.Files.ContainsKey(key)) - { - current.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - current.Files.Add(key, temp); - } - } - } - } - - // If we're supposed to scan the file internally - if (shouldInternalProcess) - { - // If quickscan is set, do so - if (quickScan) - { - List extracted = ArchiveTools.GetArchiveFileInfo(file, logger); - - foreach (Rom rom in extracted) - { - Rom newrom = rom; - newrom.Machine = new Machine(Path.GetFullPath(file), ""); - - lock (Files) - { - string key = rom.Size + "-" + rom.CRC; - if (current.Files.ContainsKey(key)) - { - current.Files[key].Add(newrom); - } - else - { - List temp = new List(); - temp.Add(newrom); - current.Files.Add(key, temp); - } - } - } - } - // Otherwise, attempt to extract the files to the temporary directory - else - { - bool encounteredErrors = ArchiveTools.ExtractArchive(file, tempSubDir, archiveScanLevel, logger); - - // If the file was an archive and was extracted successfully, check it - if (!encounteredErrors) - { - logger.Verbose(Path.GetFileName(file) + " treated like an archive"); - List extracted = Directory.EnumerateFiles(tempSubDir, "*", SearchOption.AllDirectories).ToList(); - Parallel.ForEach(extracted, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - entry => - { - Rom rom = FileTools.GetFileInfo(entry, logger, noMD5: quickScan, noSHA1: quickScan, header: headerToCheckAgainst); - rom.Machine = new Machine(Path.GetFullPath(file), ""); - - lock (Files) - { - string key = rom.Size + "-" + rom.CRC; - if (current.Files.ContainsKey(key)) - { - current.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - current.Files.Add(key, temp); - } - } - - // If we had a header, we want the full file information too - if (headerToCheckAgainst != null) - { - rom = FileTools.GetFileInfo(file, logger, noMD5: quickScan, noSHA1: quickScan); - rom.Machine = new Machine(Path.GetFullPath(file), ""); - - lock (Files) - { - string key = rom.Size + "-" + rom.CRC; - if (current.Files.ContainsKey(key)) - { - current.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - current.Files.Add(key, temp); - } - } - } - }); - } - // Otherwise, just get the info on the file itself - else if (File.Exists(file)) - { - Rom rom = FileTools.GetFileInfo(file, logger, noMD5: quickScan, noSHA1: quickScan, header: headerToCheckAgainst); - rom.Name = Path.GetFullPath(file); - - lock (Files) - { - string key = rom.Size + "-" + rom.CRC; - if (current.Files.ContainsKey(key)) - { - current.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - current.Files.Add(key, temp); - } - } - } - } - } - - // Now delete the temp directory - try - { - Directory.Delete(tempSubDir, true); - } - catch { } - }); - - logger.User("Getting hash information complete in: " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - #endregion - - // Create a mapping from destination file to source file - Dictionary toFromMap = new Dictionary(); - - #region Find all required files for rebuild - - logger.User("Determining files to rebuild"); - start = DateTime.Now; - - // Order the DATs by hash first to make things easier - logger.User("Sorting input DAT..."); - BucketByCRC(false, logger, output: false); - logger.User("Sorting found files..."); - current.BucketByCRC(false, logger, output: false); - - // Now loop over and find all files that need to be rebuilt - List keys = current.Files.Keys.ToList(); - Parallel.ForEach(keys, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - key => - { - // If we are using the DAT as a filter, treat the files one way - if (inverse) - { - // Check for duplicates - List datItems = current.Files[key]; - foreach (Rom rom in datItems) - { - // If the rom has duplicates, we skip it - if (rom.HasDuplicates(this, logger)) - { - return; - } - - // Otherwise, map the file to itself - try - { - Rom newrom = new Rom - { - Name = rom.Name.Remove(0, Path.GetDirectoryName(rom.Name).Length), - Size = rom.Size, - CRC = rom.CRC, - MD5 = rom.MD5, - SHA1 = rom.SHA1, - - Machine = new Machine - { - Name = Path.GetFileNameWithoutExtension(rom.Machine.Name), - }, - }; - newrom.Name = newrom.Name.Remove(0, (newrom.Name.StartsWith("\\") || newrom.Name.StartsWith("/") ? 1 : 0)); - - lock (toFromMap) - { - toFromMap.Add(newrom, rom); - } - } - catch { } - } - } - - // Otherwise, treat it like a standard rebuild - else - { - // If the input DAT doesn't have the key, then nothing from the current DAT are there - if (!Files.ContainsKey(key)) - { - return; - } - - // Otherwise, we try to find duplicates - List datItems = current.Files[key]; - foreach (Rom rom in datItems) - { - List found = rom.GetDuplicates(this, logger, false); - - // Now add all of the duplicates mapped to the current file - foreach (Rom mid in found) - { - try - { - lock (toFromMap) - { - toFromMap.Add(mid, rom); - } - } - catch { } - } - } - } - }); - - logger.User("Determining complete in: " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - #endregion - - // Now bucket the list of keys by game so that we can rebuild properly - SortedDictionary> keysGroupedByGame = BucketListByGame(toFromMap.Keys.ToList(), false, true, logger, output: false); - - #region Rebuild games in order - - switch (outputFormat) - { - case OutputFormat.Folder: - logger.User("Rebuilding all files to directory"); - break; - case OutputFormat.TapeArchive: - logger.User("Rebuilding all files to TAR"); - break; - case OutputFormat.Torrent7Zip: - logger.User("Rebuilding all files to Torrent7Z"); - break; - case OutputFormat.TorrentGzip: - logger.User("Rebuilding all files to TorrentGZ"); - break; - case OutputFormat.TorrentLrzip: - logger.User("Rebuilding all files to TorrentLRZ"); - break; - case OutputFormat.TorrentRar: - logger.User("Rebuilding all files to TorrentRAR"); - break; - case OutputFormat.TorrentXZ: - logger.User("Rebuilding all files to TorrentXZ"); - break; - case OutputFormat.TorrentZip: - logger.User("Rebuilding all files to TorrentZip"); - break; - } - start = DateTime.Now; - - // Now loop through the keys and create the correct output items - List games = keysGroupedByGame.Keys.ToList(); - Parallel.ForEach(games, - new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, - game => - { - // Define the temporary directory - string tempSubDir = Path.GetFullPath(Path.Combine(tempDir, Path.GetRandomFileName())) + Path.DirectorySeparatorChar; - - // Create an empty list for getting paths for rebuilding - List pathsToFiles = new List(); - - // Loop through all of the matched items in the game - List itemsInGame = keysGroupedByGame[game]; - List romsInGame = new List(); - foreach (Rom rom in itemsInGame) - { - // Get the rom that's mapped to this item - Rom source = (Rom)toFromMap[rom]; - - // If we have an empty rom or machine, there was an issue - if (source == null || source.Machine == null || source.Machine.Name == null) - { - continue; - } - - // If the file is in an archive, we need to treat it specially - string machinename = source.Machine.Name.ToLowerInvariant(); - if (machinename.EndsWith(".7z") - || machinename.EndsWith(".gz") - || machinename.EndsWith(".rar") - || machinename.EndsWith(".zip")) - { - string tempPath = ArchiveTools.ExtractItem(source.Machine.Name, Path.GetFileName(source.Name), tempSubDir, logger); - pathsToFiles.Add(tempPath); - } - - // Otherwise, we want to just add the full path - else - { - pathsToFiles.Add(source.Name); - } - - // If the size doesn't match, then we add the CRC as a postfix to the file - Rom fi = FileTools.GetFileInfo(pathsToFiles.Last(), logger); - if (fi.Size != source.Size) - { - rom.Name = Path.GetDirectoryName(rom.Name) - + (String.IsNullOrEmpty(Path.GetDirectoryName(rom.Name)) ? "" : Path.DirectorySeparatorChar.ToString()) - + Path.GetFileNameWithoutExtension(rom.Name) - + " (" + fi.CRC + ")" - + Path.GetExtension(rom.Name); - rom.CRC = fi.CRC; - rom.Size = fi.Size; - } - - // Now add the rom to the output list - romsInGame.Add(rom); - } - - // And now rebuild accordingly - switch (outputFormat) - { - case OutputFormat.Folder: - for (int i = 0; i < romsInGame.Count; i++) - { - string infile = pathsToFiles[i]; - Rom outrom = romsInGame[i]; - string outfile = Path.Combine(outDir, Style.RemovePathUnsafeCharacters(outrom.Machine.Name), outrom.Name); - - // Make sure the output folder is created - Directory.CreateDirectory(Path.GetDirectoryName(outfile)); - - // Now copy the file over - try - { - File.Copy(infile, outfile); - } - catch { } - } - break; - case OutputFormat.TapeArchive: - ArchiveTools.WriteTAR(pathsToFiles, outDir, romsInGame, logger); - break; - case OutputFormat.Torrent7Zip: - break; - case OutputFormat.TorrentGzip: - for (int i = 0; i < itemsInGame.Count; i++) - { - string infile = pathsToFiles[i]; - Rom outrom = romsInGame[i]; - outrom.Machine.Name = Style.RemovePathUnsafeCharacters(outrom.Machine.Name); - ArchiveTools.WriteTorrentGZ(infile, outDir, romba, logger); - } - break; - case OutputFormat.TorrentLrzip: - break; - case OutputFormat.TorrentRar: - break; - case OutputFormat.TorrentXZ: - break; - case OutputFormat.TorrentZip: - ArchiveTools.WriteTorrentZip(pathsToFiles, outDir, romsInGame, logger); - break; - } - - // And now clear the temp folder to get rid of any transient files - try - { - Directory.Delete(tempSubDir, true); - } - catch { } - }); - - logger.User("Rebuilding complete in: " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); - - #endregion - - return success; - } - - /// - /// Process the DAT and verify the output directory - /// - /// DAT to use to verify the directory - /// List of input directories to compare against - /// Temporary directory for archive extraction - /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise - /// Logger object for file and console output - /// True if verification was a success, false otherwise - public bool VerifyDirectory(List inputs, string tempDir, string headerToCheckAgainst, Logger logger) - { - // First create or clean the temp directory - if (!Directory.Exists(tempDir)) - { - Directory.CreateDirectory(tempDir); - } - else - { - FileTools.CleanDirectory(tempDir); - } - - bool success = true; - - /* - We want the cross section of what's the folder and what's in the DAT. Right now, it just has what's in the DAT that's not in the folder - */ - - // Then, loop through and check each of the inputs - logger.User("Processing files:\n"); - foreach (string input in inputs) - { - PopulateFromDir(input, false /* noMD5 */, false /* noSHA1 */, true /* bare */, false /* archivesAsFiles */, - true /* enableGzip */, false /* addBlanks */, false /* addDate */, tempDir /* tempDir */, false /* copyFiles */, - headerToCheckAgainst, 4 /* maxDegreeOfParallelism */, logger); - } - - // Setup the fixdat - DatFile matched = (DatFile)CloneHeader(); - matched.Files = new SortedDictionary>(); - matched.FileName = "fixDat_" + matched.FileName; - matched.Name = "fixDat_" + matched.Name; - matched.Description = "fixDat_" + matched.Description; - matched.DatFormat = DatFormat.Logiqx; - - // Now that all files are parsed, get only files found in directory - bool found = false; - foreach (List roms in Files.Values) - { - List newroms = DatItem.Merge(roms, logger); - foreach (Rom rom in newroms) - { - if (rom.SourceID == 99) - { - found = true; - string key = rom.Size + "-" + rom.CRC; - if (matched.Files.ContainsKey(key)) - { - matched.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - matched.Files.Add(key, temp); - } - } - } - } - - // Now output the fixdat to the main folder - if (found) - { - matched.WriteToFile("", logger, stats: true); - } - else - { - logger.User("No fixDat needed"); - } - - return success; - } - - #endregion - - #region Splitting [MODULAR DONE] - - /// - /// Split a DAT by input extensions - /// - /// Name of the directory to write the DATs out to - /// Parent path for replacement - /// List of extensions to split on (first DAT) - /// List of extensions to split on (second DAT) - /// Logger object for console and file writing - /// True if split succeeded, false otherwise - public bool SplitByExt(string outDir, string basepath, List extA, List extB, Logger logger) - { - // Make sure all of the extensions have a dot at the beginning - List newExtA = new List(); - foreach (string s in extA) - { - newExtA.Add((s.StartsWith(".") ? s : "." + s).ToUpperInvariant()); - } - string newExtAString = string.Join(",", newExtA); - - List newExtB = new List(); - foreach (string s in extB) - { - newExtB.Add((s.StartsWith(".") ? s : "." + s).ToUpperInvariant()); - } - string newExtBString = string.Join(",", newExtB); - - // Set all of the appropriate outputs for each of the subsets - DatFile datdataA = new DatFile - { - FileName = this.FileName + " (" + newExtAString + ")", - Name = this.Name + " (" + newExtAString + ")", - Description = this.Description + " (" + newExtAString + ")", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Files = new SortedDictionary>(), - DatFormat = this.DatFormat, - }; - DatFile datdataB = new DatFile - { - FileName = this.FileName + " (" + newExtBString + ")", - Name = this.Name + " (" + newExtBString + ")", - Description = this.Description + " (" + newExtBString + ")", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Files = new SortedDictionary>(), - DatFormat = this.DatFormat, - }; - - // If roms is empty, return false - if (this.Files.Count == 0) - { - return false; - } - - // Now separate the roms accordingly - foreach (string key in this.Files.Keys) - { - foreach (DatItem rom in this.Files[key]) - { - if (newExtA.Contains(Path.GetExtension(rom.Name.ToUpperInvariant()))) - { - if (datdataA.Files.ContainsKey(key)) - { - datdataA.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - datdataA.Files.Add(key, temp); - } - } - else if (newExtB.Contains(Path.GetExtension(rom.Name.ToUpperInvariant()))) - { - if (datdataB.Files.ContainsKey(key)) - { - datdataB.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - datdataB.Files.Add(key, temp); - } - } - else - { - if (datdataA.Files.ContainsKey(key)) - { - datdataA.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - datdataA.Files.Add(key, temp); - } - if (datdataB.Files.ContainsKey(key)) - { - datdataB.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - datdataB.Files.Add(key, temp); - } - } - } - } - - // Get the output directory - if (outDir != "") - { - outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1); - } - else - { - outDir = Path.GetDirectoryName(this.FileName); - } - - // Then write out both files - bool success = datdataA.WriteToFile(outDir, logger); - success &= datdataB.WriteToFile(outDir, logger); - - return success; - } - - /// - /// Split a DAT by best available hashes - /// - /// Name of the directory to write the DATs out to - /// Parent path for replacement - /// Logger object for console and file writing - /// True if split succeeded, false otherwise - public bool SplitByHash(string outDir, string basepath, Logger logger) - { - // Sanitize the basepath to be more predictable - basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar); - - // Create each of the respective output DATs - logger.User("Creating and populating new DATs"); - DatFile itemStatus = new DatFile - { - FileName = this.FileName + " (Nodump)", - Name = this.Name + " (Nodump)", - Description = this.Description + " (Nodump)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - MergeRoms = this.MergeRoms, - Files = new SortedDictionary>(), - }; - DatFile sha1 = new DatFile - { - FileName = this.FileName + " (SHA-1)", - Name = this.Name + " (SHA-1)", - Description = this.Description + " (SHA-1)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - MergeRoms = this.MergeRoms, - Files = new SortedDictionary>(), - }; - DatFile md5 = new DatFile - { - FileName = this.FileName + " (MD5)", - Name = this.Name + " (MD5)", - Description = this.Description + " (MD5)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - MergeRoms = this.MergeRoms, - Files = new SortedDictionary>(), - }; - DatFile crc = new DatFile - { - FileName = this.FileName + " (CRC)", - Name = this.Name + " (CRC)", - Description = this.Description + " (CRC)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - MergeRoms = this.MergeRoms, - Files = new SortedDictionary>(), - }; - - DatFile other = new DatFile - { - FileName = this.FileName + " (Other)", - Name = this.Name + " (Other)", - Description = this.Description + " (Other)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - MergeRoms = this.MergeRoms, - Files = new SortedDictionary>(), - }; - - // Now populate each of the DAT objects in turn - List keys = this.Files.Keys.ToList(); - foreach (string key in keys) - { - List roms = this.Files[key]; - foreach (DatItem rom in roms) - { - // If the file is not a Rom or Disk, continue - if (rom.Type != ItemType.Disk && rom.Type != ItemType.Rom) - { - continue; - } - - // If the file is a itemStatus - if ((rom.Type == ItemType.Rom && ((Rom)rom).ItemStatus == ItemStatus.Nodump) - || (rom.Type == ItemType.Disk && ((Disk)rom).ItemStatus == ItemStatus.Nodump)) - { - if (itemStatus.Files.ContainsKey(key)) - { - itemStatus.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - itemStatus.Files.Add(key, temp); - } - } - // If the file has a SHA-1 - else if ((rom.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)rom).SHA1)) - || (rom.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)rom).SHA1))) - { - if (sha1.Files.ContainsKey(key)) - { - sha1.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - sha1.Files.Add(key, temp); - } - } - // If the file has no SHA-1 but has an MD5 - else if ((rom.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)rom).MD5)) - || (rom.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)rom).MD5))) - { - if (md5.Files.ContainsKey(key)) - { - md5.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - md5.Files.Add(key, temp); - } - } - // If the file has no MD5 but a CRC - else if ((rom.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)rom).SHA1)) - || (rom.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)rom).SHA1))) - { - if (crc.Files.ContainsKey(key)) - { - crc.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - crc.Files.Add(key, temp); - } - } - else - { - if (other.Files.ContainsKey(key)) - { - other.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - other.Files.Add(key, temp); - } - } - } - } - - // Get the output directory - if (outDir != "") - { - outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1); - } - else - { - outDir = Path.GetDirectoryName(this.FileName); - } - - // Now, output all of the files to the output directory - logger.User("DAT information created, outputting new files"); - bool success = true; - if (itemStatus.Files.Count > 0) - { - success &= itemStatus.WriteToFile(outDir, logger); - } - if (sha1.Files.Count > 0) - { - success &= sha1.WriteToFile(outDir, logger); - } - if (md5.Files.Count > 0) - { - success &= md5.WriteToFile(outDir, logger); - } - if (crc.Files.Count > 0) - { - success &= crc.WriteToFile(outDir, logger); - } - - return success; - } - - /// - /// Split a SuperDAT by lowest available directory level - /// - /// Name of the directory to write the DATs out to - /// Parent path for replacement - /// True if short names should be used, false otherwise - /// True if original filenames should be used as the base for output filename, false otherwise - /// Logger object for console and file writing - /// True if split succeeded, false otherwise - public bool SplitByLevel(string outDir, string basepath, bool shortname, bool basedat, Logger logger) - { - // Sanitize the basepath to be more predictable - basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar); - - // First, organize by games so that we can do the right thing - BucketByGame(false, true, logger, output: false, lower: false); - - // Create a temporary DAT to add things to - DatFile tempDat = (DatFile)CloneHeader(); - tempDat.Name = null; - - // Sort the input keys - List keys = Files.Keys.ToList(); - keys.Sort(SplitByLevelSort); - - // Then, we loop over the games - foreach (string key in keys) - { - // Here, the key is the name of the game to be used for comparison - if (tempDat.Name != null && tempDat.Name != Style.GetDirectoryName(key)) - { - // Process and output the DAT - SplitByLevelHelper(tempDat, outDir, shortname, basedat, logger); - - // Reset the DAT for the next items - tempDat = (DatFile)CloneHeader(); - tempDat.Name = null; - } - - // Clean the input list and set all games to be pathless - List items = Files[key]; - items.ForEach(item => item.Machine.Name = Style.GetFileName(item.Machine.Name)); - items.ForEach(item => item.Machine.Description = Style.GetFileName(item.Machine.Description)); - - // Now add the game to the output DAT - if (tempDat.Files.ContainsKey(key)) - { - tempDat.Files[key].AddRange(items); - } - else - { - tempDat.Files.Add(key, items); - } - - // Then set the DAT name to be the parent directory name - tempDat.Name = Style.GetDirectoryName(key); - } - - // Then we write the last DAT out since it would be skipped otherwise - SplitByLevelHelper(tempDat, outDir, shortname, basedat, logger); - - return true; - } - - /// - /// Helper function for SplitByLevel to sort the input game names - /// - /// First string to compare - /// Second string to compare - /// -1 for a coming before b, 0 for a == b, 1 for a coming after b - private int SplitByLevelSort(string a, string b) - { - NaturalComparer nc = new NaturalComparer(); - int adeep = a.Count(c => c == '/' || c == '\\'); - int bdeep = b.Count(c => c == '/' || c == '\\'); - - if (adeep == bdeep) - { - return nc.Compare(a, b); - } - return adeep - bdeep; - } - - /// - /// Helper function for SplitByLevel to clean and write out a DAT - /// - /// DAT to clean and write out - /// Directory to write out to - /// True if short naming scheme should be used, false otherwise - /// True if original filenames should be used as the base for output filename, false otherwise - /// Logger object for file and console output - private void SplitByLevelHelper(DatFile datFile, string outDir, bool shortname, bool restore, Logger logger) - { - // Get the name from the DAT to use separately - string name = datFile.Name; - string expName = name.Replace("/", " - ").Replace("\\", " - "); - - // Get the path that the file will be written out to - string path = HttpUtility.HtmlDecode(String.IsNullOrEmpty(name) - ? outDir - : Path.Combine(outDir, name)); - - // Now set the new output values - datFile.FileName = HttpUtility.HtmlDecode(String.IsNullOrEmpty(name) - ? FileName - : (shortname - ? Style.GetFileName(name) - : expName - ) - ); - datFile.FileName = (restore ? FileName + " (" + datFile.FileName + ")" : datFile.FileName); - datFile.Name = Name + " (" + expName + ")"; - datFile.Description = (String.IsNullOrEmpty(Description) ? datFile.Name : Description + " (" + expName + ")"); - datFile.Type = null; - - // Write out the temporary DAT to the proper directory - datFile.WriteToFile(path, logger); - } - - /// - /// Split a DAT by type of Rom - /// - /// Name of the directory to write the DATs out to - /// Parent path for replacement - /// Logger object for console and file writing - /// True if split succeeded, false otherwise - public bool SplitByType(string outDir, string basepath, Logger logger) - { - // Sanitize the basepath to be more predictable - basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar); - - // Create each of the respective output DATs - logger.User("Creating and populating new DATs"); - DatFile romdat = new DatFile - { - FileName = this.FileName + " (ROM)", - Name = this.Name + " (ROM)", - Description = this.Description + " (ROM)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - MergeRoms = this.MergeRoms, - Files = new SortedDictionary>(), - }; - DatFile diskdat = new DatFile - { - FileName = this.FileName + " (Disk)", - Name = this.Name + " (Disk)", - Description = this.Description + " (Disk)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - MergeRoms = this.MergeRoms, - Files = new SortedDictionary>(), - }; - DatFile sampledat = new DatFile - { - FileName = this.FileName + " (Sample)", - Name = this.Name + " (Sample)", - Description = this.Description + " (Sample)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - MergeRoms = this.MergeRoms, - Files = new SortedDictionary>(), - }; - - // Now populate each of the DAT objects in turn - List keys = this.Files.Keys.ToList(); - foreach (string key in keys) - { - List roms = this.Files[key]; - foreach (DatItem rom in roms) - { - // If the file is a Rom - if (rom.Type == ItemType.Rom) - { - if (romdat.Files.ContainsKey(key)) - { - romdat.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - romdat.Files.Add(key, temp); - } - } - // If the file is a Disk - else if (rom.Type == ItemType.Disk) - { - if (diskdat.Files.ContainsKey(key)) - { - diskdat.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - diskdat.Files.Add(key, temp); - } - } - - // If the file is a Sample - else if (rom.Type == ItemType.Sample) - { - if (sampledat.Files.ContainsKey(key)) - { - sampledat.Files[key].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - sampledat.Files.Add(key, temp); - } - } - } - } - - // Get the output directory - if (outDir != "") - { - outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1); - } - else - { - outDir = Path.GetDirectoryName(this.FileName); - } - - // Now, output all of the files to the output directory - logger.User("DAT information created, outputting new files"); - bool success = true; - if (romdat.Files.Count > 0) - { - success &= romdat.WriteToFile(outDir, logger); - } - if (diskdat.Files.Count > 0) - { - success &= diskdat.WriteToFile(outDir, logger); - } - if (sampledat.Files.Count > 0) - { - success &= sampledat.WriteToFile(outDir, logger); - } - - return success; - } - - #endregion - - #region Statistics [MODULAR DONE] - - /// - /// Recalculate the statistics for the Dat - /// - public void RecalculateStats() - { - // Wipe out any stats already there - RomCount = 0; - DiskCount = 0; - TotalSize = 0; - CRCCount = 0; - MD5Count = 0; - SHA1Count = 0; - BaddumpCount = 0; - NodumpCount = 0; - - // If we have a blank Dat in any way, return - if (this == null || Files == null || Files.Count == 0) - { - return; - } - - // Loop through and add - foreach (List roms in Files.Values) - { - foreach (Rom rom in roms) - { - RomCount += (rom.Type == ItemType.Rom ? 1 : 0); - DiskCount += (rom.Type == ItemType.Disk ? 1 : 0); - TotalSize += (rom.ItemStatus == ItemStatus.Nodump ? 0 : rom.Size); - CRCCount += (String.IsNullOrEmpty(rom.CRC) ? 0 : 1); - MD5Count += (String.IsNullOrEmpty(rom.MD5) ? 0 : 1); - SHA1Count += (String.IsNullOrEmpty(rom.SHA1) ? 0 : 1); - BaddumpCount += (rom.Type == ItemType.Disk - ? (((Disk)rom).ItemStatus == ItemStatus.BadDump ? 1 : 0) - : (rom.Type == ItemType.Rom - ? (((Rom)rom).ItemStatus == ItemStatus.BadDump ? 1 : 0) - : 0) - ); - NodumpCount += (rom.Type == ItemType.Disk - ? (((Disk)rom).ItemStatus == ItemStatus.Nodump ? 1 : 0) - : (rom.Type == ItemType.Rom - ? (((Rom)rom).ItemStatus == ItemStatus.Nodump ? 1 : 0) - : 0) - ); - } - } - } - - /// - /// Output the stats for the Dat in a human-readable format - /// - /// StreamWriter representing the output file or stream for the statistics - /// Set the statistics output format to use - /// Logger object for file and console writing - /// True if numbers should be recalculated for the DAT, false otherwise (default) - /// Number of games to use, -1 means recalculate games (default) - /// True if baddumps should be included in output, false otherwise (default) - /// True if nodumps should be included in output, false otherwise (default) - public void OutputStats(StreamWriter sw, StatDatFormat statDatFormat, Logger logger, bool recalculate = false, long game = -1, bool baddumpCol = false, bool nodumpCol = false) - { - // If we're supposed to recalculate the statistics, do so - if (recalculate) - { - RecalculateStats(); - } - - BucketByGame(false, true, logger, false); - if (TotalSize < 0) - { - TotalSize = Int64.MaxValue + TotalSize; - } - - // Log the results to screen - string results = @"For '" + FileName + @"': --------------------------------------------------- - Uncompressed size: " + Style.GetBytesReadable(TotalSize) + @" - Games found: " + (game == -1 ? Files.Count : game) + @" - Roms found: " + RomCount + @" - Disks found: " + DiskCount + @" - Roms with CRC: " + CRCCount + @" - Roms with MD5: " + MD5Count + @" - Roms with SHA-1: " + SHA1Count + "\n"; - - if (baddumpCol) - { - results += " Roms with BadDump status: " + BaddumpCount + "\n"; - } - if (nodumpCol) - { - results += " Roms with Nodump status: " + NodumpCount + "\n"; - } - - logger.User(results); - - // Now write it out to file as well - string line = ""; - switch (statDatFormat) - { - case StatDatFormat.CSV: - line = "\"" + FileName + "\"," - + "\"" + Style.GetBytesReadable(TotalSize) + "\"," - + "\"" + (game == -1 ? Files.Count : game) + "\"," - + "\"" + RomCount + "\"," - + "\"" + DiskCount + "\"," - + "\"" + CRCCount + "\"," - + "\"" + MD5Count + "\"," - + "\"" + SHA1Count + "\""; - - if (baddumpCol) - { - line += ",\"" + BaddumpCount + "\""; - } - if (nodumpCol) - { - line += ",\"" + NodumpCount + "\""; - } - - line += "\n"; - break; - case StatDatFormat.HTML: - line = "\t\t\t" + HttpUtility.HtmlEncode(FileName.Remove(0, 5)) - : ">" + HttpUtility.HtmlEncode(FileName)) + "" - + "" + Style.GetBytesReadable(TotalSize) + "" - + "" + (game == -1 ? Files.Count : game) + "" - + "" + RomCount + "" - + "" + DiskCount + "" - + "" + CRCCount + "" - + "" + MD5Count + "" - + "" + SHA1Count + ""; - - if (baddumpCol) - { - line += "" + BaddumpCount + ""; - } - if (nodumpCol) - { - line += "" + NodumpCount + ""; - } - - line += "\n"; - break; - case StatDatFormat.None: - default: - line = @"'" + FileName + @"': --------------------------------------------------- - Uncompressed size: " + Style.GetBytesReadable(TotalSize) + @" - Games found: " + (game == -1 ? Files.Count : game) + @" - Roms found: " + RomCount + @" - Disks found: " + DiskCount + @" - Roms with CRC: " + CRCCount + @" - Roms with MD5: " + MD5Count + @" - Roms with SHA-1: " + SHA1Count + "\n"; - - if (baddumpCol) - { - line += " Roms with BadDump status: " + BaddumpCount + "\n"; - } - if (nodumpCol) - { - line += " Roms with Nodump status: " + NodumpCount + "\n"; - } - break; - case StatDatFormat.TSV: - line = "\"" + FileName + "\"\t" - + "\"" + Style.GetBytesReadable(TotalSize) + "\"\t" - + "\"" + (game == -1 ? Files.Count : game) + "\"\t" - + "\"" + RomCount + "\"\t" - + "\"" + DiskCount + "\"\t" - + "\"" + CRCCount + "\"\t" - + "\"" + MD5Count + "\"\t" - + "\"" + SHA1Count + "\""; - - if (baddumpCol) - { - line += "\t\"" + BaddumpCount + "\""; - } - if (nodumpCol) - { - line += "\t\"" + NodumpCount + "\""; - } - - line += "\n"; - break; - } - - // Output the line to the streamwriter - sw.Write(line); - } - - #endregion - - #region Writing [MODULAR DONE] - - /// - /// Create and open an output file for writing direct from a dictionary - /// - /// All information for creating the datfile header - /// Set the output directory - /// Logger object for console and/or file output - /// True if games should only be compared on game and file name (default), false if system and source are counted - /// True if DAT statistics should be output on write, false otherwise (default) - /// True if blank roms should be skipped on output, false otherwise (default) - /// True if files should be overwritten (default), false if they should be renamed instead - /// True if the DAT was written correctly, false otherwise - /// - /// The following features have been requested for file output: - /// - Have the ability to strip special (non-ASCII) characters from rom information - /// - public bool WriteToFile(string outDir, Logger logger, bool norename = true, bool stats = false, bool ignoreblanks = false, bool overwrite = true) - { - // If there's nothing there, abort - if (Files == null || Files.Count == 0) - { - return false; - } - - // If output directory is empty, use the current folder - if (outDir.Trim() == "") - { - outDir = Environment.CurrentDirectory; - } - - // Create the output directory if it doesn't already exist - if (!Directory.Exists(outDir)) - { - Directory.CreateDirectory(outDir); - } - - // If the DAT has no output format, default to XML - if (DatFormat == 0) - { - DatFormat = DatFormat.Logiqx; - } - - // Make sure that the three essential fields are filled in - if (String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) - { - FileName = Name = Description = "Default"; - } - else if (String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) - { - FileName = Name = Description; - } - else if (String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) - { - FileName = Description = Name; - } - else if (String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) - { - FileName = Description; - } - else if (!String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) - { - Name = Description = FileName; - } - else if (!String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) - { - Name = Description; - } - else if (!String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) - { - Description = Name; - } - else if (!String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) - { - // Nothing is needed - } - - // Output initial statistics, for kicks - if (stats) - { - StreamWriter sw = new StreamWriter(new MemoryStream()); - OutputStats(sw, StatDatFormat.None, logger, recalculate: (RomCount + DiskCount == 0), baddumpCol: true, nodumpCol: true); - sw.Dispose(); - } - - // Bucket roms by game name and optionally dedupe - BucketByGame(MergeRoms, norename, logger); - - // Get the outfile name - Dictionary outfiles = Style.CreateOutfileNames(outDir, this, overwrite); - - try - { - // Get a properly sorted set of keys - List keys = Files.Keys.ToList(); - keys.Sort(new NaturalComparer()); - - foreach (DatFormat datFormat in outfiles.Keys) - { - string outfile = outfiles[datFormat]; - - logger.User("Opening file for writing: " + outfile); - FileStream fs = File.Create(outfile); - StreamWriter sw = new StreamWriter(fs, new UTF8Encoding(true)); - - // Write out the header - WriteHeader(sw, datFormat, logger); - - // Write out each of the machines and roms - int depth = 2, last = -1; - string lastgame = null; - List splitpath = new List(); - - foreach (string key in keys) - { - List roms = Files[key]; - - for (int index = 0; index < roms.Count; index++) - { - DatItem rom = roms[index]; - - // There are apparently times when a null rom can skip by, skip them - if (rom.Name == null || rom.Machine.Name == null) - { - logger.Warning("Null rom found!"); - continue; - } - - List newsplit = rom.Machine.Name.Split('\\').ToList(); - - // If we have a different game and we're not at the start of the list, output the end of last item - if (lastgame != null && lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant()) - { - depth = WriteEndGame(sw, datFormat, rom, splitpath, newsplit, lastgame, depth, out last, logger); - } - - // If we have a new game, output the beginning of the new item - if (lastgame == null || lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant()) - { - depth = WriteStartGame(sw, datFormat, rom, newsplit, lastgame, depth, last, logger); - } - - // If we have a "null" game (created by DATFromDir or something similar), log it to file - if (rom.Type == ItemType.Rom - && ((Rom)rom).Size == -1 - && ((Rom)rom).CRC == "null" - && ((Rom)rom).MD5 == "null" - && ((Rom)rom).SHA1 == "null") - { - logger.Verbose("Empty folder found: " + rom.Machine.Name); - - // If we're in a mode that doesn't allow for actual empty folders, add the blank info - if (datFormat != DatFormat.CSV - && datFormat != DatFormat.MissFile - && datFormat != DatFormat.SabreDat - && datFormat != DatFormat.TSV) - { - rom.Name = (rom.Name == "null" ? "-" : rom.Name); - ((Rom)rom).Size = Constants.SizeZero; - ((Rom)rom).CRC = Constants.CRCZero; - ((Rom)rom).MD5 = Constants.MD5Zero; - ((Rom)rom).SHA1 = Constants.SHA1Zero; - } - - // Otherwise, set the new path and such, write out, and continue - else - { - splitpath = newsplit; - lastgame = rom.Machine.Name; - continue; - } - } - - // Now, output the rom data - WriteRomData(sw, datFormat, rom, lastgame, depth, logger, ignoreblanks); - - // Set the new data to compare against - splitpath = newsplit; - lastgame = rom.Machine.Name; - } - } - - // Write the file footer out - WriteFooter(sw, datFormat, depth, logger); - - logger.Verbose("File written!" + Environment.NewLine); - sw.Dispose(); - fs.Dispose(); - } - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - return false; - } - - return true; - } - - /// - /// Write out DAT header using the supplied StreamWriter - /// - /// StreamWriter to output to - /// Output format to write to - /// Logger object for file and console output - /// True if the data was written, false on error - private bool WriteHeader(StreamWriter sw, DatFormat datFormat, Logger logger) - { - try - { - string header = ""; - switch (datFormat) - { - case DatFormat.AttractMode: - header = "#Name;Title;Emulator;CloneOf;Year;Manufacturer;Category;Players;Rotation;Control;Status;DisplayCount;DisplayType;AltRomname;AltTitle;Extra;Buttons\n"; - break; - case DatFormat.ClrMamePro: - header = "clrmamepro (\n" + - "\tname \"" + Name + "\"\n" + - "\tdescription \"" + Description + "\"\n" + - (!String.IsNullOrEmpty(Category) ? "\tcategory \"" + Category + "\"\n" : "") + - "\tversion \"" + Version + "\"\n" + - (!String.IsNullOrEmpty(Date) ? "\tdate \"" + Date + "\"\n" : "") + - "\tauthor \"" + Author + "\"\n" + - (!String.IsNullOrEmpty(Email) ? "\temail \"" + Email + "\"\n" : "") + - (!String.IsNullOrEmpty(Homepage) ? "\thomepage \"" + Homepage + "\"\n" : "") + - (!String.IsNullOrEmpty(Url) ? "\turl \"" + Url + "\"\n" : "") + - (!String.IsNullOrEmpty(Comment) ? "\tcomment \"" + Comment + "\"\n" : "") + - (ForcePacking == ForcePacking.Unzip ? "\tforcezipping no\n" : "") + - (ForcePacking == ForcePacking.Zip ? "\tforcezipping yes\n" : "") + - (ForceMerging == ForceMerging.Full ? "\tforcemerging full\n" : "") + - (ForceMerging == ForceMerging.Split ? "\tforcemerging split\n" : "") + - ")\n"; - break; - case DatFormat.CSV: - header = "\"File Name\",\"Internal Name\",\"Description\",\"Game Name\",\"Game Description\",\"Type\",\"" + - "Rom Name\",\"Disk Name\",\"Size\",\"CRC\",\"MD5\",\"SHA1\",\"Nodump\"\n"; - break; - case DatFormat.DOSCenter: - header = "DOSCenter (\n" + - "\tName: " + Name + "\n" + - "\tDescription: " + Description + "\n" + - "\tVersion: " + Version + "\n" + - "\tDate: " + Date + "\n" + - "\tAuthor: " + Author + "\n" + - "\tHomepage: " + Homepage + "\n" + - "\tComment: " + Comment + "\n" + - ")\n"; - break; - case DatFormat.Logiqx: - header = "\n" + - "\n\n" + - "\n" + - "\t
\n" + - "\t\t" + HttpUtility.HtmlEncode(Name) + "\n" + - "\t\t" + HttpUtility.HtmlEncode(Description) + "\n" + - (!String.IsNullOrEmpty(RootDir) ? "\t\t" + HttpUtility.HtmlEncode(RootDir) + "\n" : "") + - (!String.IsNullOrEmpty(Category) ? "\t\t" + HttpUtility.HtmlEncode(Category) + "\n" : "") + - "\t\t" + HttpUtility.HtmlEncode(Version) + "\n" + - (!String.IsNullOrEmpty(Date) ? "\t\t" + HttpUtility.HtmlEncode(Date) + "\n" : "") + - "\t\t" + HttpUtility.HtmlEncode(Author) + "\n" + - (!String.IsNullOrEmpty(Email) ? "\t\t" + HttpUtility.HtmlEncode(Email) + "\n" : "") + - (!String.IsNullOrEmpty(Homepage) ? "\t\t" + HttpUtility.HtmlEncode(Homepage) + "\n" : "") + - (!String.IsNullOrEmpty(Url) ? "\t\t" + HttpUtility.HtmlEncode(Url) + "\n" : "") + - (!String.IsNullOrEmpty(Comment) ? "\t\t" + HttpUtility.HtmlEncode(Comment) + "\n" : "") + - (!String.IsNullOrEmpty(Type) ? "\t\t" + HttpUtility.HtmlEncode(Type) + "\n" : "") + - (ForcePacking != ForcePacking.None || ForceMerging != ForceMerging.None || ForceNodump != ForceNodump.None ? - "\t\t\n" - : "") + - "\t
\n"; - break; - case DatFormat.TSV: - header = "\"File Name\"\t\"Internal Name\"\t\"Description\"\t\"Game Name\"\t\"Game Description\"\t\"Type\"\t\"" + - "Rom Name\"\t\"Disk Name\"\t\"Size\"\t\"CRC\"\t\"MD5\"\t\"SHA1\"\t\"Nodump\"\n"; - break; - case DatFormat.OfflineList: - header = "\n" - + "\n" - + "\t\n" - + "\t\t" + HttpUtility.HtmlEncode(Name) + "\n" - + "\t\t" + Files.Count + "\n" - + "\t\tnone\n" - + "\t\t240\n" - + "\t\t160\n" - + "\t\t\n" - + "\t\t\t\n" - + "\t\t\t<location visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" - + "\t\t\t<publisher visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" - + "\t\t\t<sourceRom visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" - + "\t\t\t<saveType visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" - + "\t\t\t<romSize visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" - + "\t\t\t<releaseNumber visible=\"true\" inNamingOption=\"true\" default=\"false\"/>\n" - + "\t\t\t<languageNumber visible=\"true\" inNamingOption=\"true\" default=\"false\"/>\n" - + "\t\t\t<comment visible=\"true\" inNamingOption=\"true\" default=\"false\"/>\n" - + "\t\t\t<romCRC visible=\"true\" inNamingOption=\"true\" default=\"false\"/>\n" - + "\t\t\t<im1CRC visible=\"false\" inNamingOption=\"false\" default=\"false\"/>\n" - + "\t\t\t<im2CRC visible=\"false\" inNamingOption=\"false\" default=\"false\"/>\n" - + "\t\t\t<languages visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" - + "\t\t</infos>\n" - + "\t\t<canOpen>\n" - + "\t\t\t<extension>.bin</extension>\n" - + "\t\t</canOpen>\n" - + "\t\t<newDat>\n" - + "\t\t\t<datVersionURL>" + HttpUtility.HtmlEncode(Url) + "</datVersionURL>\n" - + "\t\t\t<datURL fileName=\"" + HttpUtility.HtmlEncode(FileName) + ".zip\">" + HttpUtility.HtmlEncode(Url) + "</datURL>\n" - + "\t\t\t<imURL>" + HttpUtility.HtmlEncode(Url) + "</imURL>\n" - + "\t\t</newDat>\n" - + "\t\t<search>\n" - + "\t\t\t<to value=\"location\" default=\"true\" auto=\"true\"/>\n" - + "\t\t\t<to value=\"romSize\" default=\"true\" auto=\"false\"/>\n" - + "\t\t\t<to value=\"languages\" default=\"true\" auto=\"true\"/>\n" - + "\t\t\t<to value=\"saveType\" default=\"false\" auto=\"false\"/>\n" - + "\t\t\t<to value=\"publisher\" default=\"false\" auto=\"true\"/>\n" - + "\t\t\t<to value=\"sourceRom\" default=\"false\" auto=\"true\"/>\n" - + "\t\t</search>\n" - + "\t\t<romTitle >%u - %n</romTitle>\n" - + "\t</configuration>\n" - + "\t<games>\n"; - break; - case DatFormat.RomCenter: - header = "[CREDITS]\n" + - "author=" + Author + "\n" + - "version=" + Version + "\n" + - "comment=" + Comment + "\n" + - "[DAT]\n" + - "version=2.50\n" + - "split=" + (ForceMerging == ForceMerging.Split ? "1" : "0") + "\n" + - "merge=" + (ForceMerging == ForceMerging.Full ? "1" : "0") + "\n" + - "[EMULATOR]\n" + - "refname=" + Name + "\n" + - "version=" + Description + "\n" + - "[GAMES]\n"; - break; - case DatFormat.SabreDat: - header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + - "<!DOCTYPE sabredat SYSTEM \"newdat.xsd\">\n\n" + - "<datafile>\n" + - "\t<header>\n" + - "\t\t<name>" + HttpUtility.HtmlEncode(Name) + "</name>\n" + - "\t\t<description>" + HttpUtility.HtmlEncode(Description) + "</description>\n" + - (!String.IsNullOrEmpty(RootDir) ? "\t\t<rootdir>" + HttpUtility.HtmlEncode(RootDir) + "</rootdir>\n" : "") + - (!String.IsNullOrEmpty(Category) ? "\t\t<category>" + HttpUtility.HtmlEncode(Category) + "</category>\n" : "") + - "\t\t<version>" + HttpUtility.HtmlEncode(Version) + "</version>\n" + - (!String.IsNullOrEmpty(Date) ? "\t\t<date>" + HttpUtility.HtmlEncode(Date) + "</date>\n" : "") + - "\t\t<author>" + HttpUtility.HtmlEncode(Author) + "</author>\n" + - (!String.IsNullOrEmpty(Comment) ? "\t\t<comment>" + HttpUtility.HtmlEncode(Comment) + "</comment>\n" : "") + - (!String.IsNullOrEmpty(Type) || ForcePacking != ForcePacking.None || ForceMerging != ForceMerging.None || ForceNodump != ForceNodump.None ? - "\t\t<flags>\n" + - (!String.IsNullOrEmpty(Type) ? "\t\t\t<flag name=\"type\" value=\"" + HttpUtility.HtmlEncode(Type) + "\"/>\n" : "") + - (ForcePacking == ForcePacking.Unzip ? "\t\t\t<flag name=\"forcepacking\" value=\"unzip\"/>\n" : "") + - (ForcePacking == ForcePacking.Zip ? "\t\t\t<flag name=\"forcepacking\" value=\"zip\"/>\n" : "") + - (ForceMerging == ForceMerging.Full ? "\t\t\t<flag name=\"forcemerging\" value=\"full\"/>\n" : "") + - (ForceMerging == ForceMerging.Split ? "\t\t\t<flag name=\"forcemerging\" value=\"split\"/>\n" : "") + - (ForceNodump == ForceNodump.Ignore ? "\t\t\t<flag name=\"forceitemStatus\" value=\"ignore\"/>\n" : "") + - (ForceNodump == ForceNodump.Obsolete ? "\t\t\t<flag name=\"forceitemStatus\" value=\"obsolete\"/>\n" : "") + - (ForceNodump == ForceNodump.Required ? "\t\t\t<flag name=\"forceitemStatus\" value=\"required\"/>\n" : "") + - "\t\t</flags>\n" - : "") + - "\t</header>\n" + - "\t<data>\n"; - break; - case DatFormat.SoftwareList: - header = "<?xml version=\"1.0\"?>\n" + - "<!DOCTYPE softwarelist SYSTEM \"softwarelist.dtd\">\n\n" + - "<softwarelist name=\"" + HttpUtility.HtmlEncode(Name) + "\"" + - " description=\"" + HttpUtility.HtmlEncode(Description) + "\"" + - (ForcePacking == ForcePacking.Unzip ? " forcepacking=\"unzip\"" : "") + - (ForcePacking == ForcePacking.Zip ? " forcepacking=\"zip\"" : "") + - (ForceMerging == ForceMerging.Full ? " forcemerging=\"full\"" : "") + - (ForceMerging == ForceMerging.Split ? " forcemerging=\"split\"" : "") + - (ForceNodump == ForceNodump.Ignore ? " forceitemStatus=\"ignore\"" : "") + - (ForceNodump == ForceNodump.Obsolete ? " forceitemStatus=\"obsolete\"" : "") + - (ForceNodump == ForceNodump.Required ? " forceitemStatus=\"required\"" : "") + - ">\n\n"; - break; - } - - // Write the header out - sw.Write(header); - sw.Flush(); - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - return false; - } - - return true; - } - - /// <summary> - /// Write out Game start using the supplied StreamWriter - /// </summary> - /// <param name="sw">StreamWriter to output to</param> - /// <param name="datFormat">Output format to write to</param> - /// <param name="rom">RomData object to be output</param> - /// <param name="newsplit">Split path representing the parent game (SabreDAT only)</param> - /// <param name="lastgame">The name of the last game to be output</param> - /// <param name="depth">Current depth to output file at (SabreDAT only)</param> - /// <param name="last">Last known depth to cycle back from (SabreDAT only)</param> - /// <param name="logger">Logger object for file and console output</param> - /// <returns>The new depth of the tag</returns> - private int WriteStartGame(StreamWriter sw, DatFormat datFormat, DatItem rom, List<string> newsplit, string lastgame, int depth, int last, Logger logger) - { - try - { - // No game should start with a path separator - if (rom.Machine.Name.StartsWith(Path.DirectorySeparatorChar.ToString())) - { - rom.Machine.Name = rom.Machine.Name.Substring(1); - } - - string state = ""; - switch (datFormat) - { - case DatFormat.AttractMode: - state += rom.Machine.Description + ";" - + rom.Machine.Name + ";" - + FileName + ";" - + rom.Machine.CloneOf + ";" - + rom.Machine.Year + ";" - + rom.Machine.Manufacturer + ";" - /* + rom.Machine.Category */ + ";" - /* + rom.Machine.Players */ + ";" - /* + rom.Machine.Rotation */ + ";" - /* + rom.Machine.Control */ + ";" - /* + rom.Machine.Status */ + ";" - /* + rom.Machine.DisplayCount */ + ";" - /* + rom.Machine.DisplayType */ + ";" - /* + rom.Machine.AltRomname */ + ";" - /* + rom.Machine.AltTitle */ + ";" - + rom.Machine.Comment + ";" - /* + rom.Machine.Buttons */ + "\n"; - break; - case DatFormat.ClrMamePro: - state += "game (\n\tname \"" + rom.Machine.Name + "\"\n" + - (ExcludeOf ? "" : - (String.IsNullOrEmpty(rom.Machine.RomOf) ? "" : "\tromof \"" + rom.Machine.RomOf + "\"\n") + - (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : "\tcloneof \"" + rom.Machine.CloneOf + "\"\n") + - (String.IsNullOrEmpty(rom.Machine.SampleOf) ? "" : "\tsampleof \"" + rom.Machine.SampleOf + "\"\n") - ) + - "\tdescription \"" + (String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description) + "\"\n" + - (String.IsNullOrEmpty(rom.Machine.Year) ? "" : "\tyear " + rom.Machine.Year + "\n") + - (String.IsNullOrEmpty(rom.Machine.Manufacturer) ? "" : "\tmanufacturer \"" + rom.Machine.Manufacturer + "\"\n"); - break; - case DatFormat.DOSCenter: - state += "game (\n\tname \"" + rom.Machine.Name + ".zip\"\n"; - break; - case DatFormat.Logiqx: - state += "\t<machine name=\"" + HttpUtility.HtmlEncode(rom.Machine.Name) + "\"" + - (rom.Machine.IsBios ? " isbios=\"yes\"" : "") + - (ExcludeOf ? "" : - (String.IsNullOrEmpty(rom.Machine.CloneOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.CloneOf.ToLowerInvariant()) - ? "" - : " cloneof=\"" + HttpUtility.HtmlEncode(rom.Machine.CloneOf) + "\"") + - (String.IsNullOrEmpty(rom.Machine.RomOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.RomOf.ToLowerInvariant()) - ? "" - : " romof=\"" + HttpUtility.HtmlEncode(rom.Machine.RomOf) + "\"") + - (String.IsNullOrEmpty(rom.Machine.SampleOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.SampleOf.ToLowerInvariant()) - ? "" - : " sampleof=\"" + HttpUtility.HtmlEncode(rom.Machine.SampleOf) + "\"") - ) + - ">\n" + - (String.IsNullOrEmpty(rom.Machine.Comment) ? "" : "\t\t<comment>" + HttpUtility.HtmlEncode(rom.Machine.Comment) + "</comment>\n") + - "\t\t<description>" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description)) + "</description>\n" + - (String.IsNullOrEmpty(rom.Machine.Year) ? "" : "\t\t<year>" + HttpUtility.HtmlEncode(rom.Machine.Year) + "</year>\n") + - (String.IsNullOrEmpty(rom.Machine.Manufacturer) ? "" : "\t\t<manufacturer>" + HttpUtility.HtmlEncode(rom.Machine.Manufacturer) + "</manufacturer>\n"); - break; - case DatFormat.SabreDat: - for (int i = (last == -1 ? 0 : last); i < newsplit.Count; i++) - { - for (int j = 0; j < depth - last + i - (lastgame == null ? 1 : 0); j++) - { - state += "\t"; - } - state += "<directory name=\"" + HttpUtility.HtmlEncode(newsplit[i]) + "\" description=\"" + - HttpUtility.HtmlEncode(newsplit[i]) + "\">\n"; - } - depth = depth - (last == -1 ? 0 : last) + newsplit.Count; - break; - case DatFormat.SoftwareList: - state += "\t<software name=\"" + HttpUtility.HtmlEncode(rom.Machine.Name) + "\"" - + (rom.Supported != null ? " supported=\"" + (rom.Supported == true ? "yes" : "no") + "\"" : "") + - (ExcludeOf ? "" : - (String.IsNullOrEmpty(rom.Machine.CloneOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.CloneOf.ToLowerInvariant()) - ? "" - : " cloneof=\"" + HttpUtility.HtmlEncode(rom.Machine.CloneOf) + "\"") + - (String.IsNullOrEmpty(rom.Machine.RomOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.RomOf.ToLowerInvariant()) - ? "" - : " romof=\"" + HttpUtility.HtmlEncode(rom.Machine.RomOf) + "\"") + - (String.IsNullOrEmpty(rom.Machine.SampleOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.SampleOf.ToLowerInvariant()) - ? "" - : " sampleof=\"" + HttpUtility.HtmlEncode(rom.Machine.SampleOf) + "\"") - ) + ">\n" - + "\t\t<description>" + HttpUtility.HtmlEncode(rom.Machine.Description) + "</description>\n" - + (rom.Machine.Year != null ? "\t\t<year>" + HttpUtility.HtmlEncode(rom.Machine.Year) + "</year>\n" : "") - + (rom.Publisher != null ? "\t\t<publisher>" + HttpUtility.HtmlEncode(rom.Publisher) + "</publisher>\n" : ""); - - foreach (Tuple<string, string> kvp in rom.Infos) - { - state += "\t\t<info name=\"" + HttpUtility.HtmlEncode(kvp.Item1) + "\" value=\"" + HttpUtility.HtmlEncode(kvp.Item2) + "\" />\n"; - } - break; - } - - sw.Write(state); - sw.Flush(); - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - return depth; - } - - return depth; - } - - /// <summary> - /// Write out Game start using the supplied StreamWriter - /// </summary> - /// <param name="sw">StreamWriter to output to</param> - /// <param name="datFormat">Output format to write to</param> - /// <param name="rom">RomData object to be output</param> - /// <param name="splitpath">Split path representing last kwown parent game (SabreDAT only)</param> - /// <param name="newsplit">Split path representing the parent game (SabreDAT only)</param> - /// <param name="lastgame">The name of the last game to be output</param> - /// <param name="depth">Current depth to output file at (SabreDAT only)</param> - /// <param name="last">Last known depth to cycle back from (SabreDAT only)</param> - /// <param name="logger">Logger object for file and console output</param> - /// <returns>The new depth of the tag</returns> - private int WriteEndGame(StreamWriter sw, DatFormat datFormat, DatItem rom, List<string> splitpath, List<string> newsplit, string lastgame, int depth, out int last, Logger logger) - { - last = 0; - - try - { - string state = ""; - - switch (datFormat) - { - case DatFormat.ClrMamePro: - case DatFormat.DOSCenter: - state += (String.IsNullOrEmpty(rom.Machine.SampleOf) ? "" : "\tsampleof \"" + rom.Machine.SampleOf + "\"\n") + ")\n"; - break; - case DatFormat.Logiqx: - state += "\t</machine>\n"; - break; - case DatFormat.OfflineList: - state += "\t\t</game>\n"; - break; - case DatFormat.SabreDat: - if (splitpath != null) - { - for (int i = 0; i < newsplit.Count && i < splitpath.Count; i++) - { - // Always keep track of the last seen item - last = i; - - // If we find a difference, break - if (newsplit[i] != splitpath[i]) - { - break; - } - } - - // Now that we have the last known position, take down all open folders - for (int i = depth - 1; i > last + 1; i--) - { - // Print out the number of tabs and the end folder - for (int j = 0; j < i; j++) - { - state += "\t"; - } - state += "</directory>\n"; - } - - // Reset the current depth - depth = 2 + last; - } - break; - case DatFormat.SoftwareList: - state += "\t</software>\n\n"; - break; - } - - sw.Write(state); - sw.Flush(); - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - return depth; - } - - return depth; - } - - /// <summary> - /// Write out RomData using the supplied StreamWriter - /// </summary> - /// <param name="sw">StreamWriter to output to</param> - /// <param name="datFormat">Output format to write to</param> - /// <param name="rom">RomData object to be output</param> - /// <param name="lastgame">The name of the last game to be output</param> - /// <param name="depth">Current depth to output file at (SabreDAT only)</param> - /// <param name="logger">Logger object for file and console output</param> - /// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param> - /// <returns>True if the data was written, false on error</returns> - private bool WriteRomData(StreamWriter sw, DatFormat datFormat, DatItem rom, string lastgame, int depth, Logger logger, bool ignoreblanks = false) - { - // If we are in ignore blanks mode AND we have a blank (0-size) rom, skip - if (ignoreblanks - && (rom.Type == ItemType.Rom - && (((Rom)rom).Size == 0 || ((Rom)rom).Size == -1))) - { - return true; - } - - try - { - string state = "", name = "", pre = "", post = ""; - switch (datFormat) - { - case DatFormat.ClrMamePro: - switch (rom.Type) - { - case ItemType.Archive: - state += "\tarchive ( name\"" + rom.Name + "\"" - + " )\n"; - break; - case ItemType.BiosSet: - state += "\tbiosset ( name\"" + rom.Name + "\"" - + (!String.IsNullOrEmpty(((BiosSet)rom).Description) ? " description \"" + ((BiosSet)rom).Description + "\"" : "") - + (((BiosSet)rom).Default != null - ? "default " + ((BiosSet)rom).Default.ToString().ToLowerInvariant() - : "") - + " )\n"; - break; - case ItemType.Disk: - state += "\tdisk ( name \"" + rom.Name + "\"" - + (!String.IsNullOrEmpty(((Disk)rom).MD5) ? " md5 " + ((Disk)rom).MD5.ToLowerInvariant() : "") - + (!String.IsNullOrEmpty(((Disk)rom).SHA1) ? " sha1 " + ((Disk)rom).SHA1.ToLowerInvariant() : "") - + (((Disk)rom).ItemStatus != ItemStatus.None ? " flags " + ((Disk)rom).ItemStatus.ToString().ToLowerInvariant() : "") - + " )\n"; - break; - case ItemType.Release: - state += "\trelease ( name\"" + rom.Name + "\"" - + (!String.IsNullOrEmpty(((Release)rom).Region) ? " region \"" + ((Release)rom).Region + "\"" : "") - + (!String.IsNullOrEmpty(((Release)rom).Language) ? " language \"" + ((Release)rom).Language + "\"" : "") - + (!String.IsNullOrEmpty(((Release)rom).Date) ? " date \"" + ((Release)rom).Date + "\"" : "") - + (((Release)rom).Default != null - ? "default " + ((Release)rom).Default.ToString().ToLowerInvariant() - : "") - + " )\n"; - break; - case ItemType.Rom: - state += "\trom ( name \"" + rom.Name + "\"" - + (((Rom)rom).Size != -1 ? " size " + ((Rom)rom).Size : "") - + (!String.IsNullOrEmpty(((Rom)rom).CRC) ? " crc " + ((Rom)rom).CRC.ToLowerInvariant() : "") - + (!String.IsNullOrEmpty(((Rom)rom).MD5) ? " md5 " + ((Rom)rom).MD5.ToLowerInvariant() : "") - + (!String.IsNullOrEmpty(((Rom)rom).SHA1) ? " sha1 " + ((Rom)rom).SHA1.ToLowerInvariant() : "") - + (!String.IsNullOrEmpty(((Rom)rom).Date) ? " date \"" + ((Rom)rom).Date + "\"" : "") - + (((Rom)rom).ItemStatus != ItemStatus.None ? " flags " + ((Rom)rom).ItemStatus.ToString().ToLowerInvariant() : "") - + " )\n"; - break; - case ItemType.Sample: - state += "\tsample ( name\"" + rom.Name + "\"" - + " )\n"; - break; - } - - break; - case DatFormat.CSV: - // CSV should only output Rom and Disk - if (rom.Type != ItemType.Disk && rom.Type != ItemType.Rom) - { - return true; - } - - pre = Prefix + (Quotes ? "\"" : ""); - post = (Quotes ? "\"" : "") + Postfix; - - if (rom.Type == ItemType.Rom) - { - // Check for special strings in prefix and postfix - pre = pre - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%crc%", ((Rom)rom).CRC) - .Replace("%md5%", ((Rom)rom).MD5) - .Replace("%sha1%", ((Rom)rom).SHA1) - .Replace("%size%", ((Rom)rom).Size.ToString()); - post = post - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%crc%", ((Rom)rom).CRC) - .Replace("%md5%", ((Rom)rom).MD5) - .Replace("%sha1%", ((Rom)rom).SHA1) - .Replace("%size%", ((Rom)rom).Size.ToString()); - } - else if (rom.Type == ItemType.Disk) - { - // Check for special strings in prefix and postfix - pre = pre - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%md5%", ((Disk)rom).MD5) - .Replace("%sha1%", ((Disk)rom).SHA1); - post = post - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%md5%", ((Disk)rom).MD5) - .Replace("%sha1%", ((Disk)rom).SHA1); - } - - if (rom.Type == ItemType.Rom) - { - string inline = "\"" + FileName + "\"" - + ",\"" + Name + "\"" - + ",\"" + Description + "\"" - + ",\"" + rom.Machine.Name + "\"" - + ",\"" + rom.Machine.Description + "\"" - + "," + "\"rom\"" - + ",\"" + rom.Name + "\"" - + "," + "\"\"" - + ",\"" + ((Rom)rom).Size + "\"" - + ",\"" + ((Rom)rom).CRC + "\"" - + ",\"" + ((Rom)rom).MD5 + "\"" - + ",\"" + ((Rom)rom).SHA1 + "\"" - + "," + (((Rom)rom).ItemStatus != ItemStatus.None ? "\"" + ((Rom)rom).ItemStatus.ToString() + "\"" : "\"\""); - state += pre + inline + post + "\n"; - } - else if (rom.Type == ItemType.Disk) - { - string inline = "\"" + FileName + "\"" - + ",\"" + Name + "\"" - + ",\"" + Description + "\"" - + ",\"" + rom.Machine.Name + "\"" - + ",\"" + rom.Machine.Description + "\"" - + "," + "\"disk\"" - + "," + "\"\"" - + ",\"" + rom.Name + "\"" - + "," + "\"\"" - + "," + "\"\"" - + ",\"" + ((Disk)rom).MD5 + "\"" - + ",\"" + ((Disk)rom).SHA1 + "\"" - + "," + (((Disk)rom).ItemStatus != ItemStatus.None ? "\"" + ((Disk)rom).ItemStatus.ToString() + "\"" : "\"\""); - state += pre + inline + post + "\n"; - } - break; - case DatFormat.DOSCenter: - switch (rom.Type) - { - case ItemType.Archive: - case ItemType.BiosSet: - case ItemType.Disk: - case ItemType.Release: - case ItemType.Sample: - // We don't output these at all - break; - case ItemType.Rom: - state += "\tfile ( name " + ((Rom)rom).Name - + (((Rom)rom).Size != -1 ? " size " + ((Rom)rom).Size : "") - + (!String.IsNullOrEmpty(((Rom)rom).Date) ? " date " + ((Rom)rom).Date : "") - + (!String.IsNullOrEmpty(((Rom)rom).CRC) ? " crc " + ((Rom)rom).CRC.ToLowerInvariant() : "") - + " )\n"; - break; - } - break; - case DatFormat.Logiqx: - switch (rom.Type) - { - case ItemType.Archive: - state += "\t\t<archive name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" - + "/>\n"; - break; - case ItemType.BiosSet: - state += "\t\t<biosset name\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" - + (!String.IsNullOrEmpty(((BiosSet)rom).Description) ? " description=\"" + HttpUtility.HtmlEncode(((BiosSet)rom).Description) + "\"" : "") - + (((BiosSet)rom).Default != null - ? ((BiosSet)rom).Default.ToString().ToLowerInvariant() - : "") - + "/>\n"; - break; - case ItemType.Disk: - state += "\t\t<disk name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" - + (!String.IsNullOrEmpty(((Disk)rom).MD5) ? " md5=\"" + ((Disk)rom).MD5.ToLowerInvariant() + "\"" : "") - + (!String.IsNullOrEmpty(((Disk)rom).SHA1) ? " sha1=\"" + ((Disk)rom).SHA1.ToLowerInvariant() + "\"" : "") - + (((Disk)rom).ItemStatus != ItemStatus.None ? " status=\"" + ((Disk)rom).ItemStatus.ToString().ToLowerInvariant() + "\"" : "") - + "/>\n"; - break; - case ItemType.Release: - state += "\t\t<release name\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" - + (!String.IsNullOrEmpty(((Release)rom).Region) ? " region=\"" + HttpUtility.HtmlEncode(((Release)rom).Region) + "\"" : "") - + (!String.IsNullOrEmpty(((Release)rom).Language) ? " language=\"" + HttpUtility.HtmlEncode(((Release)rom).Language) + "\"" : "") - + (!String.IsNullOrEmpty(((Release)rom).Date) ? " date=\"" + HttpUtility.HtmlEncode(((Release)rom).Date) + "\"" : "") - + (((Release)rom).Default != null - ? ((Release)rom).Default.ToString().ToLowerInvariant() - : "") - + "/>\n"; - break; - case ItemType.Rom: - state += "\t\t<rom name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" - + (((Rom)rom).Size != -1 ? " size=\"" + ((Rom)rom).Size + "\"" : "") - + (!String.IsNullOrEmpty(((Rom)rom).CRC) ? " crc=\"" + ((Rom)rom).CRC.ToLowerInvariant() + "\"" : "") - + (!String.IsNullOrEmpty(((Rom)rom).MD5) ? " md5=\"" + ((Rom)rom).MD5.ToLowerInvariant() + "\"" : "") - + (!String.IsNullOrEmpty(((Rom)rom).SHA1) ? " sha1=\"" + ((Rom)rom).SHA1.ToLowerInvariant() + "\"" : "") - + (!String.IsNullOrEmpty(((Rom)rom).Date) ? " date=\"" + ((Rom)rom).Date + "\"" : "") - + (((Rom)rom).ItemStatus != ItemStatus.None ? " status=\"" + ((Rom)rom).ItemStatus.ToString().ToLowerInvariant() + "\"" : "") - + "/>\n"; - break; - case ItemType.Sample: - state += "\t\t<file type=\"sample\" name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" - + "/>\n"; - break; - } - break; - case DatFormat.MissFile: - // Missfile should only output Rom and Disk - if (rom.Type != ItemType.Disk && rom.Type != ItemType.Rom) - { - return true; - } - - pre = Prefix + (Quotes ? "\"" : ""); - post = (Quotes ? "\"" : "") + Postfix; - - if (rom.Type == ItemType.Rom) - { - // Check for special strings in prefix and postfix - pre = pre - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%crc%", ((Rom)rom).CRC) - .Replace("%md5%", ((Rom)rom).MD5) - .Replace("%sha1%", ((Rom)rom).SHA1) - .Replace("%size%", ((Rom)rom).Size.ToString()); - post = post - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%crc%", ((Rom)rom).CRC) - .Replace("%md5%", ((Rom)rom).MD5) - .Replace("%sha1%", ((Rom)rom).SHA1) - .Replace("%size%", ((Rom)rom).Size.ToString()); - } - else if (rom.Type == ItemType.Disk) - { - // Check for special strings in prefix and postfix - pre = pre - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%md5%", ((Disk)rom).MD5) - .Replace("%sha1%", ((Disk)rom).SHA1); - post = post - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%md5%", ((Disk)rom).MD5) - .Replace("%sha1%", ((Disk)rom).SHA1); - } - - // If we're in Romba mode, the state is consistent - if (Romba) - { - if (rom.Type == ItemType.Rom) - { - // We can only write out if there's a SHA-1 - if (((Rom)rom).SHA1 != "") - { - name = ((Rom)rom).SHA1.Substring(0, 2) - + "/" + ((Rom)rom).SHA1.Substring(2, 2) - + "/" + ((Rom)rom).SHA1.Substring(4, 2) - + "/" + ((Rom)rom).SHA1.Substring(6, 2) - + "/" + ((Rom)rom).SHA1 + ".gz"; - state += pre + name + post + "\n"; - } - } - else if (rom.Type == ItemType.Disk) - { - // We can only write out if there's a SHA-1 - if (((Disk)rom).SHA1 != "") - { - name = ((Disk)rom).SHA1.Substring(0, 2) - + "/" + ((Disk)rom).SHA1.Substring(2, 2) - + "/" + ((Disk)rom).SHA1.Substring(4, 2) - + "/" + ((Disk)rom).SHA1.Substring(6, 2) - + "/" + ((Disk)rom).SHA1 + ".gz"; - state += pre + name + post + "\n"; - } - } - } - - // Otherwise, use any flags - name = (UseGame ? rom.Machine.Name : rom.Name); - if (RepExt != "" || RemExt) - { - if (RemExt) - { - RepExt = ""; - } - - string dir = Path.GetDirectoryName(name); - dir = (dir.StartsWith(Path.DirectorySeparatorChar.ToString()) ? dir.Remove(0, 1) : dir); - name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + RepExt); - } - if (AddExt != "") - { - name += AddExt; - } - if (!UseGame && GameName) - { - name = Path.Combine(rom.Machine.Name, name); - } - - if (UseGame && rom.Machine.Name != lastgame) - { - state += pre + name + post + "\n"; - lastgame = rom.Machine.Name; - } - else if (!UseGame) - { - state += pre + name + post + "\n"; - } - break; - case DatFormat.OfflineList: - state += "\t\t<game>\n" - + "\t\t\t<imageNumber>1</imageNumber>\n" - + "\t\t\t<releaseNumber>1</releaseNumber>\n" - + "\t\t\t<title>" + HttpUtility.HtmlEncode(rom.Name) + "\n" - + "\t\t\tNone\n"; - - if (rom.Type == ItemType.Rom) - { - state += "\t\t\t" + ((Rom)rom).Size + "\n"; - } - - state += "\t\t\tNone\n" - + "\t\t\t0\n" - + "\t\t\tNone\n" - + "\t\t\t0\n"; - - if (rom.Type == ItemType.Disk) - { - state += "\t\t\t\n" - + (((Disk)rom).MD5 != null - ? "\t\t\t\t" + ((Disk)rom).MD5.ToUpperInvariant() + "\n" - : "\t\t\t\t" + ((Disk)rom).SHA1.ToUpperInvariant() + "\n") - + "\t\t\t\n"; - } - else if (rom.Type == ItemType.Rom) - { - string tempext = Path.GetExtension(((Rom)rom).Name); - if (!tempext.StartsWith(".")) - { - tempext = "." + tempext; - } - - state += "\t\t\t\n" - + (((Rom)rom).CRC != null - ? "\t\t\t\t" + ((Rom)rom).CRC.ToUpperInvariant() + "\n" - : ((Rom)rom).MD5 != null - ? "\t\t\t\t" + ((Rom)rom).MD5.ToUpperInvariant() + "\n" - : "\t\t\t\t" + ((Rom)rom).SHA1.ToUpperInvariant() + "\n") - + "\t\t\t\n"; - } - - state += "\t\t\t00000000\n" - + "\t\t\t00000000\n" - + "\t\t\t\n" - + "\t\t\t0\n" - + "\t\t\n"; - break; - case DatFormat.RedumpMD5: - if (rom.Type == ItemType.Rom) - { - state += ((Rom)rom).MD5 + " *" + (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + "\n"; - } - else if (rom.Type == ItemType.Disk) - { - state += ((Disk)rom).MD5 + " *" + (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + "\n"; - } - break; - case DatFormat.RedumpSFV: - if (rom.Type == ItemType.Rom) - { - state += (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + " " + ((Rom)rom).CRC + "\n"; - } - break; - case DatFormat.RedumpSHA1: - if (rom.Type == ItemType.Rom) - { - state += ((Rom)rom).SHA1 + " *" + (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + "\n"; - } - else if (rom.Type == ItemType.Disk) - { - state += ((Disk)rom).SHA1 + " *" + (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + "\n"; - } - break; - case DatFormat.RomCenter: - if (rom.Type == ItemType.Rom) - { - state += "¬" + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.Machine.CloneOf)) + - "¬" + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.Machine.CloneOf)) + - "¬" + HttpUtility.HtmlEncode(rom.Machine.Name) + - "¬" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description)) + - "¬" + HttpUtility.HtmlEncode(rom.Name) + - "¬" + ((Rom)rom).CRC.ToLowerInvariant() + - "¬" + (((Rom)rom).Size != -1 ? ((Rom)rom).Size.ToString() : "") + "¬¬¬\n"; - } - else if (rom.Type == ItemType.Disk) - { - state += "¬" + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.Machine.CloneOf)) + - "¬" + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.Machine.CloneOf)) + - "¬" + HttpUtility.HtmlEncode(rom.Machine.Name) + - "¬" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description)) + - "¬" + HttpUtility.HtmlEncode(rom.Name) + - "¬¬¬¬¬\n"; - } - - break; - case DatFormat.SabreDat: - string prefix = ""; - for (int i = 0; i < depth; i++) - { - prefix += "\t"; - } - state += prefix; - - switch (rom.Type) - { - case ItemType.Archive: - state += "\n"; - break; - case ItemType.BiosSet: - state += "\n"; - break; - case ItemType.Disk: - state += "\n" + prefix + "\t\n" + - prefix + "\t\t\n" + - prefix + "\t\n" + - prefix + "\n" : "/>\n"); - break; - case ItemType.Release: - state += "\n"; - break; - case ItemType.Rom: - state += "\n" + prefix + "\t\n" + - prefix + "\t\t\n" + - prefix + "\t\n" + - prefix + "\n" : "/>\n"); - break; - case ItemType.Sample: - state += "\n"; - break; - } - break; - case DatFormat.SoftwareList: - state += "\t\t\n"; - - foreach (Tuple kvp in rom.Features) - { - state += "\t\t\t\n"; - } - - switch (rom.Type) - { - case ItemType.Archive: - state += "\t\t\t\n" - + "\t\t\t\t\n" - + "\t\t\t\n"; - break; - case ItemType.BiosSet: - state += "\t\t\t\n" - + "\t\t\t\t\n" - + "\t\t\t\n"; - break; - case ItemType.Disk: - state += "\t\t\t\n" - + "\t\t\t\t\n" - + "\t\t\t\n"; - break; - case ItemType.Release: - state += "\t\t\t\n" - + "\t\t\t\t\n" - + "\t\t\t\n"; - break; - case ItemType.Rom: - state += "\t\t\t\n" - + "\t\t\t\t\n" - + "\t\t\t\n"; - break; - case ItemType.Sample: - state += "\t\t\t\n" - + "\t\t\t\t\n" - + "\t\t\t\n"; - break; - } - - state += "\t\t\n"; - break; - case DatFormat.TSV: - // TSV should only output Rom and Disk - if (rom.Type != ItemType.Disk && rom.Type != ItemType.Rom) - { - return true; - } - - pre = Prefix + (Quotes ? "\"" : ""); - post = (Quotes ? "\"" : "") + Postfix; - - if (rom.Type == ItemType.Rom) - { - // Check for special strings in prefix and postfix - pre = pre - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%crc%", ((Rom)rom).CRC) - .Replace("%md5%", ((Rom)rom).MD5) - .Replace("%sha1%", ((Rom)rom).SHA1) - .Replace("%size%", ((Rom)rom).Size.ToString()); - post = post - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%crc%", ((Rom)rom).CRC) - .Replace("%md5%", ((Rom)rom).MD5) - .Replace("%sha1%", ((Rom)rom).SHA1) - .Replace("%size%", ((Rom)rom).Size.ToString()); - } - else if (rom.Type == ItemType.Disk) - { - // Check for special strings in prefix and postfix - pre = pre - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%md5%", ((Disk)rom).MD5) - .Replace("%sha1%", ((Disk)rom).SHA1); - post = post - .Replace("%game%", rom.Machine.Name) - .Replace("%name%", rom.Name) - .Replace("%md5%", ((Disk)rom).MD5) - .Replace("%sha1%", ((Disk)rom).SHA1); - } - - if (rom.Type == ItemType.Rom) - { - string inline = "\"" + FileName + "\"" - + "\t\"" + Name + "\"" - + "\t\"" + Description + "\"" - + "\t\"" + rom.Machine.Name + "\"" - + "\t\"" + rom.Machine.Description + "\"" - + "\t" + "\"rom\"" - + "\t\"" + rom.Name + "\"" - + "\t" + "\"\"" - + "\t\"" + ((Rom)rom).Size + "\"" - + "\t\"" + ((Rom)rom).CRC + "\"" - + "\t\"" + ((Rom)rom).MD5 + "\"" - + "\t\"" + ((Rom)rom).SHA1 + "\"" - + "\t" + (((Rom)rom).ItemStatus != ItemStatus.None ? "\"" + ((Rom)rom).ItemStatus.ToString() + "\"" : "\"\""); - state += pre + inline + post + "\n"; - } - else if (rom.Type == ItemType.Disk) - { - string inline = "\"" + FileName + "\"" - + "\t\"" + Name + "\"" - + "\t\"" + Description + "\"" - + "\t\"" + rom.Machine.Name + "\"" - + "\t\"" + rom.Machine.Description + "\"" - + "\t" + "\"disk\"" - + "\t" + "\"\"" - + "\t\"" + rom.Name + "\"" - + "\t" + "\"\"" - + "\t" + "\"\"" - + "\t\"" + ((Disk)rom).MD5 + "\"" - + "\t\"" + ((Disk)rom).SHA1 + "\"" - + "\t" + (((Disk)rom).ItemStatus != ItemStatus.None ? "\"" + ((Disk)rom).ItemStatus.ToString() + "\"" : "\"\""); - state += pre + inline + post + "\n"; - } - break; - } - - sw.Write(state); - sw.Flush(); - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - return false; - } - - return true; - } - - /// - /// Write out DAT footer using the supplied StreamWriter - /// - /// StreamWriter to output to - /// Output format to write to - /// Current depth to output file at (SabreDAT only) - /// Logger object for file and console output - /// True if the data was written, false on error - private bool WriteFooter(StreamWriter sw, DatFormat datFormat, int depth, Logger logger) - { - try - { - string footer = ""; - - // If we have roms, output the full footer - if (Files != null && Files.Count > 0) - { - switch (datFormat) - { - case DatFormat.ClrMamePro: - case DatFormat.DOSCenter: - footer = ")\n"; - break; - case DatFormat.Logiqx: - footer = "\t\n
\n"; - break; - case DatFormat.OfflineList: - footer = "\t\t" - + "\t\n" - + "\t\n" - + "\t\t\n" - + "\t\t\t\n" - + "\t\t\t\n" - + "\t\t\n" - + "\t\n" - + ""; - break; - case DatFormat.SabreDat: - for (int i = depth - 1; i >= 2; i--) - { - // Print out the number of tabs and the end folder - for (int j = 0; j < i; j++) - { - footer += "\t"; - } - footer += "\n"; - } - footer += "\t\n\n"; - break; - case DatFormat.SoftwareList: - footer = "\t\n\n\n"; - break; - } - } - - // Otherwise, output the abbreviated form - else - { - switch (datFormat) - { - case DatFormat.Logiqx: - case DatFormat.SabreDat: - footer = "\n"; - break; - case DatFormat.OfflineList: - footer = "\t\n" - + "\t\n" - + "\t\t\n" - + "\t\t\t\n" - + "\t\t\t\n" - + "\t\t\n" - + "\t\n" - + ""; - break; - case DatFormat.SoftwareList: - footer = "\n"; - break; - } - } - - // Write the footer out - sw.Write(footer); - sw.Flush(); - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - return false; - } - - return true; - } - - #endregion - #endregion // Instance Methods - - #region Static Methods - - #region Bucketing [MODULAR DONE] - - /// - /// Take an arbitrarily ordered List and return a Dictionary sorted by Game - /// - /// Input unsorted list - /// True if roms should be deduped, false otherwise - /// True if games should only be compared on game and file name, false if system and source are counted - /// Logger object for file and console output - /// True if the number of hashes counted is to be output (default), false otherwise - /// SortedDictionary bucketed by game name - public static SortedDictionary> BucketListByGame(List list, bool mergeroms, bool norename, Logger logger, bool output = true) - { - logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms for output"); - - SortedDictionary> sortable = new SortedDictionary>(); - long count = 0; - - // If we have a null dict or an empty one, output a new dictionary - if (list == null || list.Count == 0) - { - return sortable; - } - - // If we're merging the roms, do so - if (mergeroms) - { - list = DatItem.Merge(list, logger); - } - - // Now add each of the roms to their respective games - foreach (DatItem rom in list) - { - if (rom == null) - { - continue; - } - - count++; - string newkey = (norename ? "" - : rom.SystemID.ToString().PadLeft(10, '0') - + "-" - + rom.SourceID.ToString().PadLeft(10, '0') + "-") - + (rom.Machine == null || String.IsNullOrEmpty(rom.Machine.Name) - ? "Default" - : rom.Machine.Name.ToLowerInvariant()); - newkey = HttpUtility.HtmlEncode(newkey); - if (sortable.ContainsKey(newkey)) - { - sortable[newkey].Add(rom); - } - else - { - List temp = new List(); - temp.Add(rom); - sortable.Add(newkey, temp); - } - } - - return sortable; - } - - #endregion - - #region Statistics [MODULAR DONE] - - /// - /// Output the stats for a list of input dats as files in a human-readable format - /// - /// List of input files and folders - /// Name of the output file - /// True if single DAT stats are output, false otherwise - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - /// Set the statistics output format to use - /// Logger object for file and console output - public static void OutputStats(List inputs, string reportName, bool single, bool baddumpCol, - bool nodumpCol, StatDatFormat statDatFormat, Logger logger) - { - reportName += OutputStatsGetExtension(statDatFormat); - StreamWriter sw = new StreamWriter(File.Open(reportName, FileMode.Create, FileAccess.Write)); - - // Make sure we have all files - List> newinputs = new List>(); // item, basepath - foreach (string input in inputs) - { - if (File.Exists(input)) - { - newinputs.Add(Tuple.Create(Path.GetFullPath(input), Path.GetDirectoryName(Path.GetFullPath(input)))); - } - if (Directory.Exists(input)) - { - foreach (string file in Directory.GetFiles(input, "*", SearchOption.AllDirectories)) - { - newinputs.Add(Tuple.Create(Path.GetFullPath(file), Path.GetFullPath(input))); - } - } - } - newinputs = newinputs - .OrderBy(i => Path.GetDirectoryName(i.Item1)) - .ThenBy(i => Path.GetFileName(i.Item1)) - .ToList(); - - // Write the header, if any - OutputStatsWriteHeader(sw, statDatFormat, baddumpCol, nodumpCol); - - // Init all total variables - long totalSize = 0; - long totalGame = 0; - long totalRom = 0; - long totalDisk = 0; - long totalCRC = 0; - long totalMD5 = 0; - long totalSHA1 = 0; - long totalBaddump = 0; - long totalNodump = 0; - - // Init directory-level variables - string lastdir = null; - string basepath = null; - long dirSize = 0; - long dirGame = 0; - long dirRom = 0; - long dirDisk = 0; - long dirCRC = 0; - long dirMD5 = 0; - long dirSHA1 = 0; - long dirBaddump = 0; - long dirNodump = 0; - - // Now process each of the input files - foreach (Tuple filename in newinputs) - { - // Get the directory for the current file - string thisdir = Path.GetDirectoryName(filename.Item1); - basepath = Path.GetDirectoryName(filename.Item2); - - // If we don't have the first file and the directory has changed, show the previous directory stats and reset - if (lastdir != null && thisdir != lastdir) - { - // Output separator if needed - OutputStatsWriteMidSeparator(sw, statDatFormat, baddumpCol, nodumpCol); - - DatFile lastdirdat = new DatFile - { - FileName = "DIR: " + HttpUtility.HtmlEncode(lastdir.Remove(0, basepath.Length + (basepath.Length == 0 ? 0 : 1))), - TotalSize = dirSize, - RomCount = dirRom, - DiskCount = dirDisk, - CRCCount = dirCRC, - MD5Count = dirMD5, - SHA1Count = dirSHA1, - BaddumpCount = dirBaddump, - NodumpCount = dirNodump, - }; - lastdirdat.OutputStats(sw, statDatFormat, logger, game: dirGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); - - // Write the mid-footer, if any - OutputStatsWriteMidFooter(sw, statDatFormat, baddumpCol, nodumpCol); - - // Write the header, if any - OutputStatsWriteMidHeader(sw, statDatFormat, baddumpCol, nodumpCol); - - // Reset the directory stats - dirSize = 0; - dirGame = 0; - dirRom = 0; - dirDisk = 0; - dirCRC = 0; - dirMD5 = 0; - dirSHA1 = 0; - dirBaddump = 0; - dirNodump = 0; - } - - logger.Verbose("Beginning stat collection for '" + filename.Item1 + "'", false); - List games = new List(); - DatFile datdata = new DatFile(); - datdata.Parse(filename.Item1, 0, 0, logger); - datdata.BucketByGame(false, true, logger, false); - - // Output single DAT stats (if asked) - logger.User("Adding stats for file '" + filename.Item1 + "'\n", false); - if (single) - { - datdata.OutputStats(sw, statDatFormat, logger, baddumpCol: baddumpCol, nodumpCol: nodumpCol); - } - - // Add single DAT stats to dir - dirSize += datdata.TotalSize; - dirGame += datdata.Files.Count; - dirRom += datdata.RomCount; - dirDisk += datdata.DiskCount; - dirCRC += datdata.CRCCount; - dirMD5 += datdata.MD5Count; - dirSHA1 += datdata.SHA1Count; - dirBaddump += datdata.BaddumpCount; - dirNodump += datdata.NodumpCount; - - // Add single DAT stats to totals - totalSize += datdata.TotalSize; - totalGame += datdata.Files.Count; - totalRom += datdata.RomCount; - totalDisk += datdata.DiskCount; - totalCRC += datdata.CRCCount; - totalMD5 += datdata.MD5Count; - totalSHA1 += datdata.SHA1Count; - totalBaddump += datdata.BaddumpCount; - totalNodump += datdata.NodumpCount; - - // Make sure to assign the new directory - lastdir = thisdir; - } - - // Output the directory stats one last time - OutputStatsWriteMidSeparator(sw, statDatFormat, baddumpCol, nodumpCol); - - if (single) - { - DatFile dirdat = new DatFile - { - FileName = "DIR: " + HttpUtility.HtmlEncode(lastdir.Remove(0, basepath.Length + (basepath.Length == 0 ? 0 : 1))), - TotalSize = dirSize, - RomCount = dirRom, - DiskCount = dirDisk, - CRCCount = dirCRC, - MD5Count = dirMD5, - SHA1Count = dirSHA1, - BaddumpCount = dirBaddump, - NodumpCount = dirNodump, - }; - dirdat.OutputStats(sw, statDatFormat, logger, game: dirGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); - } - - // Write the mid-footer, if any - OutputStatsWriteMidFooter(sw, statDatFormat, baddumpCol, nodumpCol); - - // Write the header, if any - OutputStatsWriteMidHeader(sw, statDatFormat, baddumpCol, nodumpCol); - - // Reset the directory stats - dirSize = 0; - dirGame = 0; - dirRom = 0; - dirDisk = 0; - dirCRC = 0; - dirMD5 = 0; - dirSHA1 = 0; - dirNodump = 0; - - // Output total DAT stats - DatFile totaldata = new DatFile - { - FileName = "DIR: All DATs", - TotalSize = totalSize, - RomCount = totalRom, - DiskCount = totalDisk, - CRCCount = totalCRC, - MD5Count = totalMD5, - SHA1Count = totalSHA1, - BaddumpCount = totalBaddump, - NodumpCount = totalNodump, - }; - totaldata.OutputStats(sw, statDatFormat, logger, game: totalGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); - - // Output footer if needed - OutputStatsWriteFooter(sw, statDatFormat); - - sw.Flush(); - sw.Dispose(); - - logger.User(@" -Please check the log folder if the stats scrolled offscreen", false); - } - - /// - /// Get the proper extension for the stat output format - /// - /// StatDatFormat to get the extension for - /// File extension with leading period - private static string OutputStatsGetExtension(StatDatFormat statDatFormat) - { - string reportExtension = ""; - switch (statDatFormat) - { - case StatDatFormat.CSV: - reportExtension = ".csv"; - break; - case StatDatFormat.HTML: - reportExtension = ".html"; - break; - case StatDatFormat.None: - default: - reportExtension = ".txt"; - break; - case StatDatFormat.TSV: - reportExtension = ".csv"; - break; - } - return reportExtension; - } - - /// - /// Write out the header to the stream, if any exists - /// - /// StreamWriter representing the output - /// StatDatFormat representing output format - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - private static void OutputStatsWriteHeader(StreamWriter sw, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol) - { - string head = ""; - switch (statDatFormat) - { - case StatDatFormat.CSV: - break; - case StatDatFormat.HTML: - head = @" - -
- DAT Statistics Report - -
- -

DAT Statistics Report (" + DateTime.Now.ToShortDateString() + @")

- -"; - break; - case StatDatFormat.None: - default: - break; - case StatDatFormat.TSV: - break; - } - sw.Write(head); - - // Now write the mid header for those who need it - OutputStatsWriteMidHeader(sw, statDatFormat, baddumpCol, nodumpCol); - } - - /// - /// Write out the mid-header to the stream, if any exists - /// - /// StreamWriter representing the output - /// StatDatFormat representing output format - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - private static void OutputStatsWriteMidHeader(StreamWriter sw, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol) - { - string head = ""; - switch (statDatFormat) - { - case StatDatFormat.CSV: - head = "\"File Name\",\"Total Size\",\"Games\",\"Roms\",\"Disks\",\"# with CRC\",\"# with MD5\",\"# with SHA-1\"" - + (baddumpCol ? ",\"BadDumps\"" : "") + (nodumpCol ? ",\"Nodumps\"" : "") + "\n"; - break; - case StatDatFormat.HTML: - head = @" " -+ @"" -+ (baddumpCol ? "" : "") + (nodumpCol ? "" : "") + "\n"; - break; - case StatDatFormat.None: - default: - break; - case StatDatFormat.TSV: - head = "\"File Name\"\t\"Total Size\"\t\"Games\"\t\"Roms\"\t\"Disks\"\t\"# with CRC\"\t\"# with MD5\"\t\"# with SHA-1\"" - + (baddumpCol ? "\t\"BadDumps\"" : "") + (nodumpCol ? "\t\"Nodumps\"" : "") + "\n"; - break; - } - sw.Write(head); - } - - /// - /// Write out the separator to the stream, if any exists - /// - /// StreamWriter representing the output - /// StatDatFormat representing output format - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - private static void OutputStatsWriteMidSeparator(StreamWriter sw, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol) - { - string mid = ""; - switch (statDatFormat) - { - case StatDatFormat.CSV: - break; - case StatDatFormat.HTML: - mid = "\n"; - break; - case StatDatFormat.None: - default: - break; - } - sw.Write(mid); - } - - /// - /// Write out the footer-separator to the stream, if any exists - /// - /// StreamWriter representing the output - /// StatDatFormat representing output format - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - private static void OutputStatsWriteMidFooter(StreamWriter sw, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol) - { - string end = ""; - switch (statDatFormat) - { - case StatDatFormat.CSV: - end = "\n"; - break; - case StatDatFormat.HTML: - end = "\n"; - break; - case StatDatFormat.None: - default: - end = "\n"; - break; - case StatDatFormat.TSV: - end = "\n"; - break; - } - sw.Write(end); - } - - /// - /// Write out the footer to the stream, if any exists - /// - /// StreamWriter representing the output - /// StatDatFormat representing output format - private static void OutputStatsWriteFooter(StreamWriter sw, StatDatFormat statDatFormat) - { - string end = ""; - switch (statDatFormat) - { - case StatDatFormat.CSV: - break; - case StatDatFormat.HTML: - end = @"
File NameTotal SizeGamesRomsDisks# with CRC# with MD5# with SHA-1BaddumpsNodumps
- - -"; - break; - case StatDatFormat.None: - default: - break; - case StatDatFormat.TSV: - break; - } - sw.Write(end); - } - - #endregion - - #endregion // Static Methods } } diff --git a/SabreTools.Helper/Dats/Partials/DatFile.Bucketing.cs b/SabreTools.Helper/Dats/Partials/DatFile.Bucketing.cs new file mode 100644 index 00000000..fce95a1f --- /dev/null +++ b/SabreTools.Helper/Dats/Partials/DatFile.Bucketing.cs @@ -0,0 +1,494 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +using SabreTools.Helper.Data; + +namespace SabreTools.Helper.Dats +{ + public partial class DatFile + { + #region Instance Methods + + #region Bucketing [MODULAR DONE] + + /// + /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by CRC + /// + /// True if roms should be deduped, false otherwise + /// Logger object for file and console output + /// True if the number of hashes counted is to be output (default), false otherwise + public void BucketByCRC(bool mergeroms, Logger logger, bool output = true) + { + // If we already have the right sorting, trust it + if (_sortedBy == SortedBy.CRC) + { + return; + } + + // Set the sorted type + _sortedBy = SortedBy.CRC; + + SortedDictionary> sortable = new SortedDictionary>(); + long count = 0; + + // If we have a null dict or an empty one, output a new dictionary + if (Files == null || Files.Count == 0) + { + Files = sortable; + } + + logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by CRC"); + + // Process each all of the roms + List keys = Files.Keys.ToList(); + foreach (string key in keys) + { + List roms = Files[key]; + + // If we're merging the roms, do so + if (mergeroms) + { + roms = DatItem.Merge(roms, logger); + } + + // Now add each of the roms to their respective games + foreach (DatItem rom in roms) + { + count++; + string newkey = (rom.Type == ItemType.Rom ? ((Rom)rom).CRC : Constants.CRCZero); + if (sortable.ContainsKey(newkey)) + { + sortable[newkey].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + sortable.Add(newkey, temp); + } + } + } + + // Now go through and sort all of the lists + keys = sortable.Keys.ToList(); + foreach (string key in keys) + { + List sortedlist = sortable[key]; + DatItem.Sort(ref sortedlist, false); + sortable[key] = sortedlist; + } + + // Output the count if told to + if (output) + { + logger.User("A total of " + count + " file hashes will be written out to file"); + } + + // Now assign the dictionary back + Files = sortable; + } + + /// + /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by Game + /// + /// True if roms should be deduped, false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + /// Logger object for file and console output + /// True if the number of hashes counted is to be output (default), false otherwise + /// True if the game should be lowercased (default), false otherwise + public void BucketByGame(bool mergeroms, bool norename, Logger logger, bool output = true, bool lower = true) + { + // If we already have the right sorting, trust it + if (_sortedBy == SortedBy.Game) + { + return; + } + + // Set the sorted type + _sortedBy = SortedBy.Game; + + SortedDictionary> sortable = new SortedDictionary>(); + long count = 0; + + // If we have a null dict or an empty one, output a new dictionary + if (Files == null || Files.Count == 0) + { + Files = sortable; + } + + logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by game"); + + // Process each all of the roms + List keys = Files.Keys.ToList(); + foreach (string key in keys) + { + List roms = Files[key]; + + // If we're merging the roms, do so + if (mergeroms) + { + roms = DatItem.Merge(roms, logger); + } + + // Now add each of the roms to their respective games + foreach (DatItem rom in roms) + { + count++; + string newkey = (norename ? "" + : rom.SystemID.ToString().PadLeft(10, '0') + + "-" + + rom.SourceID.ToString().PadLeft(10, '0') + "-") + + (String.IsNullOrEmpty(rom.Machine.Name) + ? "Default" + : rom.Machine.Name); + if (lower) + { + newkey = newkey.ToLowerInvariant(); + } + newkey = HttpUtility.HtmlEncode(newkey); + if (sortable.ContainsKey(newkey)) + { + sortable[newkey].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + sortable.Add(newkey, temp); + } + } + } + + // Now go through and sort all of the lists + keys = sortable.Keys.ToList(); + foreach (string key in keys) + { + List sortedlist = sortable[key]; + DatItem.Sort(ref sortedlist, norename); + sortable[key] = sortedlist; + } + + // Output the count if told to + if (output) + { + logger.User("A total of " + count + " file hashes will be written out to file"); + } + + // Now assign the dictionary back + Files = sortable; + } + + /// + /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by MD5 + /// + /// True if roms should be deduped, false otherwise + /// Logger object for file and console output + /// True if the number of hashes counted is to be output (default), false otherwise + public void BucketByMD5(bool mergeroms, Logger logger, bool output = true) + { + // If we already have the right sorting, trust it + if (_sortedBy == SortedBy.MD5) + { + return; + } + + // Set the sorted type + _sortedBy = SortedBy.MD5; + + SortedDictionary> sortable = new SortedDictionary>(); + long count = 0; + + // If we have a null dict or an empty one, output a new dictionary + if (Files == null || Files.Count == 0) + { + Files = sortable; + } + + logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by MD5"); + + // Process each all of the roms + List keys = Files.Keys.ToList(); + foreach (string key in keys) + { + List roms = Files[key]; + + // If we're merging the roms, do so + if (mergeroms) + { + roms = DatItem.Merge(roms, logger); + } + + // Now add each of the roms to their respective games + foreach (DatItem rom in roms) + { + count++; + string newkey = (rom.Type == ItemType.Rom + ? ((Rom)rom).MD5 + : (rom.Type == ItemType.Disk + ? ((Disk)rom).MD5 + : Constants.MD5Zero)); + if (sortable.ContainsKey(newkey)) + { + sortable[newkey].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + sortable.Add(newkey, temp); + } + } + } + + // Now go through and sort all of the lists + keys = sortable.Keys.ToList(); + foreach (string key in keys) + { + List sortedlist = sortable[key]; + DatItem.Sort(ref sortedlist, false); + sortable[key] = sortedlist; + } + + // Output the count if told to + if (output) + { + logger.User("A total of " + count + " file hashes will be written out to file"); + } + + // Now assign the dictionary back + Files = sortable; + } + + /// + /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by SHA1 + /// + /// True if roms should be deduped, false otherwise + /// Logger object for file and console output + /// True if the number of hashes counted is to be output (default), false otherwise + public void BucketBySHA1(bool mergeroms, Logger logger, bool output = true) + { + // If we already have the right sorting, trust it + if (_sortedBy == SortedBy.SHA1) + { + return; + } + + // Set the sorted type + _sortedBy = SortedBy.SHA1; + + SortedDictionary> sortable = new SortedDictionary>(); + long count = 0; + + // If we have a null dict or an empty one, output a new dictionary + if (Files == null || Files.Count == 0) + { + Files = sortable; + } + + logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by SHA-1"); + + // Process each all of the roms + List keys = Files.Keys.ToList(); + foreach (string key in keys) + { + List roms = Files[key]; + + // If we're merging the roms, do so + if (mergeroms) + { + roms = DatItem.Merge(roms, logger); + } + + // Now add each of the roms to their respective games + foreach (DatItem rom in roms) + { + count++; + string newkey = (rom.Type == ItemType.Rom + ? ((Rom)rom).SHA1 + : (rom.Type == ItemType.Disk + ? ((Disk)rom).SHA1 + : Constants.MD5Zero)); + if (sortable.ContainsKey(newkey)) + { + sortable[newkey].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + sortable.Add(newkey, temp); + } + } + } + + // Now go through and sort all of the lists + keys = sortable.Keys.ToList(); + foreach (string key in keys) + { + List sortedlist = sortable[key]; + DatItem.Sort(ref sortedlist, false); + sortable[key] = sortedlist; + } + + // Output the count if told to + if (output) + { + logger.User("A total of " + count + " file hashes will be written out to file"); + } + + // Now assign the dictionary back + Files = sortable; + } + + /// + /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by Size + /// + /// True if roms should be deduped, false otherwise + /// Logger object for file and console output + /// True if the number of hashes counted is to be output (default), false otherwise + public void BucketBySize(bool mergeroms, Logger logger, bool output = true) + { + // If we already have the right sorting, trust it + if (_sortedBy == SortedBy.Size) + { + return; + } + + // Set the sorted type + _sortedBy = SortedBy.Size; + + SortedDictionary> sortable = new SortedDictionary>(); + long count = 0; + + // If we have a null dict or an empty one, output a new dictionary + if (Files == null || Files.Count == 0) + { + Files = sortable; + } + + logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms by size"); + + // Process each all of the roms + List keys = Files.Keys.ToList(); + foreach (string key in keys) + { + List roms = Files[key]; + + // If we're merging the roms, do so + if (mergeroms) + { + roms = DatItem.Merge(roms, logger); + } + + // Now add each of the roms to their respective games + foreach (DatItem rom in roms) + { + count++; + string newkey = (rom.Type == ItemType.Rom ? ((Rom)rom).Size.ToString() : "-1"); + if (sortable.ContainsKey(newkey)) + { + sortable[newkey].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + sortable.Add(newkey, temp); + } + } + } + + // Now go through and sort all of the lists + keys = sortable.Keys.ToList(); + foreach (string key in keys) + { + List sortedlist = sortable[key]; + DatItem.Sort(ref sortedlist, false); + sortable[key] = sortedlist; + } + + // Output the count if told to + if (output) + { + logger.User("A total of " + count + " file hashes will be written out to file"); + } + + // Now assign the dictionary back + Files = sortable; + } + + #endregion + + #endregion // Instance Methods + + #region Static Methods + + #region Bucketing [MODULAR DONE] + + /// + /// Take an arbitrarily ordered List and return a Dictionary sorted by Game + /// + /// Input unsorted list + /// True if roms should be deduped, false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + /// Logger object for file and console output + /// True if the number of hashes counted is to be output (default), false otherwise + /// SortedDictionary bucketed by game name + public static SortedDictionary> BucketListByGame(List list, bool mergeroms, bool norename, Logger logger, bool output = true) + { + logger.User("Organizing " + (mergeroms ? "and merging " : "") + "roms for output"); + + SortedDictionary> sortable = new SortedDictionary>(); + long count = 0; + + // If we have a null dict or an empty one, output a new dictionary + if (list == null || list.Count == 0) + { + return sortable; + } + + // If we're merging the roms, do so + if (mergeroms) + { + list = DatItem.Merge(list, logger); + } + + // Now add each of the roms to their respective games + foreach (DatItem rom in list) + { + if (rom == null) + { + continue; + } + + count++; + string newkey = (norename ? "" + : rom.SystemID.ToString().PadLeft(10, '0') + + "-" + + rom.SourceID.ToString().PadLeft(10, '0') + "-") + + (rom.Machine == null || String.IsNullOrEmpty(rom.Machine.Name) + ? "Default" + : rom.Machine.Name.ToLowerInvariant()); + newkey = HttpUtility.HtmlEncode(newkey); + if (sortable.ContainsKey(newkey)) + { + sortable[newkey].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + sortable.Add(newkey, temp); + } + } + + return sortable; + } + + #endregion + + #endregion // Static Methods + } +} diff --git a/SabreTools.Helper/Dats/Partials/DatFile.ConvertUpdate.cs b/SabreTools.Helper/Dats/Partials/DatFile.ConvertUpdate.cs new file mode 100644 index 00000000..d5e15249 --- /dev/null +++ b/SabreTools.Helper/Dats/Partials/DatFile.ConvertUpdate.cs @@ -0,0 +1,553 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using SabreTools.Helper.Data; +using SabreTools.Helper.Tools; + +#if MONO +using System.IO; +#else +using Alphaleonis.Win32.Filesystem; + +using SearchOption = System.IO.SearchOption; +#endif +using NaturalSort; + +namespace SabreTools.Helper.Dats +{ + public partial class DatFile + { + #region Converting and Updating [MODULAR DONE] + + /// + /// Determine if input files should be merged, diffed, or processed invidually + /// + /// Names of the input files and/or folders + /// Optional param for output directory + /// True if input files should be merged into a single file, false otherwise + /// Non-zero flag for diffing mode, zero otherwise + /// True if the cascade-diffed files should overwrite their inputs, false otherwise + /// True if the first cascaded diff file should be skipped on output, false otherwise + /// True if the date should not be appended to the default name, false otherwise [OBSOLETE] + /// True to clean the game names to WoD standard, false otherwise (default) + /// True to allow SL DATs to have game names used instead of descriptions, false otherwise (default) + /// Filter object to be passed to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Integer representing the maximum amount of parallelization to be used + /// Logging object for console and file output + public void DetermineUpdateType(List inputPaths, string outDir, bool merge, DiffMode diff, bool inplace, bool skip, + bool bare, bool clean, bool softlist, Filter filter, bool trim, bool single, string root, int maxDegreeOfParallelism, Logger logger) + { + // If we're in merging or diffing mode, use the full list of inputs + if (merge || diff != 0) + { + // Make sure there are no folders in inputs + List newInputFileNames = FileTools.GetOnlyFilesFromInputs(inputPaths, maxDegreeOfParallelism, logger, appendparent: true); + + // If we're in inverse cascade, reverse the list + if ((diff & DiffMode.ReverseCascade) != 0) + { + newInputFileNames.Reverse(); + } + + // Create a dictionary of all ROMs from the input DATs + List datHeaders = PopulateUserData(newInputFileNames, inplace, clean, softlist, + outDir, filter, trim, single, root, maxDegreeOfParallelism, logger); + + // Modify the Dictionary if necessary and output the results + if (diff != 0 && diff < DiffMode.Cascade) + { + DiffNoCascade(diff, outDir, newInputFileNames, logger); + } + // If we're in cascade and diff, output only cascaded diffs + else if (diff != 0 && diff >= DiffMode.Cascade) + { + DiffCascade(outDir, inplace, newInputFileNames, datHeaders, skip, logger); + } + // Output all entries with user-defined merge + else + { + MergeNoDiff(outDir, newInputFileNames, datHeaders, logger); + } + } + // Otherwise, loop through all of the inputs individually + else + { + Update(inputPaths, outDir, clean, softlist, filter, trim, single, root, maxDegreeOfParallelism, logger); + } + return; + } + + /// + /// Populate the user DatData object from the input files + /// + /// Filter object to be passed to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Integer representing the maximum amount of parallelization to be used + /// Logging object for console and file output + /// List of DatData objects representing headers + private List PopulateUserData(List inputs, bool inplace, bool clean, bool softlist, string outDir, + Filter filter, bool trim, bool single, string root, int maxDegreeOfParallelism, Logger logger) + { + DatFile[] datHeaders = new DatFile[inputs.Count]; + DateTime start = DateTime.Now; + logger.User("Processing individual DATs"); + + /// BEGIN + Parallel.For(0, + inputs.Count, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + i => + { + string input = inputs[i]; + logger.User("Adding DAT: " + input.Split('¬')[0]); + datHeaders[i] = new DatFile + { + DatFormat = (DatFormat != 0 ? DatFormat : 0), + Files = new SortedDictionary>(), + MergeRoms = MergeRoms, + }; + + datHeaders[i].Parse(input.Split('¬')[0], i, 0, filter, trim, single, root, logger, true, clean, softlist); + }); + + logger.User("Processing complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + logger.User("Populating internal DAT"); + Files = new SortedDictionary>(); + for (int i = 0; i < inputs.Count; i++) + { + List keys = datHeaders[i].Files.Keys.ToList(); + foreach (string key in keys) + { + if (Files.ContainsKey(key)) + { + Files[key].AddRange(datHeaders[i].Files[key]); + } + else + { + Files.Add(key, datHeaders[i].Files[key]); + } + datHeaders[i].Files.Remove(key); + } + datHeaders[i].Files = null; + } + /// END + + logger.User("Processing and populating complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + return datHeaders.ToList(); + } + + /// + /// Output non-cascading diffs + /// + /// Non-zero flag for diffing mode, zero otherwise + /// Output directory to write the DATs to + /// List of inputs to write out from + /// Logging object for console and file output + public void DiffNoCascade(DiffMode diff, string outDir, List inputs, Logger logger) + { + DateTime start = DateTime.Now; + logger.User("Initializing all output DATs"); + + // Default vars for use + string post = ""; + DatFile outerDiffData = new DatFile(); + DatFile dupeData = new DatFile(); + + // Don't have External dupes + if ((diff & DiffMode.NoDupes) != 0) + { + post = " (No Duplicates)"; + outerDiffData = (DatFile)CloneHeader(); + outerDiffData.FileName += post; + outerDiffData.Name += post; + outerDiffData.Description += post; + outerDiffData.Files = new SortedDictionary>(); + } + + // Have External dupes + if ((diff & DiffMode.Dupes) != 0) + { + post = " (Duplicates)"; + dupeData = (DatFile)CloneHeader(); + dupeData.FileName += post; + dupeData.Name += post; + dupeData.Description += post; + dupeData.Files = new SortedDictionary>(); + } + + // Create a list of DatData objects representing individual output files + List outDats = new List(); + + // Loop through each of the inputs and get or create a new DatData object + if ((diff & DiffMode.Individuals) != 0) + { + DatFile[] outDatsArray = new DatFile[inputs.Count]; + + Parallel.For(0, inputs.Count, j => + { + string innerpost = " (" + Path.GetFileNameWithoutExtension(inputs[j].Split('¬')[0]) + " Only)"; + DatFile diffData = (DatFile)CloneHeader(); + diffData.FileName += innerpost; + diffData.Name += innerpost; + diffData.Description += innerpost; + diffData.Files = new SortedDictionary>(); + outDatsArray[j] = diffData; + }); + + outDats = outDatsArray.ToList(); + } + logger.User("Initializing complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + // Now, loop through the dictionary and populate the correct DATs + start = DateTime.Now; + logger.User("Populating all output DATs"); + List keys = Files.Keys.ToList(); + foreach (string key in keys) + { + List roms = DatItem.Merge(Files[key], logger); + + if (roms != null && roms.Count > 0) + { + foreach (DatItem rom in roms) + { + // No duplicates + if ((diff & DiffMode.NoDupes) != 0 || (diff & DiffMode.Individuals) != 0) + { + if ((rom.Dupe & DupeType.Internal) != 0) + { + // Individual DATs that are output + if ((diff & DiffMode.Individuals) != 0) + { + if (outDats[rom.SystemID].Files.ContainsKey(key)) + { + outDats[rom.SystemID].Files[key].Add(rom); + } + else + { + List tl = new List(); + tl.Add(rom); + outDats[rom.SystemID].Files.Add(key, tl); + } + } + + // Merged no-duplicates DAT + if ((diff & DiffMode.NoDupes) != 0) + { + DatItem newrom = rom; + newrom.Machine.Name += " (" + Path.GetFileNameWithoutExtension(inputs[newrom.SystemID].Split('¬')[0]) + ")"; + + if (outerDiffData.Files.ContainsKey(key)) + { + outerDiffData.Files[key].Add(newrom); + } + else + { + List tl = new List(); + tl.Add(rom); + outerDiffData.Files.Add(key, tl); + } + } + } + } + + // Duplicates only + if ((diff & DiffMode.Dupes) != 0) + { + if ((rom.Dupe & DupeType.External) != 0) + { + DatItem newrom = rom; + newrom.Machine.Name += " (" + Path.GetFileNameWithoutExtension(inputs[newrom.SystemID].Split('¬')[0]) + ")"; + + if (dupeData.Files.ContainsKey(key)) + { + dupeData.Files[key].Add(newrom); + } + else + { + List tl = new List(); + tl.Add(rom); + dupeData.Files.Add(key, tl); + } + } + } + } + } + } + logger.User("Populating complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + // Finally, loop through and output each of the DATs + start = DateTime.Now; + logger.User("Outputting all created DATs"); + + // Output the difflist (a-b)+(b-a) diff + if ((diff & DiffMode.NoDupes) != 0) + { + outerDiffData.WriteToFile(outDir, logger); + } + + // Output the (ab) diff + if ((diff & DiffMode.Dupes) != 0) + { + dupeData.WriteToFile(outDir, logger); + } + + // Output the individual (a-b) DATs + if ((diff & DiffMode.Individuals) != 0) + { + for (int j = 0; j < inputs.Count; j++) + { + // If we have an output directory set, replace the path + string[] split = inputs[j].Split('¬'); + string path = outDir + (split[0] == split[1] + ? Path.GetFileName(split[0]) + : (Path.GetDirectoryName(split[0]).Remove(0, split[1].Length))); + + // If we have more than 0 roms, output + if (outDats[j].Files.Count > 0) + { + outDats[j].WriteToFile(path, logger); + } + } + } + logger.User("Outputting complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + } + + /// + /// Output cascading diffs + /// + /// Output directory to write the DATs to + /// True if cascaded diffs are outputted in-place, false otherwise + /// List of inputs to write out from + /// Dat headers used optionally + /// True if the first cascaded diff file should be skipped on output, false otherwise + /// Logging object for console and file output + public void DiffCascade(string outDir, bool inplace, List inputs, List datHeaders, bool skip, Logger logger) + { + string post = ""; + + // Create a list of DatData objects representing output files + List outDats = new List(); + + // Loop through each of the inputs and get or create a new DatData object + DateTime start = DateTime.Now; + logger.User("Initializing all output DATs"); + + DatFile[] outDatsArray = new DatFile[inputs.Count]; + + Parallel.For(0, inputs.Count, j => + { + string innerpost = " (" + Path.GetFileNameWithoutExtension(inputs[j].Split('¬')[0]) + " Only)"; + DatFile diffData; + + // If we're in inplace mode, take the appropriate DatData object already stored + if (inplace || !String.IsNullOrEmpty(outDir)) + { + diffData = datHeaders[j]; + } + else + { + diffData = (DatFile)CloneHeader(); + diffData.FileName += post; + diffData.Name += post; + diffData.Description += post; + } + diffData.Files = new SortedDictionary>(); + + outDatsArray[j] = diffData; + }); + + outDats = outDatsArray.ToList(); + logger.User("Initializing complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + // Now, loop through the dictionary and populate the correct DATs + start = DateTime.Now; + logger.User("Populating all output DATs"); + List keys = Files.Keys.ToList(); + + foreach (string key in keys) + { + List roms = DatItem.Merge(Files[key], logger); + + if (roms != null && roms.Count > 0) + { + foreach (DatItem rom in roms) + { + // There's odd cases where there are items with System ID < 0. Skip them for now + if (rom.SystemID < 0) + { + logger.Warning("Item found with a <0 SystemID: " + rom.Name); + continue; + } + + if (outDats[rom.SystemID].Files.ContainsKey(key)) + { + outDats[rom.SystemID].Files[key].Add(rom); + } + else + { + List tl = new List(); + tl.Add(rom); + outDats[rom.SystemID].Files.Add(key, tl); + } + } + } + } + logger.User("Populating complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + // Finally, loop through and output each of the DATs + start = DateTime.Now; + logger.User("Outputting all created DATs"); + for (int j = (skip ? 1 : 0); j < inputs.Count; j++) + { + // If we have an output directory set, replace the path + string path = ""; + if (inplace) + { + path = Path.GetDirectoryName(inputs[j].Split('¬')[0]); + } + else if (!String.IsNullOrEmpty(outDir)) + { + string[] split = inputs[j].Split('¬'); + path = outDir + (split[0] == split[1] + ? Path.GetFileName(split[0]) + : (Path.GetDirectoryName(split[0]).Remove(0, split[1].Length))); ; + } + + // If we have more than 0 roms, output + if (outDats[j].Files.Count > 0) + { + outDats[j].WriteToFile(path, logger); + } + } + logger.User("Outputting complete in " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + } + + /// + /// Output user defined merge + /// + /// Output directory to write the DATs to + /// List of inputs to write out from + /// Dat headers used optionally + /// Logging object for console and file output + public void MergeNoDiff(string outDir, List inputs, List datHeaders, Logger logger) + { + // If we're in SuperDAT mode, prefix all games with their respective DATs + if (Type == "SuperDAT") + { + List keys = Files.Keys.ToList(); + foreach (string key in keys) + { + List newroms = new List(); + foreach (DatItem rom in Files[key]) + { + DatItem newrom = rom; + string filename = inputs[newrom.SystemID].Split('¬')[0]; + string rootpath = inputs[newrom.SystemID].Split('¬')[1]; + + rootpath += (rootpath == "" ? "" : Path.DirectorySeparatorChar.ToString()); + filename = filename.Remove(0, rootpath.Length); + newrom.Machine.Name = Path.GetDirectoryName(filename) + Path.DirectorySeparatorChar + + Path.GetFileNameWithoutExtension(filename) + Path.DirectorySeparatorChar + + newrom.Machine.Name; + newroms.Add(newrom); + } + Files[key] = newroms; + } + } + + // Output a DAT only if there are roms + if (Files.Count != 0) + { + WriteToFile(outDir, logger); + } + } + + /// + /// Convert, update, and filter a DAT file or set of files using a base + /// + /// Names of the input files and/or folders + /// Optional param for output directory + /// True if input files should be merged into a single file, false otherwise + /// Non-zero flag for diffing mode, zero otherwise + /// True if the cascade-diffed files should overwrite their inputs, false otherwise + /// True if the first cascaded diff file should be skipped on output, false otherwise + /// True if the date should not be appended to the default name, false otherwise [OBSOLETE] + /// True to clean the game names to WoD standard, false otherwise (default) + /// True to allow SL DATs to have game names used instead of descriptions, false otherwise (default) + /// Filter object to be passed to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Integer representing the maximum amount of parallelization to be used + /// Logging object for console and file output + public void Update(List inputFileNames, string outDir, bool clean, bool softlist, Filter filter, + bool trim, bool single, string root, int maxDegreeOfParallelism, Logger logger) + { + // Sort the input filenames + inputFileNames.Sort(new NaturalComparer()); + + Parallel.ForEach(inputFileNames, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + inputFileName => + { + // Clean the input string + if (inputFileName != "") + { + inputFileName = Path.GetFullPath(inputFileName); + } + + if (File.Exists(inputFileName)) + { + DatFile innerDatdata = (DatFile)CloneHeader(); + logger.User("Processing \"" + Path.GetFileName(inputFileName) + "\""); + innerDatdata.Parse(inputFileName, 0, 0, filter, trim, single, + root, logger, true, clean, softlist, + keepext: ((innerDatdata.DatFormat & DatFormat.TSV) != 0 || (innerDatdata.DatFormat & DatFormat.CSV) != 0)); + + // If we have roms, output them + if (innerDatdata.Files.Count != 0) + { + innerDatdata.WriteToFile((outDir == "" ? Path.GetDirectoryName(inputFileName) : outDir), logger, overwrite: (outDir != "")); + } + } + else if (Directory.Exists(inputFileName)) + { + inputFileName = Path.GetFullPath(inputFileName) + Path.DirectorySeparatorChar; + + Parallel.ForEach(Directory.EnumerateFiles(inputFileName, "*", SearchOption.AllDirectories), + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + file => + { + logger.User("Processing \"" + Path.GetFullPath(file).Remove(0, inputFileName.Length) + "\""); + DatFile innerDatdata = (DatFile)Clone(); + innerDatdata.Files = null; + innerDatdata.Parse(file, 0, 0, filter, + trim, single, root, logger, true, clean, softlist, + keepext: ((innerDatdata.DatFormat & DatFormat.TSV) != 0 || (innerDatdata.DatFormat & DatFormat.CSV) != 0)); + + // If we have roms, output them + if (innerDatdata.Files != null && innerDatdata.Files.Count != 0) + { + innerDatdata.WriteToFile((outDir == "" ? Path.GetDirectoryName(file) : outDir + Path.GetDirectoryName(file).Remove(0, inputFileName.Length - 1)), logger, overwrite: (outDir != "")); + } + }); + } + else + { + logger.Error("I'm sorry but " + inputFileName + " doesn't exist!"); + } + }); + } + + #endregion + } +} diff --git a/SabreTools.Helper/Dats/Partials/DatFile.DFD.cs b/SabreTools.Helper/Dats/Partials/DatFile.DFD.cs new file mode 100644 index 00000000..32be3846 --- /dev/null +++ b/SabreTools.Helper/Dats/Partials/DatFile.DFD.cs @@ -0,0 +1,506 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using SabreTools.Helper.Data; +using SabreTools.Helper.Tools; + +#if MONO +using System.IO; +#else +using Alphaleonis.Win32.Filesystem; + +using IOException = System.IO.IOException; +using SearchOption = System.IO.SearchOption; +#endif +using SharpCompress.Common; + +namespace SabreTools.Helper.Dats +{ + public partial class DatFile + { + #region Populate DAT from Directory [MODULAR DONE, FOR NOW] + + /// + /// Create a new Dat from a directory + /// + /// Base folder to be used in creating the DAT + /// True if MD5 hashes should be skipped over, false otherwise + /// True if SHA-1 hashes should be skipped over, false otherwise + /// True if the date should be omitted from the DAT, false otherwise + /// True if archives should be treated as files, false otherwise + /// True if GZIP archives should be treated as files, false otherwise + /// True if blank items should be created for empty folders, false otherwise + /// True if dates should be archived for all files, false otherwise + /// Name of the directory to create a temp folder in (blank is current directory) + /// Output directory to + /// True if files should be copied to the temp directory before hashing, false otherwise + /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise + /// Integer representing the maximum amount of parallelization to be used + /// Logger object for console and file output + public bool PopulateFromDir(string basePath, bool noMD5, bool noSHA1, bool bare, bool archivesAsFiles, + bool enableGzip, bool addBlanks, bool addDate, string tempDir, bool copyFiles, string headerToCheckAgainst, + int maxDegreeOfParallelism, Logger logger) + { + // If the description is defined but not the name, set the name from the description + if (String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) + { + Name = Description; + } + + // If the name is defined but not the description, set the description from the name + else if (!String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) + { + Description = Name + (bare ? "" : " (" + Date + ")"); + } + + // If neither the name or description are defined, set them from the automatic values + else if (String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) + { + Name = basePath.Split(Path.DirectorySeparatorChar).Last(); + Description = Name + (bare ? "" : " (" + Date + ")"); + } + + // Make sure the dictionary is defined + if (Files == null || Files.Keys.Count == 0) + { + Files = new SortedDictionary>(); + } + + // Process the input + if (Directory.Exists(basePath)) + { + logger.Verbose("Folder found: " + basePath); + + // Process the files in the main folder + List files = Directory.EnumerateFiles(basePath, "*", SearchOption.TopDirectoryOnly).ToList(); + Parallel.ForEach(files, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + item => + { + PopulateFromDirCheckFile(item, basePath, noMD5, noSHA1, bare, archivesAsFiles, enableGzip, addBlanks, addDate, + tempDir, copyFiles, headerToCheckAgainst, maxDegreeOfParallelism, logger); + }); + + // Find all top-level subfolders + files = Directory.EnumerateDirectories(basePath, "*", SearchOption.TopDirectoryOnly).ToList(); + Parallel.ForEach(files, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + item => + { + List subfiles = Directory.EnumerateFiles(item, "*", SearchOption.AllDirectories).ToList(); + Parallel.ForEach(subfiles, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + subitem => + { + PopulateFromDirCheckFile(subitem, basePath, noMD5, noSHA1, bare, archivesAsFiles, enableGzip, addBlanks, addDate, + tempDir, copyFiles, headerToCheckAgainst, maxDegreeOfParallelism, logger); + }); + }); + + // Process the files in all subfolders + files = Directory.EnumerateFiles(basePath, "*", SearchOption.AllDirectories).ToList(); + Parallel.ForEach(files, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + item => + { + PopulateFromDirCheckFile(item, basePath, noMD5, noSHA1, bare, archivesAsFiles, enableGzip, addBlanks, addDate, + tempDir, copyFiles, headerToCheckAgainst, maxDegreeOfParallelism, logger); + }); + + // Now find all folders that are empty, if we are supposed to + if (!Romba && addBlanks) + { + List empties = Directory.EnumerateDirectories(basePath, "*", SearchOption.AllDirectories).ToList(); + Parallel.ForEach(empties, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + dir => + { + if (Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly).Count() == 0) + { + // Get the full path for the directory + string fulldir = Path.GetFullPath(dir); + + // Set the temporary variables + string gamename = ""; + string romname = ""; + + // If we have a SuperDAT, we want anything that's not the base path as the game, and the file as the rom + if (Type == "SuperDAT") + { + gamename = fulldir.Remove(0, basePath.Length + 1); + romname = "-"; + } + + // Otherwise, we want just the top level folder as the game, and the file as everything else + else + { + gamename = fulldir.Remove(0, basePath.Length + 1).Split(Path.DirectorySeparatorChar)[0]; + romname = Path.Combine(fulldir.Remove(0, basePath.Length + 1 + gamename.Length), "-"); + } + + // Sanitize the names + if (gamename.StartsWith(Path.DirectorySeparatorChar.ToString())) + { + gamename = gamename.Substring(1); + } + if (gamename.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + gamename = gamename.Substring(0, gamename.Length - 1); + } + if (romname.StartsWith(Path.DirectorySeparatorChar.ToString())) + { + romname = romname.Substring(1); + } + if (romname.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + romname = romname.Substring(0, romname.Length - 1); + } + + logger.Verbose("Adding blank empty folder: " + gamename); + Files["null"].Add(new Rom(romname, gamename)); + } + }); + } + } + else if (File.Exists(basePath)) + { + PopulateFromDirCheckFile(basePath, Path.GetDirectoryName(Path.GetDirectoryName(basePath)), noMD5, noSHA1, bare, archivesAsFiles, enableGzip, addBlanks, addDate, + tempDir, copyFiles, headerToCheckAgainst, maxDegreeOfParallelism, logger); + } + + // Now that we're done, delete the temp folder (if it's not the default) + logger.User("Cleaning temp folder"); + try + { + if (tempDir != Path.GetTempPath()) + { + Directory.Delete(tempDir, true); + } + } + catch + { + // Just absorb the error for now + } + + return true; + } + + /// + /// Check a given file for hashes, based on current settings + /// + /// Filename of the item to be checked + /// Base folder to be used in creating the DAT + /// True if MD5 hashes should be skipped over, false otherwise + /// True if SHA-1 hashes should be skipped over, false otherwise + /// True if the date should be omitted from the DAT, false otherwise + /// True if archives should be treated as files, false otherwise + /// True if GZIP archives should be treated as files, false otherwise + /// True if blank items should be created for empty folders, false otherwise + /// True if dates should be archived for all files, false otherwise + /// Name of the directory to create a temp folder in (blank is current directory) + /// True if files should be copied to the temp directory before hashing, false otherwise + /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise + /// Integer representing the maximum amount of parallelization to be used + /// Logger object for console and file output + private void PopulateFromDirCheckFile(string item, string basePath, bool noMD5, bool noSHA1, bool bare, bool archivesAsFiles, + bool enableGzip, bool addBlanks, bool addDate, string tempDir, bool copyFiles, string headerToCheckAgainst, + int maxDegreeOfParallelism, Logger logger) + { + // Define the temporary directory + string tempSubDir = Path.GetFullPath(Path.Combine(tempDir, Path.GetRandomFileName())) + Path.DirectorySeparatorChar; + + // Special case for if we are in Romba mode (all names are supposed to be SHA-1 hashes) + if (Romba) + { + Rom rom = ArchiveTools.GetTorrentGZFileInfo(item, logger); + + // If the rom is valid, write it out + if (rom != null && rom.Name != null) + { + // Add the list if it doesn't exist already + string key = rom.Size + "-" + rom.CRC; + + lock (Files) + { + if (!Files.ContainsKey(key)) + { + Files.Add(key, new List()); + } + + Files[key].Add(rom); + logger.User("File added: " + Path.GetFileNameWithoutExtension(item) + Environment.NewLine); + } + } + else + { + logger.User("File not added: " + Path.GetFileNameWithoutExtension(item) + Environment.NewLine); + return; + } + + return; + } + + // If we're copying files, copy it first and get the new filename + string newItem = item; + string newBasePath = basePath; + if (copyFiles) + { + newBasePath = Path.Combine(tempDir, Path.GetRandomFileName()); + newItem = Path.GetFullPath(Path.Combine(newBasePath, Path.GetFullPath(item).Remove(0, basePath.Length + 1))); + Directory.CreateDirectory(Path.GetDirectoryName(newItem)); + File.Copy(item, newItem, true); + } + + // If both deep hash skip flags are set, do a quickscan + if (noMD5 && noSHA1) + { + ArchiveType? type = ArchiveTools.GetCurrentArchiveType(newItem, logger); + + // If we have an archive, scan it + if (type != null && !archivesAsFiles) + { + List extracted = ArchiveTools.GetArchiveFileInfo(newItem, logger); + + foreach (Rom rom in extracted) + { + PopulateFromDirProcessFileHelper(newItem, + rom, + basePath, + (Path.GetDirectoryName(Path.GetFullPath(item)) + Path.DirectorySeparatorChar).Remove(0, basePath.Length) + Path.GetFileNameWithoutExtension(item), + logger); + } + } + // Otherwise, just get the info on the file itself + else if (File.Exists(newItem)) + { + PopulateFromDirProcessFile(newItem, "", newBasePath, noMD5, noSHA1, addDate, headerToCheckAgainst, logger); + } + } + // Otherwise, attempt to extract the files to the temporary directory + else + { + ArchiveScanLevel asl = (archivesAsFiles ? ArchiveScanLevel.SevenZipExternal : ArchiveScanLevel.SevenZipInternal) + | (!archivesAsFiles && enableGzip ? ArchiveScanLevel.GZipInternal : ArchiveScanLevel.GZipExternal) + | (archivesAsFiles ? ArchiveScanLevel.RarExternal : ArchiveScanLevel.RarInternal) + | (archivesAsFiles ? ArchiveScanLevel.ZipExternal : ArchiveScanLevel.ZipInternal); + + bool encounteredErrors = ArchiveTools.ExtractArchive(newItem, tempSubDir, asl, logger); + + // If the file was an archive and was extracted successfully, check it + if (!encounteredErrors) + { + logger.Verbose(Path.GetFileName(item) + " treated like an archive"); + List extracted = Directory.EnumerateFiles(tempSubDir, "*", SearchOption.AllDirectories).ToList(); + Parallel.ForEach(extracted, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + entry => + { + PopulateFromDirProcessFile(entry, + Path.Combine((Type == "SuperDAT" + ? (Path.GetDirectoryName(Path.GetFullPath(item)) + Path.DirectorySeparatorChar).Remove(0, basePath.Length) + : ""), + Path.GetFileNameWithoutExtension(item)), + tempSubDir, + noMD5, + noSHA1, + addDate, + headerToCheckAgainst, + logger); + }); + } + // Otherwise, just get the info on the file itself + else if (File.Exists(newItem)) + { + PopulateFromDirProcessFile(newItem, "", newBasePath, noMD5, noSHA1, addDate, headerToCheckAgainst, logger); + } + } + + // Cue to delete the file if it's a copy + if (copyFiles && item != newItem) + { + try + { + Directory.Delete(newBasePath, true); + } + catch { } + } + + // Delete the sub temp directory + if (Directory.Exists(tempSubDir)) + { + Directory.Delete(tempSubDir, true); + } + } + + /// + /// Process a single file as a file + /// + /// File to be added + /// Parent game to be used + /// Path the represents the parent directory + /// True if MD5 hashes should be skipped over, false otherwise + /// True if SHA-1 hashes should be skipped over, false otherwise + /// True if dates should be archived for all files, false otherwise + /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise + /// Logger object for console and file output + private void PopulateFromDirProcessFile(string item, string parent, string basePath, bool noMD5, bool noSHA1, bool addDate, string headerToCheckAgainst, Logger logger) + { + logger.Verbose(Path.GetFileName(item) + " treated like a file"); + Rom rom = FileTools.GetFileInfo(item, logger, noMD5: noMD5, noSHA1: noSHA1, date: addDate, header: headerToCheckAgainst); + + PopulateFromDirProcessFileHelper(item, rom, basePath, parent, logger); + } + + /// + /// Process a single file as a file (with found Rom data) + /// + /// File to be added + /// Rom data to be used to write to file + /// Path the represents the parent directory + /// Parent game to be used + private void PopulateFromDirProcessFileHelper(string item, DatItem datItem, string basepath, string parent, Logger logger) + { + // If the datItem isn't a Rom or Disk, return + if (datItem.Type != ItemType.Rom && datItem.Type != ItemType.Disk) + { + return; + } + + string key = ""; + if (datItem.Type == ItemType.Rom) + { + key = ((Rom)datItem).Size + "-" + ((Rom)datItem).CRC; + } + else + { + key = ((Disk)datItem).MD5; + } + + // Add the list if it doesn't exist already + lock (Files) + { + if (!Files.ContainsKey(key)) + { + Files.Add(key, new List()); + } + } + + try + { + // If the basepath ends with a directory separator, remove it + if (!basepath.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + basepath += Path.DirectorySeparatorChar.ToString(); + } + + // Make sure we have the full item path + item = Path.GetFullPath(item); + + // Get the data to be added as game and item names + string gamename = ""; + string romname = ""; + + // If the parent is blank, then we have a non-archive file + if (parent == "") + { + // If we have a SuperDAT, we want anything that's not the base path as the game, and the file as the rom + if (Type == "SuperDAT") + { + gamename = Path.GetDirectoryName(item.Remove(0, basepath.Length)); + romname = Path.GetFileName(item); + } + + // Otherwise, we want just the top level folder as the game, and the file as everything else + else + { + gamename = item.Remove(0, basepath.Length).Split(Path.DirectorySeparatorChar)[0]; + romname = item.Remove(0, (Path.Combine(basepath, gamename).Length)); + } + } + + // Otherwise, we assume that we have an archive + else + { + // If we have a SuperDAT, we want the archive name as the game, and the file as everything else (?) + if (Type == "SuperDAT") + { + gamename = parent; + romname = item.Remove(0, basepath.Length); + } + + // Otherwise, we want the archive name as the game, and the file as everything else + else + { + gamename = parent; + romname = item.Remove(0, basepath.Length); + } + } + + // Sanitize the names + if (gamename.StartsWith(Path.DirectorySeparatorChar.ToString())) + { + gamename = gamename.Substring(1); + } + if (gamename.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + gamename = gamename.Substring(0, gamename.Length - 1); + } + if (romname.StartsWith(Path.DirectorySeparatorChar.ToString())) + { + romname = romname.Substring(1); + } + if (romname.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + romname = romname.Substring(0, romname.Length - 1); + } + if (!String.IsNullOrEmpty(gamename) && String.IsNullOrEmpty(romname)) + { + romname = gamename; + gamename = "Default"; + } + + // Update rom information + datItem.Name = romname; + if (datItem.Machine == null) + { + datItem.Machine = new Machine + { + Name = gamename, + Description = gamename, + }; + } + else + { + datItem.Machine.Name = gamename; + datItem.Machine.Description = gamename; + } + + // Add the file information to the DAT + lock (Files) + { + if (Files.ContainsKey(key)) + { + Files[key].Add(datItem); + } + else + { + List temp = new List(); + temp.Add(datItem); + Files.Add(key, temp); + } + } + + logger.User("File added: " + romname + Environment.NewLine); + } + catch (IOException ex) + { + logger.Error(ex.ToString()); + return; + } + } + + #endregion + } +} diff --git a/SabreTools.Helper/Dats/Partials/DatFile.Parsers.cs b/SabreTools.Helper/Dats/Partials/DatFile.Parsers.cs new file mode 100644 index 00000000..1a9ca2e7 --- /dev/null +++ b/SabreTools.Helper/Dats/Partials/DatFile.Parsers.cs @@ -0,0 +1,2361 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; + +using SabreTools.Helper.Data; +using SabreTools.Helper.Tools; + +#if MONO +using System.IO; +#else +using Alphaleonis.Win32.Filesystem; + +using StreamReader = System.IO.StreamReader; +#endif + +namespace SabreTools.Helper.Dats +{ + public partial class DatFile + { + #region Parsing [MODULAR DONE, FOR NOW] + + /// + /// Parse a DAT and return all found games and roms within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// The DatData object representing found roms to this point + /// Logger object for console and/or file output + /// True if full pathnames are to be kept, false otherwise (default) + /// True if game names are sanitized, false otherwise (default) + /// True if SL XML names should be kept, false otherwise (default) + /// True if original extension should be kept, false otherwise (default) + public void Parse(string filename, int sysid, int srcid, Logger logger, bool keep = false, bool clean = false, bool softlist = false, bool keepext = false) + { + Parse(filename, sysid, srcid, new Filter(), false, false, "", logger, keep, clean, softlist, keepext); + } + + /// + /// Parse a DAT and return all found games and roms within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + /// True if full pathnames are to be kept, false otherwise (default) + /// True if game names are sanitized, false otherwise (default) + /// True if SL XML names should be kept, false otherwise (default) + /// True if original extension should be kept, false otherwise (default) + public void Parse( + // Standard Dat parsing + string filename, + int sysid, + int srcid, + + // Rom filtering + Filter filter, + + // Rom renaming + bool trim, + bool single, + string root, + + // Miscellaneous + Logger logger, + bool keep = false, + bool clean = false, + bool softlist = false, + bool keepext = false) + { + // Check the file extension first as a safeguard + string ext = Path.GetExtension(filename).ToLowerInvariant(); + if (ext.StartsWith(".")) + { + ext = ext.Substring(1); + } + if (ext != "dat" && ext != "md5" && ext != "sfv" && ext != "sha1" && ext != "txt" && ext != "xml") + { + return; + } + + // If the output filename isn't set already, get the internal filename + FileName = (String.IsNullOrEmpty(FileName) ? (keepext ? Path.GetFileName(filename) : Path.GetFileNameWithoutExtension(filename)) : FileName); + + // If the output type isn't set already, get the internal output type + DatFormat = (DatFormat == 0 ? FileTools.GetDatFormat(filename, logger) : DatFormat); + + // Make sure there's a dictionary to read to + if (Files == null) + { + Files = new SortedDictionary>(); + } + + // Now parse the correct type of DAT + switch (FileTools.GetDatFormat(filename, logger)) + { + case DatFormat.AttractMode: + ParseAttractMode(filename, sysid, srcid, filter, trim, single, root, logger, keep, clean); + break; + case DatFormat.ClrMamePro: + case DatFormat.DOSCenter: + ParseCMP(filename, sysid, srcid, filter, trim, single, root, logger, keep, clean); + break; + case DatFormat.Logiqx: + case DatFormat.OfflineList: + case DatFormat.SabreDat: + case DatFormat.SoftwareList: + ParseGenericXML(filename, sysid, srcid, filter, trim, single, root, logger, keep, clean, softlist); + break; + case DatFormat.RedumpMD5: + ParseRedumpMD5(filename, sysid, srcid, filter, trim, single, root, logger, clean); + break; + case DatFormat.RedumpSFV: + ParseRedumpSFV(filename, sysid, srcid, filter, trim, single, root, logger, clean); + break; + case DatFormat.RedumpSHA1: + ParseRedumpSHA1(filename, sysid, srcid, filter, trim, single, root, logger, clean); + break; + case DatFormat.RomCenter: + ParseRC(filename, sysid, srcid, filter, trim, single, root, logger, clean); + break; + default: + return; + } + } + + /// + /// Parse an AttractMode DAT and return all found games within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + /// True if full pathnames are to be kept, false otherwise (default) + /// True if game names are sanitized, false otherwise (default) + private void ParseAttractMode( + // Standard Dat parsing + string filename, + int sysid, + int srcid, + + // Rom filtering + Filter filter, + + // Rom renaming + bool trim, + bool single, + string root, + + // Miscellaneous + Logger logger, + bool keep, + bool clean) + { + // Open a file reader + Encoding enc = Style.GetEncoding(filename); + StreamReader sr = new StreamReader(File.OpenRead(filename), enc); + + sr.ReadLine(); // Skip the first line since it's the header + while (!sr.EndOfStream) + { + string line = sr.ReadLine(); + + /* + The gameinfo order is as follows + 0 - game name + 1 - game description + 2 - emulator name (filename) + 3 - cloneof + 4 - year + 5 - manufacturer + 6 - category + 7 - players + 8 - rotation + 9 - control + 10 - status + 11 - displaycount + 12 - displaytype + 13 - alt romname + 14 - alt title + 15 - extra + 16 - buttons + */ + + string[] gameinfo = line.Split(';'); + + Rom rom = new Rom + { + Name = "-", + Size = Constants.SizeZero, + CRC = Constants.CRCZero, + MD5 = Constants.MD5Zero, + SHA1 = Constants.SHA1Zero, + ItemStatus = ItemStatus.None, + + Machine = new Machine + { + Name = gameinfo[0], + Description = gameinfo[1], + CloneOf = gameinfo[3], + Year = gameinfo[4], + Manufacturer = gameinfo[5], + Comment = gameinfo[15], + } + }; + + // Now process and add the rom + string key = ""; + ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); + } + + sr.Dispose(); + } + + /// + /// Parse a ClrMamePro DAT and return all found games and roms within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + /// True if full pathnames are to be kept, false otherwise (default) + /// True if game names are sanitized, false otherwise (default) + private void ParseCMP( + // Standard Dat parsing + string filename, + int sysid, + int srcid, + + // Rom filtering + Filter filter, + + // Rom renaming + bool trim, + bool single, + string root, + + // Miscellaneous + Logger logger, + bool keep, + bool clean) + { + // Open a file reader + Encoding enc = Style.GetEncoding(filename); + StreamReader sr = new StreamReader(File.OpenRead(filename), enc); + + bool block = false, superdat = false; + string blockname = "", tempgamename = "", gamedesc = "", cloneof = "", + romof = "", sampleof = "", year = "", manufacturer = ""; + while (!sr.EndOfStream) + { + string line = sr.ReadLine(); + + // Comments in CMP DATs start with a # + if (line.Trim().StartsWith("#")) + { + continue; + } + + // If the line is the header or a game + if (Regex.IsMatch(line, Constants.HeaderPatternCMP)) + { + GroupCollection gc = Regex.Match(line, Constants.HeaderPatternCMP).Groups; + + if (gc[1].Value == "clrmamepro" || gc[1].Value == "romvault" || gc[1].Value.ToLowerInvariant() == "doscenter") + { + blockname = "header"; + } + + block = true; + } + + // If the line is a rom-like item and we're in a block + else if ((line.Trim().StartsWith("rom (") + || line.Trim().StartsWith("disk (") + || line.Trim().StartsWith("file (") + || (line.Trim().StartsWith("sample") && !line.Trim().StartsWith("sampleof")) + ) && block) + { + ItemType temptype = ItemType.Rom; + if (line.Trim().StartsWith("rom (")) + { + temptype = ItemType.Rom; + } + else if (line.Trim().StartsWith("disk (")) + { + temptype = ItemType.Disk; + } + else if (line.Trim().StartsWith("file (")) + { + temptype = ItemType.Rom; + } + else if (line.Trim().StartsWith("sample")) + { + temptype = ItemType.Sample; + } + + // Create the proper DatItem based on the type + DatItem item; + switch (temptype) + { + case ItemType.Archive: + item = new Archive(); + break; + case ItemType.BiosSet: + item = new BiosSet(); + break; + case ItemType.Disk: + item = new Disk(); + break; + case ItemType.Release: + item = new Release(); + break; + case ItemType.Sample: + item = new Sample(); + break; + case ItemType.Rom: + default: + item = new Rom(); + break; + } + + // Then populate it with information + item.Machine = new Machine + { + Name = tempgamename, + Description = gamedesc, + CloneOf = cloneof, + RomOf = romof, + SampleOf = sampleof, + Manufacturer = manufacturer, + Year = year, + }; + + item.SystemID = sysid; + item.SourceID = srcid; + + // Get the blank key to write to + string key = ""; + + // If we have a sample, treat it special + if (temptype == ItemType.Sample) + { + line = line.Trim().Remove(0, 6).Trim().Replace("\"", ""); // Remove "sample" from the input string + item.Name = line; + + // Now process and add the sample + key = ""; + ParseAddHelper(item, filter, trim, single, root, clean, logger, out key); + + continue; + } + + // Get the line split by spaces and quotes + string[] gc = Style.SplitLineAsCMP(line); + + // Special cases for DOSCenter DATs only because of how the lines are arranged + if (line.Trim().StartsWith("file (")) + { + // Loop over the specifics + for (int i = 0; i < gc.Length; i++) + { + // Names are not quoted, for some stupid reason + if (gc[i] == "name") + { + // Advance to the first part of the name + i++; + item.Name = gc[i]; + + // Advance to the next item, adding until we find "size" + i++; + while (i < gc.Length && gc[i] != "size" && gc[i] != "date" && gc[i] != "crc") + { + item.Name += " " + gc[i]; + i++; + } + } + + // Get the size from the next part + else if (gc[i] == "size") + { + i++; + long tempsize = -1; + if (!Int64.TryParse(gc[i], out tempsize)) + { + tempsize = 0; + } + ((Rom)item).Size = tempsize; + i++; + } + + // Get the date from the next part + else if (gc[i] == "date") + { + i++; + ((Rom)item).Date = gc[i].Replace("\"", "") + " " + gc[i + 1].Replace("\"", ""); + i += 3; + } + + // Get the CRC from the next part + else if (gc[i] == "crc") + { + i++; + ((Rom)item).CRC = gc[i].Replace("\"", "").ToLowerInvariant(); + } + } + + // Now process and add the rom + key = ""; + ParseAddHelper(item, filter, trim, single, root, clean, logger, out key); + continue; + } + + // Loop over all attributes normally and add them if possible + for (int i = 0; i < gc.Length; i++) + { + // Look at the current item and use it if possible + string quoteless = gc[i].Replace("\"", ""); + switch (quoteless) + { + //If the item is empty, we automatically skip it because it's a fluke + case "": + continue; + + // Special cases for standalone item statuses + case "baddump": + if (item.Type == ItemType.Rom) + { + ((Rom)item).ItemStatus = ItemStatus.BadDump; + } + else if (item.Type == ItemType.Disk) + { + ((Disk)item).ItemStatus = ItemStatus.BadDump; + } + break; + case "good": + if (item.Type == ItemType.Rom) + { + ((Rom)item).ItemStatus = ItemStatus.Good; + } + else if (item.Type == ItemType.Disk) + { + ((Disk)item).ItemStatus = ItemStatus.Good; + } + break; + case "nodump": + if (item.Type == ItemType.Rom) + { + ((Rom)item).ItemStatus = ItemStatus.Nodump; + } + else if (item.Type == ItemType.Disk) + { + ((Disk)item).ItemStatus = ItemStatus.Nodump; + } + break; + case "verified": + if (item.Type == ItemType.Rom) + { + ((Rom)item).ItemStatus = ItemStatus.Verified; + } + else if (item.Type == ItemType.Disk) + { + ((Disk)item).ItemStatus = ItemStatus.Verified; + } + break; + + // Regular attributes + case "name": + i++; + quoteless = gc[i].Replace("\"", ""); + item.Name = quoteless; + break; + case "size": + if (item.Type == ItemType.Rom) + { + i++; + quoteless = gc[i].Replace("\"", ""); + long size = -1; + if (Int64.TryParse(quoteless, out size)) + { + ((Rom)item).Size = size; + } + } + + break; + case "crc": + if (item.Type == ItemType.Rom) + { + i++; + quoteless = gc[i].Replace("\"", ""); + ((Rom)item).CRC = quoteless.ToLowerInvariant(); + } + break; + case "md5": + if (item.Type == ItemType.Rom) + { + i++; + quoteless = gc[i].Replace("\"", ""); + ((Rom)item).MD5 = quoteless.ToLowerInvariant(); + } + else if (item.Type == ItemType.Disk) + { + i++; + quoteless = gc[i].Replace("\"", ""); + ((Disk)item).MD5 = quoteless.ToLowerInvariant(); + } + break; + case "sha1": + if (item.Type == ItemType.Rom) + { + i++; + quoteless = gc[i].Replace("\"", ""); + ((Rom)item).SHA1 = quoteless.ToLowerInvariant(); + } + else if (item.Type == ItemType.Disk) + { + i++; + quoteless = gc[i].Replace("\"", ""); + ((Disk)item).SHA1 = quoteless.ToLowerInvariant(); + } + break; + case "status": + case "flags": + i++; + quoteless = gc[i].Replace("\"", ""); + if (quoteless.ToLowerInvariant() == "good") + { + if (item.Type == ItemType.Rom) + { + ((Rom)item).ItemStatus = ItemStatus.Good; + } + else if (item.Type == ItemType.Disk) + { + ((Disk)item).ItemStatus = ItemStatus.Good; + } + } + else if (quoteless.ToLowerInvariant() == "baddump") + { + if (item.Type == ItemType.Rom) + { + ((Rom)item).ItemStatus = ItemStatus.BadDump; + } + else if (item.Type == ItemType.Disk) + { + ((Disk)item).ItemStatus = ItemStatus.BadDump; + } + } + else if (quoteless.ToLowerInvariant() == "nodump") + { + if (item.Type == ItemType.Rom) + { + ((Rom)item).ItemStatus = ItemStatus.Nodump; + } + else if (item.Type == ItemType.Disk) + { + ((Disk)item).ItemStatus = ItemStatus.Nodump; + } + } + else if (quoteless.ToLowerInvariant() == "verified") + { + if (item.Type == ItemType.Rom) + { + ((Rom)item).ItemStatus = ItemStatus.Verified; + } + else if (item.Type == ItemType.Disk) + { + ((Disk)item).ItemStatus = ItemStatus.Verified; + } + } + break; + case "date": + if (item.Type == ItemType.Rom) + { + i++; + quoteless = gc[i].Replace("\"", "") + " " + gc[i + 1].Replace("\"", ""); + ((Rom)item).Date = quoteless; + } + i++; + break; + } + } + + // Now process and add the rom + key = ""; + ParseAddHelper(item, filter, trim, single, root, clean, logger, out key); + } + + // If the line is anything but a rom or disk and we're in a block + else if (Regex.IsMatch(line, Constants.ItemPatternCMP) && block) + { + GroupCollection gc = Regex.Match(line, Constants.ItemPatternCMP).Groups; + + if (blockname != "header") + { + string itemval = gc[2].Value.Replace("\"", ""); + switch (gc[1].Value) + { + case "name": + tempgamename = (itemval.ToLowerInvariant().EndsWith(".zip") ? itemval.Remove(itemval.Length - 4) : itemval); + break; + case "description": + gamedesc = itemval; + break; + case "romof": + romof = itemval; + break; + case "cloneof": + cloneof = itemval; + break; + case "year": + year = itemval; + break; + case "manufacturer": + manufacturer = itemval; + break; + case "sampleof": + sampleof = itemval; + break; + } + } + else + { + string itemval = gc[2].Value.Replace("\"", ""); + + if (line.Trim().StartsWith("Name:")) + { + Name = (String.IsNullOrEmpty(Name) ? line.Substring(6) : Name); + superdat = superdat || itemval.Contains(" - SuperDAT"); + if (keep && superdat) + { + Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); + } + continue; + } + + switch (gc[1].Value) + { + case "name": + case "Name:": + Name = (String.IsNullOrEmpty(Name) ? itemval : Name); + superdat = superdat || itemval.Contains(" - SuperDAT"); + if (keep && superdat) + { + Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); + } + break; + case "description": + case "Description:": + Description = (String.IsNullOrEmpty(Description) ? itemval : Description); + break; + case "rootdir": + RootDir = (String.IsNullOrEmpty(RootDir) ? itemval : RootDir); + break; + case "category": + Category = (String.IsNullOrEmpty(Category) ? itemval : Category); + break; + case "version": + case "Version:": + Version = (String.IsNullOrEmpty(Version) ? itemval : Version); + break; + case "date": + case "Date:": + Date = (String.IsNullOrEmpty(Date) ? itemval : Date); + break; + case "author": + case "Author:": + Author = (String.IsNullOrEmpty(Author) ? itemval : Author); + break; + case "email": + Email = (String.IsNullOrEmpty(Email) ? itemval : Email); + break; + case "homepage": + case "Homepage:": + Homepage = (String.IsNullOrEmpty(Homepage) ? itemval : Homepage); + break; + case "url": + Url = (String.IsNullOrEmpty(Url) ? itemval : Url); + break; + case "comment": + case "Comment:": + Comment = (String.IsNullOrEmpty(Comment) ? itemval : Comment); + break; + case "header": + Header = (String.IsNullOrEmpty(Header) ? itemval : Header); + break; + case "type": + Type = (String.IsNullOrEmpty(Type) ? itemval : Type); + superdat = superdat || itemval.Contains("SuperDAT"); + break; + case "forcemerging": + switch (itemval) + { + case "none": + ForceMerging = ForceMerging.None; + break; + case "split": + ForceMerging = ForceMerging.Split; + break; + case "full": + ForceMerging = ForceMerging.Full; + break; + } + break; + case "forcezipping": + switch (itemval) + { + case "yes": + ForcePacking = ForcePacking.Zip; + break; + case "no": + ForcePacking = ForcePacking.Unzip; + break; + } + break; + case "forcepacking": + switch (itemval) + { + case "zip": + ForcePacking = ForcePacking.Zip; + break; + case "unzip": + ForcePacking = ForcePacking.Unzip; + break; + } + break; + } + } + } + + // If we find an end bracket that's not associated with anything else, the block is done + else if (Regex.IsMatch(line, Constants.EndPatternCMP) && block) + { + block = false; + blockname = ""; tempgamename = ""; gamedesc = ""; cloneof = ""; + romof = ""; sampleof = ""; year = ""; manufacturer = ""; + } + } + + sr.Dispose(); + } + + /// + /// Parse an XML DAT (Logiqx, OfflineList, SabreDAT, and Software List) and return all found games and roms within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + /// True if full pathnames are to be kept, false otherwise (default) + /// True if game names are sanitized, false otherwise (default) + /// True if SL XML names should be kept, false otherwise (default) + private void ParseGenericXML( + // Standard Dat parsing + string filename, + int sysid, + int srcid, + + // Rom filtering + Filter filter, + + // Rom renaming + bool trim, + bool single, + string root, + + // Miscellaneous + Logger logger, + bool keep, + bool clean, + bool softlist) + { + // Prepare all internal variables + XmlReader subreader, headreader, flagreader; + bool superdat = false, empty = true; + string key = "", date = ""; + long size = -1; + ItemStatus its = ItemStatus.None; + List parent = new List(); + + Encoding enc = Style.GetEncoding(filename); + XmlReader xtr = FileTools.GetXmlTextReader(filename, logger); + + // If we got a null reader, just return + if (xtr == null) + { + return; + } + + // Otherwise, read the file to the end + try + { + xtr.MoveToContent(); + while (!xtr.EOF) + { + // If we're ending a folder or game, take care of possibly empty games and removing from the parent + if (xtr.NodeType == XmlNodeType.EndElement && (xtr.Name == "directory" || xtr.Name == "dir")) + { + // If we didn't find any items in the folder, make sure to add the blank rom + if (empty) + { + string tempgame = String.Join("\\", parent); + Rom rom = new Rom("null", tempgame); + + // Now process and add the rom + ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); + } + + // Regardless, end the current folder + int parentcount = parent.Count; + if (parentcount == 0) + { + logger.Verbose("Empty parent: " + String.Join("\\", parent)); + empty = true; + } + + // If we have an end folder element, remove one item from the parent, if possible + if (parentcount > 0) + { + parent.RemoveAt(parent.Count - 1); + if (keep && parentcount > 1) + { + Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); + superdat = true; + } + } + } + + // We only want elements + if (xtr.NodeType != XmlNodeType.Element) + { + xtr.Read(); + continue; + } + + switch (xtr.Name) + { + // Handle MAME listxml since they're halfway between a SL and a Logiqx XML + case "mame": + if (xtr.GetAttribute("build") != null) + { + Name = (String.IsNullOrEmpty(Name) ? xtr.GetAttribute("build") : Name); + Description = (String.IsNullOrEmpty(Description) ? Name : Name); + } + xtr.Read(); + break; + // New software lists have this behavior + case "softwarelist": + if (xtr.GetAttribute("name") != null) + { + Name = (String.IsNullOrEmpty(Name) ? xtr.GetAttribute("name") : Name); + } + if (xtr.GetAttribute("description") != null) + { + Description = (String.IsNullOrEmpty(Description) ? xtr.GetAttribute("description") : Description); + } + if (xtr.GetAttribute("forcemerging") != null) + { + switch (xtr.GetAttribute("forcemerging")) + { + case "split": + ForceMerging = ForceMerging.Split; + break; + case "none": + ForceMerging = ForceMerging.None; + break; + case "full": + ForceMerging = ForceMerging.Full; + break; + } + } + if (xtr.GetAttribute("forceitemStatus") != null) + { + switch (xtr.GetAttribute("forceitemStatus")) + { + case "obsolete": + ForceNodump = ForceNodump.Obsolete; + break; + case "required": + ForceNodump = ForceNodump.Required; + break; + case "ignore": + ForceNodump = ForceNodump.Ignore; + break; + } + } + if (xtr.GetAttribute("forcepacking") != null) + { + switch (xtr.GetAttribute("forcepacking")) + { + case "zip": + ForcePacking = ForcePacking.Zip; + break; + case "unzip": + ForcePacking = ForcePacking.Unzip; + break; + } + } + xtr.Read(); + break; + // Handle M1 DATs since they're 99% the same as a SL DAT + case "m1": + Name = (String.IsNullOrEmpty(Name) ? "M1" : Name); + Description = (String.IsNullOrEmpty(Description) ? "M1" : Description); + if (xtr.GetAttribute("version") != null) + { + Version = (String.IsNullOrEmpty(Version) ? xtr.GetAttribute("version") : Version); + } + xtr.Read(); + break; + // OfflineList has a different header format + case "configuration": + headreader = xtr.ReadSubtree(); + + // If there's no subtree to the header, skip it + if (headreader == null) + { + xtr.Skip(); + continue; + } + + // Otherwise, read what we can from the header + while (!headreader.EOF) + { + // We only want elements + if (headreader.NodeType != XmlNodeType.Element || headreader.Name == "configuration") + { + headreader.Read(); + continue; + } + + // Get all header items (ONLY OVERWRITE IF THERE'S NO DATA) + string content = ""; + switch (headreader.Name.ToLowerInvariant()) + { + case "datname": + content = headreader.ReadElementContentAsString(); ; + Name = (String.IsNullOrEmpty(Name) ? content : Name); + superdat = superdat || content.Contains(" - SuperDAT"); + if (keep && superdat) + { + Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); + } + break; + case "datversionurl": + content = headreader.ReadElementContentAsString(); ; + Url = (String.IsNullOrEmpty(Name) ? content : Url); + break; + default: + headreader.Read(); + break; + } + } + + break; + // We want to process the entire subtree of the header + case "header": + headreader = xtr.ReadSubtree(); + + // If there's no subtree to the header, skip it + if (headreader == null) + { + xtr.Skip(); + continue; + } + + // Otherwise, read what we can from the header + while (!headreader.EOF) + { + // We only want elements + if (headreader.NodeType != XmlNodeType.Element || headreader.Name == "header") + { + headreader.Read(); + continue; + } + + // Get all header items (ONLY OVERWRITE IF THERE'S NO DATA) + string content = ""; + switch (headreader.Name) + { + case "name": + content = headreader.ReadElementContentAsString(); ; + Name = (String.IsNullOrEmpty(Name) ? content : Name); + superdat = superdat || content.Contains(" - SuperDAT"); + if (keep && superdat) + { + Type = (String.IsNullOrEmpty(Type) ? "SuperDAT" : Type); + } + break; + case "description": + content = headreader.ReadElementContentAsString(); + Description = (String.IsNullOrEmpty(Description) ? content : Description); + break; + case "rootdir": + content = headreader.ReadElementContentAsString(); + RootDir = (String.IsNullOrEmpty(RootDir) ? content : RootDir); + break; + case "category": + content = headreader.ReadElementContentAsString(); + Category = (String.IsNullOrEmpty(Category) ? content : Category); + break; + case "version": + content = headreader.ReadElementContentAsString(); + Version = (String.IsNullOrEmpty(Version) ? content : Version); + break; + case "date": + content = headreader.ReadElementContentAsString(); + Date = (String.IsNullOrEmpty(Date) ? content.Replace(".", "/") : Date); + break; + case "author": + content = headreader.ReadElementContentAsString(); + Author = (String.IsNullOrEmpty(Author) ? content : Author); + + // Special cases for SabreDAT + Email = (String.IsNullOrEmpty(Email) && !String.IsNullOrEmpty(headreader.GetAttribute("email")) ? + headreader.GetAttribute("email") : Email); + Homepage = (String.IsNullOrEmpty(Homepage) && !String.IsNullOrEmpty(headreader.GetAttribute("homepage")) ? + headreader.GetAttribute("homepage") : Email); + Url = (String.IsNullOrEmpty(Url) && !String.IsNullOrEmpty(headreader.GetAttribute("url")) ? + headreader.GetAttribute("url") : Email); + break; + case "email": + content = headreader.ReadElementContentAsString(); + Email = (String.IsNullOrEmpty(Email) ? content : Email); + break; + case "homepage": + content = headreader.ReadElementContentAsString(); + Homepage = (String.IsNullOrEmpty(Homepage) ? content : Homepage); + break; + case "url": + content = headreader.ReadElementContentAsString(); + Url = (String.IsNullOrEmpty(Url) ? content : Url); + break; + case "comment": + content = headreader.ReadElementContentAsString(); + Comment = (String.IsNullOrEmpty(Comment) ? content : Comment); + break; + case "type": + content = headreader.ReadElementContentAsString(); + Type = (String.IsNullOrEmpty(Type) ? content : Type); + superdat = superdat || content.Contains("SuperDAT"); + break; + case "clrmamepro": + case "romcenter": + if (headreader.GetAttribute("header") != null) + { + Header = (String.IsNullOrEmpty(Header) ? headreader.GetAttribute("header") : Header); + } + if (headreader.GetAttribute("plugin") != null) + { + Header = (String.IsNullOrEmpty(Header) ? headreader.GetAttribute("plugin") : Header); + } + if (headreader.GetAttribute("forcemerging") != null) + { + switch (headreader.GetAttribute("forcemerging")) + { + case "split": + ForceMerging = ForceMerging.Split; + break; + case "none": + ForceMerging = ForceMerging.None; + break; + case "full": + ForceMerging = ForceMerging.Full; + break; + } + } + if (headreader.GetAttribute("forceitemStatus") != null) + { + switch (headreader.GetAttribute("forceitemStatus")) + { + case "obsolete": + ForceNodump = ForceNodump.Obsolete; + break; + case "required": + ForceNodump = ForceNodump.Required; + break; + case "ignore": + ForceNodump = ForceNodump.Ignore; + break; + } + } + if (headreader.GetAttribute("forcepacking") != null) + { + switch (headreader.GetAttribute("forcepacking")) + { + case "zip": + ForcePacking = ForcePacking.Zip; + break; + case "unzip": + ForcePacking = ForcePacking.Unzip; + break; + } + } + headreader.Read(); + break; + case "flags": + flagreader = xtr.ReadSubtree(); + + // If we somehow have a null flag section, skip it + if (flagreader == null) + { + xtr.Skip(); + continue; + } + + while (!flagreader.EOF) + { + // We only want elements + if (flagreader.NodeType != XmlNodeType.Element || flagreader.Name == "flags") + { + flagreader.Read(); + continue; + } + + switch (flagreader.Name) + { + case "flag": + if (flagreader.GetAttribute("name") != null && flagreader.GetAttribute("value") != null) + { + content = flagreader.GetAttribute("value"); + switch (flagreader.GetAttribute("name")) + { + case "type": + Type = (String.IsNullOrEmpty(Type) ? content : Type); + superdat = superdat || content.Contains("SuperDAT"); + break; + case "forcemerging": + switch (content) + { + case "split": + ForceMerging = ForceMerging.Split; + break; + case "none": + ForceMerging = ForceMerging.None; + break; + case "full": + ForceMerging = ForceMerging.Full; + break; + } + break; + case "forceitemStatus": + switch (content) + { + case "obsolete": + ForceNodump = ForceNodump.Obsolete; + break; + case "required": + ForceNodump = ForceNodump.Required; + break; + case "ignore": + ForceNodump = ForceNodump.Ignore; + break; + } + break; + case "forcepacking": + switch (content) + { + case "zip": + ForcePacking = ForcePacking.Zip; + break; + case "unzip": + ForcePacking = ForcePacking.Unzip; + break; + } + break; + } + } + flagreader.Read(); + break; + default: + flagreader.Read(); + break; + } + } + headreader.Skip(); + break; + default: + headreader.Read(); + break; + } + } + + // Skip the header node now that we've processed it + xtr.Skip(); + break; + case "machine": + case "game": + case "software": + string temptype = xtr.Name, publisher = "", partname = "", partinterface = "", areaname = ""; + bool? supported = null; + long? areasize = null; + List> infos = new List>(); + List> features = new List>(); + + // We want to process the entire subtree of the game + subreader = xtr.ReadSubtree(); + + // Safeguard for interesting case of "software" without anything except roms + bool software = false; + + // If we have an empty machine, skip it + if (subreader == null) + { + xtr.Skip(); + continue; + } + + // Otherwise, add what is possible + subreader.MoveToContent(); + + // Create a new machine + Machine machine = new Machine + { + Name = xtr.GetAttribute("name"), + Description = xtr.GetAttribute("name"), + RomOf = xtr.GetAttribute("romof") ?? "", + CloneOf = xtr.GetAttribute("cloneof") ?? "", + SampleOf = xtr.GetAttribute("sampleof") ?? "", + }; + + if (subreader.GetAttribute("supported") != null) + { + switch (subreader.GetAttribute("supported")) + { + case "no": + supported = false; + break; + case "yes": + supported = true; + break; + } + } + + if (superdat && !keep) + { + string tempout = Regex.Match(machine.Name, @".*?\\(.*)").Groups[1].Value; + if (tempout != "") + { + machine.Name = tempout; + } + } + // Get the name of the game from the parent + else if (superdat && keep && parent.Count > 0) + { + machine.Name = String.Join("\\", parent) + "\\" + machine.Name; + } + + // Special offline list parts + string ext = ""; + string releaseNumber = ""; + + while (software || !subreader.EOF) + { + software = false; + + // We only want elements + if (subreader.NodeType != XmlNodeType.Element) + { + if (subreader.NodeType == XmlNodeType.EndElement && subreader.Name == "part") + { + partname = ""; + partinterface = ""; + features = new List>(); + } + if (subreader.NodeType == XmlNodeType.EndElement && (subreader.Name == "dataarea" || subreader.Name == "diskarea")) + { + areaname = ""; + areasize = null; + } + + subreader.Read(); + continue; + } + + // Get the roms from the machine + switch (subreader.Name) + { + // For OfflineList only + case "title": + machine.Name = subreader.ReadElementContentAsString(); + break; + case "releaseNumber": + releaseNumber = subreader.ReadElementContentAsString(); + break; + case "romSize": + if (!Int64.TryParse(subreader.ReadElementContentAsString(), out size)) + { + size = -1; + } + break; + case "romCRC": + empty = false; + + ext = (subreader.GetAttribute("extension") != null ? subreader.GetAttribute("extension") : ""); + + DatItem olrom = new Rom + { + Name = releaseNumber + " - " + machine.Name + ext, + Size = size, + CRC = subreader.ReadElementContentAsString(), + ItemStatus = ItemStatus.None, + + Machine = machine, + }; + + // Now process and add the rom + ParseAddHelper(olrom, filter, trim, single, root, clean, logger, out key); + break; + + // For Software List only + case "publisher": + publisher = subreader.ReadElementContentAsString(); + break; + case "info": + infos.Add(Tuple.Create(subreader.GetAttribute("name"), subreader.GetAttribute("value"))); + subreader.Read(); + break; + case "part": + partname = subreader.GetAttribute("name"); + partinterface = subreader.GetAttribute("interface"); + subreader.Read(); + break; + case "feature": + features.Add(Tuple.Create(subreader.GetAttribute("name"), subreader.GetAttribute("value"))); + subreader.Read(); + break; + case "dataarea": + case "diskarea": + areaname = subreader.GetAttribute("name"); + long areasizetemp = -1; + if (Int64.TryParse(subreader.GetAttribute("size"), out areasizetemp)) + { + areasize = areasizetemp; + } + subreader.Read(); + break; + + // For Logiqx, SabreDAT, and Software List + case "description": + machine.Description = subreader.ReadElementContentAsString(); + if (!softlist && temptype == "software") + { + machine.Name = machine.Description.Replace('/', '_').Replace("\"", "''"); + } + break; + case "year": + machine.Year = subreader.ReadElementContentAsString(); + break; + case "manufacturer": + machine.Manufacturer = subreader.ReadElementContentAsString(); + break; + case "release": + empty = false; + + bool? defaultrel = null; + if (subreader.GetAttribute("default") != null) + { + if (subreader.GetAttribute("default") == "yes") + { + defaultrel = true; + } + else if (subreader.GetAttribute("default") == "no") + { + defaultrel = false; + } + } + + DatItem relrom = new Release + { + Name = subreader.GetAttribute("name"), + Region = subreader.GetAttribute("region"), + Language = subreader.GetAttribute("language"), + Date = date, + Default = defaultrel, + + Machine = machine, + + Supported = supported, + Publisher = publisher, + Infos = infos, + PartName = partname, + PartInterface = partinterface, + Features = features, + AreaName = areaname, + AreaSize = areasize, + }; + + // Now process and add the rom + ParseAddHelper(relrom, filter, trim, single, root, clean, logger, out key); + + subreader.Read(); + break; + case "biosset": + empty = false; + + bool? defaultbios = null; + if (subreader.GetAttribute("default") != null) + { + if (subreader.GetAttribute("default") == "yes") + { + defaultbios = true; + } + else if (subreader.GetAttribute("default") == "no") + { + defaultbios = false; + } + } + + DatItem biosrom = new BiosSet + { + Name = subreader.GetAttribute("name"), + Description = subreader.GetAttribute("description"), + Default = defaultbios, + + Machine = machine, + + Supported = supported, + Publisher = publisher, + Infos = infos, + PartName = partname, + PartInterface = partinterface, + Features = features, + AreaName = areaname, + AreaSize = areasize, + + SystemID = sysid, + System = filename, + SourceID = srcid, + }; + + // Now process and add the rom + ParseAddHelper(biosrom, filter, trim, single, root, clean, logger, out key); + + subreader.Read(); + break; + case "archive": + empty = false; + + DatItem archiverom = new Archive + { + Name = subreader.GetAttribute("name"), + + Machine = machine, + + Supported = supported, + Publisher = publisher, + Infos = infos, + PartName = partname, + PartInterface = partinterface, + Features = features, + AreaName = areaname, + AreaSize = areasize, + + SystemID = sysid, + System = filename, + SourceID = srcid, + }; + + // Now process and add the rom + ParseAddHelper(archiverom, filter, trim, single, root, clean, logger, out key); + + subreader.Read(); + break; + case "sample": + empty = false; + + DatItem samplerom = new Sample + { + Name = subreader.GetAttribute("name"), + + Machine = machine, + + Supported = supported, + Publisher = publisher, + Infos = infos, + PartName = partname, + PartInterface = partinterface, + Features = features, + AreaName = areaname, + AreaSize = areasize, + + SystemID = sysid, + System = filename, + SourceID = srcid, + }; + + // Now process and add the rom + ParseAddHelper(samplerom, filter, trim, single, root, clean, logger, out key); + + subreader.Read(); + break; + case "rom": + case "disk": + empty = false; + + // If the rom has a status, flag it + its = ItemStatus.None; + if (subreader.GetAttribute("flags") == "good" || subreader.GetAttribute("status") == "good") + { + its = ItemStatus.Good; + } + if (subreader.GetAttribute("flags") == "baddump" || subreader.GetAttribute("status") == "baddump") + { + logger.Verbose("Bad dump detected: " + + (subreader.GetAttribute("name") != null && subreader.GetAttribute("name") != "" ? "\"" + xtr.GetAttribute("name") + "\"" : "ROM NAME NOT FOUND")); + its = ItemStatus.BadDump; + } + if (subreader.GetAttribute("flags") == "nodump" || subreader.GetAttribute("status") == "nodump") + { + logger.Verbose("Nodump detected: " + + (subreader.GetAttribute("name") != null && subreader.GetAttribute("name") != "" ? "\"" + xtr.GetAttribute("name") + "\"" : "ROM NAME NOT FOUND")); + its = ItemStatus.Nodump; + } + if (subreader.GetAttribute("flags") == "verified" || subreader.GetAttribute("status") == "verified") + { + its = ItemStatus.Verified; + } + + // If the rom has a Date attached, read it in and then sanitize it + date = ""; + if (subreader.GetAttribute("date") != null) + { + DateTime dateTime = DateTime.Now; + if (DateTime.TryParse(subreader.GetAttribute("date"), out dateTime)) + { + date = dateTime.ToString(); + } + else + { + date = subreader.GetAttribute("date"); + } + } + + // Take care of hex-sized files + size = -1; + if (subreader.GetAttribute("size") != null && subreader.GetAttribute("size").Contains("0x")) + { + size = Convert.ToInt64(subreader.GetAttribute("size"), 16); + } + else if (subreader.GetAttribute("size") != null) + { + Int64.TryParse(subreader.GetAttribute("size"), out size); + } + + // If the rom is continue or ignore, add the size to the previous rom + if (subreader.GetAttribute("loadflag") == "continue" || subreader.GetAttribute("loadflag") == "ignore") + { + int index = Files[key].Count() - 1; + DatItem lastrom = Files[key][index]; + if (lastrom.Type == ItemType.Rom) + { + ((Rom)lastrom).Size += size; + } + Files[key].RemoveAt(index); + Files[key].Add(lastrom); + subreader.Read(); + continue; + } + + // If we're in clean mode, sanitize the game name + if (clean) + { + machine.Name = Style.CleanGameName(machine.Name.Split(Path.DirectorySeparatorChar)); + } + + DatItem inrom; + switch (subreader.Name.ToLowerInvariant()) + { + case "disk": + inrom = new Disk + { + Name = subreader.GetAttribute("name"), + MD5 = subreader.GetAttribute("md5")?.ToLowerInvariant(), + SHA1 = subreader.GetAttribute("sha1")?.ToLowerInvariant(), + ItemStatus = its, + + Machine = machine, + + Supported = supported, + Publisher = publisher, + Infos = infos, + PartName = partname, + PartInterface = partinterface, + Features = features, + AreaName = areaname, + AreaSize = areasize, + + SystemID = sysid, + System = filename, + SourceID = srcid, + }; + break; + case "rom": + default: + inrom = new Rom + { + Name = subreader.GetAttribute("name"), + Size = size, + CRC = subreader.GetAttribute("crc"), + MD5 = subreader.GetAttribute("md5")?.ToLowerInvariant(), + SHA1 = subreader.GetAttribute("sha1")?.ToLowerInvariant(), + ItemStatus = its, + Date = date, + + Machine = machine, + + Supported = supported, + Publisher = publisher, + Infos = infos, + PartName = partname, + PartInterface = partinterface, + Features = features, + AreaName = areaname, + AreaSize = areasize, + + SystemID = sysid, + System = filename, + SourceID = srcid, + }; + break; + } + + // Now process and add the rom + ParseAddHelper(inrom, filter, trim, single, root, clean, logger, out key); + + subreader.Read(); + break; + default: + subreader.Read(); + break; + } + } + xtr.Skip(); + break; + case "dir": + case "directory": + // Set SuperDAT flag for all SabreDAT inputs, regardless of depth + superdat = true; + if (keep) + { + Type = (Type == "" ? "SuperDAT" : Type); + } + + string foldername = (xtr.GetAttribute("name") == null ? "" : xtr.GetAttribute("name")); + if (foldername != "") + { + parent.Add(foldername); + } + + xtr.Read(); + break; + case "file": + empty = false; + + // If the rom is itemStatus, flag it + its = ItemStatus.None; + flagreader = xtr.ReadSubtree(); + + // If the subtree is empty, skip it + if (flagreader == null) + { + xtr.Skip(); + continue; + } + + while (!flagreader.EOF) + { + // We only want elements + if (flagreader.NodeType != XmlNodeType.Element || flagreader.Name == "flags") + { + flagreader.Read(); + continue; + } + + switch (flagreader.Name) + { + case "flag": + case "status": + if (flagreader.GetAttribute("name") != null && flagreader.GetAttribute("value") != null) + { + string content = flagreader.GetAttribute("value"); + switch (flagreader.GetAttribute("name")) + { + case "good": + its = ItemStatus.Good; + break; + case "baddump": + logger.Verbose("Bad dump detected: " + (xtr.GetAttribute("name") != null && xtr.GetAttribute("name") != "" ? + "\"" + xtr.GetAttribute("name") + "\"" : "ROM NAME NOT FOUND")); + its = ItemStatus.BadDump; + break; + case "nodump": + logger.Verbose("Nodump detected: " + (xtr.GetAttribute("name") != null && xtr.GetAttribute("name") != "" ? + "\"" + xtr.GetAttribute("name") + "\"" : "ROM NAME NOT FOUND")); + its = ItemStatus.Nodump; + break; + case "verified": + its = ItemStatus.Verified; + break; + } + } + break; + } + + flagreader.Read(); + } + + // If the rom has a Date attached, read it in and then sanitize it + date = ""; + if (xtr.GetAttribute("date") != null) + { + date = DateTime.Parse(xtr.GetAttribute("date")).ToString(); + } + + // Take care of hex-sized files + size = -1; + if (xtr.GetAttribute("size") != null && xtr.GetAttribute("size").Contains("0x")) + { + size = Convert.ToInt64(xtr.GetAttribute("size"), 16); + } + else if (xtr.GetAttribute("size") != null) + { + Int64.TryParse(xtr.GetAttribute("size"), out size); + } + + // If the rom is continue or ignore, add the size to the previous rom + if (xtr.GetAttribute("loadflag") == "continue" || xtr.GetAttribute("loadflag") == "ignore") + { + int index = Files[key].Count() - 1; + DatItem lastrom = Files[key][index]; + if (lastrom.Type == ItemType.Rom) + { + ((Rom)lastrom).Size += size; + } + Files[key].RemoveAt(index); + Files[key].Add(lastrom); + continue; + } + + Machine dir = new Machine(); + + // Get the name of the game from the parent + dir.Name = String.Join("\\", parent); + dir.Description = dir.Name; + + // If we aren't keeping names, trim out the path + if (!keep || !superdat) + { + string tempout = Regex.Match(dir.Name, @".*?\\(.*)").Groups[1].Value; + if (tempout != "") + { + dir.Name = tempout; + } + } + + DatItem rom; + switch (xtr.GetAttribute("type").ToLowerInvariant()) + { + case "disk": + rom = new Disk + { + Name = xtr.GetAttribute("name"), + MD5 = xtr.GetAttribute("md5")?.ToLowerInvariant(), + SHA1 = xtr.GetAttribute("sha1")?.ToLowerInvariant(), + ItemStatus = its, + + Machine = dir, + + SystemID = sysid, + System = filename, + SourceID = srcid, + }; + break; + case "rom": + default: + rom = new Rom + { + Name = xtr.GetAttribute("name"), + Size = size, + CRC = xtr.GetAttribute("crc")?.ToLowerInvariant(), + MD5 = xtr.GetAttribute("md5")?.ToLowerInvariant(), + SHA1 = xtr.GetAttribute("sha1")?.ToLowerInvariant(), + ItemStatus = its, + Date = date, + + Machine = dir, + + SystemID = sysid, + System = filename, + SourceID = srcid, + }; + break; + } + + // Now process and add the rom + ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); + + xtr.Read(); + break; + default: + xtr.Read(); + break; + } + } + } + catch (Exception ex) + { + logger.Warning(ex.ToString()); + + // For XML errors, just skip the affected node + xtr?.Read(); + } + + xtr.Dispose(); + } + + /// + /// Parse a Redump MD5 and return all found games and roms within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + /// True if game names are sanitized, false otherwise (default) + private void ParseRedumpMD5( + // Standard Dat parsing + string filename, + int sysid, + int srcid, + + // Rom filtering + Filter filter, + + // Rom renaming + bool trim, + bool single, + string root, + + // Miscellaneous + Logger logger, + bool clean) + { + // Open a file reader + Encoding enc = Style.GetEncoding(filename); + StreamReader sr = new StreamReader(File.OpenRead(filename), enc); + + while (!sr.EndOfStream) + { + string line = sr.ReadLine(); + + Rom rom = new Rom + { + Name = line.Split(' ')[1].Replace("*", String.Empty), + Size = -1, + MD5 = line.Split(' ')[0], + ItemStatus = ItemStatus.None, + + Machine = new Machine + { + Name = Path.GetFileNameWithoutExtension(filename), + }, + + SystemID = sysid, + SourceID = srcid, + }; + + // Now process and add the rom + string key = ""; + ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); + } + + sr.Dispose(); + } + + /// + /// Parse a Redump SFV and return all found games and roms within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + /// True if game names are sanitized, false otherwise (default) + private void ParseRedumpSFV( + // Standard Dat parsing + string filename, + int sysid, + int srcid, + + // Rom filtering + Filter filter, + + // Rom renaming + bool trim, + bool single, + string root, + + // Miscellaneous + Logger logger, + bool clean) + { + // Open a file reader + Encoding enc = Style.GetEncoding(filename); + StreamReader sr = new StreamReader(File.OpenRead(filename), enc); + + while (!sr.EndOfStream) + { + string line = sr.ReadLine(); + + Rom rom = new Rom + { + Name = line.Split(' ')[0].Replace("*", String.Empty), + Size = -1, + CRC = line.Split(' ')[1], + ItemStatus = ItemStatus.None, + + Machine = new Machine + { + Name = Path.GetFileNameWithoutExtension(filename), + }, + + SystemID = sysid, + SourceID = srcid, + }; + + // Now process and add the rom + string key = ""; + ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); + } + + sr.Dispose(); + } + + /// + /// Parse a Redump SHA-1 and return all found games and roms within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + /// True if game names are sanitized, false otherwise (default) + private void ParseRedumpSHA1( + // Standard Dat parsing + string filename, + int sysid, + int srcid, + + // Rom filtering + Filter filter, + + // Rom renaming + bool trim, + bool single, + string root, + + // Miscellaneous + Logger logger, + bool clean) + { + // Open a file reader + Encoding enc = Style.GetEncoding(filename); + StreamReader sr = new StreamReader(File.OpenRead(filename), enc); + + while (!sr.EndOfStream) + { + string line = sr.ReadLine(); + + Rom rom = new Rom + { + Name = line.Split(' ')[1].Replace("*", String.Empty), + Size = -1, + SHA1 = line.Split(' ')[0], + ItemStatus = ItemStatus.None, + + Machine = new Machine + { + Name = Path.GetFileNameWithoutExtension(filename), + }, + + SystemID = sysid, + SourceID = srcid, + }; + + // Now process and add the rom + string key = ""; + ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); + } + + sr.Dispose(); + } + + /// + /// Parse a RomCenter DAT and return all found games and roms within + /// + /// Name of the file to be parsed + /// System ID for the DAT + /// Source ID for the DAT + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + /// True if game names are sanitized, false otherwise (default) + private void ParseRC( + // Standard Dat parsing + string filename, + int sysid, + int srcid, + + // Rom filtering + Filter filter, + + // Rom renaming + bool trim, + bool single, + string root, + + // Miscellaneous + Logger logger, + bool clean) + { + // Open a file reader + Encoding enc = Style.GetEncoding(filename); + StreamReader sr = new StreamReader(File.OpenRead(filename), enc); + + string blocktype = ""; + while (!sr.EndOfStream) + { + string line = sr.ReadLine(); + + // If the line is the start of the credits section + if (line.ToLowerInvariant().StartsWith("[credits]")) + { + blocktype = "credits"; + } + // If the line is the start of the dat section + else if (line.ToLowerInvariant().StartsWith("[dat]")) + { + blocktype = "dat"; + } + // If the line is the start of the emulator section + else if (line.ToLowerInvariant().StartsWith("[emulator]")) + { + blocktype = "emulator"; + } + // If the line is the start of the game section + else if (line.ToLowerInvariant().StartsWith("[games]")) + { + blocktype = "games"; + } + // Otherwise, it's not a section and it's data, so get out all data + else + { + // If we have an author + if (line.ToLowerInvariant().StartsWith("author=")) + { + Author = (String.IsNullOrEmpty(Author) ? line.Split('=')[1] : Author); + } + // If we have one of the three version tags + else if (line.ToLowerInvariant().StartsWith("version=")) + { + switch (blocktype) + { + case "credits": + Version = (String.IsNullOrEmpty(Version) ? line.Split('=')[1] : Version); + break; + case "emulator": + Description = (String.IsNullOrEmpty(Description) ? line.Split('=')[1] : Description); + break; + } + } + // If we have a URL + else if (line.ToLowerInvariant().StartsWith("url=")) + { + Url = (String.IsNullOrEmpty(Url) ? line.Split('=')[1] : Url); + } + // If we have a comment + else if (line.ToLowerInvariant().StartsWith("comment=")) + { + Comment = (String.IsNullOrEmpty(Comment) ? line.Split('=')[1] : Comment); + } + // If we have the split flag + else if (line.ToLowerInvariant().StartsWith("split=")) + { + int split = 0; + if (Int32.TryParse(line.Split('=')[1], out split)) + { + if (split == 1) + { + ForceMerging = ForceMerging.Split; + } + } + } + // If we have the merge tag + else if (line.ToLowerInvariant().StartsWith("merge=")) + { + int merge = 0; + if (Int32.TryParse(line.Split('=')[1], out merge)) + { + if (merge == 1) + { + ForceMerging = ForceMerging.Full; + } + } + } + // If we have the refname tag + else if (line.ToLowerInvariant().StartsWith("refname=")) + { + Name = (String.IsNullOrEmpty(Name) ? line.Split('=')[1] : Name); + } + // If we have a rom + else if (line.StartsWith("¬")) + { + // Some old RC DATs have this behavior + if (line.Contains("¬N¬O")) + { + line = line.Replace("¬N¬O", "") + "¬¬"; + } + + /* + The rominfo order is as follows: + 1 - parent name + 2 - parent description + 3 - game name + 4 - game description + 5 - rom name + 6 - rom crc + 7 - rom size + 8 - romof name + 9 - merge name + */ + string[] rominfo = line.Split('¬'); + + // Try getting the size separately + long size = 0; + if (!Int64.TryParse(rominfo[7], out size)) + { + size = 0; + } + + Rom rom = new Rom + { + Name = rominfo[5], + Size = size, + CRC = rominfo[6], + ItemStatus = ItemStatus.None, + + Machine = new Machine + { + Name = rominfo[3], + Description = rominfo[4], + CloneOf = rominfo[1], + RomOf = rominfo[8], + }, + + SystemID = sysid, + SourceID = srcid, + }; + + // Now process and add the rom + string key = ""; + ParseAddHelper(rom, filter, trim, single, root, clean, logger, out key); + } + } + } + + sr.Dispose(); + } + + /// + /// Add a rom to the Dat after checking + /// + /// Item data to check against + /// Filter object for passing to the DatItem level + /// True if we are supposed to trim names to NTFS length, false otherwise + /// True if all games should be replaced by '!', false otherwise + /// String representing root directory to compare against for length calculation + /// Logger object for console and/or file output + private void ParseAddHelper(DatItem item, Filter filter, bool trim, bool single, string root, bool clean, Logger logger, out string key) + { + key = ""; + + // If there's no name in the rom, we log and skip it + if (item.Name == null) + { + logger.Warning("Rom with no name found! Skipping..."); + return; + } + + // If we're in cleaning mode, sanitize the game name + item.Machine.Name = (clean ? Style.CleanGameName(item.Machine.Name) : item.Machine.Name); + + // If we have a Rom or a Disk, clean the hash data + if (item.Type == ItemType.Rom) + { + Rom itemRom = (Rom)item; + + // Sanitize the hashes from null, hex sizes, and "true blank" strings + itemRom.CRC = Style.CleanHashData(itemRom.CRC, Constants.CRCLength); + itemRom.MD5 = Style.CleanHashData(itemRom.MD5, Constants.MD5Length); + itemRom.SHA1 = Style.CleanHashData(itemRom.SHA1, Constants.SHA1Length); + + // If we have a rom and it's missing size AND the hashes match a 0-byte file, fill in the rest of the info + if ((itemRom.Size == 0 || itemRom.Size == -1) + && ((itemRom.CRC == Constants.CRCZero || String.IsNullOrEmpty(itemRom.CRC)) + || itemRom.MD5 == Constants.MD5Zero + || itemRom.SHA1 == Constants.SHA1Zero)) + { + itemRom.Size = Constants.SizeZero; + itemRom.CRC = Constants.CRCZero; + itemRom.MD5 = Constants.MD5Zero; + itemRom.SHA1 = Constants.SHA1Zero; + } + // If the file has no size and it's not the above case, skip and log + else if (itemRom.ItemStatus != ItemStatus.Nodump && (itemRom.Size == 0 || itemRom.Size == -1)) + { + logger.Verbose("Incomplete entry for \"" + itemRom.Name + "\" will be output as nodump"); + itemRom.ItemStatus = ItemStatus.Nodump; + } + // If the file has a size but aboslutely no hashes, skip and log + else if (itemRom.ItemStatus != ItemStatus.Nodump + && itemRom.Size > 0 + && String.IsNullOrEmpty(itemRom.CRC) + && String.IsNullOrEmpty(itemRom.MD5) + && String.IsNullOrEmpty(itemRom.SHA1)) + { + logger.Verbose("Incomplete entry for \"" + itemRom.Name + "\" will be output as nodump"); + itemRom.ItemStatus = ItemStatus.Nodump; + } + + item = itemRom; + } + else if (item.Type == ItemType.Disk) + { + Disk itemDisk = (Disk)item; + + // Sanitize the hashes from null, hex sizes, and "true blank" strings + itemDisk.MD5 = Style.CleanHashData(itemDisk.MD5, Constants.MD5Length); + itemDisk.SHA1 = Style.CleanHashData(itemDisk.SHA1, Constants.SHA1Length); + + // If the file has aboslutely no hashes, skip and log + if (itemDisk.ItemStatus != ItemStatus.Nodump + && String.IsNullOrEmpty(itemDisk.MD5) + && String.IsNullOrEmpty(itemDisk.SHA1)) + { + logger.Verbose("Incomplete entry for \"" + itemDisk.Name + "\" will be output as nodump"); + itemDisk.ItemStatus = ItemStatus.Nodump; + } + + item = itemDisk; + } + + // If the rom passes the filter, include it + if (filter.ItemPasses(item, logger)) + { + // If we are in single game mode, rename all games + if (single) + { + item.Machine.Name = "!"; + } + + // If we are in NTFS trim mode, trim the game name + if (trim) + { + // Windows max name length is 260 + int usableLength = 260 - item.Machine.Name.Length - root.Length; + if (item.Name.Length > usableLength) + { + string ext = Path.GetExtension(item.Name); + item.Name = item.Name.Substring(0, usableLength - ext.Length); + item.Name += ext; + } + } + + lock (Files) + { + // Get the key and add statistical data + switch (item.Type) + { + case ItemType.Archive: + case ItemType.BiosSet: + case ItemType.Release: + case ItemType.Sample: + key = item.Type.ToString(); + break; + case ItemType.Disk: + key = ((Disk)item).MD5; + + // Add statistical data + DiskCount += 1; + TotalSize += 0; + MD5Count += (String.IsNullOrEmpty(((Disk)item).MD5) ? 0 : 1); + SHA1Count += (String.IsNullOrEmpty(((Disk)item).SHA1) ? 0 : 1); + BaddumpCount += (((Disk)item).ItemStatus == ItemStatus.BadDump ? 1 : 0); + NodumpCount += (((Disk)item).ItemStatus == ItemStatus.Nodump ? 1 : 0); + break; + case ItemType.Rom: + key = ((Rom)item).Size + "-" + ((Rom)item).CRC; + + // Add statistical data + RomCount += 1; + TotalSize += (((Rom)item).ItemStatus == ItemStatus.Nodump ? 0 : ((Rom)item).Size); + CRCCount += (String.IsNullOrEmpty(((Rom)item).CRC) ? 0 : 1); + MD5Count += (String.IsNullOrEmpty(((Rom)item).MD5) ? 0 : 1); + SHA1Count += (String.IsNullOrEmpty(((Rom)item).SHA1) ? 0 : 1); + BaddumpCount += (((Rom)item).ItemStatus == ItemStatus.BadDump ? 1 : 0); + NodumpCount += (((Rom)item).ItemStatus == ItemStatus.Nodump ? 1 : 0); + break; + default: + key = "default"; + break; + } + + // Add the item to the DAT + if (Files.ContainsKey(key)) + { + Files[key].Add(item); + } + else + { + List newvalue = new List(); + newvalue.Add(item); + Files.Add(key, newvalue); + } + } + } + } + + #endregion + } +} diff --git a/SabreTools.Helper/Dats/Partials/DatFile.Rebuild.cs b/SabreTools.Helper/Dats/Partials/DatFile.Rebuild.cs new file mode 100644 index 00000000..94d71be0 --- /dev/null +++ b/SabreTools.Helper/Dats/Partials/DatFile.Rebuild.cs @@ -0,0 +1,645 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using SabreTools.Helper.Data; +using SabreTools.Helper.Skippers; +using SabreTools.Helper.Tools; + +#if MONO +using System.IO; +#else +using Alphaleonis.Win32.Filesystem; + +using SearchOption = System.IO.SearchOption; +#endif + +namespace SabreTools.Helper.Dats +{ + public partial class DatFile + { + #region Rebuilding and Verifying [MODULAR DONE, FOR NOW] + + /// + /// Process the DAT and find all matches in input files and folders + /// + /// List of input files/folders to check + /// Output directory to use to build to + /// Temporary directory for archive extraction + /// True to enable external scanning of archives, false otherwise + /// True if the date from the DAT should be used if available, false otherwise + /// True if input files should be deleted, false otherwise + /// True if the DAT should be used as a filter instead of a template, false otherwise + /// Output format that files should be written to + /// True if files should be output in Romba depot folders, false otherwise + /// ArchiveScanLevel representing the archive handling levels + /// True if the updated DAT should be output, false otherwise + /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise + /// Logger object for file and console output + /// True if rebuilding was a success, false otherwise + public bool RebuildToOutput(List inputs, string outDir, string tempDir, bool quickScan, bool date, + bool delete, bool inverse, OutputFormat outputFormat, bool romba, ArchiveScanLevel archiveScanLevel, bool updateDat, + string headerToCheckAgainst, int maxDegreeOfParallelism, Logger logger) + { + #region Perform setup + + // If the DAT is not populated and inverse is not set, inform the user and quit + if ((Files == null || Files.Count == 0) && !inverse) + { + logger.User("No entries were found to rebuild, exiting..."); + return false; + } + + // Check that the output directory exists + if (!Directory.Exists(outDir)) + { + Directory.CreateDirectory(outDir); + outDir = Path.GetFullPath(outDir); + } + + // Check the temp directory + if (String.IsNullOrEmpty(tempDir)) + { + tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + } + + // Then create or clean the temp directory + if (!Directory.Exists(tempDir)) + { + Directory.CreateDirectory(tempDir); + } + else + { + FileTools.CleanDirectory(tempDir); + } + + // Preload the Skipper list + int listcount = Skipper.List.Count; + + #endregion + + bool success = true; + DatFile matched = new DatFile(); + List files = new List(); + + #region Retrieve a list of all files + + logger.User("Retrieving list all files from input"); + DateTime start = DateTime.Now; + + // Create a list of just files from inputs + Parallel.ForEach(inputs, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism, }, + input => { + if (File.Exists(input)) + { + logger.Verbose("File found: '" + input + "'"); + files.Add(Path.GetFullPath(input)); + } + else if (Directory.Exists(input)) + { + logger.Verbose("Directory found: '" + input + "'"); + Parallel.ForEach(Directory.EnumerateFiles(input, "*", SearchOption.AllDirectories), + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism, }, + file => + { + logger.Verbose("File found: '" + file + "'"); + files.Add(Path.GetFullPath(file)); + }); + } + else + { + logger.Error("'" + input + "' is not a file or directory!"); + } + }); + logger.User("Retrieving complete in: " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + #endregion + + DatFile current = new DatFile(); + Dictionary fileToSkipperRule = new Dictionary(); + + #region Create a dat from input files + + logger.User("Getting hash information for all input files"); + start = DateTime.Now; + + // Now that we have a list of just files, we get a DAT from the input files + Parallel.ForEach(files, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + file => + { + // If we somehow have a null filename, return + if (file == null) + { + return; + } + + // Define the temporary directory + string tempSubDir = Path.GetFullPath(Path.Combine(tempDir, Path.GetRandomFileName())) + Path.DirectorySeparatorChar; + + // Get the required scanning level for the file + bool shouldExternalProcess = false; + bool shouldInternalProcess = false; + ArchiveTools.GetInternalExternalProcess(file, archiveScanLevel, logger, out shouldExternalProcess, out shouldInternalProcess); + + // If we're supposed to scan the file externally + if (shouldExternalProcess) + { + Rom rom = FileTools.GetFileInfo(file, logger, noMD5: quickScan, noSHA1: quickScan, header: headerToCheckAgainst); + rom.Name = Path.GetFullPath(file); + + lock (Files) + { + string key = rom.Size + "-" + rom.CRC; + if (current.Files.ContainsKey(key)) + { + current.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + current.Files.Add(key, temp); + } + } + + // If we had a header, we want the full file information too + if (headerToCheckAgainst != null) + { + rom = FileTools.GetFileInfo(file, logger, noMD5: quickScan, noSHA1: quickScan); + rom.Name = Path.GetFullPath(file); + + lock (Files) + { + string key = rom.Size + "-" + rom.CRC; + if (current.Files.ContainsKey(key)) + { + current.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + current.Files.Add(key, temp); + } + } + } + } + + // If we're supposed to scan the file internally + if (shouldInternalProcess) + { + // If quickscan is set, do so + if (quickScan) + { + List extracted = ArchiveTools.GetArchiveFileInfo(file, logger); + + foreach (Rom rom in extracted) + { + Rom newrom = rom; + newrom.Machine = new Machine(Path.GetFullPath(file), ""); + + lock (Files) + { + string key = rom.Size + "-" + rom.CRC; + if (current.Files.ContainsKey(key)) + { + current.Files[key].Add(newrom); + } + else + { + List temp = new List(); + temp.Add(newrom); + current.Files.Add(key, temp); + } + } + } + } + // Otherwise, attempt to extract the files to the temporary directory + else + { + bool encounteredErrors = ArchiveTools.ExtractArchive(file, tempSubDir, archiveScanLevel, logger); + + // If the file was an archive and was extracted successfully, check it + if (!encounteredErrors) + { + logger.Verbose(Path.GetFileName(file) + " treated like an archive"); + List extracted = Directory.EnumerateFiles(tempSubDir, "*", SearchOption.AllDirectories).ToList(); + Parallel.ForEach(extracted, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + entry => + { + Rom rom = FileTools.GetFileInfo(entry, logger, noMD5: quickScan, noSHA1: quickScan, header: headerToCheckAgainst); + rom.Machine = new Machine(Path.GetFullPath(file), ""); + + lock (Files) + { + string key = rom.Size + "-" + rom.CRC; + if (current.Files.ContainsKey(key)) + { + current.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + current.Files.Add(key, temp); + } + } + + // If we had a header, we want the full file information too + if (headerToCheckAgainst != null) + { + rom = FileTools.GetFileInfo(file, logger, noMD5: quickScan, noSHA1: quickScan); + rom.Machine = new Machine(Path.GetFullPath(file), ""); + + lock (Files) + { + string key = rom.Size + "-" + rom.CRC; + if (current.Files.ContainsKey(key)) + { + current.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + current.Files.Add(key, temp); + } + } + } + }); + } + // Otherwise, just get the info on the file itself + else if (File.Exists(file)) + { + Rom rom = FileTools.GetFileInfo(file, logger, noMD5: quickScan, noSHA1: quickScan, header: headerToCheckAgainst); + rom.Name = Path.GetFullPath(file); + + lock (Files) + { + string key = rom.Size + "-" + rom.CRC; + if (current.Files.ContainsKey(key)) + { + current.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + current.Files.Add(key, temp); + } + } + } + } + } + + // Now delete the temp directory + try + { + Directory.Delete(tempSubDir, true); + } + catch { } + }); + + logger.User("Getting hash information complete in: " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + #endregion + + // Create a mapping from destination file to source file + Dictionary toFromMap = new Dictionary(); + + #region Find all required files for rebuild + + logger.User("Determining files to rebuild"); + start = DateTime.Now; + + // Order the DATs by hash first to make things easier + logger.User("Sorting input DAT..."); + BucketByCRC(false, logger, output: false); + logger.User("Sorting found files..."); + current.BucketByCRC(false, logger, output: false); + + // Now loop over and find all files that need to be rebuilt + List keys = current.Files.Keys.ToList(); + Parallel.ForEach(keys, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + key => + { + // If we are using the DAT as a filter, treat the files one way + if (inverse) + { + // Check for duplicates + List datItems = current.Files[key]; + foreach (Rom rom in datItems) + { + // If the rom has duplicates, we skip it + if (rom.HasDuplicates(this, logger)) + { + return; + } + + // Otherwise, map the file to itself + try + { + Rom newrom = new Rom + { + Name = rom.Name.Remove(0, Path.GetDirectoryName(rom.Name).Length), + Size = rom.Size, + CRC = rom.CRC, + MD5 = rom.MD5, + SHA1 = rom.SHA1, + + Machine = new Machine + { + Name = Path.GetFileNameWithoutExtension(rom.Machine.Name), + }, + }; + newrom.Name = newrom.Name.Remove(0, (newrom.Name.StartsWith("\\") || newrom.Name.StartsWith("/") ? 1 : 0)); + + lock (toFromMap) + { + toFromMap.Add(newrom, rom); + } + } + catch { } + } + } + + // Otherwise, treat it like a standard rebuild + else + { + // If the input DAT doesn't have the key, then nothing from the current DAT are there + if (!Files.ContainsKey(key)) + { + return; + } + + // Otherwise, we try to find duplicates + List datItems = current.Files[key]; + foreach (Rom rom in datItems) + { + List found = rom.GetDuplicates(this, logger, false); + + // Now add all of the duplicates mapped to the current file + foreach (Rom mid in found) + { + try + { + lock (toFromMap) + { + toFromMap.Add(mid, rom); + } + } + catch { } + } + } + } + }); + + logger.User("Determining complete in: " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + #endregion + + // Now bucket the list of keys by game so that we can rebuild properly + SortedDictionary> keysGroupedByGame = BucketListByGame(toFromMap.Keys.ToList(), false, true, logger, output: false); + + #region Rebuild games in order + + switch (outputFormat) + { + case OutputFormat.Folder: + logger.User("Rebuilding all files to directory"); + break; + case OutputFormat.TapeArchive: + logger.User("Rebuilding all files to TAR"); + break; + case OutputFormat.Torrent7Zip: + logger.User("Rebuilding all files to Torrent7Z"); + break; + case OutputFormat.TorrentGzip: + logger.User("Rebuilding all files to TorrentGZ"); + break; + case OutputFormat.TorrentLrzip: + logger.User("Rebuilding all files to TorrentLRZ"); + break; + case OutputFormat.TorrentRar: + logger.User("Rebuilding all files to TorrentRAR"); + break; + case OutputFormat.TorrentXZ: + logger.User("Rebuilding all files to TorrentXZ"); + break; + case OutputFormat.TorrentZip: + logger.User("Rebuilding all files to TorrentZip"); + break; + } + start = DateTime.Now; + + // Now loop through the keys and create the correct output items + List games = keysGroupedByGame.Keys.ToList(); + Parallel.ForEach(games, + new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, + game => + { + // Define the temporary directory + string tempSubDir = Path.GetFullPath(Path.Combine(tempDir, Path.GetRandomFileName())) + Path.DirectorySeparatorChar; + + // Create an empty list for getting paths for rebuilding + List pathsToFiles = new List(); + + // Loop through all of the matched items in the game + List itemsInGame = keysGroupedByGame[game]; + List romsInGame = new List(); + foreach (Rom rom in itemsInGame) + { + // Get the rom that's mapped to this item + Rom source = (Rom)toFromMap[rom]; + + // If we have an empty rom or machine, there was an issue + if (source == null || source.Machine == null || source.Machine.Name == null) + { + continue; + } + + // If the file is in an archive, we need to treat it specially + string machinename = source.Machine.Name.ToLowerInvariant(); + if (machinename.EndsWith(".7z") + || machinename.EndsWith(".gz") + || machinename.EndsWith(".rar") + || machinename.EndsWith(".zip")) + { + string tempPath = ArchiveTools.ExtractItem(source.Machine.Name, Path.GetFileName(source.Name), tempSubDir, logger); + pathsToFiles.Add(tempPath); + } + + // Otherwise, we want to just add the full path + else + { + pathsToFiles.Add(source.Name); + } + + // If the size doesn't match, then we add the CRC as a postfix to the file + Rom fi = FileTools.GetFileInfo(pathsToFiles.Last(), logger); + if (fi.Size != source.Size) + { + rom.Name = Path.GetDirectoryName(rom.Name) + + (String.IsNullOrEmpty(Path.GetDirectoryName(rom.Name)) ? "" : Path.DirectorySeparatorChar.ToString()) + + Path.GetFileNameWithoutExtension(rom.Name) + + " (" + fi.CRC + ")" + + Path.GetExtension(rom.Name); + rom.CRC = fi.CRC; + rom.Size = fi.Size; + } + + // Now add the rom to the output list + romsInGame.Add(rom); + } + + // And now rebuild accordingly + switch (outputFormat) + { + case OutputFormat.Folder: + for (int i = 0; i < romsInGame.Count; i++) + { + string infile = pathsToFiles[i]; + Rom outrom = romsInGame[i]; + string outfile = Path.Combine(outDir, Style.RemovePathUnsafeCharacters(outrom.Machine.Name), outrom.Name); + + // Make sure the output folder is created + Directory.CreateDirectory(Path.GetDirectoryName(outfile)); + + // Now copy the file over + try + { + File.Copy(infile, outfile); + } + catch { } + } + break; + case OutputFormat.TapeArchive: + ArchiveTools.WriteTAR(pathsToFiles, outDir, romsInGame, logger); + break; + case OutputFormat.Torrent7Zip: + break; + case OutputFormat.TorrentGzip: + for (int i = 0; i < itemsInGame.Count; i++) + { + string infile = pathsToFiles[i]; + Rom outrom = romsInGame[i]; + outrom.Machine.Name = Style.RemovePathUnsafeCharacters(outrom.Machine.Name); + ArchiveTools.WriteTorrentGZ(infile, outDir, romba, logger); + } + break; + case OutputFormat.TorrentLrzip: + break; + case OutputFormat.TorrentRar: + break; + case OutputFormat.TorrentXZ: + break; + case OutputFormat.TorrentZip: + ArchiveTools.WriteTorrentZip(pathsToFiles, outDir, romsInGame, logger); + break; + } + + // And now clear the temp folder to get rid of any transient files + try + { + Directory.Delete(tempSubDir, true); + } + catch { } + }); + + logger.User("Rebuilding complete in: " + DateTime.Now.Subtract(start).ToString(@"hh\:mm\:ss\.fffff")); + + #endregion + + return success; + } + + /// + /// Process the DAT and verify the output directory + /// + /// DAT to use to verify the directory + /// List of input directories to compare against + /// Temporary directory for archive extraction + /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise + /// Logger object for file and console output + /// True if verification was a success, false otherwise + public bool VerifyDirectory(List inputs, string tempDir, string headerToCheckAgainst, Logger logger) + { + // First create or clean the temp directory + if (!Directory.Exists(tempDir)) + { + Directory.CreateDirectory(tempDir); + } + else + { + FileTools.CleanDirectory(tempDir); + } + + bool success = true; + + /* + We want the cross section of what's the folder and what's in the DAT. Right now, it just has what's in the DAT that's not in the folder + */ + + // Then, loop through and check each of the inputs + logger.User("Processing files:\n"); + foreach (string input in inputs) + { + PopulateFromDir(input, false /* noMD5 */, false /* noSHA1 */, true /* bare */, false /* archivesAsFiles */, + true /* enableGzip */, false /* addBlanks */, false /* addDate */, tempDir /* tempDir */, false /* copyFiles */, + headerToCheckAgainst, 4 /* maxDegreeOfParallelism */, logger); + } + + // Setup the fixdat + DatFile matched = (DatFile)CloneHeader(); + matched.Files = new SortedDictionary>(); + matched.FileName = "fixDat_" + matched.FileName; + matched.Name = "fixDat_" + matched.Name; + matched.Description = "fixDat_" + matched.Description; + matched.DatFormat = DatFormat.Logiqx; + + // Now that all files are parsed, get only files found in directory + bool found = false; + foreach (List roms in Files.Values) + { + List newroms = DatItem.Merge(roms, logger); + foreach (Rom rom in newroms) + { + if (rom.SourceID == 99) + { + found = true; + string key = rom.Size + "-" + rom.CRC; + if (matched.Files.ContainsKey(key)) + { + matched.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + matched.Files.Add(key, temp); + } + } + } + } + + // Now output the fixdat to the main folder + if (found) + { + matched.WriteToFile("", logger, stats: true); + } + else + { + logger.User("No fixDat needed"); + } + + return success; + } + + #endregion + } +} diff --git a/SabreTools.Helper/Dats/Partials/DatFile.Splitters.cs b/SabreTools.Helper/Dats/Partials/DatFile.Splitters.cs new file mode 100644 index 00000000..7b36b557 --- /dev/null +++ b/SabreTools.Helper/Dats/Partials/DatFile.Splitters.cs @@ -0,0 +1,694 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +using SabreTools.Helper.Data; +using SabreTools.Helper.Tools; + +#if MONO +using System.IO; +#else +using Alphaleonis.Win32.Filesystem; +#endif +using NaturalSort; + +namespace SabreTools.Helper.Dats +{ + public partial class DatFile + { + #region Splitting [MODULAR DONE] + + /// + /// Split a DAT by input extensions + /// + /// Name of the directory to write the DATs out to + /// Parent path for replacement + /// List of extensions to split on (first DAT) + /// List of extensions to split on (second DAT) + /// Logger object for console and file writing + /// True if split succeeded, false otherwise + public bool SplitByExt(string outDir, string basepath, List extA, List extB, Logger logger) + { + // Make sure all of the extensions have a dot at the beginning + List newExtA = new List(); + foreach (string s in extA) + { + newExtA.Add((s.StartsWith(".") ? s : "." + s).ToUpperInvariant()); + } + string newExtAString = string.Join(",", newExtA); + + List newExtB = new List(); + foreach (string s in extB) + { + newExtB.Add((s.StartsWith(".") ? s : "." + s).ToUpperInvariant()); + } + string newExtBString = string.Join(",", newExtB); + + // Set all of the appropriate outputs for each of the subsets + DatFile datdataA = new DatFile + { + FileName = this.FileName + " (" + newExtAString + ")", + Name = this.Name + " (" + newExtAString + ")", + Description = this.Description + " (" + newExtAString + ")", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Files = new SortedDictionary>(), + DatFormat = this.DatFormat, + }; + DatFile datdataB = new DatFile + { + FileName = this.FileName + " (" + newExtBString + ")", + Name = this.Name + " (" + newExtBString + ")", + Description = this.Description + " (" + newExtBString + ")", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Files = new SortedDictionary>(), + DatFormat = this.DatFormat, + }; + + // If roms is empty, return false + if (this.Files.Count == 0) + { + return false; + } + + // Now separate the roms accordingly + foreach (string key in this.Files.Keys) + { + foreach (DatItem rom in this.Files[key]) + { + if (newExtA.Contains(Path.GetExtension(rom.Name.ToUpperInvariant()))) + { + if (datdataA.Files.ContainsKey(key)) + { + datdataA.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + datdataA.Files.Add(key, temp); + } + } + else if (newExtB.Contains(Path.GetExtension(rom.Name.ToUpperInvariant()))) + { + if (datdataB.Files.ContainsKey(key)) + { + datdataB.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + datdataB.Files.Add(key, temp); + } + } + else + { + if (datdataA.Files.ContainsKey(key)) + { + datdataA.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + datdataA.Files.Add(key, temp); + } + if (datdataB.Files.ContainsKey(key)) + { + datdataB.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + datdataB.Files.Add(key, temp); + } + } + } + } + + // Get the output directory + if (outDir != "") + { + outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1); + } + else + { + outDir = Path.GetDirectoryName(this.FileName); + } + + // Then write out both files + bool success = datdataA.WriteToFile(outDir, logger); + success &= datdataB.WriteToFile(outDir, logger); + + return success; + } + + /// + /// Split a DAT by best available hashes + /// + /// Name of the directory to write the DATs out to + /// Parent path for replacement + /// Logger object for console and file writing + /// True if split succeeded, false otherwise + public bool SplitByHash(string outDir, string basepath, Logger logger) + { + // Sanitize the basepath to be more predictable + basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar); + + // Create each of the respective output DATs + logger.User("Creating and populating new DATs"); + DatFile itemStatus = new DatFile + { + FileName = this.FileName + " (Nodump)", + Name = this.Name + " (Nodump)", + Description = this.Description + " (Nodump)", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + MergeRoms = this.MergeRoms, + Files = new SortedDictionary>(), + }; + DatFile sha1 = new DatFile + { + FileName = this.FileName + " (SHA-1)", + Name = this.Name + " (SHA-1)", + Description = this.Description + " (SHA-1)", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + MergeRoms = this.MergeRoms, + Files = new SortedDictionary>(), + }; + DatFile md5 = new DatFile + { + FileName = this.FileName + " (MD5)", + Name = this.Name + " (MD5)", + Description = this.Description + " (MD5)", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + MergeRoms = this.MergeRoms, + Files = new SortedDictionary>(), + }; + DatFile crc = new DatFile + { + FileName = this.FileName + " (CRC)", + Name = this.Name + " (CRC)", + Description = this.Description + " (CRC)", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + MergeRoms = this.MergeRoms, + Files = new SortedDictionary>(), + }; + + DatFile other = new DatFile + { + FileName = this.FileName + " (Other)", + Name = this.Name + " (Other)", + Description = this.Description + " (Other)", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + MergeRoms = this.MergeRoms, + Files = new SortedDictionary>(), + }; + + // Now populate each of the DAT objects in turn + List keys = this.Files.Keys.ToList(); + foreach (string key in keys) + { + List roms = this.Files[key]; + foreach (DatItem rom in roms) + { + // If the file is not a Rom or Disk, continue + if (rom.Type != ItemType.Disk && rom.Type != ItemType.Rom) + { + continue; + } + + // If the file is a itemStatus + if ((rom.Type == ItemType.Rom && ((Rom)rom).ItemStatus == ItemStatus.Nodump) + || (rom.Type == ItemType.Disk && ((Disk)rom).ItemStatus == ItemStatus.Nodump)) + { + if (itemStatus.Files.ContainsKey(key)) + { + itemStatus.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + itemStatus.Files.Add(key, temp); + } + } + // If the file has a SHA-1 + else if ((rom.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)rom).SHA1)) + || (rom.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)rom).SHA1))) + { + if (sha1.Files.ContainsKey(key)) + { + sha1.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + sha1.Files.Add(key, temp); + } + } + // If the file has no SHA-1 but has an MD5 + else if ((rom.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)rom).MD5)) + || (rom.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)rom).MD5))) + { + if (md5.Files.ContainsKey(key)) + { + md5.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + md5.Files.Add(key, temp); + } + } + // If the file has no MD5 but a CRC + else if ((rom.Type == ItemType.Rom && !String.IsNullOrEmpty(((Rom)rom).SHA1)) + || (rom.Type == ItemType.Disk && !String.IsNullOrEmpty(((Disk)rom).SHA1))) + { + if (crc.Files.ContainsKey(key)) + { + crc.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + crc.Files.Add(key, temp); + } + } + else + { + if (other.Files.ContainsKey(key)) + { + other.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + other.Files.Add(key, temp); + } + } + } + } + + // Get the output directory + if (outDir != "") + { + outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1); + } + else + { + outDir = Path.GetDirectoryName(this.FileName); + } + + // Now, output all of the files to the output directory + logger.User("DAT information created, outputting new files"); + bool success = true; + if (itemStatus.Files.Count > 0) + { + success &= itemStatus.WriteToFile(outDir, logger); + } + if (sha1.Files.Count > 0) + { + success &= sha1.WriteToFile(outDir, logger); + } + if (md5.Files.Count > 0) + { + success &= md5.WriteToFile(outDir, logger); + } + if (crc.Files.Count > 0) + { + success &= crc.WriteToFile(outDir, logger); + } + + return success; + } + + /// + /// Split a SuperDAT by lowest available directory level + /// + /// Name of the directory to write the DATs out to + /// Parent path for replacement + /// True if short names should be used, false otherwise + /// True if original filenames should be used as the base for output filename, false otherwise + /// Logger object for console and file writing + /// True if split succeeded, false otherwise + public bool SplitByLevel(string outDir, string basepath, bool shortname, bool basedat, Logger logger) + { + // Sanitize the basepath to be more predictable + basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar); + + // First, organize by games so that we can do the right thing + BucketByGame(false, true, logger, output: false, lower: false); + + // Create a temporary DAT to add things to + DatFile tempDat = (DatFile)CloneHeader(); + tempDat.Name = null; + + // Sort the input keys + List keys = Files.Keys.ToList(); + keys.Sort(SplitByLevelSort); + + // Then, we loop over the games + foreach (string key in keys) + { + // Here, the key is the name of the game to be used for comparison + if (tempDat.Name != null && tempDat.Name != Style.GetDirectoryName(key)) + { + // Process and output the DAT + SplitByLevelHelper(tempDat, outDir, shortname, basedat, logger); + + // Reset the DAT for the next items + tempDat = (DatFile)CloneHeader(); + tempDat.Name = null; + } + + // Clean the input list and set all games to be pathless + List items = Files[key]; + items.ForEach(item => item.Machine.Name = Style.GetFileName(item.Machine.Name)); + items.ForEach(item => item.Machine.Description = Style.GetFileName(item.Machine.Description)); + + // Now add the game to the output DAT + if (tempDat.Files.ContainsKey(key)) + { + tempDat.Files[key].AddRange(items); + } + else + { + tempDat.Files.Add(key, items); + } + + // Then set the DAT name to be the parent directory name + tempDat.Name = Style.GetDirectoryName(key); + } + + // Then we write the last DAT out since it would be skipped otherwise + SplitByLevelHelper(tempDat, outDir, shortname, basedat, logger); + + return true; + } + + /// + /// Helper function for SplitByLevel to sort the input game names + /// + /// First string to compare + /// Second string to compare + /// -1 for a coming before b, 0 for a == b, 1 for a coming after b + private int SplitByLevelSort(string a, string b) + { + NaturalComparer nc = new NaturalComparer(); + int adeep = a.Count(c => c == '/' || c == '\\'); + int bdeep = b.Count(c => c == '/' || c == '\\'); + + if (adeep == bdeep) + { + return nc.Compare(a, b); + } + return adeep - bdeep; + } + + /// + /// Helper function for SplitByLevel to clean and write out a DAT + /// + /// DAT to clean and write out + /// Directory to write out to + /// True if short naming scheme should be used, false otherwise + /// True if original filenames should be used as the base for output filename, false otherwise + /// Logger object for file and console output + private void SplitByLevelHelper(DatFile datFile, string outDir, bool shortname, bool restore, Logger logger) + { + // Get the name from the DAT to use separately + string name = datFile.Name; + string expName = name.Replace("/", " - ").Replace("\\", " - "); + + // Get the path that the file will be written out to + string path = HttpUtility.HtmlDecode(String.IsNullOrEmpty(name) + ? outDir + : Path.Combine(outDir, name)); + + // Now set the new output values + datFile.FileName = HttpUtility.HtmlDecode(String.IsNullOrEmpty(name) + ? FileName + : (shortname + ? Style.GetFileName(name) + : expName + ) + ); + datFile.FileName = (restore ? FileName + " (" + datFile.FileName + ")" : datFile.FileName); + datFile.Name = Name + " (" + expName + ")"; + datFile.Description = (String.IsNullOrEmpty(Description) ? datFile.Name : Description + " (" + expName + ")"); + datFile.Type = null; + + // Write out the temporary DAT to the proper directory + datFile.WriteToFile(path, logger); + } + + /// + /// Split a DAT by type of Rom + /// + /// Name of the directory to write the DATs out to + /// Parent path for replacement + /// Logger object for console and file writing + /// True if split succeeded, false otherwise + public bool SplitByType(string outDir, string basepath, Logger logger) + { + // Sanitize the basepath to be more predictable + basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar); + + // Create each of the respective output DATs + logger.User("Creating and populating new DATs"); + DatFile romdat = new DatFile + { + FileName = this.FileName + " (ROM)", + Name = this.Name + " (ROM)", + Description = this.Description + " (ROM)", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + MergeRoms = this.MergeRoms, + Files = new SortedDictionary>(), + }; + DatFile diskdat = new DatFile + { + FileName = this.FileName + " (Disk)", + Name = this.Name + " (Disk)", + Description = this.Description + " (Disk)", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + MergeRoms = this.MergeRoms, + Files = new SortedDictionary>(), + }; + DatFile sampledat = new DatFile + { + FileName = this.FileName + " (Sample)", + Name = this.Name + " (Sample)", + Description = this.Description + " (Sample)", + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + MergeRoms = this.MergeRoms, + Files = new SortedDictionary>(), + }; + + // Now populate each of the DAT objects in turn + List keys = this.Files.Keys.ToList(); + foreach (string key in keys) + { + List roms = this.Files[key]; + foreach (DatItem rom in roms) + { + // If the file is a Rom + if (rom.Type == ItemType.Rom) + { + if (romdat.Files.ContainsKey(key)) + { + romdat.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + romdat.Files.Add(key, temp); + } + } + // If the file is a Disk + else if (rom.Type == ItemType.Disk) + { + if (diskdat.Files.ContainsKey(key)) + { + diskdat.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + diskdat.Files.Add(key, temp); + } + } + + // If the file is a Sample + else if (rom.Type == ItemType.Sample) + { + if (sampledat.Files.ContainsKey(key)) + { + sampledat.Files[key].Add(rom); + } + else + { + List temp = new List(); + temp.Add(rom); + sampledat.Files.Add(key, temp); + } + } + } + } + + // Get the output directory + if (outDir != "") + { + outDir = outDir + Path.GetDirectoryName(this.FileName).Remove(0, basepath.Length - 1); + } + else + { + outDir = Path.GetDirectoryName(this.FileName); + } + + // Now, output all of the files to the output directory + logger.User("DAT information created, outputting new files"); + bool success = true; + if (romdat.Files.Count > 0) + { + success &= romdat.WriteToFile(outDir, logger); + } + if (diskdat.Files.Count > 0) + { + success &= diskdat.WriteToFile(outDir, logger); + } + if (sampledat.Files.Count > 0) + { + success &= sampledat.WriteToFile(outDir, logger); + } + + return success; + } + + #endregion + } +} diff --git a/SabreTools.Helper/Dats/Partials/DatFile.Statistics.cs b/SabreTools.Helper/Dats/Partials/DatFile.Statistics.cs new file mode 100644 index 00000000..97bda150 --- /dev/null +++ b/SabreTools.Helper/Dats/Partials/DatFile.Statistics.cs @@ -0,0 +1,640 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +using SabreTools.Helper.Data; +using SabreTools.Helper.Tools; + +#if MONO +using System.IO; +#else +using Alphaleonis.Win32.Filesystem; + +using FileAccess = System.IO.FileAccess; +using FileMode = System.IO.FileMode; +using SearchOption = System.IO.SearchOption; +using StreamWriter = System.IO.StreamWriter; +#endif + +namespace SabreTools.Helper.Dats +{ + public partial class DatFile + { + #region Instance Methods + + #region Statistics [MODULAR DONE] + + /// + /// Recalculate the statistics for the Dat + /// + public void RecalculateStats() + { + // Wipe out any stats already there + RomCount = 0; + DiskCount = 0; + TotalSize = 0; + CRCCount = 0; + MD5Count = 0; + SHA1Count = 0; + BaddumpCount = 0; + NodumpCount = 0; + + // If we have a blank Dat in any way, return + if (this == null || Files == null || Files.Count == 0) + { + return; + } + + // Loop through and add + foreach (List roms in Files.Values) + { + foreach (Rom rom in roms) + { + RomCount += (rom.Type == ItemType.Rom ? 1 : 0); + DiskCount += (rom.Type == ItemType.Disk ? 1 : 0); + TotalSize += (rom.ItemStatus == ItemStatus.Nodump ? 0 : rom.Size); + CRCCount += (String.IsNullOrEmpty(rom.CRC) ? 0 : 1); + MD5Count += (String.IsNullOrEmpty(rom.MD5) ? 0 : 1); + SHA1Count += (String.IsNullOrEmpty(rom.SHA1) ? 0 : 1); + BaddumpCount += (rom.Type == ItemType.Disk + ? (((Disk)rom).ItemStatus == ItemStatus.BadDump ? 1 : 0) + : (rom.Type == ItemType.Rom + ? (((Rom)rom).ItemStatus == ItemStatus.BadDump ? 1 : 0) + : 0) + ); + NodumpCount += (rom.Type == ItemType.Disk + ? (((Disk)rom).ItemStatus == ItemStatus.Nodump ? 1 : 0) + : (rom.Type == ItemType.Rom + ? (((Rom)rom).ItemStatus == ItemStatus.Nodump ? 1 : 0) + : 0) + ); + } + } + } + + /// + /// Output the stats for the Dat in a human-readable format + /// + /// StreamWriter representing the output file or stream for the statistics + /// Set the statistics output format to use + /// Logger object for file and console writing + /// True if numbers should be recalculated for the DAT, false otherwise (default) + /// Number of games to use, -1 means recalculate games (default) + /// True if baddumps should be included in output, false otherwise (default) + /// True if nodumps should be included in output, false otherwise (default) + public void OutputStats(StreamWriter sw, StatDatFormat statDatFormat, Logger logger, bool recalculate = false, long game = -1, bool baddumpCol = false, bool nodumpCol = false) + { + // If we're supposed to recalculate the statistics, do so + if (recalculate) + { + RecalculateStats(); + } + + BucketByGame(false, true, logger, false); + if (TotalSize < 0) + { + TotalSize = Int64.MaxValue + TotalSize; + } + + // Log the results to screen + string results = @"For '" + FileName + @"': +-------------------------------------------------- + Uncompressed size: " + Style.GetBytesReadable(TotalSize) + @" + Games found: " + (game == -1 ? Files.Count : game) + @" + Roms found: " + RomCount + @" + Disks found: " + DiskCount + @" + Roms with CRC: " + CRCCount + @" + Roms with MD5: " + MD5Count + @" + Roms with SHA-1: " + SHA1Count + "\n"; + + if (baddumpCol) + { + results += " Roms with BadDump status: " + BaddumpCount + "\n"; + } + if (nodumpCol) + { + results += " Roms with Nodump status: " + NodumpCount + "\n"; + } + + logger.User(results); + + // Now write it out to file as well + string line = ""; + switch (statDatFormat) + { + case StatDatFormat.CSV: + line = "\"" + FileName + "\"," + + "\"" + Style.GetBytesReadable(TotalSize) + "\"," + + "\"" + (game == -1 ? Files.Count : game) + "\"," + + "\"" + RomCount + "\"," + + "\"" + DiskCount + "\"," + + "\"" + CRCCount + "\"," + + "\"" + MD5Count + "\"," + + "\"" + SHA1Count + "\""; + + if (baddumpCol) + { + line += ",\"" + BaddumpCount + "\""; + } + if (nodumpCol) + { + line += ",\"" + NodumpCount + "\""; + } + + line += "\n"; + break; + case StatDatFormat.HTML: + line = "\t\t\t" + HttpUtility.HtmlEncode(FileName.Remove(0, 5)) + : ">" + HttpUtility.HtmlEncode(FileName)) + "" + + "" + Style.GetBytesReadable(TotalSize) + "" + + "" + (game == -1 ? Files.Count : game) + "" + + "" + RomCount + "" + + "" + DiskCount + "" + + "" + CRCCount + "" + + "" + MD5Count + "" + + "" + SHA1Count + ""; + + if (baddumpCol) + { + line += "" + BaddumpCount + ""; + } + if (nodumpCol) + { + line += "" + NodumpCount + ""; + } + + line += "\n"; + break; + case StatDatFormat.None: + default: + line = @"'" + FileName + @"': +-------------------------------------------------- + Uncompressed size: " + Style.GetBytesReadable(TotalSize) + @" + Games found: " + (game == -1 ? Files.Count : game) + @" + Roms found: " + RomCount + @" + Disks found: " + DiskCount + @" + Roms with CRC: " + CRCCount + @" + Roms with MD5: " + MD5Count + @" + Roms with SHA-1: " + SHA1Count + "\n"; + + if (baddumpCol) + { + line += " Roms with BadDump status: " + BaddumpCount + "\n"; + } + if (nodumpCol) + { + line += " Roms with Nodump status: " + NodumpCount + "\n"; + } + break; + case StatDatFormat.TSV: + line = "\"" + FileName + "\"\t" + + "\"" + Style.GetBytesReadable(TotalSize) + "\"\t" + + "\"" + (game == -1 ? Files.Count : game) + "\"\t" + + "\"" + RomCount + "\"\t" + + "\"" + DiskCount + "\"\t" + + "\"" + CRCCount + "\"\t" + + "\"" + MD5Count + "\"\t" + + "\"" + SHA1Count + "\""; + + if (baddumpCol) + { + line += "\t\"" + BaddumpCount + "\""; + } + if (nodumpCol) + { + line += "\t\"" + NodumpCount + "\""; + } + + line += "\n"; + break; + } + + // Output the line to the streamwriter + sw.Write(line); + } + + #endregion + + #endregion // Instance Methods + + #region Static Methods + + #region Statistics [MODULAR DONE] + + /// + /// Output the stats for a list of input dats as files in a human-readable format + /// + /// List of input files and folders + /// Name of the output file + /// True if single DAT stats are output, false otherwise + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + /// Set the statistics output format to use + /// Logger object for file and console output + public static void OutputStats(List inputs, string reportName, bool single, bool baddumpCol, + bool nodumpCol, StatDatFormat statDatFormat, Logger logger) + { + reportName += OutputStatsGetExtension(statDatFormat); + StreamWriter sw = new StreamWriter(File.Open(reportName, FileMode.Create, FileAccess.Write)); + + // Make sure we have all files + List> newinputs = new List>(); // item, basepath + foreach (string input in inputs) + { + if (File.Exists(input)) + { + newinputs.Add(Tuple.Create(Path.GetFullPath(input), Path.GetDirectoryName(Path.GetFullPath(input)))); + } + if (Directory.Exists(input)) + { + foreach (string file in Directory.GetFiles(input, "*", SearchOption.AllDirectories)) + { + newinputs.Add(Tuple.Create(Path.GetFullPath(file), Path.GetFullPath(input))); + } + } + } + newinputs = newinputs + .OrderBy(i => Path.GetDirectoryName(i.Item1)) + .ThenBy(i => Path.GetFileName(i.Item1)) + .ToList(); + + // Write the header, if any + OutputStatsWriteHeader(sw, statDatFormat, baddumpCol, nodumpCol); + + // Init all total variables + long totalSize = 0; + long totalGame = 0; + long totalRom = 0; + long totalDisk = 0; + long totalCRC = 0; + long totalMD5 = 0; + long totalSHA1 = 0; + long totalBaddump = 0; + long totalNodump = 0; + + // Init directory-level variables + string lastdir = null; + string basepath = null; + long dirSize = 0; + long dirGame = 0; + long dirRom = 0; + long dirDisk = 0; + long dirCRC = 0; + long dirMD5 = 0; + long dirSHA1 = 0; + long dirBaddump = 0; + long dirNodump = 0; + + // Now process each of the input files + foreach (Tuple filename in newinputs) + { + // Get the directory for the current file + string thisdir = Path.GetDirectoryName(filename.Item1); + basepath = Path.GetDirectoryName(filename.Item2); + + // If we don't have the first file and the directory has changed, show the previous directory stats and reset + if (lastdir != null && thisdir != lastdir) + { + // Output separator if needed + OutputStatsWriteMidSeparator(sw, statDatFormat, baddumpCol, nodumpCol); + + DatFile lastdirdat = new DatFile + { + FileName = "DIR: " + HttpUtility.HtmlEncode(lastdir.Remove(0, basepath.Length + (basepath.Length == 0 ? 0 : 1))), + TotalSize = dirSize, + RomCount = dirRom, + DiskCount = dirDisk, + CRCCount = dirCRC, + MD5Count = dirMD5, + SHA1Count = dirSHA1, + BaddumpCount = dirBaddump, + NodumpCount = dirNodump, + }; + lastdirdat.OutputStats(sw, statDatFormat, logger, game: dirGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); + + // Write the mid-footer, if any + OutputStatsWriteMidFooter(sw, statDatFormat, baddumpCol, nodumpCol); + + // Write the header, if any + OutputStatsWriteMidHeader(sw, statDatFormat, baddumpCol, nodumpCol); + + // Reset the directory stats + dirSize = 0; + dirGame = 0; + dirRom = 0; + dirDisk = 0; + dirCRC = 0; + dirMD5 = 0; + dirSHA1 = 0; + dirBaddump = 0; + dirNodump = 0; + } + + logger.Verbose("Beginning stat collection for '" + filename.Item1 + "'", false); + List games = new List(); + DatFile datdata = new DatFile(); + datdata.Parse(filename.Item1, 0, 0, logger); + datdata.BucketByGame(false, true, logger, false); + + // Output single DAT stats (if asked) + logger.User("Adding stats for file '" + filename.Item1 + "'\n", false); + if (single) + { + datdata.OutputStats(sw, statDatFormat, logger, baddumpCol: baddumpCol, nodumpCol: nodumpCol); + } + + // Add single DAT stats to dir + dirSize += datdata.TotalSize; + dirGame += datdata.Files.Count; + dirRom += datdata.RomCount; + dirDisk += datdata.DiskCount; + dirCRC += datdata.CRCCount; + dirMD5 += datdata.MD5Count; + dirSHA1 += datdata.SHA1Count; + dirBaddump += datdata.BaddumpCount; + dirNodump += datdata.NodumpCount; + + // Add single DAT stats to totals + totalSize += datdata.TotalSize; + totalGame += datdata.Files.Count; + totalRom += datdata.RomCount; + totalDisk += datdata.DiskCount; + totalCRC += datdata.CRCCount; + totalMD5 += datdata.MD5Count; + totalSHA1 += datdata.SHA1Count; + totalBaddump += datdata.BaddumpCount; + totalNodump += datdata.NodumpCount; + + // Make sure to assign the new directory + lastdir = thisdir; + } + + // Output the directory stats one last time + OutputStatsWriteMidSeparator(sw, statDatFormat, baddumpCol, nodumpCol); + + if (single) + { + DatFile dirdat = new DatFile + { + FileName = "DIR: " + HttpUtility.HtmlEncode(lastdir.Remove(0, basepath.Length + (basepath.Length == 0 ? 0 : 1))), + TotalSize = dirSize, + RomCount = dirRom, + DiskCount = dirDisk, + CRCCount = dirCRC, + MD5Count = dirMD5, + SHA1Count = dirSHA1, + BaddumpCount = dirBaddump, + NodumpCount = dirNodump, + }; + dirdat.OutputStats(sw, statDatFormat, logger, game: dirGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); + } + + // Write the mid-footer, if any + OutputStatsWriteMidFooter(sw, statDatFormat, baddumpCol, nodumpCol); + + // Write the header, if any + OutputStatsWriteMidHeader(sw, statDatFormat, baddumpCol, nodumpCol); + + // Reset the directory stats + dirSize = 0; + dirGame = 0; + dirRom = 0; + dirDisk = 0; + dirCRC = 0; + dirMD5 = 0; + dirSHA1 = 0; + dirNodump = 0; + + // Output total DAT stats + DatFile totaldata = new DatFile + { + FileName = "DIR: All DATs", + TotalSize = totalSize, + RomCount = totalRom, + DiskCount = totalDisk, + CRCCount = totalCRC, + MD5Count = totalMD5, + SHA1Count = totalSHA1, + BaddumpCount = totalBaddump, + NodumpCount = totalNodump, + }; + totaldata.OutputStats(sw, statDatFormat, logger, game: totalGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); + + // Output footer if needed + OutputStatsWriteFooter(sw, statDatFormat); + + sw.Flush(); + sw.Dispose(); + + logger.User(@" +Please check the log folder if the stats scrolled offscreen", false); + } + + /// + /// Get the proper extension for the stat output format + /// + /// StatDatFormat to get the extension for + /// File extension with leading period + private static string OutputStatsGetExtension(StatDatFormat statDatFormat) + { + string reportExtension = ""; + switch (statDatFormat) + { + case StatDatFormat.CSV: + reportExtension = ".csv"; + break; + case StatDatFormat.HTML: + reportExtension = ".html"; + break; + case StatDatFormat.None: + default: + reportExtension = ".txt"; + break; + case StatDatFormat.TSV: + reportExtension = ".csv"; + break; + } + return reportExtension; + } + + /// + /// Write out the header to the stream, if any exists + /// + /// StreamWriter representing the output + /// StatDatFormat representing output format + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private static void OutputStatsWriteHeader(StreamWriter sw, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol) + { + string head = ""; + switch (statDatFormat) + { + case StatDatFormat.CSV: + break; + case StatDatFormat.HTML: + head = @" + +
+ DAT Statistics Report + +
+ +

DAT Statistics Report (" + DateTime.Now.ToShortDateString() + @")

+ +"; + break; + case StatDatFormat.None: + default: + break; + case StatDatFormat.TSV: + break; + } + sw.Write(head); + + // Now write the mid header for those who need it + OutputStatsWriteMidHeader(sw, statDatFormat, baddumpCol, nodumpCol); + } + + /// + /// Write out the mid-header to the stream, if any exists + /// + /// StreamWriter representing the output + /// StatDatFormat representing output format + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private static void OutputStatsWriteMidHeader(StreamWriter sw, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol) + { + string head = ""; + switch (statDatFormat) + { + case StatDatFormat.CSV: + head = "\"File Name\",\"Total Size\",\"Games\",\"Roms\",\"Disks\",\"# with CRC\",\"# with MD5\",\"# with SHA-1\"" + + (baddumpCol ? ",\"BadDumps\"" : "") + (nodumpCol ? ",\"Nodumps\"" : "") + "\n"; + break; + case StatDatFormat.HTML: + head = @" " ++ @"" ++ (baddumpCol ? "" : "") + (nodumpCol ? "" : "") + "\n"; + break; + case StatDatFormat.None: + default: + break; + case StatDatFormat.TSV: + head = "\"File Name\"\t\"Total Size\"\t\"Games\"\t\"Roms\"\t\"Disks\"\t\"# with CRC\"\t\"# with MD5\"\t\"# with SHA-1\"" + + (baddumpCol ? "\t\"BadDumps\"" : "") + (nodumpCol ? "\t\"Nodumps\"" : "") + "\n"; + break; + } + sw.Write(head); + } + + /// + /// Write out the separator to the stream, if any exists + /// + /// StreamWriter representing the output + /// StatDatFormat representing output format + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private static void OutputStatsWriteMidSeparator(StreamWriter sw, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol) + { + string mid = ""; + switch (statDatFormat) + { + case StatDatFormat.CSV: + break; + case StatDatFormat.HTML: + mid = "\n"; + break; + case StatDatFormat.None: + default: + break; + } + sw.Write(mid); + } + + /// + /// Write out the footer-separator to the stream, if any exists + /// + /// StreamWriter representing the output + /// StatDatFormat representing output format + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private static void OutputStatsWriteMidFooter(StreamWriter sw, StatDatFormat statDatFormat, bool baddumpCol, bool nodumpCol) + { + string end = ""; + switch (statDatFormat) + { + case StatDatFormat.CSV: + end = "\n"; + break; + case StatDatFormat.HTML: + end = "\n"; + break; + case StatDatFormat.None: + default: + end = "\n"; + break; + case StatDatFormat.TSV: + end = "\n"; + break; + } + sw.Write(end); + } + + /// + /// Write out the footer to the stream, if any exists + /// + /// StreamWriter representing the output + /// StatDatFormat representing output format + private static void OutputStatsWriteFooter(StreamWriter sw, StatDatFormat statDatFormat) + { + string end = ""; + switch (statDatFormat) + { + case StatDatFormat.CSV: + break; + case StatDatFormat.HTML: + end = @"
File NameTotal SizeGamesRomsDisks# with CRC# with MD5# with SHA-1BaddumpsNodumps
+ + +"; + break; + case StatDatFormat.None: + default: + break; + case StatDatFormat.TSV: + break; + } + sw.Write(end); + } + + #endregion + + #endregion // Static Methods + } +} diff --git a/SabreTools.Helper/Dats/Partials/DatFile.Writers.cs b/SabreTools.Helper/Dats/Partials/DatFile.Writers.cs new file mode 100644 index 00000000..2cbf27a1 --- /dev/null +++ b/SabreTools.Helper/Dats/Partials/DatFile.Writers.cs @@ -0,0 +1,1373 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web; + +using SabreTools.Helper.Data; +using SabreTools.Helper.Tools; + +#if MONO +using System.IO; +#else +using Alphaleonis.Win32.Filesystem; + +using FileStream = System.IO.FileStream; +using MemoryStream = System.IO.MemoryStream; +using StreamWriter = System.IO.StreamWriter; +#endif +using NaturalSort; + +namespace SabreTools.Helper.Dats +{ + public partial class DatFile + { + #region Writing [MODULAR DONE] + + /// + /// Create and open an output file for writing direct from a dictionary + /// + /// All information for creating the datfile header + /// Set the output directory + /// Logger object for console and/or file output + /// True if games should only be compared on game and file name (default), false if system and source are counted + /// True if DAT statistics should be output on write, false otherwise (default) + /// True if blank roms should be skipped on output, false otherwise (default) + /// True if files should be overwritten (default), false if they should be renamed instead + /// True if the DAT was written correctly, false otherwise + /// + /// The following features have been requested for file output: + /// - Have the ability to strip special (non-ASCII) characters from rom information + /// + public bool WriteToFile(string outDir, Logger logger, bool norename = true, bool stats = false, bool ignoreblanks = false, bool overwrite = true) + { + // If there's nothing there, abort + if (Files == null || Files.Count == 0) + { + return false; + } + + // If output directory is empty, use the current folder + if (outDir.Trim() == "") + { + outDir = Environment.CurrentDirectory; + } + + // Create the output directory if it doesn't already exist + if (!Directory.Exists(outDir)) + { + Directory.CreateDirectory(outDir); + } + + // If the DAT has no output format, default to XML + if (DatFormat == 0) + { + DatFormat = DatFormat.Logiqx; + } + + // Make sure that the three essential fields are filled in + if (String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) + { + FileName = Name = Description = "Default"; + } + else if (String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) + { + FileName = Name = Description; + } + else if (String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) + { + FileName = Description = Name; + } + else if (String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) + { + FileName = Description; + } + else if (!String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) + { + Name = Description = FileName; + } + else if (!String.IsNullOrEmpty(FileName) && String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) + { + Name = Description; + } + else if (!String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && String.IsNullOrEmpty(Description)) + { + Description = Name; + } + else if (!String.IsNullOrEmpty(FileName) && !String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(Description)) + { + // Nothing is needed + } + + // Output initial statistics, for kicks + if (stats) + { + StreamWriter sw = new StreamWriter(new MemoryStream()); + OutputStats(sw, StatDatFormat.None, logger, recalculate: (RomCount + DiskCount == 0), baddumpCol: true, nodumpCol: true); + sw.Dispose(); + } + + // Bucket roms by game name and optionally dedupe + BucketByGame(MergeRoms, norename, logger); + + // Get the outfile name + Dictionary outfiles = Style.CreateOutfileNames(outDir, this, overwrite); + + try + { + // Get a properly sorted set of keys + List keys = Files.Keys.ToList(); + keys.Sort(new NaturalComparer()); + + foreach (DatFormat datFormat in outfiles.Keys) + { + string outfile = outfiles[datFormat]; + + logger.User("Opening file for writing: " + outfile); + FileStream fs = File.Create(outfile); + StreamWriter sw = new StreamWriter(fs, new UTF8Encoding(true)); + + // Write out the header + WriteHeader(sw, datFormat, logger); + + // Write out each of the machines and roms + int depth = 2, last = -1; + string lastgame = null; + List splitpath = new List(); + + foreach (string key in keys) + { + List roms = Files[key]; + + for (int index = 0; index < roms.Count; index++) + { + DatItem rom = roms[index]; + + // There are apparently times when a null rom can skip by, skip them + if (rom.Name == null || rom.Machine.Name == null) + { + logger.Warning("Null rom found!"); + continue; + } + + List newsplit = rom.Machine.Name.Split('\\').ToList(); + + // If we have a different game and we're not at the start of the list, output the end of last item + if (lastgame != null && lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant()) + { + depth = WriteEndGame(sw, datFormat, rom, splitpath, newsplit, lastgame, depth, out last, logger); + } + + // If we have a new game, output the beginning of the new item + if (lastgame == null || lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant()) + { + depth = WriteStartGame(sw, datFormat, rom, newsplit, lastgame, depth, last, logger); + } + + // If we have a "null" game (created by DATFromDir or something similar), log it to file + if (rom.Type == ItemType.Rom + && ((Rom)rom).Size == -1 + && ((Rom)rom).CRC == "null" + && ((Rom)rom).MD5 == "null" + && ((Rom)rom).SHA1 == "null") + { + logger.Verbose("Empty folder found: " + rom.Machine.Name); + + // If we're in a mode that doesn't allow for actual empty folders, add the blank info + if (datFormat != DatFormat.CSV + && datFormat != DatFormat.MissFile + && datFormat != DatFormat.SabreDat + && datFormat != DatFormat.TSV) + { + rom.Name = (rom.Name == "null" ? "-" : rom.Name); + ((Rom)rom).Size = Constants.SizeZero; + ((Rom)rom).CRC = Constants.CRCZero; + ((Rom)rom).MD5 = Constants.MD5Zero; + ((Rom)rom).SHA1 = Constants.SHA1Zero; + } + + // Otherwise, set the new path and such, write out, and continue + else + { + splitpath = newsplit; + lastgame = rom.Machine.Name; + continue; + } + } + + // Now, output the rom data + WriteRomData(sw, datFormat, rom, lastgame, depth, logger, ignoreblanks); + + // Set the new data to compare against + splitpath = newsplit; + lastgame = rom.Machine.Name; + } + } + + // Write the file footer out + WriteFooter(sw, datFormat, depth, logger); + + logger.Verbose("File written!" + Environment.NewLine); + sw.Dispose(); + fs.Dispose(); + } + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + return false; + } + + return true; + } + + /// + /// Write out DAT header using the supplied StreamWriter + /// + /// StreamWriter to output to + /// Output format to write to + /// Logger object for file and console output + /// True if the data was written, false on error + private bool WriteHeader(StreamWriter sw, DatFormat datFormat, Logger logger) + { + try + { + string header = ""; + switch (datFormat) + { + case DatFormat.AttractMode: + header = "#Name;Title;Emulator;CloneOf;Year;Manufacturer;Category;Players;Rotation;Control;Status;DisplayCount;DisplayType;AltRomname;AltTitle;Extra;Buttons\n"; + break; + case DatFormat.ClrMamePro: + header = "clrmamepro (\n" + + "\tname \"" + Name + "\"\n" + + "\tdescription \"" + Description + "\"\n" + + (!String.IsNullOrEmpty(Category) ? "\tcategory \"" + Category + "\"\n" : "") + + "\tversion \"" + Version + "\"\n" + + (!String.IsNullOrEmpty(Date) ? "\tdate \"" + Date + "\"\n" : "") + + "\tauthor \"" + Author + "\"\n" + + (!String.IsNullOrEmpty(Email) ? "\temail \"" + Email + "\"\n" : "") + + (!String.IsNullOrEmpty(Homepage) ? "\thomepage \"" + Homepage + "\"\n" : "") + + (!String.IsNullOrEmpty(Url) ? "\turl \"" + Url + "\"\n" : "") + + (!String.IsNullOrEmpty(Comment) ? "\tcomment \"" + Comment + "\"\n" : "") + + (ForcePacking == ForcePacking.Unzip ? "\tforcezipping no\n" : "") + + (ForcePacking == ForcePacking.Zip ? "\tforcezipping yes\n" : "") + + (ForceMerging == ForceMerging.Full ? "\tforcemerging full\n" : "") + + (ForceMerging == ForceMerging.Split ? "\tforcemerging split\n" : "") + + ")\n"; + break; + case DatFormat.CSV: + header = "\"File Name\",\"Internal Name\",\"Description\",\"Game Name\",\"Game Description\",\"Type\",\"" + + "Rom Name\",\"Disk Name\",\"Size\",\"CRC\",\"MD5\",\"SHA1\",\"Nodump\"\n"; + break; + case DatFormat.DOSCenter: + header = "DOSCenter (\n" + + "\tName: " + Name + "\n" + + "\tDescription: " + Description + "\n" + + "\tVersion: " + Version + "\n" + + "\tDate: " + Date + "\n" + + "\tAuthor: " + Author + "\n" + + "\tHomepage: " + Homepage + "\n" + + "\tComment: " + Comment + "\n" + + ")\n"; + break; + case DatFormat.Logiqx: + header = "\n" + + "\n\n" + + "\n" + + "\t
\n" + + "\t\t" + HttpUtility.HtmlEncode(Name) + "\n" + + "\t\t" + HttpUtility.HtmlEncode(Description) + "\n" + + (!String.IsNullOrEmpty(RootDir) ? "\t\t" + HttpUtility.HtmlEncode(RootDir) + "\n" : "") + + (!String.IsNullOrEmpty(Category) ? "\t\t" + HttpUtility.HtmlEncode(Category) + "\n" : "") + + "\t\t" + HttpUtility.HtmlEncode(Version) + "\n" + + (!String.IsNullOrEmpty(Date) ? "\t\t" + HttpUtility.HtmlEncode(Date) + "\n" : "") + + "\t\t" + HttpUtility.HtmlEncode(Author) + "\n" + + (!String.IsNullOrEmpty(Email) ? "\t\t" + HttpUtility.HtmlEncode(Email) + "\n" : "") + + (!String.IsNullOrEmpty(Homepage) ? "\t\t" + HttpUtility.HtmlEncode(Homepage) + "\n" : "") + + (!String.IsNullOrEmpty(Url) ? "\t\t" + HttpUtility.HtmlEncode(Url) + "\n" : "") + + (!String.IsNullOrEmpty(Comment) ? "\t\t" + HttpUtility.HtmlEncode(Comment) + "\n" : "") + + (!String.IsNullOrEmpty(Type) ? "\t\t" + HttpUtility.HtmlEncode(Type) + "\n" : "") + + (ForcePacking != ForcePacking.None || ForceMerging != ForceMerging.None || ForceNodump != ForceNodump.None ? + "\t\t\n" + : "") + + "\t
\n"; + break; + case DatFormat.TSV: + header = "\"File Name\"\t\"Internal Name\"\t\"Description\"\t\"Game Name\"\t\"Game Description\"\t\"Type\"\t\"" + + "Rom Name\"\t\"Disk Name\"\t\"Size\"\t\"CRC\"\t\"MD5\"\t\"SHA1\"\t\"Nodump\"\n"; + break; + case DatFormat.OfflineList: + header = "\n" + + "\n" + + "\t\n" + + "\t\t" + HttpUtility.HtmlEncode(Name) + "\n" + + "\t\t" + Files.Count + "\n" + + "\t\tnone\n" + + "\t\t240\n" + + "\t\t160\n" + + "\t\t\n" + + "\t\t\t\n" + + "\t\t\t<location visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" + + "\t\t\t<publisher visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" + + "\t\t\t<sourceRom visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" + + "\t\t\t<saveType visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" + + "\t\t\t<romSize visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" + + "\t\t\t<releaseNumber visible=\"true\" inNamingOption=\"true\" default=\"false\"/>\n" + + "\t\t\t<languageNumber visible=\"true\" inNamingOption=\"true\" default=\"false\"/>\n" + + "\t\t\t<comment visible=\"true\" inNamingOption=\"true\" default=\"false\"/>\n" + + "\t\t\t<romCRC visible=\"true\" inNamingOption=\"true\" default=\"false\"/>\n" + + "\t\t\t<im1CRC visible=\"false\" inNamingOption=\"false\" default=\"false\"/>\n" + + "\t\t\t<im2CRC visible=\"false\" inNamingOption=\"false\" default=\"false\"/>\n" + + "\t\t\t<languages visible=\"true\" inNamingOption=\"true\" default=\"true\"/>\n" + + "\t\t</infos>\n" + + "\t\t<canOpen>\n" + + "\t\t\t<extension>.bin</extension>\n" + + "\t\t</canOpen>\n" + + "\t\t<newDat>\n" + + "\t\t\t<datVersionURL>" + HttpUtility.HtmlEncode(Url) + "</datVersionURL>\n" + + "\t\t\t<datURL fileName=\"" + HttpUtility.HtmlEncode(FileName) + ".zip\">" + HttpUtility.HtmlEncode(Url) + "</datURL>\n" + + "\t\t\t<imURL>" + HttpUtility.HtmlEncode(Url) + "</imURL>\n" + + "\t\t</newDat>\n" + + "\t\t<search>\n" + + "\t\t\t<to value=\"location\" default=\"true\" auto=\"true\"/>\n" + + "\t\t\t<to value=\"romSize\" default=\"true\" auto=\"false\"/>\n" + + "\t\t\t<to value=\"languages\" default=\"true\" auto=\"true\"/>\n" + + "\t\t\t<to value=\"saveType\" default=\"false\" auto=\"false\"/>\n" + + "\t\t\t<to value=\"publisher\" default=\"false\" auto=\"true\"/>\n" + + "\t\t\t<to value=\"sourceRom\" default=\"false\" auto=\"true\"/>\n" + + "\t\t</search>\n" + + "\t\t<romTitle >%u - %n</romTitle>\n" + + "\t</configuration>\n" + + "\t<games>\n"; + break; + case DatFormat.RomCenter: + header = "[CREDITS]\n" + + "author=" + Author + "\n" + + "version=" + Version + "\n" + + "comment=" + Comment + "\n" + + "[DAT]\n" + + "version=2.50\n" + + "split=" + (ForceMerging == ForceMerging.Split ? "1" : "0") + "\n" + + "merge=" + (ForceMerging == ForceMerging.Full ? "1" : "0") + "\n" + + "[EMULATOR]\n" + + "refname=" + Name + "\n" + + "version=" + Description + "\n" + + "[GAMES]\n"; + break; + case DatFormat.SabreDat: + header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<!DOCTYPE sabredat SYSTEM \"newdat.xsd\">\n\n" + + "<datafile>\n" + + "\t<header>\n" + + "\t\t<name>" + HttpUtility.HtmlEncode(Name) + "</name>\n" + + "\t\t<description>" + HttpUtility.HtmlEncode(Description) + "</description>\n" + + (!String.IsNullOrEmpty(RootDir) ? "\t\t<rootdir>" + HttpUtility.HtmlEncode(RootDir) + "</rootdir>\n" : "") + + (!String.IsNullOrEmpty(Category) ? "\t\t<category>" + HttpUtility.HtmlEncode(Category) + "</category>\n" : "") + + "\t\t<version>" + HttpUtility.HtmlEncode(Version) + "</version>\n" + + (!String.IsNullOrEmpty(Date) ? "\t\t<date>" + HttpUtility.HtmlEncode(Date) + "</date>\n" : "") + + "\t\t<author>" + HttpUtility.HtmlEncode(Author) + "</author>\n" + + (!String.IsNullOrEmpty(Comment) ? "\t\t<comment>" + HttpUtility.HtmlEncode(Comment) + "</comment>\n" : "") + + (!String.IsNullOrEmpty(Type) || ForcePacking != ForcePacking.None || ForceMerging != ForceMerging.None || ForceNodump != ForceNodump.None ? + "\t\t<flags>\n" + + (!String.IsNullOrEmpty(Type) ? "\t\t\t<flag name=\"type\" value=\"" + HttpUtility.HtmlEncode(Type) + "\"/>\n" : "") + + (ForcePacking == ForcePacking.Unzip ? "\t\t\t<flag name=\"forcepacking\" value=\"unzip\"/>\n" : "") + + (ForcePacking == ForcePacking.Zip ? "\t\t\t<flag name=\"forcepacking\" value=\"zip\"/>\n" : "") + + (ForceMerging == ForceMerging.Full ? "\t\t\t<flag name=\"forcemerging\" value=\"full\"/>\n" : "") + + (ForceMerging == ForceMerging.Split ? "\t\t\t<flag name=\"forcemerging\" value=\"split\"/>\n" : "") + + (ForceNodump == ForceNodump.Ignore ? "\t\t\t<flag name=\"forceitemStatus\" value=\"ignore\"/>\n" : "") + + (ForceNodump == ForceNodump.Obsolete ? "\t\t\t<flag name=\"forceitemStatus\" value=\"obsolete\"/>\n" : "") + + (ForceNodump == ForceNodump.Required ? "\t\t\t<flag name=\"forceitemStatus\" value=\"required\"/>\n" : "") + + "\t\t</flags>\n" + : "") + + "\t</header>\n" + + "\t<data>\n"; + break; + case DatFormat.SoftwareList: + header = "<?xml version=\"1.0\"?>\n" + + "<!DOCTYPE softwarelist SYSTEM \"softwarelist.dtd\">\n\n" + + "<softwarelist name=\"" + HttpUtility.HtmlEncode(Name) + "\"" + + " description=\"" + HttpUtility.HtmlEncode(Description) + "\"" + + (ForcePacking == ForcePacking.Unzip ? " forcepacking=\"unzip\"" : "") + + (ForcePacking == ForcePacking.Zip ? " forcepacking=\"zip\"" : "") + + (ForceMerging == ForceMerging.Full ? " forcemerging=\"full\"" : "") + + (ForceMerging == ForceMerging.Split ? " forcemerging=\"split\"" : "") + + (ForceNodump == ForceNodump.Ignore ? " forceitemStatus=\"ignore\"" : "") + + (ForceNodump == ForceNodump.Obsolete ? " forceitemStatus=\"obsolete\"" : "") + + (ForceNodump == ForceNodump.Required ? " forceitemStatus=\"required\"" : "") + + ">\n\n"; + break; + } + + // Write the header out + sw.Write(header); + sw.Flush(); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + return false; + } + + return true; + } + + /// <summary> + /// Write out Game start using the supplied StreamWriter + /// </summary> + /// <param name="sw">StreamWriter to output to</param> + /// <param name="datFormat">Output format to write to</param> + /// <param name="rom">RomData object to be output</param> + /// <param name="newsplit">Split path representing the parent game (SabreDAT only)</param> + /// <param name="lastgame">The name of the last game to be output</param> + /// <param name="depth">Current depth to output file at (SabreDAT only)</param> + /// <param name="last">Last known depth to cycle back from (SabreDAT only)</param> + /// <param name="logger">Logger object for file and console output</param> + /// <returns>The new depth of the tag</returns> + private int WriteStartGame(StreamWriter sw, DatFormat datFormat, DatItem rom, List<string> newsplit, string lastgame, int depth, int last, Logger logger) + { + try + { + // No game should start with a path separator + if (rom.Machine.Name.StartsWith(Path.DirectorySeparatorChar.ToString())) + { + rom.Machine.Name = rom.Machine.Name.Substring(1); + } + + string state = ""; + switch (datFormat) + { + case DatFormat.AttractMode: + state += rom.Machine.Description + ";" + + rom.Machine.Name + ";" + + FileName + ";" + + rom.Machine.CloneOf + ";" + + rom.Machine.Year + ";" + + rom.Machine.Manufacturer + ";" + /* + rom.Machine.Category */ + ";" + /* + rom.Machine.Players */ + ";" + /* + rom.Machine.Rotation */ + ";" + /* + rom.Machine.Control */ + ";" + /* + rom.Machine.Status */ + ";" + /* + rom.Machine.DisplayCount */ + ";" + /* + rom.Machine.DisplayType */ + ";" + /* + rom.Machine.AltRomname */ + ";" + /* + rom.Machine.AltTitle */ + ";" + + rom.Machine.Comment + ";" + /* + rom.Machine.Buttons */ + "\n"; + break; + case DatFormat.ClrMamePro: + state += "game (\n\tname \"" + rom.Machine.Name + "\"\n" + + (ExcludeOf ? "" : + (String.IsNullOrEmpty(rom.Machine.RomOf) ? "" : "\tromof \"" + rom.Machine.RomOf + "\"\n") + + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : "\tcloneof \"" + rom.Machine.CloneOf + "\"\n") + + (String.IsNullOrEmpty(rom.Machine.SampleOf) ? "" : "\tsampleof \"" + rom.Machine.SampleOf + "\"\n") + ) + + "\tdescription \"" + (String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description) + "\"\n" + + (String.IsNullOrEmpty(rom.Machine.Year) ? "" : "\tyear " + rom.Machine.Year + "\n") + + (String.IsNullOrEmpty(rom.Machine.Manufacturer) ? "" : "\tmanufacturer \"" + rom.Machine.Manufacturer + "\"\n"); + break; + case DatFormat.DOSCenter: + state += "game (\n\tname \"" + rom.Machine.Name + ".zip\"\n"; + break; + case DatFormat.Logiqx: + state += "\t<machine name=\"" + HttpUtility.HtmlEncode(rom.Machine.Name) + "\"" + + (rom.Machine.IsBios ? " isbios=\"yes\"" : "") + + (ExcludeOf ? "" : + (String.IsNullOrEmpty(rom.Machine.CloneOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.CloneOf.ToLowerInvariant()) + ? "" + : " cloneof=\"" + HttpUtility.HtmlEncode(rom.Machine.CloneOf) + "\"") + + (String.IsNullOrEmpty(rom.Machine.RomOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.RomOf.ToLowerInvariant()) + ? "" + : " romof=\"" + HttpUtility.HtmlEncode(rom.Machine.RomOf) + "\"") + + (String.IsNullOrEmpty(rom.Machine.SampleOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.SampleOf.ToLowerInvariant()) + ? "" + : " sampleof=\"" + HttpUtility.HtmlEncode(rom.Machine.SampleOf) + "\"") + ) + + ">\n" + + (String.IsNullOrEmpty(rom.Machine.Comment) ? "" : "\t\t<comment>" + HttpUtility.HtmlEncode(rom.Machine.Comment) + "</comment>\n") + + "\t\t<description>" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description)) + "</description>\n" + + (String.IsNullOrEmpty(rom.Machine.Year) ? "" : "\t\t<year>" + HttpUtility.HtmlEncode(rom.Machine.Year) + "</year>\n") + + (String.IsNullOrEmpty(rom.Machine.Manufacturer) ? "" : "\t\t<manufacturer>" + HttpUtility.HtmlEncode(rom.Machine.Manufacturer) + "</manufacturer>\n"); + break; + case DatFormat.SabreDat: + for (int i = (last == -1 ? 0 : last); i < newsplit.Count; i++) + { + for (int j = 0; j < depth - last + i - (lastgame == null ? 1 : 0); j++) + { + state += "\t"; + } + state += "<directory name=\"" + HttpUtility.HtmlEncode(newsplit[i]) + "\" description=\"" + + HttpUtility.HtmlEncode(newsplit[i]) + "\">\n"; + } + depth = depth - (last == -1 ? 0 : last) + newsplit.Count; + break; + case DatFormat.SoftwareList: + state += "\t<software name=\"" + HttpUtility.HtmlEncode(rom.Machine.Name) + "\"" + + (rom.Supported != null ? " supported=\"" + (rom.Supported == true ? "yes" : "no") + "\"" : "") + + (ExcludeOf ? "" : + (String.IsNullOrEmpty(rom.Machine.CloneOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.CloneOf.ToLowerInvariant()) + ? "" + : " cloneof=\"" + HttpUtility.HtmlEncode(rom.Machine.CloneOf) + "\"") + + (String.IsNullOrEmpty(rom.Machine.RomOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.RomOf.ToLowerInvariant()) + ? "" + : " romof=\"" + HttpUtility.HtmlEncode(rom.Machine.RomOf) + "\"") + + (String.IsNullOrEmpty(rom.Machine.SampleOf) || (rom.Machine.Name.ToLowerInvariant() == rom.Machine.SampleOf.ToLowerInvariant()) + ? "" + : " sampleof=\"" + HttpUtility.HtmlEncode(rom.Machine.SampleOf) + "\"") + ) + ">\n" + + "\t\t<description>" + HttpUtility.HtmlEncode(rom.Machine.Description) + "</description>\n" + + (rom.Machine.Year != null ? "\t\t<year>" + HttpUtility.HtmlEncode(rom.Machine.Year) + "</year>\n" : "") + + (rom.Publisher != null ? "\t\t<publisher>" + HttpUtility.HtmlEncode(rom.Publisher) + "</publisher>\n" : ""); + + foreach (Tuple<string, string> kvp in rom.Infos) + { + state += "\t\t<info name=\"" + HttpUtility.HtmlEncode(kvp.Item1) + "\" value=\"" + HttpUtility.HtmlEncode(kvp.Item2) + "\" />\n"; + } + break; + } + + sw.Write(state); + sw.Flush(); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + return depth; + } + + return depth; + } + + /// <summary> + /// Write out Game start using the supplied StreamWriter + /// </summary> + /// <param name="sw">StreamWriter to output to</param> + /// <param name="datFormat">Output format to write to</param> + /// <param name="rom">RomData object to be output</param> + /// <param name="splitpath">Split path representing last kwown parent game (SabreDAT only)</param> + /// <param name="newsplit">Split path representing the parent game (SabreDAT only)</param> + /// <param name="lastgame">The name of the last game to be output</param> + /// <param name="depth">Current depth to output file at (SabreDAT only)</param> + /// <param name="last">Last known depth to cycle back from (SabreDAT only)</param> + /// <param name="logger">Logger object for file and console output</param> + /// <returns>The new depth of the tag</returns> + private int WriteEndGame(StreamWriter sw, DatFormat datFormat, DatItem rom, List<string> splitpath, List<string> newsplit, string lastgame, int depth, out int last, Logger logger) + { + last = 0; + + try + { + string state = ""; + + switch (datFormat) + { + case DatFormat.ClrMamePro: + case DatFormat.DOSCenter: + state += (String.IsNullOrEmpty(rom.Machine.SampleOf) ? "" : "\tsampleof \"" + rom.Machine.SampleOf + "\"\n") + ")\n"; + break; + case DatFormat.Logiqx: + state += "\t</machine>\n"; + break; + case DatFormat.OfflineList: + state += "\t\t</game>\n"; + break; + case DatFormat.SabreDat: + if (splitpath != null) + { + for (int i = 0; i < newsplit.Count && i < splitpath.Count; i++) + { + // Always keep track of the last seen item + last = i; + + // If we find a difference, break + if (newsplit[i] != splitpath[i]) + { + break; + } + } + + // Now that we have the last known position, take down all open folders + for (int i = depth - 1; i > last + 1; i--) + { + // Print out the number of tabs and the end folder + for (int j = 0; j < i; j++) + { + state += "\t"; + } + state += "</directory>\n"; + } + + // Reset the current depth + depth = 2 + last; + } + break; + case DatFormat.SoftwareList: + state += "\t</software>\n\n"; + break; + } + + sw.Write(state); + sw.Flush(); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + return depth; + } + + return depth; + } + + /// <summary> + /// Write out RomData using the supplied StreamWriter + /// </summary> + /// <param name="sw">StreamWriter to output to</param> + /// <param name="datFormat">Output format to write to</param> + /// <param name="rom">RomData object to be output</param> + /// <param name="lastgame">The name of the last game to be output</param> + /// <param name="depth">Current depth to output file at (SabreDAT only)</param> + /// <param name="logger">Logger object for file and console output</param> + /// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param> + /// <returns>True if the data was written, false on error</returns> + private bool WriteRomData(StreamWriter sw, DatFormat datFormat, DatItem rom, string lastgame, int depth, Logger logger, bool ignoreblanks = false) + { + // If we are in ignore blanks mode AND we have a blank (0-size) rom, skip + if (ignoreblanks + && (rom.Type == ItemType.Rom + && (((Rom)rom).Size == 0 || ((Rom)rom).Size == -1))) + { + return true; + } + + try + { + string state = "", name = "", pre = "", post = ""; + switch (datFormat) + { + case DatFormat.ClrMamePro: + switch (rom.Type) + { + case ItemType.Archive: + state += "\tarchive ( name\"" + rom.Name + "\"" + + " )\n"; + break; + case ItemType.BiosSet: + state += "\tbiosset ( name\"" + rom.Name + "\"" + + (!String.IsNullOrEmpty(((BiosSet)rom).Description) ? " description \"" + ((BiosSet)rom).Description + "\"" : "") + + (((BiosSet)rom).Default != null + ? "default " + ((BiosSet)rom).Default.ToString().ToLowerInvariant() + : "") + + " )\n"; + break; + case ItemType.Disk: + state += "\tdisk ( name \"" + rom.Name + "\"" + + (!String.IsNullOrEmpty(((Disk)rom).MD5) ? " md5 " + ((Disk)rom).MD5.ToLowerInvariant() : "") + + (!String.IsNullOrEmpty(((Disk)rom).SHA1) ? " sha1 " + ((Disk)rom).SHA1.ToLowerInvariant() : "") + + (((Disk)rom).ItemStatus != ItemStatus.None ? " flags " + ((Disk)rom).ItemStatus.ToString().ToLowerInvariant() : "") + + " )\n"; + break; + case ItemType.Release: + state += "\trelease ( name\"" + rom.Name + "\"" + + (!String.IsNullOrEmpty(((Release)rom).Region) ? " region \"" + ((Release)rom).Region + "\"" : "") + + (!String.IsNullOrEmpty(((Release)rom).Language) ? " language \"" + ((Release)rom).Language + "\"" : "") + + (!String.IsNullOrEmpty(((Release)rom).Date) ? " date \"" + ((Release)rom).Date + "\"" : "") + + (((Release)rom).Default != null + ? "default " + ((Release)rom).Default.ToString().ToLowerInvariant() + : "") + + " )\n"; + break; + case ItemType.Rom: + state += "\trom ( name \"" + rom.Name + "\"" + + (((Rom)rom).Size != -1 ? " size " + ((Rom)rom).Size : "") + + (!String.IsNullOrEmpty(((Rom)rom).CRC) ? " crc " + ((Rom)rom).CRC.ToLowerInvariant() : "") + + (!String.IsNullOrEmpty(((Rom)rom).MD5) ? " md5 " + ((Rom)rom).MD5.ToLowerInvariant() : "") + + (!String.IsNullOrEmpty(((Rom)rom).SHA1) ? " sha1 " + ((Rom)rom).SHA1.ToLowerInvariant() : "") + + (!String.IsNullOrEmpty(((Rom)rom).Date) ? " date \"" + ((Rom)rom).Date + "\"" : "") + + (((Rom)rom).ItemStatus != ItemStatus.None ? " flags " + ((Rom)rom).ItemStatus.ToString().ToLowerInvariant() : "") + + " )\n"; + break; + case ItemType.Sample: + state += "\tsample ( name\"" + rom.Name + "\"" + + " )\n"; + break; + } + + break; + case DatFormat.CSV: + // CSV should only output Rom and Disk + if (rom.Type != ItemType.Disk && rom.Type != ItemType.Rom) + { + return true; + } + + pre = Prefix + (Quotes ? "\"" : ""); + post = (Quotes ? "\"" : "") + Postfix; + + if (rom.Type == ItemType.Rom) + { + // Check for special strings in prefix and postfix + pre = pre + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%crc%", ((Rom)rom).CRC) + .Replace("%md5%", ((Rom)rom).MD5) + .Replace("%sha1%", ((Rom)rom).SHA1) + .Replace("%size%", ((Rom)rom).Size.ToString()); + post = post + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%crc%", ((Rom)rom).CRC) + .Replace("%md5%", ((Rom)rom).MD5) + .Replace("%sha1%", ((Rom)rom).SHA1) + .Replace("%size%", ((Rom)rom).Size.ToString()); + } + else if (rom.Type == ItemType.Disk) + { + // Check for special strings in prefix and postfix + pre = pre + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%md5%", ((Disk)rom).MD5) + .Replace("%sha1%", ((Disk)rom).SHA1); + post = post + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%md5%", ((Disk)rom).MD5) + .Replace("%sha1%", ((Disk)rom).SHA1); + } + + if (rom.Type == ItemType.Rom) + { + string inline = "\"" + FileName + "\"" + + ",\"" + Name + "\"" + + ",\"" + Description + "\"" + + ",\"" + rom.Machine.Name + "\"" + + ",\"" + rom.Machine.Description + "\"" + + "," + "\"rom\"" + + ",\"" + rom.Name + "\"" + + "," + "\"\"" + + ",\"" + ((Rom)rom).Size + "\"" + + ",\"" + ((Rom)rom).CRC + "\"" + + ",\"" + ((Rom)rom).MD5 + "\"" + + ",\"" + ((Rom)rom).SHA1 + "\"" + + "," + (((Rom)rom).ItemStatus != ItemStatus.None ? "\"" + ((Rom)rom).ItemStatus.ToString() + "\"" : "\"\""); + state += pre + inline + post + "\n"; + } + else if (rom.Type == ItemType.Disk) + { + string inline = "\"" + FileName + "\"" + + ",\"" + Name + "\"" + + ",\"" + Description + "\"" + + ",\"" + rom.Machine.Name + "\"" + + ",\"" + rom.Machine.Description + "\"" + + "," + "\"disk\"" + + "," + "\"\"" + + ",\"" + rom.Name + "\"" + + "," + "\"\"" + + "," + "\"\"" + + ",\"" + ((Disk)rom).MD5 + "\"" + + ",\"" + ((Disk)rom).SHA1 + "\"" + + "," + (((Disk)rom).ItemStatus != ItemStatus.None ? "\"" + ((Disk)rom).ItemStatus.ToString() + "\"" : "\"\""); + state += pre + inline + post + "\n"; + } + break; + case DatFormat.DOSCenter: + switch (rom.Type) + { + case ItemType.Archive: + case ItemType.BiosSet: + case ItemType.Disk: + case ItemType.Release: + case ItemType.Sample: + // We don't output these at all + break; + case ItemType.Rom: + state += "\tfile ( name " + ((Rom)rom).Name + + (((Rom)rom).Size != -1 ? " size " + ((Rom)rom).Size : "") + + (!String.IsNullOrEmpty(((Rom)rom).Date) ? " date " + ((Rom)rom).Date : "") + + (!String.IsNullOrEmpty(((Rom)rom).CRC) ? " crc " + ((Rom)rom).CRC.ToLowerInvariant() : "") + + " )\n"; + break; + } + break; + case DatFormat.Logiqx: + switch (rom.Type) + { + case ItemType.Archive: + state += "\t\t<archive name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" + + "/>\n"; + break; + case ItemType.BiosSet: + state += "\t\t<biosset name\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" + + (!String.IsNullOrEmpty(((BiosSet)rom).Description) ? " description=\"" + HttpUtility.HtmlEncode(((BiosSet)rom).Description) + "\"" : "") + + (((BiosSet)rom).Default != null + ? ((BiosSet)rom).Default.ToString().ToLowerInvariant() + : "") + + "/>\n"; + break; + case ItemType.Disk: + state += "\t\t<disk name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" + + (!String.IsNullOrEmpty(((Disk)rom).MD5) ? " md5=\"" + ((Disk)rom).MD5.ToLowerInvariant() + "\"" : "") + + (!String.IsNullOrEmpty(((Disk)rom).SHA1) ? " sha1=\"" + ((Disk)rom).SHA1.ToLowerInvariant() + "\"" : "") + + (((Disk)rom).ItemStatus != ItemStatus.None ? " status=\"" + ((Disk)rom).ItemStatus.ToString().ToLowerInvariant() + "\"" : "") + + "/>\n"; + break; + case ItemType.Release: + state += "\t\t<release name\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" + + (!String.IsNullOrEmpty(((Release)rom).Region) ? " region=\"" + HttpUtility.HtmlEncode(((Release)rom).Region) + "\"" : "") + + (!String.IsNullOrEmpty(((Release)rom).Language) ? " language=\"" + HttpUtility.HtmlEncode(((Release)rom).Language) + "\"" : "") + + (!String.IsNullOrEmpty(((Release)rom).Date) ? " date=\"" + HttpUtility.HtmlEncode(((Release)rom).Date) + "\"" : "") + + (((Release)rom).Default != null + ? ((Release)rom).Default.ToString().ToLowerInvariant() + : "") + + "/>\n"; + break; + case ItemType.Rom: + state += "\t\t<rom name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" + + (((Rom)rom).Size != -1 ? " size=\"" + ((Rom)rom).Size + "\"" : "") + + (!String.IsNullOrEmpty(((Rom)rom).CRC) ? " crc=\"" + ((Rom)rom).CRC.ToLowerInvariant() + "\"" : "") + + (!String.IsNullOrEmpty(((Rom)rom).MD5) ? " md5=\"" + ((Rom)rom).MD5.ToLowerInvariant() + "\"" : "") + + (!String.IsNullOrEmpty(((Rom)rom).SHA1) ? " sha1=\"" + ((Rom)rom).SHA1.ToLowerInvariant() + "\"" : "") + + (!String.IsNullOrEmpty(((Rom)rom).Date) ? " date=\"" + ((Rom)rom).Date + "\"" : "") + + (((Rom)rom).ItemStatus != ItemStatus.None ? " status=\"" + ((Rom)rom).ItemStatus.ToString().ToLowerInvariant() + "\"" : "") + + "/>\n"; + break; + case ItemType.Sample: + state += "\t\t<file type=\"sample\" name=\"" + HttpUtility.HtmlEncode(rom.Name) + "\"" + + "/>\n"; + break; + } + break; + case DatFormat.MissFile: + // Missfile should only output Rom and Disk + if (rom.Type != ItemType.Disk && rom.Type != ItemType.Rom) + { + return true; + } + + pre = Prefix + (Quotes ? "\"" : ""); + post = (Quotes ? "\"" : "") + Postfix; + + if (rom.Type == ItemType.Rom) + { + // Check for special strings in prefix and postfix + pre = pre + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%crc%", ((Rom)rom).CRC) + .Replace("%md5%", ((Rom)rom).MD5) + .Replace("%sha1%", ((Rom)rom).SHA1) + .Replace("%size%", ((Rom)rom).Size.ToString()); + post = post + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%crc%", ((Rom)rom).CRC) + .Replace("%md5%", ((Rom)rom).MD5) + .Replace("%sha1%", ((Rom)rom).SHA1) + .Replace("%size%", ((Rom)rom).Size.ToString()); + } + else if (rom.Type == ItemType.Disk) + { + // Check for special strings in prefix and postfix + pre = pre + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%md5%", ((Disk)rom).MD5) + .Replace("%sha1%", ((Disk)rom).SHA1); + post = post + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%md5%", ((Disk)rom).MD5) + .Replace("%sha1%", ((Disk)rom).SHA1); + } + + // If we're in Romba mode, the state is consistent + if (Romba) + { + if (rom.Type == ItemType.Rom) + { + // We can only write out if there's a SHA-1 + if (((Rom)rom).SHA1 != "") + { + name = ((Rom)rom).SHA1.Substring(0, 2) + + "/" + ((Rom)rom).SHA1.Substring(2, 2) + + "/" + ((Rom)rom).SHA1.Substring(4, 2) + + "/" + ((Rom)rom).SHA1.Substring(6, 2) + + "/" + ((Rom)rom).SHA1 + ".gz"; + state += pre + name + post + "\n"; + } + } + else if (rom.Type == ItemType.Disk) + { + // We can only write out if there's a SHA-1 + if (((Disk)rom).SHA1 != "") + { + name = ((Disk)rom).SHA1.Substring(0, 2) + + "/" + ((Disk)rom).SHA1.Substring(2, 2) + + "/" + ((Disk)rom).SHA1.Substring(4, 2) + + "/" + ((Disk)rom).SHA1.Substring(6, 2) + + "/" + ((Disk)rom).SHA1 + ".gz"; + state += pre + name + post + "\n"; + } + } + } + + // Otherwise, use any flags + name = (UseGame ? rom.Machine.Name : rom.Name); + if (RepExt != "" || RemExt) + { + if (RemExt) + { + RepExt = ""; + } + + string dir = Path.GetDirectoryName(name); + dir = (dir.StartsWith(Path.DirectorySeparatorChar.ToString()) ? dir.Remove(0, 1) : dir); + name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + RepExt); + } + if (AddExt != "") + { + name += AddExt; + } + if (!UseGame && GameName) + { + name = Path.Combine(rom.Machine.Name, name); + } + + if (UseGame && rom.Machine.Name != lastgame) + { + state += pre + name + post + "\n"; + lastgame = rom.Machine.Name; + } + else if (!UseGame) + { + state += pre + name + post + "\n"; + } + break; + case DatFormat.OfflineList: + state += "\t\t<game>\n" + + "\t\t\t<imageNumber>1</imageNumber>\n" + + "\t\t\t<releaseNumber>1</releaseNumber>\n" + + "\t\t\t<title>" + HttpUtility.HtmlEncode(rom.Name) + "\n" + + "\t\t\tNone\n"; + + if (rom.Type == ItemType.Rom) + { + state += "\t\t\t" + ((Rom)rom).Size + "\n"; + } + + state += "\t\t\tNone\n" + + "\t\t\t0\n" + + "\t\t\tNone\n" + + "\t\t\t0\n"; + + if (rom.Type == ItemType.Disk) + { + state += "\t\t\t\n" + + (((Disk)rom).MD5 != null + ? "\t\t\t\t" + ((Disk)rom).MD5.ToUpperInvariant() + "\n" + : "\t\t\t\t" + ((Disk)rom).SHA1.ToUpperInvariant() + "\n") + + "\t\t\t\n"; + } + else if (rom.Type == ItemType.Rom) + { + string tempext = Path.GetExtension(((Rom)rom).Name); + if (!tempext.StartsWith(".")) + { + tempext = "." + tempext; + } + + state += "\t\t\t\n" + + (((Rom)rom).CRC != null + ? "\t\t\t\t" + ((Rom)rom).CRC.ToUpperInvariant() + "\n" + : ((Rom)rom).MD5 != null + ? "\t\t\t\t" + ((Rom)rom).MD5.ToUpperInvariant() + "\n" + : "\t\t\t\t" + ((Rom)rom).SHA1.ToUpperInvariant() + "\n") + + "\t\t\t\n"; + } + + state += "\t\t\t00000000\n" + + "\t\t\t00000000\n" + + "\t\t\t\n" + + "\t\t\t0\n" + + "\t\t\n"; + break; + case DatFormat.RedumpMD5: + if (rom.Type == ItemType.Rom) + { + state += ((Rom)rom).MD5 + " *" + (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + "\n"; + } + else if (rom.Type == ItemType.Disk) + { + state += ((Disk)rom).MD5 + " *" + (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + "\n"; + } + break; + case DatFormat.RedumpSFV: + if (rom.Type == ItemType.Rom) + { + state += (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + " " + ((Rom)rom).CRC + "\n"; + } + break; + case DatFormat.RedumpSHA1: + if (rom.Type == ItemType.Rom) + { + state += ((Rom)rom).SHA1 + " *" + (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + "\n"; + } + else if (rom.Type == ItemType.Disk) + { + state += ((Disk)rom).SHA1 + " *" + (GameName ? rom.Machine.Name + Path.DirectorySeparatorChar : "") + rom.Name + "\n"; + } + break; + case DatFormat.RomCenter: + if (rom.Type == ItemType.Rom) + { + state += "¬" + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.Machine.CloneOf)) + + "¬" + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.Machine.CloneOf)) + + "¬" + HttpUtility.HtmlEncode(rom.Machine.Name) + + "¬" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description)) + + "¬" + HttpUtility.HtmlEncode(rom.Name) + + "¬" + ((Rom)rom).CRC.ToLowerInvariant() + + "¬" + (((Rom)rom).Size != -1 ? ((Rom)rom).Size.ToString() : "") + "¬¬¬\n"; + } + else if (rom.Type == ItemType.Disk) + { + state += "¬" + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.Machine.CloneOf)) + + "¬" + (String.IsNullOrEmpty(rom.Machine.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.Machine.CloneOf)) + + "¬" + HttpUtility.HtmlEncode(rom.Machine.Name) + + "¬" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.Machine.Description) ? rom.Machine.Name : rom.Machine.Description)) + + "¬" + HttpUtility.HtmlEncode(rom.Name) + + "¬¬¬¬¬\n"; + } + + break; + case DatFormat.SabreDat: + string prefix = ""; + for (int i = 0; i < depth; i++) + { + prefix += "\t"; + } + state += prefix; + + switch (rom.Type) + { + case ItemType.Archive: + state += "\n"; + break; + case ItemType.BiosSet: + state += "\n"; + break; + case ItemType.Disk: + state += "\n" + prefix + "\t\n" + + prefix + "\t\t\n" + + prefix + "\t\n" + + prefix + "\n" : "/>\n"); + break; + case ItemType.Release: + state += "\n"; + break; + case ItemType.Rom: + state += "\n" + prefix + "\t\n" + + prefix + "\t\t\n" + + prefix + "\t\n" + + prefix + "\n" : "/>\n"); + break; + case ItemType.Sample: + state += "\n"; + break; + } + break; + case DatFormat.SoftwareList: + state += "\t\t\n"; + + foreach (Tuple kvp in rom.Features) + { + state += "\t\t\t\n"; + } + + switch (rom.Type) + { + case ItemType.Archive: + state += "\t\t\t\n" + + "\t\t\t\t\n" + + "\t\t\t\n"; + break; + case ItemType.BiosSet: + state += "\t\t\t\n" + + "\t\t\t\t\n" + + "\t\t\t\n"; + break; + case ItemType.Disk: + state += "\t\t\t\n" + + "\t\t\t\t\n" + + "\t\t\t\n"; + break; + case ItemType.Release: + state += "\t\t\t\n" + + "\t\t\t\t\n" + + "\t\t\t\n"; + break; + case ItemType.Rom: + state += "\t\t\t\n" + + "\t\t\t\t\n" + + "\t\t\t\n"; + break; + case ItemType.Sample: + state += "\t\t\t\n" + + "\t\t\t\t\n" + + "\t\t\t\n"; + break; + } + + state += "\t\t\n"; + break; + case DatFormat.TSV: + // TSV should only output Rom and Disk + if (rom.Type != ItemType.Disk && rom.Type != ItemType.Rom) + { + return true; + } + + pre = Prefix + (Quotes ? "\"" : ""); + post = (Quotes ? "\"" : "") + Postfix; + + if (rom.Type == ItemType.Rom) + { + // Check for special strings in prefix and postfix + pre = pre + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%crc%", ((Rom)rom).CRC) + .Replace("%md5%", ((Rom)rom).MD5) + .Replace("%sha1%", ((Rom)rom).SHA1) + .Replace("%size%", ((Rom)rom).Size.ToString()); + post = post + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%crc%", ((Rom)rom).CRC) + .Replace("%md5%", ((Rom)rom).MD5) + .Replace("%sha1%", ((Rom)rom).SHA1) + .Replace("%size%", ((Rom)rom).Size.ToString()); + } + else if (rom.Type == ItemType.Disk) + { + // Check for special strings in prefix and postfix + pre = pre + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%md5%", ((Disk)rom).MD5) + .Replace("%sha1%", ((Disk)rom).SHA1); + post = post + .Replace("%game%", rom.Machine.Name) + .Replace("%name%", rom.Name) + .Replace("%md5%", ((Disk)rom).MD5) + .Replace("%sha1%", ((Disk)rom).SHA1); + } + + if (rom.Type == ItemType.Rom) + { + string inline = "\"" + FileName + "\"" + + "\t\"" + Name + "\"" + + "\t\"" + Description + "\"" + + "\t\"" + rom.Machine.Name + "\"" + + "\t\"" + rom.Machine.Description + "\"" + + "\t" + "\"rom\"" + + "\t\"" + rom.Name + "\"" + + "\t" + "\"\"" + + "\t\"" + ((Rom)rom).Size + "\"" + + "\t\"" + ((Rom)rom).CRC + "\"" + + "\t\"" + ((Rom)rom).MD5 + "\"" + + "\t\"" + ((Rom)rom).SHA1 + "\"" + + "\t" + (((Rom)rom).ItemStatus != ItemStatus.None ? "\"" + ((Rom)rom).ItemStatus.ToString() + "\"" : "\"\""); + state += pre + inline + post + "\n"; + } + else if (rom.Type == ItemType.Disk) + { + string inline = "\"" + FileName + "\"" + + "\t\"" + Name + "\"" + + "\t\"" + Description + "\"" + + "\t\"" + rom.Machine.Name + "\"" + + "\t\"" + rom.Machine.Description + "\"" + + "\t" + "\"disk\"" + + "\t" + "\"\"" + + "\t\"" + rom.Name + "\"" + + "\t" + "\"\"" + + "\t" + "\"\"" + + "\t\"" + ((Disk)rom).MD5 + "\"" + + "\t\"" + ((Disk)rom).SHA1 + "\"" + + "\t" + (((Disk)rom).ItemStatus != ItemStatus.None ? "\"" + ((Disk)rom).ItemStatus.ToString() + "\"" : "\"\""); + state += pre + inline + post + "\n"; + } + break; + } + + sw.Write(state); + sw.Flush(); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + return false; + } + + return true; + } + + /// + /// Write out DAT footer using the supplied StreamWriter + /// + /// StreamWriter to output to + /// Output format to write to + /// Current depth to output file at (SabreDAT only) + /// Logger object for file and console output + /// True if the data was written, false on error + private bool WriteFooter(StreamWriter sw, DatFormat datFormat, int depth, Logger logger) + { + try + { + string footer = ""; + + // If we have roms, output the full footer + if (Files != null && Files.Count > 0) + { + switch (datFormat) + { + case DatFormat.ClrMamePro: + case DatFormat.DOSCenter: + footer = ")\n"; + break; + case DatFormat.Logiqx: + footer = "\t\n
\n"; + break; + case DatFormat.OfflineList: + footer = "\t\t" + + "\t\n" + + "\t\n" + + "\t\t\n" + + "\t\t\t\n" + + "\t\t\t\n" + + "\t\t\n" + + "\t\n" + + ""; + break; + case DatFormat.SabreDat: + for (int i = depth - 1; i >= 2; i--) + { + // Print out the number of tabs and the end folder + for (int j = 0; j < i; j++) + { + footer += "\t"; + } + footer += "\n"; + } + footer += "\t\n\n"; + break; + case DatFormat.SoftwareList: + footer = "\t\n\n\n"; + break; + } + } + + // Otherwise, output the abbreviated form + else + { + switch (datFormat) + { + case DatFormat.Logiqx: + case DatFormat.SabreDat: + footer = "\n"; + break; + case DatFormat.OfflineList: + footer = "\t\n" + + "\t\n" + + "\t\t\n" + + "\t\t\t\n" + + "\t\t\t\n" + + "\t\t\n" + + "\t\n" + + ""; + break; + case DatFormat.SoftwareList: + footer = "\n"; + break; + } + } + + // Write the footer out + sw.Write(footer); + sw.Flush(); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + return false; + } + + return true; + } + + #endregion + } +} diff --git a/SabreTools.Helper/SabreTools.Helper.csproj b/SabreTools.Helper/SabreTools.Helper.csproj index 81a8fe7a..efd138c4 100644 --- a/SabreTools.Helper/SabreTools.Helper.csproj +++ b/SabreTools.Helper/SabreTools.Helper.csproj @@ -109,7 +109,15 @@ + + + + + + + +