using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web; using SabreTools.Library.Data; using SabreTools.Library.Items; #if MONO using System.IO; #else using Alphaleonis.Win32.Filesystem; #endif namespace SabreTools.Library.DatFiles { /// /// Represents a format-agnostic DAT /// public partial class DatFile { #region Instance Methods #region Bucketing /// /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by a user-defined method /// /// SortedBy enum representing how to sort the individual items /// Dedupe type that should be used /// True if the key should be lowercased (default), false otherwise /// True if games should only be compared on game and file name, false if system and source are counted public void BucketBy(SortedBy bucketBy, DedupeType deduperoms, bool lower = true, bool norename = true) { // If we have a situation where there's no dictionary or no keys at all, we skip if (_items == null || _items.Count == 0) { return; } Globals.Logger.User("Organizing roms by {0}" + (deduperoms != DedupeType.None ? " and merging" : ""), bucketBy); // If the sorted type isn't the same, we want to sort the dictionary accordingly if (_sortedBy != bucketBy) { // Set the sorted type _sortedBy = bucketBy; // First do the initial sort of all of the roms inplace List oldkeys = Keys.ToList(); Parallel.ForEach(oldkeys, Globals.ParallelOptions, key => { // Get the unsorted current list List roms = this[key]; // Now add each of the roms to their respective games foreach (DatItem rom in roms) { // We want to get the key most appropriate for the given sorting type string newkey = GetKey(rom, bucketBy, lower, norename); // Add the DatItem to the dictionary Add(newkey, rom); } // Finally, remove the entire original key Remove(key); }); } // Now go through and sort all of the individual lists List keys = Keys.ToList(); Parallel.ForEach(keys, Globals.ParallelOptions, key => { // Get the possibly unsorted list List sortedlist = this[key]; // Sort the list of items to be consistent DatItem.Sort(ref sortedlist, false); // If we're merging the roms, do so if (deduperoms == DedupeType.Full || (deduperoms == DedupeType.Game && bucketBy == SortedBy.Game)) { sortedlist = DatItem.Merge(sortedlist); } // Add the list back to the dictionary Remove(key); AddRange(key, sortedlist); }); } /// /// Get the dictionary key that should be used for a given item and sorting type /// /// DatItem to get the key for /// SortedBy enum representing what key to get /// True if the key should be lowercased (default), false otherwise /// True if games should only be compared on game and file name, false if system and source are counted /// String representing the key to be used for the DatItem private string GetKey(DatItem item, SortedBy sortedBy, bool lower = true, bool norename = true) { // Set the output key as the default blank string string key = ""; // Now determine what the key should be based on the sortedBy value switch (sortedBy) { case SortedBy.CRC: key = (item.Type == ItemType.Rom ? ((Rom)item).CRC : Constants.CRCZero); break; case SortedBy.Game: key = (norename ? "" : item.SystemID.ToString().PadLeft(10, '0') + "-" + item.SourceID.ToString().PadLeft(10, '0') + "-") + (String.IsNullOrEmpty(item.MachineName) ? "Default" : item.MachineName); if (lower) { key = key.ToLowerInvariant(); } if (key == null) { key = "null"; } key = HttpUtility.HtmlEncode(key); break; case SortedBy.MD5: key = (item.Type == ItemType.Rom ? ((Rom)item).MD5 : (item.Type == ItemType.Disk ? ((Disk)item).MD5 : Constants.MD5Zero)); break; case SortedBy.SHA1: key = (item.Type == ItemType.Rom ? ((Rom)item).SHA1 : (item.Type == ItemType.Disk ? ((Disk)item).SHA1 : Constants.SHA1Zero)); break; case SortedBy.SHA256: key = (item.Type == ItemType.Rom ? ((Rom)item).SHA256 : (item.Type == ItemType.Disk ? ((Disk)item).SHA256 : Constants.SHA256Zero)); break; case SortedBy.SHA384: key = (item.Type == ItemType.Rom ? ((Rom)item).SHA384 : (item.Type == ItemType.Disk ? ((Disk)item).SHA384 : Constants.SHA384Zero)); break; case SortedBy.SHA512: key = (item.Type == ItemType.Rom ? ((Rom)item).SHA512 : (item.Type == ItemType.Disk ? ((Disk)item).SHA512 : Constants.SHA512Zero)); break; } // Double and triple check the key for corner cases if (key == null) { key = ""; } return key; } #endregion #region Filtering /// /// Filter a DAT based on input parameters and modify the items /// /// Filter object for passing to the DatItem level /// True if we are supposed to trim names to NTFS length, false otherwise /// True if all games should be replaced by '!', false otherwise /// String representing root directory to compare against for length calculation public void Filter(Filter filter, bool single, bool trim, string root) { try { // Loop over every key in the dictionary List keys = Keys.ToList(); foreach (string key in keys) { // For every item in the current key List items = this[key]; List newitems = new List(); foreach (DatItem item in items) { // If the rom passes the filter, include it if (filter.ItemPasses(item)) { // 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 the list and add the item back lock (newitems) { newitems.Add(item); } } } Remove(key); AddRange(key, newitems); } } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); } } /// /// Use game descriptions as names in the DAT, updating cloneof/romof/sampleof /// public void MachineDescriptionToName() { try { // First we want to get a mapping for all games to description ConcurrentDictionary mapping = new ConcurrentDictionary(); List keys = Keys.ToList(); Parallel.ForEach(keys, Globals.ParallelOptions, key => { List items = this[key]; foreach (DatItem item in items) { // If the key mapping doesn't exist, add it if (!mapping.ContainsKey(item.MachineName)) { mapping.TryAdd(item.MachineName, item.MachineDescription.Replace('/', '_').Replace("\"", "''")); } } }); // Now we loop through every item and update accordingly keys = Keys.ToList(); Parallel.ForEach(keys, Globals.ParallelOptions, key => { List items = this[key]; List newItems = new List(); foreach (DatItem item in items) { // Update machine name if (!String.IsNullOrEmpty(item.MachineName) && mapping.ContainsKey(item.MachineName)) { item.MachineName = mapping[item.MachineName]; } // Update cloneof if (!String.IsNullOrEmpty(item.CloneOf) && mapping.ContainsKey(item.CloneOf)) { item.CloneOf = mapping[item.CloneOf]; } // Update romof if (!String.IsNullOrEmpty(item.RomOf) && mapping.ContainsKey(item.RomOf)) { item.RomOf = mapping[item.RomOf]; } // Update sampleof if (!String.IsNullOrEmpty(item.SampleOf) && mapping.ContainsKey(item.SampleOf)) { item.SampleOf = mapping[item.SampleOf]; } // Add the new item to the output list newItems.Add(item); } // Replace the old list of roms with the new one Remove(key); AddRange(key, newItems); }); } catch (Exception ex) { Globals.Logger.Warning(ex.ToString()); } } /// /// Strip the given hash types from the DAT /// public void StripHashesFromItems() { // Output the logging statement Globals.Logger.User("Stripping requested hashes"); // Now process all of the roms List keys = Keys.ToList(); Parallel.ForEach(keys, Globals.ParallelOptions, key => { List items = this[key]; for (int j = 0; j < items.Count; j++) { DatItem item = items[j]; if (item.Type == ItemType.Rom) { Rom rom = (Rom)item; if ((StripHash & Hash.MD5) != 0) { rom.MD5 = null; } if ((StripHash & Hash.SHA1) != 0) { rom.SHA1 = null; } if ((StripHash & Hash.SHA256) != 0) { rom.SHA256 = null; } if ((StripHash & Hash.SHA384) != 0) { rom.SHA384 = null; } if ((StripHash & Hash.SHA512) != 0) { rom.SHA512 = null; } items[j] = rom; } else if (item.Type == ItemType.Disk) { Disk disk = (Disk)item; if ((StripHash & Hash.MD5) != 0) { disk.MD5 = null; } if ((StripHash & Hash.SHA1) != 0) { disk.SHA1 = null; } if ((StripHash & Hash.SHA256) != 0) { disk.SHA256 = null; } if ((StripHash & Hash.SHA384) != 0) { disk.SHA384 = null; } if ((StripHash & Hash.SHA512) != 0) { disk.SHA512 = null; } items[j] = disk; } } Remove(key); AddRange(key, items); }); } #endregion #region Merging/Splitting Methods /// /// Use cdevice_ref tags to get full non-merged sets and remove parenting tags /// /// Dedupe type to be used public void CreateDeviceNonMergedSets(DedupeType mergeroms) { Globals.Logger.User("Creating device non-merged sets from the DAT"); // For sake of ease, the first thing we want to do is sort by game BucketBy(SortedBy.Game, mergeroms, norename: true); _sortedBy = SortedBy.Default; // Now we want to loop through all of the games and set the correct information AddRomsFromDevices(); // Then, remove the romof and cloneof tags so it's not picked up by the manager RemoveTagsFromChild(); // Finally, remove all sets that are labeled as bios or device //RemoveBiosAndDeviceSets(logger); } /// /// Use cloneof tags to create non-merged sets and remove the tags plus using the device_ref tags to get full sets /// /// Dedupe type to be used public void CreateFullyNonMergedSets(DedupeType mergeroms) { Globals.Logger.User("Creating fully non-merged sets from the DAT"); // For sake of ease, the first thing we want to do is sort by game BucketBy(SortedBy.Game, mergeroms, norename: true); _sortedBy = SortedBy.Default; // Now we want to loop through all of the games and set the correct information AddRomsFromDevices(); AddRomsFromParent(); // Now that we have looped through the cloneof tags, we loop through the romof tags AddRomsFromBios(); // Then, remove the romof and cloneof tags so it's not picked up by the manager RemoveTagsFromChild(); // Finally, remove all sets that are labeled as bios or device //RemoveBiosAndDeviceSets(logger); } /// /// Use cloneof tags to create merged sets and remove the tags /// /// Dedupe type to be used public void CreateMergedSets(DedupeType mergeroms) { Globals.Logger.User("Creating merged sets from the DAT"); // For sake of ease, the first thing we want to do is sort by game BucketBy(SortedBy.Game, mergeroms, norename: true); _sortedBy = SortedBy.Default; // Now we want to loop through all of the games and set the correct information AddRomsFromChildren(); // Now that we have looped through the cloneof tags, we loop through the romof tags RemoveBiosRomsFromChild(); // Finally, remove the romof and cloneof tags so it's not picked up by the manager RemoveTagsFromChild(); } /// /// Use cloneof tags to create non-merged sets and remove the tags /// /// Dedupe type to be used public void CreateNonMergedSets(DedupeType mergeroms) { Globals.Logger.User("Creating non-merged sets from the DAT"); // For sake of ease, the first thing we want to do is sort by game BucketBy(SortedBy.Game, mergeroms, norename: true); _sortedBy = SortedBy.Default; // Now we want to loop through all of the games and set the correct information AddRomsFromParent(); // Now that we have looped through the cloneof tags, we loop through the romof tags RemoveBiosRomsFromChild(); // Finally, remove the romof and cloneof tags so it's not picked up by the manager RemoveTagsFromChild(); } /// /// Use cloneof and romof tags to create split sets and remove the tags /// /// Dedupe type to be used public void CreateSplitSets(DedupeType mergeroms) { Globals.Logger.User("Creating split sets from the DAT"); // For sake of ease, the first thing we want to do is sort by game BucketBy(SortedBy.Game, mergeroms, norename: true); _sortedBy = SortedBy.Default; // Now we want to loop through all of the games and set the correct information RemoveRomsFromChild(); // Now that we have looped through the cloneof tags, we loop through the romof tags RemoveBiosRomsFromChild(); // Finally, remove the romof and cloneof tags so it's not picked up by the manager RemoveTagsFromChild(); } #endregion #region Merging/Splitting Helper Methods /// /// Use romof tags to add roms to the children /// private void AddRomsFromBios() { List games = Keys.ToList(); foreach (string game in games) { // If the game has no items in it, we want to continue if (this[game].Count == 0) { continue; } // Determine if the game has a parent or not string parent = null; if (!String.IsNullOrEmpty(this[game][0].RomOf)) { parent = this[game][0].RomOf; } // If the parent doesnt exist, we want to continue if (String.IsNullOrEmpty(parent)) { continue; } // If the parent doesn't have any items, we want to continue if (this[parent].Count == 0) { continue; } // If the parent exists and has items, we copy the items from the parent to the current game DatItem copyFrom = this[game][0]; List parentItems = this[parent]; foreach (DatItem item in parentItems) { DatItem datItem = (DatItem)item.Clone(); datItem.CopyMachineInformation(copyFrom); if (this[game].Where(i => i.Name == datItem.Name).Count() == 0 && !this[game].Contains(datItem)) { Add(game, datItem); } } } } /// /// Use device_ref tags to add roms to the children /// private void AddRomsFromDevices() { List games = Keys.ToList(); foreach (string game in games) { // If the game has no devices, we continue if (this[game][0].Devices == null || this[game][0].Devices.Count == 0) { continue; } // Determine if the game has any devices or not List devices = this[game][0].Devices; foreach (string device in devices) { // If the device doesn't exist then we continue if (this[device].Count == 0) { continue; } // Otherwise, copy the items from the device to the current game DatItem copyFrom = this[game][0]; List devItems = this[device]; foreach (DatItem item in devItems) { DatItem datItem = (DatItem)item.Clone(); datItem.CopyMachineInformation(copyFrom); if (this[game].Where(i => i.Name == datItem.Name).Count() == 0 && !this[game].Contains(datItem)) { Add(game, datItem); } } } } } /// /// Use cloneof tags to add roms to the children, setting the new romof tag in the process /// private void AddRomsFromParent() { List games = Keys.ToList(); foreach (string game in games) { // If the game has no items in it, we want to continue if (this[game].Count == 0) { continue; } // Determine if the game has a parent or not string parent = null; if (!String.IsNullOrEmpty(this[game][0].CloneOf)) { parent = this[game][0].CloneOf; } // If the parent doesnt exist, we want to continue if (String.IsNullOrEmpty(parent)) { continue; } // If the parent doesn't have any items, we want to continue if (this[parent].Count == 0) { continue; } // If the parent exists and has items, we copy the items from the parent to the current game DatItem copyFrom = this[game][0]; List parentItems = this[parent]; foreach (DatItem item in parentItems) { DatItem datItem = (DatItem)item.Clone(); datItem.CopyMachineInformation(copyFrom); if (this[game].Where(i => i.Name == datItem.Name).Count() == 0 && !this[game].Contains(datItem)) { Add(game, datItem); } } // Now we want to get the parent romof tag and put it in each of the items List items = this[game]; string romof = this[parent][0].RomOf; foreach (DatItem item in items) { item.RomOf = romof; } } } /// /// Use cloneof tags to add roms to the parents, removing the child sets in the process /// private void AddRomsFromChildren() { List games = Keys.ToList(); foreach (string game in games) { // Determine if the game has a parent or not string parent = null; if (!String.IsNullOrEmpty(this[game][0].CloneOf)) { parent = this[game][0].CloneOf; } // If there is no parent, then we continue if (String.IsNullOrEmpty(parent)) { continue; } // Otherwise, move the items from the current game to a subfolder of the parent game DatItem copyFrom = this[parent].Count == 0 ? new Rom { MachineName = parent, MachineDescription = parent } : this[parent][0]; List items = this[game]; foreach (DatItem item in items) { // If the disk doesn't have a valid merge tag OR the merged file doesn't exist in the parent, then add it if (item.Type == ItemType.Disk && (item.MergeTag == null || !this[parent].Select(i => i.Name).Contains(item.MergeTag))) { item.CopyMachineInformation(copyFrom); Add(parent, item); } // Otherwise, if the parent doesn't already contain the non-disk, add it else if (item.Type != ItemType.Disk && !this[parent].Contains(item)) { // Rename the child so it's in a subfolder item.Name = item.Name + "\\" + item.Name; // Update the machine to be the new parent item.CopyMachineInformation(copyFrom); // Add the rom to the parent set Add(parent, item); } } // Then, remove the old game so it's not picked up by the writer Remove(game); } } /// /// Remove all BIOS and device sets /// private void RemoveBiosAndDeviceSets() { List games = Keys.ToList(); foreach (string game in games) { if (this[game].Count > 0 && (this[game][0].MachineType == MachineType.Bios || this[game][0].MachineType == MachineType.Device)) { Remove(game); } } } /// /// Use romof tags to remove roms from the children /// private void RemoveBiosRomsFromChild() { // Loop through the romof tags List games = Keys.ToList(); foreach (string game in games) { // If the game has no items in it, we want to continue if (this[game].Count == 0) { continue; } // Determine if the game has a parent or not string parent = null; if (!String.IsNullOrEmpty(this[game][0].RomOf)) { parent = this[game][0].RomOf; } // If the parent doesnt exist, we want to continue if (String.IsNullOrEmpty(parent)) { continue; } // If the parent doesn't have any items, we want to continue if (this[parent].Count == 0) { continue; } // If the parent exists and has items, we remove the items that are in the parent from the current game List parentItems = this[parent]; foreach (DatItem item in parentItems) { DatItem datItem = (DatItem)item.Clone(); Remove(game, datItem); } } } /// /// Use cloneof tags to remove roms from the children /// private void RemoveRomsFromChild() { List games = Keys.ToList(); foreach (string game in games) { // If the game has no items in it, we want to continue if (this[game].Count == 0) { continue; } // Determine if the game has a parent or not string parent = null; if (!String.IsNullOrEmpty(this[game][0].CloneOf)) { parent = this[game][0].CloneOf; } // If the parent doesnt exist, we want to continue if (String.IsNullOrEmpty(parent)) { continue; } // If the parent doesn't have any items, we want to continue if (this[parent].Count == 0) { continue; } // If the parent exists and has items, we copy the items from the parent to the current game List parentItems = this[parent]; foreach (DatItem item in parentItems) { DatItem datItem = (DatItem)item.Clone(); Remove(game, datItem); } // Now we want to get the parent romof tag and put it in each of the items List items = this[game]; string romof = this[parent][0].RomOf; foreach (DatItem item in items) { item.RomOf = romof; } } } /// /// Remove all romof and cloneof tags from all games /// private void RemoveTagsFromChild() { List games = Keys.ToList(); foreach (string game in games) { List items = this[game]; foreach (DatItem item in items) { item.CloneOf = null; item.RomOf = null; } } } #endregion #endregion // Instance Methods } }