using System; using System.Collections; #if NET40_OR_GREATER || NETCOREAPP using System.Collections.Concurrent; #endif using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; #if NET40_OR_GREATER || NETCOREAPP using System.Threading.Tasks; #endif using System.Xml.Serialization; using Newtonsoft.Json; using SabreTools.Core.Filter; using SabreTools.Core.Tools; using SabreTools.DatItems; using SabreTools.DatItems.Formats; using SabreTools.Hashing; using SabreTools.IO.Logging; using SabreTools.Matching.Compare; namespace SabreTools.DatFiles { /// /// Item dictionary with statistics, bucketing, and sorting /// [JsonObject("items"), XmlRoot("items")] public class ItemDictionary : IDictionary?> { #region Private instance variables /// /// Determine the bucketing key for all items /// private ItemKey bucketedBy; /// /// Determine merging type for all items /// private DedupeType mergedBy; /// /// Internal dictionary for the class /// #if NET40_OR_GREATER || NETCOREAPP private readonly ConcurrentDictionary?> _items = []; #else private readonly Dictionary?> _items = []; #endif /// /// Logging object /// private readonly Logger _logger; #endregion #region Publically available fields #region Keys /// /// Get the keys from the file dictionary /// /// List of the keys [JsonIgnore, XmlIgnore] public ICollection Keys { get { return _items.Keys; } } /// /// Get the keys in sorted order from the file dictionary /// /// List of the keys in sorted order [JsonIgnore, XmlIgnore] public List SortedKeys { get { List keys = [.. _items.Keys]; keys.Sort(new NaturalComparer()); return keys; } } #endregion #region Statistics /// /// DAT statistics /// [JsonIgnore, XmlIgnore] public DatStatistics DatStatistics { get; } = new DatStatistics(); #endregion #endregion #region Constructors /// /// Generic constructor /// public ItemDictionary() { bucketedBy = ItemKey.NULL; mergedBy = DedupeType.None; _logger = new Logger(this); } #endregion #region Accessors /// /// Passthrough to access the file dictionary /// /// Key in the dictionary to reference public List? this[string key] { get { // Explicit lock for some weird corner cases lock (key) { // Ensure the key exists EnsureKey(key); // Now return the value return _items[key]; } } set { Remove(key); if (value == null) _items[key] = null; else Add(key, value); } } /// /// Add a value to the file dictionary /// /// Key in the dictionary to add to /// Value to add to the dictionary public void Add(string key, DatItem value) { // Explicit lock for some weird corner cases lock (key) { // Ensure the key exists EnsureKey(key); // If item is null, don't add it if (value == null) return; // Now add the value _items[key]!.Add(value); // Now update the statistics DatStatistics.AddItemStatistics(value); } } /// /// Add a range of values to the file dictionary /// /// Key in the dictionary to add to /// Value to add to the dictionary public void Add(string key, List? value) { // Explicit lock for some weird corner cases lock (key) { // If the value is null or empty, just return if (value == null || value.Count == 0) return; // Ensure the key exists EnsureKey(key); // Now add the value _items[key]!.AddRange(value); // Now update the statistics foreach (DatItem item in value) { DatStatistics.AddItemStatistics(item); } } } /// /// Add a DatItem to the dictionary after checking /// /// Item data to check against /// True to only add item statistics while parsing, false otherwise /// The key for the item public string AddItem(DatItem item, bool statsOnly) { string key; // If we have a Disk, Media, or Rom, clean the hash data if (item is Disk disk) { // If the file has aboslutely no hashes, skip and log if (disk.GetStringFieldValue(Models.Metadata.Disk.StatusKey).AsEnumValue() != ItemStatus.Nodump && string.IsNullOrEmpty(disk.GetStringFieldValue(Models.Metadata.Disk.MD5Key)) && string.IsNullOrEmpty(disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key))) { _logger.Verbose($"Incomplete entry for '{disk.GetName()}' will be output as nodump"); disk.SetFieldValue(Models.Metadata.Disk.StatusKey, ItemStatus.Nodump.AsStringValue()); } item = disk; } if (item is Media media) { // If the file has aboslutely no hashes, skip and log if (string.IsNullOrEmpty(media.GetStringFieldValue(Models.Metadata.Media.MD5Key)) && string.IsNullOrEmpty(media.GetStringFieldValue(Models.Metadata.Media.SHA1Key)) && string.IsNullOrEmpty(media.GetStringFieldValue(Models.Metadata.Media.SHA256Key)) && string.IsNullOrEmpty(media.GetStringFieldValue(Models.Metadata.Media.SpamSumKey))) { _logger.Verbose($"Incomplete entry for '{media.GetName()}' will be output as nodump"); } item = media; } else if (item is Rom rom) { long? size = rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey); // If we have the case where there is SHA-1 and nothing else, we don't fill in any other part of the data if (size == null && !rom.HasHashes()) { // No-op, just catch it so it doesn't go further //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Entry with only SHA-1 found - '{rom.GetName()}'"); } // 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 else if ((size == 0 || size == null) && (string.IsNullOrEmpty(rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey)) || rom.HasZeroHash())) { rom.SetFieldValue(Models.Metadata.Rom.SizeKey, Constants.SizeZero.ToString()); rom.SetFieldValue(Models.Metadata.Rom.CRCKey, ZeroHash.CRC32Str); rom.SetFieldValue(Models.Metadata.Rom.MD2Key, null); // ZeroHash.GetString(HashType.MD2) rom.SetFieldValue(Models.Metadata.Rom.MD4Key, null); // ZeroHash.GetString(HashType.MD4) rom.SetFieldValue(Models.Metadata.Rom.MD5Key, ZeroHash.MD5Str); rom.SetFieldValue(Models.Metadata.Rom.SHA1Key, ZeroHash.SHA1Str); rom.SetFieldValue(Models.Metadata.Rom.SHA256Key, null); // ZeroHash.SHA256Str; rom.SetFieldValue(Models.Metadata.Rom.SHA384Key, null); // ZeroHash.SHA384Str; rom.SetFieldValue(Models.Metadata.Rom.SHA512Key, null); // ZeroHash.SHA512Str; rom.SetFieldValue(Models.Metadata.Rom.SpamSumKey, null); // ZeroHash.SpamSumStr; } // If the file has no size and it's not the above case, skip and log else if (rom.GetStringFieldValue(Models.Metadata.Rom.StatusKey).AsEnumValue() != ItemStatus.Nodump && (size == 0 || size == null)) { //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Incomplete entry for '{rom.GetName()}' will be output as nodump"); rom.SetFieldValue(Models.Metadata.Rom.StatusKey, ItemStatus.Nodump.AsStringValue()); } // If the file has a size but aboslutely no hashes, skip and log else if (rom.GetStringFieldValue(Models.Metadata.Rom.StatusKey).AsEnumValue() != ItemStatus.Nodump && size != null && size > 0 && !rom.HasHashes()) { //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Incomplete entry for '{rom.GetName()}' will be output as nodump"); rom.SetFieldValue(Models.Metadata.Rom.StatusKey, ItemStatus.Nodump.AsStringValue()); } item = rom; } // Get the key and add the file key = item.GetKey(ItemKey.Machine); // If only adding statistics, we add an empty key for games and then just item stats if (statsOnly) { EnsureKey(key); DatStatistics.AddItemStatistics(item); } else { Add(key, item); } return key; } /// /// Remove any keys that have null or empty values /// internal void ClearEmpty() { string[] keys = [.. Keys]; foreach (string key in keys) { #if NET40_OR_GREATER || NETCOREAPP // If the key doesn't exist, skip if (!_items.TryGetValue(key, out var value)) continue; // If the value is null, remove else if (value == null) _items.TryRemove(key, out _); // If there are no non-blank items, remove else if (value!.FindIndex(i => i != null && i is not Blank) == -1) _items.TryRemove(key, out _); #else // If the key doesn't exist, skip if (!_items.ContainsKey(key)) continue; // If the value is null, remove else if (_items[key] == null) _items.Remove(key); // If there are no non-blank items, remove else if (_items[key]!.FindIndex(i => i != null && i is not Blank) == -1) _items.Remove(key); #endif } } /// /// Remove all items marked for removal /// internal void ClearMarked() { string[] keys = [.. Keys]; foreach (string key in keys) { // Skip invalid item lists List? oldItemList = this[key]; if (oldItemList == null) return; List newItemList = oldItemList.FindAll(i => i.GetBoolFieldValue(DatItem.RemoveKey) != true); Remove(key); Add(key, newItemList); } } /// /// Get if the file dictionary contains the key /// /// Key in the dictionary to check /// True if the key exists, false otherwise public bool ContainsKey(string key) { // If the key is null, we return false since keys can't be null if (key == null) return false; // Explicit lock for some weird corner cases lock (key) { return _items.ContainsKey(key); } } /// /// Get if the file dictionary contains the key and value /// /// Key in the dictionary to check /// Value in the dictionary to check /// True if the key exists, false otherwise public bool Contains(string key, DatItem value) { // If the key is null, we return false since keys can't be null if (key == null) return false; // Explicit lock for some weird corner cases lock (key) { #if NET40_OR_GREATER || NETCOREAPP if (_items.TryGetValue(key, out var list) && list != null) return list.Contains(value); #else if (_items.ContainsKey(key) && _items[key] != null) return _items[key]!.Contains(value); #endif } return false; } /// /// Ensure the key exists in the items dictionary /// /// Key to ensure public void EnsureKey(string key) { // If the key is missing from the dictionary, add it if (!_items.ContainsKey(key)) #if NET40_OR_GREATER || NETCOREAPP _items.TryAdd(key, []); #else _items[key] = []; #endif } /// /// Get the items associated with a bucket name /// public List GetItemsForBucket(string bucketName, bool filter = false) { if (!_items.ContainsKey(bucketName)) return []; var items = _items[bucketName]; if (items == null) return []; var datItems = new List(); foreach (DatItem item in items) { if (!filter || item.GetBoolFieldValue(DatItem.RemoveKey) != true) datItems.Add(item); } return datItems; } /// /// Remove a key from the file dictionary if it exists /// /// Key in the dictionary to remove public bool Remove(string key) { // Explicit lock for some weird corner cases lock (key) { // If the key doesn't exist, return if (!ContainsKey(key) || _items[key] == null) return false; // Remove the statistics first foreach (DatItem item in _items[key]!) { DatStatistics.RemoveItemStatistics(item); } // Remove the key from the dictionary #if NET40_OR_GREATER || NETCOREAPP return _items.TryRemove(key, out _); #else return _items.Remove(key); #endif } } /// /// Remove the first instance of a value from the file dictionary if it exists /// /// Key in the dictionary to remove from /// Value to remove from the dictionary public bool Remove(string key, DatItem value) { // Explicit lock for some weird corner cases lock (key) { // If the key and value doesn't exist, return if (!Contains(key, value) || _items[key] == null) return false; // Remove the statistics first DatStatistics.RemoveItemStatistics(value); return _items[key]!.Remove(value); } } /// /// Reset a key from the file dictionary if it exists /// /// Key in the dictionary to reset public bool Reset(string key) { // If the key doesn't exist, return if (!ContainsKey(key) || _items[key] == null) return false; // Remove the statistics first foreach (DatItem item in _items[key]!) { DatStatistics.RemoveItemStatistics(item); } // Remove the key from the dictionary _items[key] = []; return true; } /// /// Override the internal ItemKey value /// /// public void SetBucketedBy(ItemKey newBucket) { bucketedBy = newBucket; } #endregion #region Bucketing /// /// Take the arbitrarily bucketed Files Dictionary and convert to one bucketed by a user-defined method /// /// ItemKey enum representing how to bucket 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 internal void BucketBy(ItemKey bucketBy, DedupeType dedupeType, 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; // If the sorted type isn't the same, we want to sort the dictionary accordingly if (bucketedBy != bucketBy && bucketBy != ItemKey.NULL) { _logger.User($"Organizing roms by {bucketBy}"); PerformBucketing(bucketBy, lower, norename); } // If the merge type isn't the same, we want to merge the dictionary accordingly if (mergedBy != dedupeType) { _logger.User($"Deduping roms by {dedupeType}"); PerformDeduplication(bucketBy, dedupeType); } // If the merge type is the same, we want to sort the dictionary to be consistent else { _logger.User($"Sorting roms by {bucketBy}"); PerformSorting(); } } /// /// List all duplicates found in a DAT based on a DatItem /// /// Item to try to match /// True if the DAT is already sorted accordingly, false otherwise (default) /// List of matched DatItem objects internal List GetDuplicates(DatItem datItem, bool sorted = false) { List output = []; // Check for an empty rom list first if (DatStatistics.TotalCount == 0) return output; // We want to get the proper key for the DatItem string key = SortAndGetKey(datItem, sorted); // If the key doesn't exist, return the empty list if (!ContainsKey(key)) return output; // Try to find duplicates List? roms = this[key]; if (roms == null) return output; List left = []; for (int i = 0; i < roms.Count; i++) { DatItem other = roms[i]; if (other.GetBoolFieldValue(DatItem.RemoveKey) == true) continue; if (datItem.Equals(other)) { other.SetFieldValue(DatItem.RemoveKey, true); output.Add(other); } else { left.Add(other); } } // Add back all roms with the proper flags Remove(key); Add(key, output); Add(key, left); return output; } /// /// Check if a DAT contains the given DatItem /// /// Item to try to match /// True if the DAT is already sorted accordingly, false otherwise (default) /// True if it contains the rom, false otherwise internal bool HasDuplicates(DatItem datItem, bool sorted = false) { // Check for an empty rom list first if (DatStatistics.TotalCount == 0) return false; // We want to get the proper key for the DatItem string key = SortAndGetKey(datItem, sorted); // If the key doesn't exist, return the empty list if (!ContainsKey(key)) return false; // Try to find duplicates List? roms = this[key]; if (roms == null) return false; return roms.FindIndex(r => datItem.Equals(r)) > -1; } /// /// Get the highest-order Field value that represents the statistics /// private ItemKey GetBestAvailable() { // Get the required counts long diskCount = DatStatistics.GetItemCount(ItemType.Disk); long mediaCount = DatStatistics.GetItemCount(ItemType.Media); long romCount = DatStatistics.GetItemCount(ItemType.Rom); long nodumpCount = DatStatistics.GetStatusCount(ItemStatus.Nodump); // If all items are supposed to have a SHA-512, we bucket by that if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA512)) return ItemKey.SHA512; // If all items are supposed to have a SHA-384, we bucket by that else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA384)) return ItemKey.SHA384; // If all items are supposed to have a SHA-256, we bucket by that else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA256)) return ItemKey.SHA256; // If all items are supposed to have a SHA-1, we bucket by that else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA1)) return ItemKey.SHA1; // If all items are supposed to have a MD5, we bucket by that else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD5)) return ItemKey.MD5; // If all items are supposed to have a MD4, we bucket by that else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD4)) return ItemKey.MD4; // If all items are supposed to have a MD2, we bucket by that else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD2)) return ItemKey.MD2; // Otherwise, we bucket by CRC else return ItemKey.CRC; } /// /// Perform bucketing based on the item key provided /// /// ItemKey enum representing how to bucket the individual items /// True if the key should be lowercased, false otherwise /// True if games should only be compared on game and file name, false if system and source are counted private void PerformBucketing(ItemKey bucketBy, bool lower, bool norename) { // Set the sorted type bucketedBy = bucketBy; // Reset the merged type since this might change the merge mergedBy = DedupeType.None; // First do the initial sort of all of the roms inplace List oldkeys = [.. Keys]; #if NET452_OR_GREATER || NETCOREAPP Parallel.For(0, oldkeys.Count, Core.Globals.ParallelOptions, k => #elif NET40_OR_GREATER Parallel.For(0, oldkeys.Count, k => #else for (int k = 0; k < oldkeys.Count; k++) #endif { string key = oldkeys[k]; if (this[key] == null) Remove(key); // Now add each of the roms to their respective keys for (int i = 0; i < this[key]!.Count; i++) { DatItem item = this[key]![i]; if (item == null) continue; // We want to get the key most appropriate for the given sorting type string newkey = item.GetKey(bucketBy, lower, norename); // If the key is different, move the item to the new key if (newkey != key) { Add(newkey, item); Remove(key, item); i--; // This make sure that the pointer stays on the correct since one was removed } } // If the key is now empty, remove it if (this[key]!.Count == 0) Remove(key); #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } /// /// Perform deduplication based on the deduplication type provided /// /// ItemKey enum representing how to bucket the individual items /// Dedupe type that should be used private void PerformDeduplication(ItemKey bucketBy, DedupeType dedupeType) { // Set the sorted type mergedBy = dedupeType; List keys = [.. Keys]; #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(keys, Core.Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(keys, key => #else foreach (var key in keys) #endif { // Get the possibly unsorted list List? sortedlist = this[key]; if (sortedlist == null) #if NET40_OR_GREATER || NETCOREAPP return; #else continue; #endif // Sort the list of items to be consistent DatFileTool.Sort(ref sortedlist, false); // If we're merging the roms, do so if (dedupeType == DedupeType.Full || (dedupeType == DedupeType.Game && bucketBy == ItemKey.Machine)) sortedlist = DatFileTool.Merge(sortedlist); // Add the list back to the dictionary Reset(key); Add(key, sortedlist); #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } /// /// Perform inplace sorting of the dictionary /// private void PerformSorting() { List keys = [.. Keys]; #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(keys, Core.Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(keys, key => #else foreach (var key in keys) #endif { // Get the possibly unsorted list List? sortedlist = this[key]; // Sort the list of items to be consistent if (sortedlist != null) DatFileTool.Sort(ref sortedlist, false); #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } /// /// Sort the input DAT and get the key to be used by the item /// /// Item to try to match /// True if the DAT is already sorted accordingly, false otherwise (default) /// Key to try to use private string SortAndGetKey(DatItem datItem, bool sorted = false) { // If we're not already sorted, take care of it if (!sorted) BucketBy(GetBestAvailable(), DedupeType.None); // Now that we have the sorted type, we get the proper key return datItem.GetKey(bucketedBy); } #endregion // TODO: All internal, can this be put into a better location? #region Filtering /// /// Execute all filters in a filter runner on the items in the dictionary /// /// Preconfigured filter runner to use internal void ExecuteFilters(FilterRunner filterRunner) { List keys = [.. Keys]; #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(keys, Core.Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(keys, key => #else foreach (var key in keys) #endif { List? items = this[key]; if (items == null) #if NET40_OR_GREATER || NETCOREAPP return; #else continue; #endif // Filter all items in the current key List newItems = []; foreach (var item in items) { if (item.PassesFilter(filterRunner)) newItems.Add(item); } // Set the value in the key to the new set this[key] = newItems; #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } /// /// Use game descriptions as names, updating cloneof/romof/sampleof /// /// True if the error that is thrown should be thrown back to the caller, false otherwise internal void MachineDescriptionToName(bool throwOnError = false) { try { // First we want to get a mapping for all games to description var mapping = CreateMachineToDescriptionMapping(); // Now we loop through every item and update accordingly UpdateMachineNamesFromDescriptions(mapping); } catch (Exception ex) when (!throwOnError) { _logger.Warning(ex.ToString()); } } /// /// Ensure that all roms are in their own game (or at least try to ensure) /// internal void SetOneRomPerGame() { // For each rom, we want to update the game to be "/" #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(Keys, Core.Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(Keys, key => #else foreach (var key in Keys) #endif { var items = this[key]; if (items == null) #if NET40_OR_GREATER || NETCOREAPP return; #else continue; #endif for (int i = 0; i < items.Count; i++) { SetOneRomPerGame(items[i]); } #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } /// /// Filter a DAT using 1G1R logic given an ordered set of regions /// /// List of regions in order of priority /// /// In the most technical sense, the way that the region list is being used does not /// confine its values to be just regions. Since it's essentially acting like a /// specialized version of the machine name filter, anything that is usually encapsulated /// in parenthesis would be matched on, including disc numbers, languages, editions, /// and anything else commonly used. Please note that, unlike other existing 1G1R /// solutions, this does not have the ability to contain custom mappings of parent /// to clone sets based on name, nor does it have the ability to match on the /// Release DatItem type. /// internal void SetOneGamePerRegion(List regionList) { // If we have null region list, make it empty regionList ??= []; // For sake of ease, the first thing we want to do is bucket by game BucketBy(ItemKey.Machine, DedupeType.None, norename: true); // Then we want to get a mapping of all machines to parents Dictionary> parents = []; foreach (string key in Keys) { DatItem item = this[key]![0]; // Get machine information Machine? machine = item.GetFieldValue(DatItem.MachineKey); string? machineName = machine?.GetStringFieldValue(Models.Metadata.Machine.NameKey)?.ToLowerInvariant(); if (machine == null || machineName == null) continue; // Get the string values string? cloneOf = machine.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey)?.ToLowerInvariant(); string? romOf = machine.GetStringFieldValue(Models.Metadata.Machine.RomOfKey)?.ToLowerInvariant(); // Match on CloneOf first if (!string.IsNullOrEmpty(cloneOf)) { if (!parents.ContainsKey(cloneOf!)) parents.Add(cloneOf!, []); parents[cloneOf!].Add(machineName); } // Then by RomOf else if (!string.IsNullOrEmpty(romOf)) { if (!parents.ContainsKey(romOf!)) parents.Add(romOf!, []); parents[romOf!].Add(machineName); } // Otherwise, treat it as a parent else { if (!parents.ContainsKey(machineName)) parents.Add(machineName, []); parents[machineName].Add(machineName); } } // Once we have the full list of mappings, filter out games to keep foreach (string key in parents.Keys) { // Find the first machine that matches the regions in order, if possible string? machine = default; foreach (string region in regionList) { machine = parents[key].Find(m => Regex.IsMatch(m, @"\(.*" + region + @".*\)", RegexOptions.IgnoreCase)); if (machine != default) break; } // If we didn't get a match, use the parent if (machine == default) machine = key; // Remove the key from the list parents[key].Remove(machine); // Remove the rest of the items from this key parents[key].ForEach(k => Remove(k)); } // Finally, strip out the parent tags RemoveTagsFromChild(); } /// /// Strip the dates from the beginning of scene-style set names /// internal void StripSceneDatesFromItems() { // Set the regex pattern to use string pattern = @"([0-9]{2}\.[0-9]{2}\.[0-9]{2}-)(.*?-.*?)"; // Now process all of the roms #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(Keys, Core.Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(Keys, key => #else foreach (var key in Keys) #endif { var items = this[key]; if (items == null) #if NET40_OR_GREATER || NETCOREAPP return; #else continue; #endif for (int j = 0; j < items.Count; j++) { DatItem item = items[j]; if (Regex.IsMatch(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!, pattern)) item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.NameKey, Regex.Replace(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!, pattern, "$2")); if (Regex.IsMatch(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey)!, pattern)) item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.DescriptionKey, Regex.Replace(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey)!, pattern, "$2")); items[j] = item; } Remove(key); Add(key, items); #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } /// /// Create machine to description mapping dictionary /// private IDictionary CreateMachineToDescriptionMapping() { #if NET40_OR_GREATER || NETCOREAPP ConcurrentDictionary mapping = new(); #else Dictionary mapping = []; #endif #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(Keys, Core.Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(Keys, key => #else foreach (var key in Keys) #endif { var items = this[key]; if (items == null) #if NET40_OR_GREATER || NETCOREAPP return; #else continue; #endif foreach (DatItem item in items) { // If the key mapping doesn't exist, add it #if NET40_OR_GREATER || NETCOREAPP mapping.TryAdd(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!, item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey)!.Replace('/', '_').Replace("\"", "''").Replace(":", " -")); #else mapping[item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!] = item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey)!.Replace('/', '_').Replace("\"", "''").Replace(":", " -"); #endif } #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif return mapping; } /// /// Set internal names to match One Rom Per Game (ORPG) logic /// /// DatItem to run logic on private static void SetOneRomPerGame(DatItem datItem) { // If the item name is null string? machineName = datItem.GetName(); if (machineName == null) return; // Get the current machine var machine = datItem.GetFieldValue(DatItem.MachineKey); if (machine == null) return; // Remove extensions from Rom items if (datItem is Rom) { string[] splitname = machineName.Split('.'); machineName = machine.GetStringFieldValue(Models.Metadata.Machine.NameKey) + $"/{string.Join(".", splitname, 0, splitname.Length > 1 ? splitname.Length - 1 : 1)}"; } // Strip off "Default" prefix only for ORPG if (machineName.StartsWith("Default")) machineName = machineName.Substring("Default".Length + 1); datItem.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.NameKey, machineName); datItem.SetName(Path.GetFileName(datItem.GetName())); } /// /// Update machine names from descriptions according to mappings /// private void UpdateMachineNamesFromDescriptions(IDictionary mapping) { #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(Keys, Core.Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(Keys, key => #else foreach (var key in Keys) #endif { var items = this[key]; if (items == null) #if NET40_OR_GREATER || NETCOREAPP return; #else continue; #endif List newItems = []; foreach (DatItem item in items) { // Update machine name if (!string.IsNullOrEmpty(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)) && mapping.ContainsKey(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!)) item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.NameKey, mapping[item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!]); // Update cloneof if (!string.IsNullOrEmpty(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey)) && mapping.ContainsKey(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey)!)) item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.CloneOfKey, mapping[item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey)!]); // Update romof if (!string.IsNullOrEmpty(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.RomOfKey)) && mapping.ContainsKey(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.RomOfKey)!)) item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.RomOfKey, mapping[item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.RomOfKey)!]); // Update sampleof if (!string.IsNullOrEmpty(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.SampleOfKey)) && mapping.ContainsKey(item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.SampleOfKey)!)) item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.SampleOfKey, mapping[item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.SampleOfKey)!]); // Add the new item to the output list newItems.Add(item); } // Replace the old list of roms with the new one Remove(key); Add(key, newItems); #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } #endregion // TODO: All internal, can this be put into a better location? #region Splitting /// /// Use romof tags to add roms to the children /// internal void AddRomsFromBios() { List games = [.. Keys]; games.Sort(); foreach (string game in games) { // If the game has no items in it, we want to continue var items = this[game]; if (items == null || items.Count == 0) continue; // Get the machine var machine = items[0].GetFieldValue(DatItem.MachineKey); if (machine == null) continue; // Get the bios parent string? romOf = machine.GetStringFieldValue(Models.Metadata.Machine.RomOfKey); if (string.IsNullOrEmpty(romOf)) continue; // If the parent doesn't have any items, we want to continue var parentItems = this[romOf!]; if (parentItems == null || parentItems.Count == 0) continue; // If the parent exists and has items, we copy the items from the parent to the current game DatItem copyFrom = items[0]; foreach (DatItem item in parentItems) { DatItem datItem = (DatItem)item.Clone(); datItem.CopyMachineInformation(copyFrom); if (items.FindIndex(i => i.GetName() == datItem.GetName()) == -1 && !items.Contains(datItem)) Add(game, datItem); } } } /// /// Use device_ref and optionally slotoption tags to add roms to the children /// /// True if only child device sets are touched, false for non-device sets (default) /// True if slotoptions tags are used as well, false otherwise internal bool AddRomsFromDevices(bool dev, bool useSlotOptions) { bool foundnew = false; List machines = [.. Keys]; machines.Sort(); foreach (string machine in machines) { // If the machine doesn't have items, we continue var datItems = this[machine]; if (datItems == null || datItems.Count == 0) continue; // If the machine (is/is not) a device, we want to continue if (dev ^ (datItems[0].GetFieldValue(DatItem.MachineKey)!.GetBoolFieldValue(Models.Metadata.Machine.IsDeviceKey) == true)) continue; // Get all device reference names from the current machine List deviceReferences = datItems .FindAll(i => i is DeviceRef) .ConvertAll(i => i as DeviceRef) .ConvertAll(dr => dr!.GetName()) .Distinct() .ToList(); // Get all slot option names from the current machine List slotOptions = datItems .FindAll(i => i is Slot) .ConvertAll(i => i as Slot) .FindAll(s => s!.SlotOptionsSpecified) .SelectMany(s => s!.GetFieldValue(Models.Metadata.Slot.SlotOptionKey)!) .Select(so => so.GetStringFieldValue(Models.Metadata.SlotOption.DevNameKey)) .Distinct() .ToList(); // If we're checking device references if (deviceReferences.Count > 0) { // Loop through all names and check the corresponding machines var newDeviceReferences = new HashSet(); foreach (string? deviceReference in deviceReferences) { // If the device reference is missing if (string.IsNullOrEmpty(deviceReference)) continue; // Add to the list of new device reference names var devItems = this[deviceReference!]; if (devItems == null || devItems.Count == 0) continue; newDeviceReferences.UnionWith(devItems .FindAll(i => i is DeviceRef) .ConvertAll(i => (i as DeviceRef)!.GetName()!)); // Set new machine information and add to the current machine DatItem copyFrom = datItems[0]; foreach (DatItem item in devItems) { // If the parent machine doesn't already contain this item, add it if (!datItems.Exists(i => i.GetStringFieldValue(Models.Metadata.DatItem.TypeKey) == item.GetStringFieldValue(Models.Metadata.DatItem.TypeKey) && i.GetName() == item.GetName())) { // Set that we found new items foundnew = true; // Clone the item and then add it DatItem datItem = (DatItem)item.Clone(); datItem.CopyMachineInformation(copyFrom); Add(machine, datItem); } } } // Now that every device reference is accounted for, add the new list of device references, if they don't already exist foreach (string deviceReference in newDeviceReferences) { if (!deviceReferences.Contains(deviceReference)) { var deviceRef = new DeviceRef(); deviceRef.SetName(deviceReference); datItems.Add(deviceRef); } } } // If we're checking slotoptions if (useSlotOptions && slotOptions.Count > 0) { // Loop through all names and check the corresponding machines var newSlotOptions = new HashSet(); foreach (string? slotOption in slotOptions) { // If the slot option is missing if (string.IsNullOrEmpty(slotOption)) // If the machine doesn't exist then we continue continue; // Add to the list of new slot option names var slotItems = this[slotOption!]; if (slotItems == null || slotItems.Count == 0) continue; newSlotOptions.UnionWith(slotItems .FindAll(i => i is Slot) .FindAll(s => (s as Slot)!.SlotOptionsSpecified) .SelectMany(s => (s as Slot)!.GetFieldValue(Models.Metadata.Slot.SlotOptionKey)!) .Select(o => o.GetStringFieldValue(Models.Metadata.SlotOption.DevNameKey)!)); // Set new machine information and add to the current machine DatItem copyFrom = datItems[0]; foreach (DatItem item in slotItems) { // If the parent machine doesn't already contain this item, add it if (!datItems.Exists(i => i.GetStringFieldValue(Models.Metadata.DatItem.TypeKey) == item.GetStringFieldValue(Models.Metadata.DatItem.TypeKey) && i.GetName() == item.GetName())) { // Set that we found new items foundnew = true; // Clone the item and then add it DatItem datItem = (DatItem)item.Clone(); datItem.CopyMachineInformation(copyFrom); Add(machine, datItem); } } } // Now that every device is accounted for, add the new list of slot options, if they don't already exist foreach (string slotOption in newSlotOptions) { if (!slotOptions.Contains(slotOption)) { var slotOptionItem = new SlotOption(); slotOptionItem.SetFieldValue(Models.Metadata.SlotOption.DevNameKey, slotOption); var slotItem = new Slot(); slotItem.SetFieldValue(Models.Metadata.Slot.SlotOptionKey, [slotOptionItem]); datItems.Add(slotItem); } } } } return foundnew; } /// /// Use cloneof tags to add roms to the children, setting the new romof tag in the process /// internal void AddRomsFromParent() { List games = [.. Keys]; games.Sort(); foreach (string game in games) { // If the game has no items in it, we want to continue var items = this[game]; if (items == null || items.Count == 0) continue; // Get the machine var machine = items[0].GetFieldValue(DatItem.MachineKey); if (machine == null) continue; // Get the clone parent string? cloneOf = machine.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey); if (string.IsNullOrEmpty(cloneOf)) continue; // If the parent doesn't have any items, we want to continue var parentItems = this[cloneOf!]; if (parentItems == null || parentItems.Count == 0) continue; // If the parent exists and has items, we copy the items from the parent to the current game DatItem copyFrom = items[0]; foreach (DatItem item in parentItems) { DatItem datItem = (DatItem)item.Clone(); datItem.CopyMachineInformation(copyFrom); if (items.FindIndex(i => string.Equals(i.GetName(), datItem.GetName(), StringComparison.OrdinalIgnoreCase)) == -1 && !items.Contains(datItem)) { Add(game, datItem); } } // Now we want to get the parent romof tag and put it in each of the items items = this[game]; string? romof = this[cloneOf!]![0].GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.RomOfKey); foreach (DatItem item in items!) { item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.RomOfKey, romof); } } } /// /// Use cloneof tags to add roms to the parents, removing the child sets in the process /// /// True to add DatItems to subfolder of parent (not including Disk), false otherwise /// True to skip checking for duplicate ROMs in parent, false otherwise internal void AddRomsFromChildren(bool subfolder, bool skipDedup) { List games = [.. Keys]; games.Sort(); foreach (string game in games) { // If the game has no items in it, we want to continue var items = this[game]; if (items == null || items.Count == 0) continue; // Get the machine var machine = items[0].GetFieldValue(DatItem.MachineKey); if (machine == null) continue; // Get the clone parent string? cloneOf = machine.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey); if (string.IsNullOrEmpty(cloneOf)) continue; // Get the parent items var parentItems = this[cloneOf!]; // Otherwise, move the items from the current game to a subfolder of the parent game DatItem copyFrom; if (parentItems == null || parentItems.Count == 0) { copyFrom = new Rom(); copyFrom.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.NameKey, cloneOf); copyFrom.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.DescriptionKey, cloneOf); } else { copyFrom = parentItems[0]; } items = this[game]; foreach (DatItem item in items!) { // Special disk handling if (item is Disk disk) { string? mergeTag = disk.GetStringFieldValue(Models.Metadata.Disk.MergeKey); // If the merge tag exists and the parent already contains it, skip if (mergeTag != null && this[cloneOf!]! .FindAll(i => i is Disk) .ConvertAll(i => (i as Disk)!.GetName()).Contains(mergeTag)) { continue; } // If the merge tag exists but the parent doesn't contain it, add to parent else if (mergeTag != null && !this[cloneOf!]! .FindAll(i => i is Disk) .ConvertAll(i => (i as Disk)!.GetName()).Contains(mergeTag)) { disk.CopyMachineInformation(copyFrom); Add(cloneOf!, disk); } // If there is no merge tag, add to parent else if (mergeTag == null) { disk.CopyMachineInformation(copyFrom); Add(cloneOf!, disk); } } // Special rom handling else if (item is Rom rom) { // If the merge tag exists and the parent already contains it, skip if (rom.GetStringFieldValue(Models.Metadata.Rom.MergeKey) != null && this[cloneOf!]! .FindAll(i => i is Rom) .ConvertAll(i => (i as Rom)!.GetName()) .Contains(rom.GetStringFieldValue(Models.Metadata.Rom.MergeKey))) { continue; } // If the merge tag exists but the parent doesn't contain it, add to subfolder of parent else if (rom.GetStringFieldValue(Models.Metadata.Rom.MergeKey) != null && !this[cloneOf!]! .FindAll(i => i is Rom) .ConvertAll(i => (i as Rom)!.GetName()) .Contains(rom.GetStringFieldValue(Models.Metadata.Rom.MergeKey))) { if (subfolder) rom.SetName($"{rom.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)}\\{rom.GetName()}"); rom.CopyMachineInformation(copyFrom); Add(cloneOf!, rom); } // If the parent doesn't already contain this item, add to subfolder of parent else if (!this[cloneOf!]!.Contains(item) || skipDedup) { if (subfolder) rom.SetName($"{item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)}\\{rom.GetName()}"); rom.CopyMachineInformation(copyFrom); Add(cloneOf!, rom); } } // All other that would be missing to subfolder of parent else if (!this[cloneOf!]!.Contains(item)) { if (subfolder) item.SetName($"{item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)}\\{item.GetName()}"); item.CopyMachineInformation(copyFrom); Add(cloneOf!, item); } } // Then, remove the old game so it's not picked up by the writer Remove(game); } } /// /// Remove all BIOS and device sets /// internal void RemoveBiosAndDeviceSets() { List games = [.. Keys]; games.Sort(); foreach (string game in games) { // If the game has no items in it, we want to continue var items = this[game]; if (items == null || items.Count == 0) continue; // Get the machine var machine = items[0].GetFieldValue(DatItem.MachineKey); if (machine == null) continue; // Remove flagged items if ((machine.GetBoolFieldValue(Models.Metadata.Machine.IsBiosKey) == true) || (machine.GetBoolFieldValue(Models.Metadata.Machine.IsDeviceKey) == true)) { Remove(game); } } } /// /// Use romof tags to remove bios roms from children /// /// True if only child Bios sets are touched, false for non-bios sets internal void RemoveBiosRomsFromChild(bool bios) { // Loop through the romof tags List games = [.. Keys]; games.Sort(); foreach (string game in games) { // If the game has no items in it, we want to continue var items = this[game]; if (items == null || items.Count == 0) continue; // Get the machine var machine = items[0].GetFieldValue(DatItem.MachineKey); if (machine == null) continue; // If the game (is/is not) a bios, we want to continue if (bios ^ (machine.GetBoolFieldValue(Models.Metadata.Machine.IsBiosKey) == true)) continue; // Get the bios parent string? romOf = machine.GetStringFieldValue(Models.Metadata.Machine.RomOfKey); if (string.IsNullOrEmpty(romOf)) continue; // If the parent doesn't have any items, we want to continue var parentItems = this[romOf!]; if (parentItems == null || parentItems.Count == 0) continue; // If the parent exists and has items, we remove the items that are in the parent from the current game foreach (DatItem item in parentItems) { DatItem datItem = (DatItem)item.Clone(); while (items.Contains(datItem)) { Remove(game, datItem); } } } } /// /// Use cloneof tags to remove roms from the children /// internal void RemoveRomsFromChild() { List games = [.. Keys]; games.Sort(); foreach (string game in games) { // If the game has no items in it, we want to continue var items = this[game]; if (items == null || items.Count == 0) continue; // Get the machine var machine = items[0].GetFieldValue(DatItem.MachineKey); if (machine == null) continue; // Get the clone parent string? cloneOf = machine.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey); if (string.IsNullOrEmpty(cloneOf)) continue; // If the parent doesn't have any items, we want to continue var parentItems = this[cloneOf!]; if (parentItems == null || parentItems.Count == 0) continue; // If the parent exists and has items, we remove the parent items from the current game foreach (DatItem item in parentItems!) { DatItem datItem = (DatItem)item.Clone(); while (items.Contains(datItem)) { Remove(game, datItem); } } // Now we want to get the parent romof tag and put it in each of the remaining items items = this[game]; string? romof = this[cloneOf!]![0].GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.RomOfKey); foreach (DatItem item in items!) { item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.RomOfKey, romof); } } } /// /// Remove all romof and cloneof tags from all games /// internal void RemoveTagsFromChild() { List games = [.. Keys]; games.Sort(); foreach (string game in games) { // If the game has no items in it, we want to continue var items = this[game]; if (items == null || items.Count == 0) continue; foreach (DatItem item in items) { item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.CloneOfKey, null); item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.RomOfKey, null); item.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.SampleOfKey, null); } } } #endregion #region Statistics /// /// Recalculate the statistics for the Dat /// public void RecalculateStats() { // Wipe out any stats already there DatStatistics.ResetStatistics(); // If we have a blank Dat in any way, return if (_items == null) return; // Loop through and add foreach (string key in _items.Keys) { List? datItems = _items[key]; if (datItems == null) continue; foreach (DatItem item in datItems) { DatStatistics.AddItemStatistics(item); } } } #endregion #region IDictionary Implementations public ICollection?> Values => ((IDictionary?>)_items).Values; public int Count => ((ICollection?>>)_items).Count; public bool IsReadOnly => ((ICollection?>>)_items).IsReadOnly; public bool TryGetValue(string key, out List? value) { return ((IDictionary?>)_items).TryGetValue(key, out value); } public void Add(KeyValuePair?> item) { ((ICollection?>>)_items).Add(item); } public void Clear() { ((ICollection?>>)_items).Clear(); } public bool Contains(KeyValuePair?> item) { return ((ICollection?>>)_items).Contains(item); } public void CopyTo(KeyValuePair?>[] array, int arrayIndex) { ((ICollection?>>)_items).CopyTo(array, arrayIndex); } public bool Remove(KeyValuePair?> item) { return ((ICollection?>>)_items).Remove(item); } public IEnumerator?>> GetEnumerator() { return ((IEnumerable?>>)_items).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } #endregion } }