using System; using System.Collections.Generic; using System.IO; 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; using NaturalSort; using SharpCompress.Common; namespace SabreTools.Helper.Dats { public class DatFile : ICloneable { #region Private instance variables // Data common to most DAT types private string _fileName; private string _name; private string _description; private string _rootDir; private string _category; private string _version; private string _date; private string _author; private string _email; private string _homepage; private string _url; private string _comment; private string _header; private string _type; // Generally only used for SuperDAT private ForceMerging _forceMerging; private ForceNodump _forceNodump; private ForcePacking _forcePacking; private DatFormat _datFormat; private bool _excludeOf; private bool _mergeRoms; private SortedDictionary> _files; private SortedBy _sortedBy; // Data specific to the Miss DAT type private bool _useGame; private string _prefix; private string _postfix; private bool _quotes; private string _repExt; private string _addExt; private bool _remExt; private bool _gameName; private bool _romba; // Statistical data related to the DAT private long _romCount; private long _diskCount; private long _totalSize; private long _crcCount; private long _md5Count; private long _sha1Count; private long _baddumpCount; private long _nodumpCount; #endregion #region Publicly facing variables // Data common to most DAT types public string FileName { get { return _fileName; } set { _fileName = value; } } public string Name { get { return _name; } set { _name = value; } } public string Description { get { return _description; } set { _description = value; } } public string RootDir { get { return _rootDir; } set { _rootDir = value; } } public string Category { get { return _category; } set { _category = value; } } public string Version { get { return _version; } set { _version = value; } } public string Date { get { return _date; } set { _date = value; } } public string Author { get { return _author; } set { _author = value; } } public string Email { get { return _email; } set { _email = value; } } public string Homepage { get { return _homepage; } set { _homepage = value; } } public string Url { get { return _url; } set { _url = value; } } public string Comment { get { return _comment; } set { _comment = value; } } public string Header { get { return _header; } set { _header = value; } } public string Type // Generally only used for SuperDAT { get { return _type; } set { _type = value; } } public ForceMerging ForceMerging { get { return _forceMerging; } set { _forceMerging = value; } } public ForceNodump ForceNodump { get { return _forceNodump; } set { _forceNodump = value; } } public ForcePacking ForcePacking { get { return _forcePacking; } set { _forcePacking = value; } } public DatFormat DatFormat { get { return _datFormat; } set { _datFormat = value; } } public bool ExcludeOf { get { return _excludeOf; } set { _excludeOf = value; } } public bool MergeRoms { get { return _mergeRoms; } set { _mergeRoms = value; } } public SortedDictionary> Files { get { if (_files == null) { _files = new SortedDictionary>(); } return _files; } set { _files = value; } } public SortedBy SortedBy { get { return _sortedBy; } set { _sortedBy = value; } } // Data specific to the Miss DAT type public bool UseGame { get { return _useGame; } set { _useGame = value; } } public string Prefix { get { return _prefix; } set { _prefix = value; } } public string Postfix { get { return _postfix; } set { _postfix = value; } } public bool Quotes { get { return _quotes; } set { _quotes = value; } } public string RepExt { get { return _repExt; } set { _repExt = value; } } public string AddExt { get { return _addExt; } set { _addExt = value; } } public bool RemExt { get { return _remExt; } set { _remExt = value; } } public bool GameName { get { return _gameName; } set { _gameName = value; } } public bool Romba { get { return _romba; } set { _romba = value; } } // Statistical data related to the DAT public long RomCount { get { return _romCount; } set { _romCount = value; } } public long DiskCount { get { return _diskCount; } set { _diskCount = value; } } public long TotalSize { get { return _totalSize; } set { _totalSize = value; } } public long CRCCount { get { return _crcCount; } set { _crcCount = value; } } public long MD5Count { get { return _md5Count; } set { _md5Count = value; } } public long SHA1Count { get { return _sha1Count; } set { _sha1Count = value; } } public long BaddumpCount { get { return _baddumpCount; } set { _baddumpCount = value; } } public long NodumpCount { get { return _nodumpCount; } set { _nodumpCount = value; } } #endregion #region Instance Methods #region Bucketing /// /// 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 public void BucketByGame(bool mergeroms, bool norename, Logger logger, bool output = 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); 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 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; } /// /// 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 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; } #endregion #region Cloning Methods public object Clone() { return new DatFile { FileName = _fileName, Name = _name, Description = _description, RootDir = _rootDir, Category = _category, Version = _version, Date = _date, Author = _author, Email = _email, Homepage = _homepage, Url = _url, Comment = _comment, Header = _header, Type = _type, ForceMerging = _forceMerging, ForceNodump = _forceNodump, ForcePacking = _forcePacking, ExcludeOf = _excludeOf, DatFormat = _datFormat, MergeRoms = _mergeRoms, Files = _files, SortedBy = _sortedBy, UseGame = _useGame, Prefix = _prefix, Postfix = _postfix, Quotes = _quotes, RepExt = _repExt, AddExt = _addExt, RemExt = _remExt, GameName = _gameName, Romba = _romba, RomCount = _romCount, DiskCount = _diskCount, TotalSize = _totalSize, CRCCount = _crcCount, MD5Count = _md5Count, SHA1Count = _sha1Count, BaddumpCount = _baddumpCount, NodumpCount = _nodumpCount, }; } public object CloneHeader() { return new DatFile { FileName = _fileName, Name = _name, Description = _description, RootDir = _rootDir, Category = _category, Version = _version, Date = _date, Author = _author, Email = _email, Homepage = _homepage, Url = _url, Comment = _comment, Header = _header, Type = _type, ForceMerging = _forceMerging, ForceNodump = _forceNodump, ForcePacking = _forcePacking, ExcludeOf = _excludeOf, DatFormat = _datFormat, MergeRoms = _mergeRoms, Files = new SortedDictionary>(), SortedBy = SortedBy.Default, UseGame = _useGame, Prefix = _prefix, Postfix = _postfix, Quotes = _quotes, RepExt = _repExt, AddExt = _addExt, RemExt = _remExt, GameName = _gameName, Romba = _romba, }; } #endregion #region Converting and Updating /// /// 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 inputFileNames, 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 = new List(); foreach (string input in inputFileNames) { if (Directory.Exists(input)) { List files = FileTools.RetrieveFiles(input, new List()); foreach (string file in files) { try { newInputFileNames.Add(Path.GetFullPath(file) + "¬" + Path.GetFullPath(input)); } catch (PathTooLongException) { logger.Warning("The path for " + file + " was too long"); } catch (Exception ex) { logger.Error(ex.ToString()); } } } else if (File.Exists(input)) { try { newInputFileNames.Add(Path.GetFullPath(input) + "¬" + Path.GetDirectoryName(Path.GetFullPath(input))); } catch (PathTooLongException) { logger.Warning("The path for " + input + " was too long"); } catch (Exception ex) { logger.Error(ex.ToString()); } } } // 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(inputFileNames, 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"); 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; } 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 path = outDir + (Path.GetDirectoryName(inputs[j].Split('¬')[0]).Remove(0, inputs[j].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)) { path = outDir + (Path.GetDirectoryName(inputs[j].Split('¬')[0]).Remove(0, inputs[j].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) { 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 /// /// 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.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 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; // 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; } // Otherwise, process the rest of the line else { string[] gc = line.Trim().Split(' '); // Loop over all attributes and add them if possible bool quote = false; string attrib = "", val = ""; for (int i = 2; i < gc.Length; i++) { //If the item is empty, we automatically skip it because it's a fluke if (gc[i].Trim() == String.Empty) { continue; } // Special cases for standalone item statuses else if (gc[i] == "baddump" && attrib != "name" && attrib != "status" && attrib != "flags") { if (item.Type == ItemType.Rom) { ((Rom)item).ItemStatus = ItemStatus.BadDump; } else if (item.Type == ItemType.Disk) { ((Disk)item).ItemStatus = ItemStatus.BadDump; } } else if (gc[i] == "good" && attrib != "name" && attrib != "status" && attrib != "flags") { if (item.Type == ItemType.Rom) { ((Rom)item).ItemStatus = ItemStatus.Good; } else if (item.Type == ItemType.Disk) { ((Disk)item).ItemStatus = ItemStatus.Good; } } else if (gc[i] == "nodump" && attrib != "name" && attrib != "status" && attrib != "flags") { if (item.Type == ItemType.Rom) { ((Rom)item).ItemStatus = ItemStatus.Nodump; } else if (item.Type == ItemType.Disk) { ((Disk)item).ItemStatus = ItemStatus.Nodump; } } else if (gc[i] == "verified" && attrib != "name" && attrib != "status" && attrib != "flags") { if (item.Type == ItemType.Rom) { ((Rom)item).ItemStatus = ItemStatus.Verified; } else if (item.Type == ItemType.Disk) { ((Disk)item).ItemStatus = ItemStatus.Verified; } } // Special cases for DOSCenter DATs only else if (line.Trim().StartsWith("file (")) { // Loop over the specifics for (int j = i; j < gc.Length; j++) { // Names are not quoted, for some stupid reason if (gc[j] == "name") { // Advance to the first part of the name j++; item.Name = gc[j]; // Advance to the next item, adding until we find "size" j++; while (j < gc.Length && gc[j] != "size" && gc[j] != "date" && gc[j] != "crc") { item.Name += " " + gc[j]; j++; } } // Get the size from the next part else if (gc[j] == "size") { j++; long tempsize = -1; if (!Int64.TryParse(gc[j], out tempsize)) { tempsize = 0; } ((Rom)item).Size = tempsize; j++; } // Get the date from the next part else if (gc[j] == "date") { j++; ((Rom)item).Date = gc[j].Replace("\"", "") + " " + gc[j + 1].Replace("\"", ""); j += 3; } // Get the CRC from the next part else if (gc[j] == "crc") { j++; ((Rom)item).CRC = gc[j].Replace("\"", "").ToLowerInvariant(); } } } // Even number of quotes, not in a quote, not in attribute else if (Regex.Matches(gc[i], "\"").Count % 2 == 0 && !quote && attrib == "") { attrib = gc[i].Replace("\"", ""); } // Even number of quotes, not in a quote, in attribute else if (Regex.Matches(gc[i], "\"").Count % 2 == 0 && !quote && attrib != "") { switch (attrib.ToLowerInvariant()) { case "name": item.Name = gc[i].Replace("\"", ""); break; case "size": if (item.Type == ItemType.Rom) { long size = -1; if (Int64.TryParse(gc[i].Replace("\"", ""), out size)) { ((Rom)item).Size = size; } } break; case "crc": if (item.Type == ItemType.Rom) { ((Rom)item).CRC = gc[i].Replace("\"", "").ToLowerInvariant(); } break; case "md5": if (item.Type == ItemType.Rom) { ((Rom)item).MD5 = gc[i].Replace("\"", "").ToLowerInvariant(); } else if (item.Type == ItemType.Disk) { ((Disk)item).MD5 = gc[i].Replace("\"", "").ToLowerInvariant(); } break; case "sha1": if (item.Type == ItemType.Rom) { ((Rom)item).SHA1 = gc[i].Replace("\"", "").ToLowerInvariant(); } else if (item.Type == ItemType.Disk) { ((Disk)item).SHA1 = gc[i].Replace("\"", "").ToLowerInvariant(); } break; case "status": case "flags": if (gc[i].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 (gc[i].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 (gc[i].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 (gc[i].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) { ((Rom)item).Date = gc[i].Replace("\"", "") + " " + gc[i + 1].Replace("\"", ""); } i++; break; // Special cases for item statuses 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 "baddump": if (item.Type == ItemType.Rom) { ((Rom)item).ItemStatus = ItemStatus.BadDump; } else if (item.Type == ItemType.Disk) { ((Disk)item).ItemStatus = ItemStatus.BadDump; } 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; } attrib = ""; } // Even number of quotes, in a quote, not in attribute else if (Regex.Matches(gc[i], "\"").Count % 2 == 0 && quote && attrib == "") { // Attributes can't have quoted names } // Even number of quotes, in a quote, in attribute else if (Regex.Matches(gc[i], "\"").Count % 2 == 0 && quote && attrib != "") { val += " " + gc[i]; } // Odd number of quotes, not in a quote, not in attribute else if (Regex.Matches(gc[i], "\"").Count % 2 == 1 && !quote && attrib == "") { // Attributes can't have quoted names } // Odd number of quotes, not in a quote, in attribute else if (Regex.Matches(gc[i], "\"").Count % 2 == 1 && !quote && attrib != "") { val = gc[i].Replace("\"", ""); quote = true; } // Odd number of quotes, in a quote, not in attribute else if (Regex.Matches(gc[i], "\"").Count % 2 == 1 && quote && attrib == "") { quote = false; } // Odd number of quotes, in a quote, in attribute else if (Regex.Matches(gc[i], "\"").Count % 2 == 1 && quote && attrib != "") { val += " " + gc[i].Replace("\"", ""); switch (attrib.ToLowerInvariant()) { case "name": item.Name = val; break; case "size": if (item.Type == ItemType.Rom) { long size = -1; if (Int64.TryParse(gc[i].Replace("\"", ""), out size)) { ((Rom)item).Size = size; } } break; case "crc": if (item.Type == ItemType.Rom) { ((Rom)item).CRC = gc[i].Replace("\"", "").ToLowerInvariant(); } break; case "md5": if (item.Type == ItemType.Rom) { ((Rom)item).MD5 = gc[i].Replace("\"", "").ToLowerInvariant(); } else if (item.Type == ItemType.Disk) { ((Disk)item).MD5 = gc[i].Replace("\"", "").ToLowerInvariant(); } break; case "sha1": if (item.Type == ItemType.Rom) { ((Rom)item).SHA1 = gc[i].Replace("\"", "").ToLowerInvariant(); } else if (item.Type == ItemType.Disk) { ((Disk)item).SHA1 = gc[i].Replace("\"", "").ToLowerInvariant(); } break; case "status": case "flags": if (gc[i].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 (gc[i].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 (gc[i].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 (gc[i].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) { ((Rom)item).Date = gc[i].Replace("\"", "") + " " + gc[i + 1].Replace("\"", ""); } i++; break; } quote = false; attrib = ""; val = ""; } } } // Now process and add the rom string 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 /// /// 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 PopulateDatFromDir(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 + ")"); } // Process the input if (Directory.Exists(basePath)) { logger.Verbose("Folder found: " + basePath); // Process the files in all subfolders List files = Directory.EnumerateFiles(basePath, "*", SearchOption.AllDirectories).ToList(); Parallel.ForEach(files, new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, item => { DFDProcessPossibleArchive(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)) { DFDProcessPossibleArchive(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 DFDProcessPossibleArchive(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.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) { DFDProcessFileHelper(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)) { DFDProcessFile(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 => { DFDProcessFile(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)) { DFDProcessFile(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 DFDProcessFile(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); DFDProcessFileHelper(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 DFDProcessFileHelper(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); } // 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 /// /// 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 => { // 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 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, 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]; 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) { PopulateDatFromDir(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 /// /// 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 /// Logger object for console and file writing /// True if split succeeded, false otherwise public bool SplitByLevel(string outDir, string basepath, 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); // Create a temporary DAT to add things to DatFile tempDat = (DatFile)CloneHeader(); tempDat.Name = null; // Then, we loop over the games foreach (string key in Files.Keys) { // Here, the key is the name of the game to be used for comparison if (tempDat.Name != null && tempDat.Name != Path.GetDirectoryName(key)) { // Get the path that the file will be written out to string path = HttpUtility.HtmlDecode(String.IsNullOrEmpty(tempDat.Name) ? outDir : Path.Combine(outDir, tempDat.Name)); // Now set the new output values tempDat.FileName = HttpUtility.HtmlDecode(String.IsNullOrEmpty(tempDat.Name) ? FileName : tempDat.Name.Replace(Path.DirectorySeparatorChar.ToString(), " - ").Replace(Path.AltDirectorySeparatorChar.ToString(), " - ")); tempDat.Description += " (" + tempDat.Name.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-') + ")"; tempDat.Name = Name + " (" + tempDat.Name.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-') + ")"; tempDat.Type = null; // Write out the temporary DAT to the proper directory tempDat.WriteToFile(path, 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 = Path.GetFileName(item.Machine.Name)); items.ForEach(item => item.Machine.Description = Path.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 = Path.GetDirectoryName(key); } // Then we write the last DAT out since it would be skipped otherwise string lastpath = HttpUtility.HtmlDecode(String.IsNullOrEmpty(tempDat.Name) ? outDir : Path.Combine(outDir, tempDat.Name)); // Now set the new output values tempDat.FileName = HttpUtility.HtmlDecode(String.IsNullOrEmpty(tempDat.Name) ? FileName : tempDat.Name.Replace(Path.DirectorySeparatorChar.ToString(), " - ").Replace(Path.AltDirectorySeparatorChar.ToString(), " - ")); tempDat.Description += " (" + tempDat.Name.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-') + ")"; tempDat.Name = Name + " (" + tempDat.Name.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-') + ")"; tempDat.Type = null; // Write out the temporary DAT to the proper directory tempDat.WriteToFile(lastpath, logger); return true; } /// /// 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 /// /// 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 /// /// 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 { 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(); // Get a properly sorted set of keys List keys = Files.Keys.ToList(); keys.Sort(new NaturalComparer()); 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.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.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 /// /// 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 /// /// 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 } }