using SharpCompress.Common; 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; namespace SabreTools.Helper { 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 OutputFormat _outputFormat; private bool _mergeRoms; private SortedDictionary> _files; // 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; private bool? _xsv; // true for tab-deliminated output, false for comma-deliminated output // 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 OutputFormat OutputFormat { get { return _outputFormat; } set { _outputFormat = 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; } } // 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; } } public bool? XSV // true for tab-deliminated output, false for comma-deliminated output { get { return _xsv; } set { _xsv = 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 Constructors /// /// Create a default, empty Dat object /// public DatFile() { // Nothing needs to be done } /// /// Create a new Dat object with the included information (standard Dats) /// /// New filename /// New name /// New description /// New rootdir /// New category /// New version /// New date /// New author /// New email /// New homepage /// New URL /// New comment /// New header /// True to set SuperDAT type, false otherwise /// None, Split, Full /// None, Obsolete, Required, Ignore /// None, Zip, Unzip /// Non-zero flag for output format, zero otherwise for default /// True to dedupe the roms in the DAT, false otherwise (default) /// SortedDictionary of lists of DatItem objects public DatFile(string fileName, string name, string description, string rootDir, string category, string version, string date, string author, string email, string homepage, string url, string comment, string header, string type, ForceMerging forceMerging, ForceNodump forceNodump, ForcePacking forcePacking, OutputFormat outputFormat, bool mergeRoms, SortedDictionary> files) { _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; _outputFormat = outputFormat; _mergeRoms = mergeRoms; _files = files; _romCount = 0; _diskCount = 0; _totalSize = 0; _crcCount = 0; _md5Count = 0; _sha1Count = 0; _nodumpCount = 0; } /// /// Create a new Dat object with the included information (missfile) /// /// New filename /// New name /// New description /// Non-zero flag for output format, zero otherwise for default /// True to dedupe the roms in the DAT, false otherwise (default) /// SortedDictionary of lists of DatItem objects /// True if games are to be used in output, false if roms are /// Generic prefix to be added to each line /// Generic postfix to be added to each line /// Add quotes to each item /// Replace all extensions with another /// Add an extension to all items /// Remove all extensions /// Add the dat name as a directory prefix /// Output files in romba format /// True to output files in TSV format, false to output files in CSV format, null otherwise public DatFile(string fileName, string name, string description, OutputFormat outputFormat, bool mergeRoms, SortedDictionary> files, bool useGame, string prefix, string postfix, bool quotes, string repExt, string addExt, bool remExt, bool gameName, bool romba, bool? xsv) { _fileName = fileName; _name = name; _description = description; _outputFormat = outputFormat; _mergeRoms = mergeRoms; _files = files; _useGame = useGame; _prefix = prefix; _postfix = postfix; _quotes = quotes; _repExt = repExt; _addExt = addExt; _remExt = remExt; _gameName = gameName; _romba = romba; _xsv = xsv; _romCount = 0; _diskCount = 0; _totalSize = 0; _crcCount = 0; _md5Count = 0; _sha1Count = 0; _nodumpCount = 0; } #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) { 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 (Files == null || Files.Count == 0) { Files = sortable; } // 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.MachineName) ? "Default" : rom.MachineName.ToLowerInvariant()); newkey = HttpUtility.HtmlEncode(newkey); if (sortable.ContainsKey(newkey)) { sortable[newkey].Add(rom); } else { List temp = new List(); temp.Add(rom); sortable.Add(newkey, temp); } } } // Now go through and sort all of the lists keys = sortable.Keys.ToList(); foreach (string key in keys) { List sortedlist = sortable[key]; DatItem.Sort(ref sortedlist, norename); sortable[key] = sortedlist; } // Output the count if told to if (output) { logger.User("A total of " + count + " file hashes will be written out to file"); } // Now assign the dictionary back Files = sortable; } #endregion #region Cloning Methods public object Clone() { return new DatFile { FileName = this.FileName, Name = this.Name, Description = this.Description, RootDir = this.RootDir, 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, OutputFormat = this.OutputFormat, MergeRoms = this.MergeRoms, Files = this.Files, UseGame = this.UseGame, Prefix = this.Prefix, Postfix = this.Postfix, Quotes = this.Quotes, RepExt = this.RepExt, AddExt = this.AddExt, RemExt = this.RemExt, GameName = this.GameName, Romba = this.Romba, XSV = this.XSV, RomCount = this.RomCount, DiskCount = this.DiskCount, TotalSize = this.TotalSize, CRCCount = this.CRCCount, MD5Count = this.MD5Count, SHA1Count = this.SHA1Count, BaddumpCount = this.BaddumpCount, NodumpCount = this.NodumpCount, }; } public object CloneHeader() { return new DatFile { FileName = this.FileName, Name = this.Name, Description = this.Description, RootDir = this.RootDir, 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, OutputFormat = this.OutputFormat, MergeRoms = this.MergeRoms, Files = new SortedDictionary>(), UseGame = this.UseGame, Prefix = this.Prefix, Postfix = this.Postfix, Quotes = this.Quotes, RepExt = this.RepExt, AddExt = this.AddExt, RemExt = this.RemExt, GameName = this.GameName, Romba = this.Romba, XSV = this.XSV, }; } #endregion #region Converting and Updating /// /// 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 diffed files should be cascade diffed, false if diffed files should be reverse cascaded, null 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) /// Name of the game to match (can use asterisk-partials) /// Name of the rom to match (can use asterisk-partials) /// Type of the rom to match /// Find roms greater than or equal to this size /// Find roms less than or equal to this size /// Find roms equal to this size /// CRC of the rom to match (can use asterisk-partials) /// MD5 of the rom to match (can use asterisk-partials) /// SHA-1 of the rom to match (can use asterisk-partials) /// Select roms with the given status /// 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 merge, DiffMode diff, bool? cascade, bool inplace, bool skip, bool bare, bool clean, bool softlist, string gamename, string romname, string romtype, long sgt, long slt, long seq, string crc, string md5, string sha1, ItemStatus itemStatus, 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 (cascade == false) { newInputFileNames.Reverse(); } // Create a dictionary of all ROMs from the input DATs List datHeaders = PopulateUserData(newInputFileNames, inplace, clean, softlist, outDir, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, maxDegreeOfParallelism, logger); // Modify the Dictionary if necessary and output the results if (diff != 0 && cascade == null) { DiffNoCascade(diff, outDir, newInputFileNames, logger); } // If we're in cascade and diff, output only cascaded diffs else if (diff != 0 && cascade != null) { 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 { 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, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, logger, true, clean, softlist, keepext: (innerDatdata.XSV != null)); // 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, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, logger, true, clean, keepext: (XSV != null)); // 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!"); } }); } return; } /// /// Populate the user DatData object from the input files /// /// Name of the game to match (can use asterisk-partials) /// Name of the rom to match (can use asterisk-partials) /// Type of the rom to match /// Find roms greater than or equal to this size /// Find roms less than or equal to this size /// Find roms equal to this size /// CRC of the rom to match (can use asterisk-partials) /// MD5 of the rom to match (can use asterisk-partials) /// SHA-1 of the rom to match (can use asterisk-partials) /// Select roms with the given status /// 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, string gamename, string romname, string romtype, long sgt, long slt, long seq, string crc, string md5, string sha1, ItemStatus itemStatus, 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 { OutputFormat = (OutputFormat != 0 ? OutputFormat : 0), Files = new SortedDictionary>(), MergeRoms = MergeRoms, }; datHeaders[i].Parse(input.Split('¬')[0], i, 0, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, 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.ExternalHash) { // 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.MachineName += " (" + 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.ExternalHash) { DatItem newrom = rom; newrom.MachineName += " (" + 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.MachineName = Path.GetDirectoryName(filename) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(filename) + Path.DirectorySeparatorChar + newrom.MachineName; newroms.Add(newrom); } Files[key] = newroms; } } // Output a DAT only if there are roms if (Files.Count != 0) { WriteToFile(outDir, logger); } } #endregion #region DAT 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, null, null, null, -1, -1, -1, null, null, null, ItemStatus.NULL, 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 /// Name of the game to match (can use asterisk-partials) /// Name of the rom to match (can use asterisk-partials) /// Type of the rom to match /// Find roms greater than or equal to this size /// Find roms less than or equal to this size /// Find roms equal to this size /// CRC of the rom to match (can use asterisk-partials) /// MD5 of the rom to match (can use asterisk-partials) /// SHA-1 of the rom to match (can use asterisk-partials) /// Select roms with the given status /// 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 string gamename, string romname, string romtype, long sgt, long slt, long seq, string crc, string md5, string sha1, ItemStatus itemStatus, // 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 OutputFormat = (OutputFormat == 0 ? FileTools.GetOutputFormat(filename, logger) : OutputFormat); // Make sure there's a dictionary to read to if (Files == null) { Files = new SortedDictionary>(); } // Now parse the correct type of DAT switch (FileTools.GetOutputFormat(filename, logger)) { case OutputFormat.ClrMamePro: case OutputFormat.DOSCenter: ParseCMP(filename, sysid, srcid, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, logger, keep, clean); break; case OutputFormat.OfflineList: case OutputFormat.SabreDat: case OutputFormat.SoftwareList: case OutputFormat.Xml: ParseXML(filename, sysid, srcid, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, logger, keep, clean, softlist); break; case OutputFormat.RomCenter: ParseRC(filename, sysid, srcid, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, 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 /// Name of the game to match (can use asterisk-partials) /// Name of the rom to match (can use asterisk-partials) /// Type of the rom to match /// Find roms greater than or equal to this size /// Find roms less than or equal to this size /// Find roms equal to this size /// CRC of the rom to match (can use asterisk-partials) /// MD5 of the rom to match (can use asterisk-partials) /// SHA-1 of the rom to match (can use asterisk-partials) /// Select roms with the given status /// 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 string gamename, string romname, string romtype, long sgt, long slt, long seq, string crc, string md5, string sha1, ItemStatus itemStatus, // 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.MachineName = tempgamename; item.MachineDescription = gamedesc; item.CloneOf = cloneof; item.RomOf = romof; item.SampleOf = sampleof; item.Manufacturer = manufacturer; item.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 item statuses else if (gc[i] == "baddump" && 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] == "nodump" && 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; } } // 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 "flags": if (val.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 (val.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 (val.ToLowerInvariant() == "itemStatus") { if (item.Type == ItemType.Rom) { ((Rom)item).ItemStatus = ItemStatus.Nodump; } else if (item.Type == ItemType.Disk) { ((Disk)item).ItemStatus = ItemStatus.Nodump; } } else if (val.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 "flags": if (val.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 (val.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 (val.ToLowerInvariant() == "itemStatus") { if (item.Type == ItemType.Rom) { ((Rom)item).ItemStatus = ItemStatus.Nodump; } else if (item.Type == ItemType.Disk) { ((Disk)item).ItemStatus = ItemStatus.Nodump; } } else if (val.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, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, 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("\"", ""); 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 = ""; } } 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 /// Name of the game to match (can use asterisk-partials) /// Name of the rom to match (can use asterisk-partials) /// Type of the rom to match /// Find roms greater than or equal to this size /// Find roms less than or equal to this size /// Find roms equal to this size /// CRC of the rom to match (can use asterisk-partials) /// MD5 of the rom to match (can use asterisk-partials) /// SHA-1 of the rom to match (can use asterisk-partials) /// Select roms with the given status /// 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 string gamename, string romname, string romtype, long sgt, long slt, long seq, string crc, string md5, string sha1, ItemStatus itemStatus, // 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().Contains("[credits]")) { blocktype = "credits"; } // If the line is the start of the dat section else if (line.ToLowerInvariant().Contains("[dat]")) { blocktype = "dat"; } // If the line is the start of the emulator section else if (line.ToLowerInvariant().Contains("[emulator]")) { blocktype = "emulator"; } // If the line is the start of the game section else if (line.ToLowerInvariant().Contains("[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.StartsWith("author=")) { Author = (String.IsNullOrEmpty(Author) ? line.Split('=')[1] : Author); } // If we have one of the three version tags else if (line.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 comment else if (line.StartsWith("comment=")) { Comment = (String.IsNullOrEmpty(Comment) ? line.Split('=')[1] : Comment); } // If we have the split flag else if (line.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.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.StartsWith("refname=")) { Name = (String.IsNullOrEmpty(Name) ? line.Split('=')[1] : Name); } // If we have a rom else if (line.StartsWith("¬")) { /* 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('¬'); Rom rom = new Rom(rominfo[5], Int64.Parse(rominfo[7]), rominfo[6], null, null, ItemStatus.None, null, rominfo[3], null, rominfo[4], null, null, rominfo[8], rominfo[1], null, null, false, null, null, sysid, null, srcid, null); // Now process and add the rom string key = ""; ParseAddHelper(rom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, clean, logger, out key); } } } 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 /// Name of the game to match (can use asterisk-partials) /// Name of the rom to match (can use asterisk-partials) /// Type of the rom to match /// Find roms greater than or equal to this size /// Find roms less than or equal to this size /// Find roms equal to this size /// CRC of the rom to match (can use asterisk-partials) /// MD5 of the rom to match (can use asterisk-partials) /// SHA-1 of the rom to match (can use asterisk-partials) /// Select roms with the given status /// 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 ParseXML( // Standard Dat parsing string filename, int sysid, int srcid, // Rom filtering string gamename, string romname, string romtype, long sgt, long slt, long seq, string crc, string md5, string sha1, ItemStatus itemStatus, // 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(); XmlTextReader xtr = FileTools.GetXmlTextReader(filename, logger); // If we got a null reader, just return if (xtr == null) { return; } // Otherwise, read the file to the end 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, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, 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; string tempname = "", gamedesc = "", romof = "", cloneof = "", sampleof = "", year = "", manufacturer = ""; // 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(); if (!softlist && temptype == "software" && subreader.ReadToFollowing("description")) { tempname = subreader.ReadElementContentAsString(); gamedesc = tempname; tempname = tempname.Replace('/', '_').Replace("\"", "''"); software = true; } else { tempname = xtr.GetAttribute("name"); romof = (xtr.GetAttribute("romof") != null ? xtr.GetAttribute("romof") : ""); cloneof = (xtr.GetAttribute("cloneof") != null ? xtr.GetAttribute("cloneof") : ""); sampleof = (xtr.GetAttribute("sampleof") != null ? xtr.GetAttribute("sampleof") : ""); } if (superdat && !keep) { string tempout = Regex.Match(tempname, @".*?\\(.*)").Groups[1].Value; if (tempout != "") { tempname = tempout; } } // Get the name of the game from the parent else if (superdat && keep && parent.Count > 0) { tempname = String.Join("\\", parent) + "\\" + tempname; } // Special offline list parts string ext = ""; string releaseNumber = ""; while (software || !subreader.EOF) { software = false; // We only want elements if (subreader.NodeType != XmlNodeType.Element) { subreader.Read(); continue; } // Get the roms from the machine switch (subreader.Name) { // For offline lists only case "title": tempname = 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(releaseNumber + " - " + tempname + ext, size, subreader.ReadElementContentAsString(), null, null, ItemStatus.None, null, tempname, null, tempname, null, null, null, null, null, null, false, null, null, sysid, null, srcid, ""); // Now process and add the rom ParseAddHelper(olrom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, clean, logger, out key); break; // For Logiqx, SabreDAT, and Software List case "description": gamedesc = subreader.ReadElementContentAsString(); break; case "year": year = subreader.ReadElementContentAsString(); break; case "manufacturer": 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(subreader.GetAttribute("name"), subreader.GetAttribute("region"), subreader.GetAttribute("language"), date, defaultrel); // Now process and add the rom ParseAddHelper(relrom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, 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(subreader.GetAttribute("name"), subreader.GetAttribute("description"), defaultbios, tempname, null, gamedesc, null, null, romof, cloneof, sampleof, null, false, null, null, sysid, filename, srcid, null); // Now process and add the rom ParseAddHelper(biosrom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, clean, logger, out key); subreader.Read(); break; case "archive": empty = false; DatItem archiverom = new Archive(subreader.GetAttribute("name"), tempname, null, gamedesc, null, null, romof, cloneof, sampleof, null, false, null, null, sysid, filename, srcid, null); // Now process and add the rom ParseAddHelper(archiverom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, clean, logger, out key); subreader.Read(); break; case "sample": empty = false; DatItem samplerom = new Sample(subreader.GetAttribute("name"), tempname, null, gamedesc, null, null, romof, cloneof, sampleof, null, false, null, null, sysid, filename, srcid, null); // Now process and add the rom ParseAddHelper(samplerom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, 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") == "itemStatus" || subreader.GetAttribute("status") == "itemStatus") { 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) { tempname = Style.CleanGameName(tempname.Split(Path.DirectorySeparatorChar)); } DatItem inrom; switch (subreader.Name.ToLowerInvariant()) { case "disk": inrom = new Disk(subreader.GetAttribute("name"), subreader.GetAttribute("md5"), subreader.GetAttribute("sha1"), its, tempname, null, gamedesc, null, null, romof, cloneof, sampleof, null, false, null, null, sysid, filename, srcid, null); break; case "rom": default: inrom = new Rom(subreader.GetAttribute("name"), size, subreader.GetAttribute("crc"), subreader.GetAttribute("md5"), subreader.GetAttribute("sha1"), its, date, tempname, null, gamedesc, null, null, romof, cloneof, sampleof, null, false, null, null, sysid, filename, srcid, null); break; } // Now process and add the rom ParseAddHelper(inrom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, clean, logger, out key); subreader.Read(); break; default: subreader.Read(); break; } } // If we didn't find any items in the folder, make sure to add the blank rom if (empty) { tempname = (parent.Count > 0 ? String.Join("\\", parent) + Path.DirectorySeparatorChar : "") + tempname; Rom inrom = new Rom("null", tempname); // Now process and add the rom ParseAddHelper(inrom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, clean, logger, out key); // Regardless, end the current folder if (parent.Count == 0) { empty = true; } } 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 "itemStatus": 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; } // Get the name of the game from the parent tempname = String.Join("\\", parent); // If we aren't keeping names, trim out the path if (!keep || !superdat) { string tempout = Regex.Match(tempname, @".*?\\(.*)").Groups[1].Value; if (tempout != "") { tempname = tempout; } } DatItem rom; switch (xtr.GetAttribute("type").ToLowerInvariant()) { case "disk": rom = new Disk(xtr.GetAttribute("name"), xtr.GetAttribute("md5")?.ToLowerInvariant(), xtr.GetAttribute("sha1")?.ToLowerInvariant(), its, tempname, null, tempname, null, null, null, null, null, null, false, null, null, sysid, filename, srcid, null); break; case "rom": default: rom = new Rom(xtr.GetAttribute("name"), size, xtr.GetAttribute("crc")?.ToLowerInvariant(), xtr.GetAttribute("md5")?.ToLowerInvariant(), xtr.GetAttribute("sha1")?.ToLowerInvariant(), its, date, tempname, null, tempname, null, null, null, null, null, null, false, null, null, sysid, filename, srcid, null); break; } // Now process and add the rom ParseAddHelper(rom, gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, trim, single, root, clean, logger, out key); xtr.Read(); break; default: xtr.Read(); break; } } xtr.Dispose(); } /// /// Add a rom to the Dat after checking /// /// Item data to check against /// Name of the game to match (can use asterisk-partials) /// Name of the rom to match (can use asterisk-partials) /// Type of the rom to match /// Find roms greater than or equal to this size /// Find roms less than or equal to this size /// Find roms equal to this size /// CRC of the rom to match (can use asterisk-partials) /// MD5 of the rom to match (can use asterisk-partials) /// SHA-1 of the rom to match (can use asterisk-partials) /// Select roms with the given status /// 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, string gamename, string romname, string romtype, long sgt, long slt, long seq, string crc, string md5, string sha1, ItemStatus itemStatus, 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 (String.IsNullOrEmpty(item.Name)) { logger.Warning("Rom with no name found! Skipping..."); return; } // If we're in cleaning mode, sanitize the game name item.MachineName = (clean ? Style.CleanGameName(item.MachineName) : item.MachineName); // 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; itemRom.ItemStatus = ItemStatus.Nodump; } // If the file has no size and it's not the above case, skip and log else if (itemRom.Size == 0 || itemRom.Size == -1) { logger.Warning("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.Size > 0 && String.IsNullOrEmpty(itemRom.CRC) && String.IsNullOrEmpty(itemRom.MD5) && String.IsNullOrEmpty(itemRom.SHA1)) { logger.Warning("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 (String.IsNullOrEmpty(itemDisk.MD5) && String.IsNullOrEmpty(itemDisk.SHA1)) { logger.Warning("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 (item.Filter(gamename, romname, romtype, sgt, slt, seq, crc, md5, sha1, itemStatus, logger)) { // If we are in single game mode, rename all games if (single) { item.MachineName = "!"; } // If we are in NTFS trim mode, trim the game name if (trim) { // Windows max name length is 260 int usableLength = 260 - item.MachineName.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 DAT 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 (OutputFormat == 0) { OutputFormat = OutputFormat.Xml; } // 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, StatOutputFormat.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 (OutputFormat outputFormat in outfiles.Keys) { string outfile = outfiles[outputFormat]; 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, outputFormat, 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.MachineName == null) { logger.Warning("Null rom found!"); continue; } List newsplit = rom.MachineName.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.MachineName.ToLowerInvariant()) { depth = WriteEndGame(sw, outputFormat, 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.MachineName.ToLowerInvariant()) { depth = WriteStartGame(sw, outputFormat, 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.MachineName); // If we're in a mode that doesn't allow for actual empty folders, add the blank info if (outputFormat != OutputFormat.SabreDat && outputFormat != OutputFormat.MissFile) { 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.MachineName; continue; } } // Now, output the rom data WriteRomData(sw, outputFormat, rom, lastgame, depth, logger, ignoreblanks); // Set the new data to compare against splitpath = newsplit; lastgame = rom.MachineName; } } // Write the file footer out WriteFooter(sw, outputFormat, 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, OutputFormat outputFormat, Logger logger) { try { string header = ""; switch (outputFormat) { case OutputFormat.ClrMamePro: header = "clrmamepro (\n" + "\tname \"" + Name + "\"\n" + "\tdescription \"" + Description + "\"\n" + "\tcategory \"" + Category + "\"\n" + "\tversion \"" + Version + "\"\n" + "\tdate \"" + Date + "\"\n" + "\tauthor \"" + Author + "\"\n" + "\temail \"" + Email + "\"\n" + "\thomepage \"" + Homepage + "\"\n" + "\turl \"" + Url + "\"\n" + "\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 OutputFormat.DOSCenter: header = "DOSCenter (\n" + "Name: " + Name + "\"\n" + "Description: " + Description + "\"\n" + "Version: " + Version + "\"\n" + "Date: " + Date + "\"\n" + "Author: " + Author + "\"\n" + "Homepage: " + Homepage + "\"\n" + "Comment: " + Comment + "\"\n" + ")\n"; break; case OutputFormat.MissFile: if (XSV == true) { 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"; } else if (XSV == false) { header = "\"File Name\",\"Internal Name\",\"Description\",\"Game Name\",\"Game Description\",\"Type\",\"" + "Rom Name\",\"Disk Name\",\"Size\",\"CRC\",\"MD5\",\"SHA1\",\"Nodump\"\n"; } break; case OutputFormat.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 OutputFormat.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 OutputFormat.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 OutputFormat.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; case OutputFormat.Xml: header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE datafile PUBLIC \"-//Logiqx//DTD ROM Management Datafile//EN\" \"http://www.logiqx.com/Dats/datafile.dtd\">\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(Email) ? "\t\t<email>" + HttpUtility.HtmlEncode(Email) + "</email>\n" : "") + (!String.IsNullOrEmpty(Homepage) ? "\t\t<homepage>" + HttpUtility.HtmlEncode(Homepage) + "</homepage>\n" : "") + (!String.IsNullOrEmpty(Url) ? "\t\t<url>" + HttpUtility.HtmlEncode(Url) + "</url>\n" : "") + (!String.IsNullOrEmpty(Comment) ? "\t\t<comment>" + HttpUtility.HtmlEncode(Comment) + "</comment>\n" : "") + (!String.IsNullOrEmpty(Type) ? "\t\t<type>" + HttpUtility.HtmlEncode(Type) + "</type>\n" : "") + (ForcePacking != ForcePacking.None || ForceMerging != ForceMerging.None || ForceNodump != ForceNodump.None ? "\t\t<clrmamepro" + (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" : "") + "\t</header>\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="outputFormat">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, OutputFormat outputFormat, DatItem rom, List<string> newsplit, string lastgame, int depth, int last, Logger logger) { try { // No game should start with a path separator if (rom.MachineName.StartsWith(Path.DirectorySeparatorChar.ToString())) { rom.MachineName = rom.MachineName.Substring(1); } string state = ""; switch (outputFormat) { case OutputFormat.ClrMamePro: state += "game (\n\tname \"" + rom.MachineName + "\"\n" + (String.IsNullOrEmpty(rom.RomOf) ? "" : "\tromof \"" + rom.RomOf + "\"\n") + (String.IsNullOrEmpty(rom.CloneOf) ? "" : "\tcloneof \"" + rom.CloneOf + "\"\n") + "\tdescription \"" + (String.IsNullOrEmpty(rom.MachineDescription) ? rom.MachineName : rom.MachineDescription) + "\"\n" + (String.IsNullOrEmpty(rom.Year) ? "" : "\tyear " + rom.Year + "\n") + (String.IsNullOrEmpty(rom.Manufacturer) ? "" : "\tmanufacturer \"" + rom.Manufacturer + "\"\n"); break; case OutputFormat.DOSCenter: state += "game (\n\tname \"" + rom.MachineName + ".zip\"\n"; break; case OutputFormat.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 OutputFormat.SoftwareList: state += "\t<software name=\"" + HttpUtility.HtmlEncode(rom.MachineName) + "\">\n" + "\t\t<description>" + HttpUtility.HtmlEncode(rom.MachineDescription) + "</description>\n" + (rom.Year != null ? "\t\t<year>" + HttpUtility.HtmlEncode(rom.Year) + "</year>\n" : "") + "\t\t<part name=\"flop1\" interface=\"flop1\">\n"; break; case OutputFormat.Xml: state += "\t<machine name=\"" + HttpUtility.HtmlEncode(rom.MachineName) + "\"" + (rom.IsBios ? " isbios=\"yes\"" : "") + (String.IsNullOrEmpty(rom.CloneOf) ? "" : " cloneof=\"" + HttpUtility.HtmlEncode(rom.CloneOf) + "\"") + (String.IsNullOrEmpty(rom.RomOf) ? "" : " romof=\"" + HttpUtility.HtmlEncode(rom.RomOf) + "\"") + (String.IsNullOrEmpty(rom.SampleOf) ? "" : " sampleof=\"" + HttpUtility.HtmlEncode(rom.SampleOf) + "\"") + ">\n" + (String.IsNullOrEmpty(rom.Comment) ? "" : "\t\t<comment>" + HttpUtility.HtmlEncode(rom.Comment) + "</comment>\n") + "\t\t<description>" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.MachineDescription) ? rom.MachineName : rom.MachineDescription)) + "</description>\n" + (String.IsNullOrEmpty(rom.Year) ? "" : "\t\t<year>" + HttpUtility.HtmlEncode(rom.Year) + "</year>\n") + (String.IsNullOrEmpty(rom.Manufacturer) ? "" : "\t\t<manufacturer>" + HttpUtility.HtmlEncode(rom.Manufacturer) + "</manufacturer>\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="outputFormat">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, OutputFormat outputFormat, DatItem rom, List<string> splitpath, List<string> newsplit, string lastgame, int depth, out int last, Logger logger) { last = 0; try { string state = ""; switch (outputFormat) { case OutputFormat.ClrMamePro: case OutputFormat.DOSCenter: state += (String.IsNullOrEmpty(rom.SampleOf) ? "" : "\tsampleof \"" + rom.SampleOf + "\"\n") + ")\n"; break; case OutputFormat.OfflineList: state += "\t\t</game>\n"; break; case OutputFormat.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 OutputFormat.SoftwareList: state += "\t</software>\n\n"; break; case OutputFormat.Xml: state += "\t</machine>\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="outputFormat">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, OutputFormat outputFormat, 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 = ""; switch (outputFormat) { case OutputFormat.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 OutputFormat.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 OutputFormat.MissFile: // Missfile should only output Rom and Disk if (rom.Type != ItemType.Disk && rom.Type != ItemType.Disk) { return true; } string pre = Prefix + (Quotes ? "\"" : ""); string post = (Quotes ? "\"" : "") + Postfix; if (rom.Type == ItemType.Rom) { // Check for special strings in prefix and postfix pre = pre .Replace("%game%", rom.MachineName) .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.MachineName) .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.MachineName) .Replace("%name%", rom.Name) .Replace("%md5%", ((Disk)rom).MD5) .Replace("%sha1%", ((Disk)rom).SHA1); post = post .Replace("%game%", rom.MachineName) .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 != "") { string 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 != "") { string 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"; } } } // If we're in TSV/CSV mode, similarly the state is consistent else if (XSV != null) { string separator = (XSV == true ? "\t" : ","); if (rom.Type == ItemType.Rom) { string inline = "\"" + FileName + "\"" + separator + "\"" + Name + "\"" + separator + "\"" + Description + "\"" + separator + "\"" + rom.MachineName + "\"" + separator + "\"" + rom.MachineDescription + "\"" + separator + "\"rom\"" + separator + "\"" + rom.Name + "\"" + separator + "\"\"" + separator + "\"" + ((Rom)rom).Size + "\"" + separator + "\"" + ((Rom)rom).CRC + "\"" + separator + "\"" + ((Rom)rom).MD5 + "\"" + separator + "\"" + ((Rom)rom).SHA1 + "\"" + separator + (((Rom)rom).ItemStatus != ItemStatus.None ? "\"" + ((Rom)rom).ItemStatus.ToString() + "\"" : "\"\""); state += pre + inline + post + "\n"; } else if (rom.Type == ItemType.Disk) { string inline = "\"" + FileName + "\"" + separator + "\"" + Name + "\"" + separator + "\"" + Description + "\"" + separator + "\"" + rom.MachineName + "\"" + separator + "\"" + rom.MachineDescription + "\"" + separator + "\"disk\"" + separator + "\"\"" + separator + "\"" + rom.Name + "\"" + separator + "\"\"" + separator + "\"\"" + separator + "\"" + ((Disk)rom).MD5 + "\"" + separator + "\"" + ((Disk)rom).SHA1 + "\"" + separator + (((Disk)rom).ItemStatus != ItemStatus.None ? "\"" + ((Disk)rom).ItemStatus.ToString() + "\"" : "\"\""); state += pre + inline + post + "\n"; } } // Otherwise, use any flags else { string name = (UseGame ? rom.MachineName : 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.MachineName, name); } if (UseGame && rom.MachineName != lastgame) { state += pre + name + post + "\n"; lastgame = rom.MachineName; } else if (!UseGame) { state += pre + name + post + "\n"; } } break; case OutputFormat.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 OutputFormat.RedumpMD5: if (rom.Type == ItemType.Rom) { state += ((Rom)rom).MD5 + " *" + rom.Name + "\n"; } else if (rom.Type == ItemType.Disk) { state += ((Disk)rom).MD5 + " *" + rom.Name + "\n"; } break; case OutputFormat.RedumpSFV: if (rom.Type == ItemType.Rom) { state += rom.Name + " " + ((Rom)rom).CRC + "\n"; } break; case OutputFormat.RedumpSHA1: if (rom.Type == ItemType.Rom) { state += ((Rom)rom).SHA1 + " *" + rom.Name + "\n"; } else if (rom.Type == ItemType.Disk) { state += ((Disk)rom).SHA1 + " *" + rom.Name + "\n"; } break; case OutputFormat.RomCenter: if (rom.Type == ItemType.Rom) { state += "¬" + (String.IsNullOrEmpty(rom.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.CloneOf)) + "¬" + (String.IsNullOrEmpty(rom.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.CloneOf)) + "¬" + HttpUtility.HtmlEncode(rom.MachineName) + "¬" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.MachineDescription) ? rom.MachineName : rom.MachineDescription)) + "¬" + 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.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.CloneOf)) + "¬" + (String.IsNullOrEmpty(rom.CloneOf) ? "" : HttpUtility.HtmlEncode(rom.CloneOf)) + "¬" + HttpUtility.HtmlEncode(rom.MachineName) + "¬" + HttpUtility.HtmlEncode((String.IsNullOrEmpty(rom.MachineDescription) ? rom.MachineName : rom.MachineDescription)) + "¬" + HttpUtility.HtmlEncode(rom.Name) + "¬¬¬¬¬\n"; } break; case OutputFormat.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 OutputFormat.SoftwareList: 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; } break; case OutputFormat.Xml: switch (rom.Type) { case ItemType.Archive: state += "\t\t\n"; break; case ItemType.BiosSet: state += "\t\t\n"; break; case ItemType.Disk: state += "\t\t\n"; break; case ItemType.Release: state += "\t\t\n"; break; case ItemType.Rom: state += "\t\t\n"; break; case ItemType.Sample: state += "\t\t\n"; break; } 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, OutputFormat outputFormat, int depth, Logger logger) { try { string footer = ""; // If we have roms, output the full footer if (Files != null && Files.Count > 0) { switch (outputFormat) { case OutputFormat.ClrMamePro: case OutputFormat.DOSCenter: footer = ")\n"; break; case OutputFormat.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 OutputFormat.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 OutputFormat.SoftwareList: footer = "\t\n\n\n"; break; case OutputFormat.Xml: footer = "\t\n\n"; break; } } // Otherwise, output the abbreviated form else { switch (outputFormat) { case OutputFormat.OfflineList: footer = "\t\n" + "\t\n" + "\t\t\n" + "\t\t\t\n" + "\t\t\t\n" + "\t\t\n" + "\t\n" + ""; break; case OutputFormat.SoftwareList: footer = "\n"; break; case OutputFormat.SabreDat: case OutputFormat.Xml: footer = "\n"; break; } } // Write the footer out sw.Write(footer); sw.Flush(); } catch (Exception ex) { logger.Error(ex.ToString()); return false; } return true; } #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) /// True if files should be copied to the temp directory before hashing, false 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, 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 folder 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, 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)); } }); } // 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 /// 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, 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, logger); } } // Otherwise, attempt to extract the files to the temporary directory else { bool encounteredErrors = ArchiveTools.ExtractArchive(newItem, tempSubDir, (archivesAsFiles ? ArchiveScanLevel.External : ArchiveScanLevel.Internal), (!archivesAsFiles && enableGzip ? ArchiveScanLevel.Internal : ArchiveScanLevel.External), (archivesAsFiles ? ArchiveScanLevel.External : ArchiveScanLevel.Internal), (archivesAsFiles ? ArchiveScanLevel.External : ArchiveScanLevel.Internal), 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, logger); }); } // Otherwise, just get the info on the file itself else if (File.Exists(newItem)) { DFDProcessFile(newItem, "", newBasePath, noMD5, noSHA1, addDate, 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 /// Path the represents the parent directory /// Parent game to be used private void DFDProcessFile(string item, string parent, string basePath, bool noMD5, bool noSHA1, bool addDate, Logger logger) { logger.Verbose(Path.GetFileName(item) + " treated like a file"); Rom rom = FileTools.GetSingleFileInfo(item, noMD5: noMD5, noSHA1: noSHA1, date: addDate); 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; datItem.MachineName = gamename; datItem.MachineDescription = gamename; // Add the file information to the DAT lock (Files) { Files[key].Add(datItem); } logger.User("File added: " + romname + Environment.NewLine); } catch (IOException ex) { logger.Error(ex.ToString()); return; } } #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, StatOutputFormat statOutputFormat, 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 (statOutputFormat) { case StatOutputFormat.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 StatOutputFormat.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 StatOutputFormat.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 StatOutputFormat.TSV: line = "\"" + FileName + "\"\t" + "\"" + Style.GetBytesReadable(TotalSize) + "\"\t" + "\"" + (game == -1 ? Files.Count : game) + "\"\t" + "\"" + RomCount + "\"\t" + "\"" + DiskCount + "\"\t" + "\"" + CRCCount + "\"\t" + "\"" + MD5Count + "\"\t" + "\"" + SHA1Count + "\""; if (baddumpCol) { line += "\t\"" + BaddumpCount + "\""; } if (nodumpCol) { line += "\t\"" + NodumpCount + "\""; } line += "\n"; break; } // Output the line to the streamwriter sw.Write(line); } #endregion #endregion // Instance Methods #region Static Methods #region 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) { count++; string newkey = (norename ? "" : rom.SystemID.ToString().PadLeft(10, '0') + "-" + rom.SourceID.ToString().PadLeft(10, '0') + "-") + (String.IsNullOrEmpty(rom.MachineName) ? "Default" : rom.MachineName.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 DAT Splitting /// /// Split a DAT by input extensions /// /// Name of the file to be split /// 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 static bool SplitByExt(string filename, 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); // Get the file format OutputFormat outputFormat = FileTools.GetOutputFormat(filename, logger); if (outputFormat == 0) { return true; } // Get the file data to be split DatFile datdata = new DatFile(); datdata.Parse(filename, 0, 0, logger, softlist: true); // Set all of the appropriate outputs for each of the subsets DatFile datdataA = new DatFile { FileName = datdata.FileName + " (" + newExtAString + ")", Name = datdata.Name + " (" + newExtAString + ")", Description = datdata.Description + " (" + newExtAString + ")", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Files = new SortedDictionary>(), OutputFormat = outputFormat, }; DatFile datdataB = new DatFile { FileName = datdata.FileName + " (" + newExtBString + ")", Name = datdata.Name + " (" + newExtBString + ")", Description = datdata.Description + " (" + newExtBString + ")", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Files = new SortedDictionary>(), OutputFormat = outputFormat, }; // If roms is empty, return false if (datdata.Files.Count == 0) { return false; } // Now separate the roms accordingly foreach (string key in datdata.Files.Keys) { foreach (DatItem rom in datdata.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(filename).Remove(0, basepath.Length - 1); } else { outDir = Path.GetDirectoryName(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 file to be split /// 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 static bool SplitByHash(string filename, string outDir, string basepath, Logger logger) { // Sanitize the basepath to be more predictable basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar); // Get the file format OutputFormat outputFormat = FileTools.GetOutputFormat(filename, logger); if (outputFormat == 0) { return true; } // Get the file data to be split DatFile datdata = new DatFile(); datdata.Parse(filename, 0, 0, logger, true, softlist: true); // Create each of the respective output DATs logger.User("Creating and populating new DATs"); DatFile itemStatus = new DatFile { FileName = datdata.FileName + " (Nodump)", Name = datdata.Name + " (Nodump)", Description = datdata.Description + " (Nodump)", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Header = datdata.Header, Type = datdata.Type, ForceMerging = datdata.ForceMerging, ForceNodump = datdata.ForceNodump, ForcePacking = datdata.ForcePacking, OutputFormat = outputFormat, MergeRoms = datdata.MergeRoms, Files = new SortedDictionary>(), }; DatFile sha1 = new DatFile { FileName = datdata.FileName + " (SHA-1)", Name = datdata.Name + " (SHA-1)", Description = datdata.Description + " (SHA-1)", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Header = datdata.Header, Type = datdata.Type, ForceMerging = datdata.ForceMerging, ForceNodump = datdata.ForceNodump, ForcePacking = datdata.ForcePacking, OutputFormat = outputFormat, MergeRoms = datdata.MergeRoms, Files = new SortedDictionary>(), }; DatFile md5 = new DatFile { FileName = datdata.FileName + " (MD5)", Name = datdata.Name + " (MD5)", Description = datdata.Description + " (MD5)", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Header = datdata.Header, Type = datdata.Type, ForceMerging = datdata.ForceMerging, ForceNodump = datdata.ForceNodump, ForcePacking = datdata.ForcePacking, OutputFormat = outputFormat, MergeRoms = datdata.MergeRoms, Files = new SortedDictionary>(), }; DatFile crc = new DatFile { FileName = datdata.FileName + " (CRC)", Name = datdata.Name + " (CRC)", Description = datdata.Description + " (CRC)", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Header = datdata.Header, Type = datdata.Type, ForceMerging = datdata.ForceMerging, ForceNodump = datdata.ForceNodump, ForcePacking = datdata.ForcePacking, OutputFormat = outputFormat, MergeRoms = datdata.MergeRoms, Files = new SortedDictionary>(), }; DatFile other = new DatFile { FileName = datdata.FileName + " (Other)", Name = datdata.Name + " (Other)", Description = datdata.Description + " (Other)", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Header = datdata.Header, Type = datdata.Type, ForceMerging = datdata.ForceMerging, ForceNodump = datdata.ForceNodump, ForcePacking = datdata.ForcePacking, OutputFormat = outputFormat, MergeRoms = datdata.MergeRoms, Files = new SortedDictionary>(), }; // Now populate each of the DAT objects in turn List keys = datdata.Files.Keys.ToList(); foreach (string key in keys) { List roms = datdata.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(filename).Remove(0, basepath.Length - 1); } else { outDir = Path.GetDirectoryName(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 DAT by type of Rom /// /// Name of the file to be split /// 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 static bool SplitByType(string filename, string outDir, string basepath, Logger logger) { // Sanitize the basepath to be more predictable basepath = (basepath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basepath : basepath + Path.DirectorySeparatorChar); // Get the file format OutputFormat outputFormat = FileTools.GetOutputFormat(filename, logger); if (outputFormat == 0) { return true; } // Get the file data to be split DatFile datdata = new DatFile(); datdata.Parse(filename, 0, 0, logger, true, softlist: true); // Create each of the respective output DATs logger.User("Creating and populating new DATs"); DatFile romdat = new DatFile { FileName = datdata.FileName + " (ROM)", Name = datdata.Name + " (ROM)", Description = datdata.Description + " (ROM)", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Header = datdata.Header, Type = datdata.Type, ForceMerging = datdata.ForceMerging, ForceNodump = datdata.ForceNodump, ForcePacking = datdata.ForcePacking, OutputFormat = outputFormat, MergeRoms = datdata.MergeRoms, Files = new SortedDictionary>(), }; DatFile diskdat = new DatFile { FileName = datdata.FileName + " (Disk)", Name = datdata.Name + " (Disk)", Description = datdata.Description + " (Disk)", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Header = datdata.Header, Type = datdata.Type, ForceMerging = datdata.ForceMerging, ForceNodump = datdata.ForceNodump, ForcePacking = datdata.ForcePacking, OutputFormat = outputFormat, MergeRoms = datdata.MergeRoms, Files = new SortedDictionary>(), }; DatFile sampledat = new DatFile { FileName = datdata.FileName + " (Sample)", Name = datdata.Name + " (Sample)", Description = datdata.Description + " (Sample)", Category = datdata.Category, Version = datdata.Version, Date = datdata.Date, Author = datdata.Author, Email = datdata.Email, Homepage = datdata.Homepage, Url = datdata.Url, Comment = datdata.Comment, Header = datdata.Header, Type = datdata.Type, ForceMerging = datdata.ForceMerging, ForceNodump = datdata.ForceNodump, ForcePacking = datdata.ForcePacking, OutputFormat = outputFormat, MergeRoms = datdata.MergeRoms, Files = new SortedDictionary>(), }; // Now populate each of the DAT objects in turn List keys = datdata.Files.Keys.ToList(); foreach (string key in keys) { List roms = datdata.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(filename).Remove(0, basepath.Length - 1); } else { outDir = Path.GetDirectoryName(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 /// /// 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, StatOutputFormat statOutputFormat, Logger logger) { reportName += OutputStatsGetExtension(statOutputFormat); 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, statOutputFormat, 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, statOutputFormat, 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, statOutputFormat, logger, game: dirGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); // Write the mid-footer, if any OutputStatsWriteMidFooter(sw, statOutputFormat, baddumpCol, nodumpCol); // Write the header, if any OutputStatsWriteMidHeader(sw, statOutputFormat, 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 + "'", 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 + "'\n", false); if (single) { datdata.OutputStats(sw, statOutputFormat, 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, statOutputFormat, 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, statOutputFormat, logger, game: dirGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); } // Write the mid-footer, if any OutputStatsWriteMidFooter(sw, statOutputFormat, baddumpCol, nodumpCol); // Write the header, if any OutputStatsWriteMidHeader(sw, statOutputFormat, 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, statOutputFormat, logger, game: totalGame, baddumpCol: baddumpCol, nodumpCol: nodumpCol); // Output footer if needed OutputStatsWriteFooter(sw, statOutputFormat); 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 /// /// StatOutputFormat to get the extension for /// File extension with leading period private static string OutputStatsGetExtension(StatOutputFormat statOutputFormat) { string reportExtension = ""; switch (statOutputFormat) { case StatOutputFormat.CSV: reportExtension = ".csv"; break; case StatOutputFormat.HTML: reportExtension = ".html"; break; case StatOutputFormat.None: default: reportExtension = ".txt"; break; case StatOutputFormat.TSV: reportExtension = ".csv"; break; } return reportExtension; } /// /// Write out the header to the stream, if any exists /// /// StreamWriter representing the output /// StatOutputFormat 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, StatOutputFormat statOutputFormat, bool baddumpCol, bool nodumpCol) { string head = ""; switch (statOutputFormat) { case StatOutputFormat.CSV: break; case StatOutputFormat.HTML: head = @"
DAT Statistics Report

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

"; break; case StatOutputFormat.None: default: break; case StatOutputFormat.TSV: break; } sw.Write(head); // Now write the mid header for those who need it OutputStatsWriteMidHeader(sw, statOutputFormat, baddumpCol, nodumpCol); } /// /// Write out the mid-header to the stream, if any exists /// /// StreamWriter representing the output /// StatOutputFormat 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, StatOutputFormat statOutputFormat, bool baddumpCol, bool nodumpCol) { string head = ""; switch (statOutputFormat) { case StatOutputFormat.CSV: head = "\"File Name\",\"Total Size\",\"Games\",\"Roms\",\"Disks\",\"# with CRC\",\"# with MD5\",\"# with SHA-1\"" + (baddumpCol ? ",\"BadDumps\"" : "") + (nodumpCol ? ",\"Nodumps\"" : "") + "\n"; break; case StatOutputFormat.HTML: head = @" " + @"" + (baddumpCol ? "" : "") + (nodumpCol ? "" : "") + "\n"; break; case StatOutputFormat.None: default: break; case StatOutputFormat.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 /// StatOutputFormat 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, StatOutputFormat statOutputFormat, bool baddumpCol, bool nodumpCol) { string mid = ""; switch (statOutputFormat) { case StatOutputFormat.CSV: break; case StatOutputFormat.HTML: mid = "\n"; break; case StatOutputFormat.None: default: break; } sw.Write(mid); } /// /// Write out the footer-separator to the stream, if any exists /// /// StreamWriter representing the output /// StatOutputFormat 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, StatOutputFormat statOutputFormat, bool baddumpCol, bool nodumpCol) { string end = ""; switch (statOutputFormat) { case StatOutputFormat.CSV: end = "\n"; break; case StatOutputFormat.HTML: end = "\n"; break; case StatOutputFormat.None: default: end = "\n"; break; case StatOutputFormat.TSV: end = "\n"; break; } sw.Write(end); } /// /// Write out the footer to the stream, if any exists /// /// StreamWriter representing the output /// StatOutputFormat representing output format private static void OutputStatsWriteFooter(StreamWriter sw, StatOutputFormat statOutputFormat) { string end = ""; switch (statOutputFormat) { case StatOutputFormat.CSV: break; case StatOutputFormat.HTML: end = @"
File NameTotal SizeGamesRomsDisks# with CRC# with MD5# with SHA-1BaddumpsNodumps
"; break; case StatOutputFormat.None: default: break; case StatOutputFormat.TSV: break; } sw.Write(end); } #endregion #endregion // Static Methods } }