using System.Collections; #if NET40_OR_GREATER || NETCOREAPP using System.Collections.Concurrent; #endif using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; #if NET40_OR_GREATER || NETCOREAPP using System.Threading.Tasks; #endif using System.Xml.Serialization; using Newtonsoft.Json; using SabreTools.Core; using SabreTools.Core.Tools; using SabreTools.DatItems; using SabreTools.DatItems.Formats; using SabreTools.Hashing; using SabreTools.Logging; using SabreTools.Matching; namespace SabreTools.DatFiles { /// /// Item dictionary with statistics, bucketing, and sorting /// /// /// TODO: Make this into a database model instead of just an in-memory object /// This will help handle extremely large sets /// [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 /// /// Lock for statistics calculation /// private readonly object statsLock = new(); /// /// 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 { var keys = items.Keys.ToList(); keys.Sort(new NaturalComparer()); return keys; } } #endregion #region Statistics /// /// Overall item count /// [JsonIgnore, XmlIgnore] public long TotalCount { get; private set; } = 0; /// /// Number of items for each item type /// [JsonIgnore, XmlIgnore] public Dictionary ItemCounts { get; private set; } = []; /// /// Number of machines /// /// Special count only used by statistics output [JsonIgnore, XmlIgnore] public long GameCount { get; set; } = 0; /// /// Total uncompressed size /// [JsonIgnore, XmlIgnore] public long TotalSize { get; private set; } = 0; /// /// Number of items for each hash type /// [JsonIgnore, XmlIgnore] public Dictionary HashCounts { get; private set; } = []; /// /// Number of items for each item status /// [JsonIgnore, XmlIgnore] public Dictionary StatusCounts { get; private set; } = []; /// /// Number of items with the remove flag /// [JsonIgnore, XmlIgnore] public long RemovedCount { get; private set; } = 0; #endregion #endregion #region Accessors /// /// Passthrough to access the file dictionary /// /// Key in the dictionary to reference public ConcurrentList? 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 AddRange(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 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, ConcurrentList? value) { AddRange(key, value); } /// /// Add to the statistics given a DatItem /// /// Item to add info from public void AddItemStatistics(DatItem item) { lock (statsLock) { // No matter what the item is, we increment the count TotalCount++; // Increment removal count if (item.GetFieldValue(DatItem.RemoveKey) == true) RemovedCount++; // Increment the item count for the type AddItemCount(item.GetFieldValue(Models.Metadata.DatItem.TypeKey)); // Some item types require special processing switch (item) { case Disk disk: if (disk.GetFieldValue(Models.Metadata.Disk.StatusKey) != ItemStatus.Nodump) { AddHashCount(HashType.MD5, string.IsNullOrEmpty(disk.GetFieldValue(Models.Metadata.Disk.MD5Key)) ? 0 : 1); AddHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.GetFieldValue(Models.Metadata.Disk.SHA1Key)) ? 0 : 1); } AddStatusCount(ItemStatus.BadDump, disk.GetFieldValue(Models.Metadata.Disk.StatusKey) == ItemStatus.BadDump ? 1 : 0); AddStatusCount(ItemStatus.Good, disk.GetFieldValue(Models.Metadata.Disk.StatusKey) == ItemStatus.Good ? 1 : 0); AddStatusCount(ItemStatus.Nodump, disk.GetFieldValue(Models.Metadata.Disk.StatusKey) == ItemStatus.Nodump ? 1 : 0); AddStatusCount(ItemStatus.Verified, disk.GetFieldValue(Models.Metadata.Disk.StatusKey) == ItemStatus.Verified ? 1 : 0); break; case Media media: AddHashCount(HashType.MD5, string.IsNullOrEmpty(media.GetFieldValue(Models.Metadata.Media.MD5Key)) ? 0 : 1); AddHashCount(HashType.SHA1, string.IsNullOrEmpty(media.GetFieldValue(Models.Metadata.Media.SHA1Key)) ? 0 : 1); AddHashCount(HashType.SHA256, string.IsNullOrEmpty(media.GetFieldValue(Models.Metadata.Media.SHA256Key)) ? 0 : 1); AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.GetFieldValue(Models.Metadata.Media.SpamSumKey)) ? 0 : 1); break; case Rom rom: if (rom.GetFieldValue(Models.Metadata.Rom.StatusKey) != ItemStatus.Nodump) { TotalSize += NumberHelper.ConvertToInt64(rom.GetFieldValue(Models.Metadata.Rom.SizeKey)) ?? 0; AddHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.CRCKey)) ? 0 : 1); AddHashCount(HashType.MD5, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.MD5Key)) ? 0 : 1); AddHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SHA1Key)) ? 0 : 1); AddHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SHA256Key)) ? 0 : 1); AddHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SHA384Key)) ? 0 : 1); AddHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SHA512Key)) ? 0 : 1); AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SpamSumKey)) ? 0 : 1); } AddStatusCount(ItemStatus.BadDump, rom.GetFieldValue(Models.Metadata.Rom.StatusKey) == ItemStatus.BadDump ? 1 : 0); AddStatusCount(ItemStatus.Good, rom.GetFieldValue(Models.Metadata.Rom.StatusKey) == ItemStatus.Good ? 1 : 0); AddStatusCount(ItemStatus.Nodump, rom.GetFieldValue(Models.Metadata.Rom.StatusKey) == ItemStatus.Nodump ? 1 : 0); AddStatusCount(ItemStatus.Verified, rom.GetFieldValue(Models.Metadata.Rom.StatusKey) == ItemStatus.Verified ? 1 : 0); break; } } } /// /// Add a range of values to the file dictionary /// /// Key in the dictionary to add to /// Value to add to the dictionary public void AddRange(string key, ConcurrentList? value) { // Explicit lock for some weird corner cases lock (key) { // If the value is null or empty, just return if (value == null || !value.Any()) return; // Ensure the key exists EnsureKey(key); // Now add the value items[key]!.AddRange(value); // Now update the statistics foreach (DatItem item in value) { AddItemStatistics(item); } } } /// /// Add statistics from another DatStats object /// /// DatStats object to add from public void AddStatistics(ItemDictionary stats) { TotalCount += stats.Count; // Loop through and add stats for all items foreach (var itemCountKvp in stats.ItemCounts) { AddItemCount(itemCountKvp.Key, itemCountKvp.Value); } GameCount += stats.GameCount; TotalSize += stats.TotalSize; // Individual hash counts foreach (var hashCountKvp in stats.HashCounts) { AddHashCount(hashCountKvp.Key, hashCountKvp.Value); } // Individual status counts foreach (var statusCountKvp in stats.StatusCounts) { AddStatusCount(statusCountKvp.Key, statusCountKvp.Value); } RemovedCount += stats.RemovedCount; } /// /// 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 (items.ContainsKey(key) && items[key] != null) return items[key]!.Contains(value); } 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 a list of filtered items for a given key /// /// Key in the dictionary to retrieve public ConcurrentList FilteredItems(string key) { lock (key) { // Get the list, if possible ConcurrentList? fi = items[key]; if (fi == null) return []; // Filter the list return fi.Where(i => i != null) .Where(i => i.GetFieldValue(DatItem.RemoveKey) != true) .Where(i => i.GetFieldValue(DatItem.MachineKey) != null) .ToConcurrentList(); } } /// /// 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]!) { 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 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]!) { RemoveItemStatistics(item); } // Remove the key from the dictionary items[key] = []; return true; } /// /// Override the internal ItemKey value /// /// public void SetBucketedBy(ItemKey newBucket) { bucketedBy = newBucket; } /// /// Remove from the statistics given a DatItem /// /// Item to remove info for public void RemoveItemStatistics(DatItem item) { // If we have a null item, we can't do anything if (item == null) return; lock (statsLock) { // No matter what the item is, we decrease the count TotalCount--; // Decrement removal count if (item.GetFieldValue(DatItem.RemoveKey) == true) RemovedCount--; // Decrement the item count for the type RemoveItemCount(item.GetFieldValue(Models.Metadata.DatItem.TypeKey)); // Some item types require special processing switch (item) { case Disk disk: if (disk.GetFieldValue(Models.Metadata.Disk.StatusKey) != ItemStatus.Nodump) { RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(disk.GetFieldValue(Models.Metadata.Disk.MD5Key)) ? 0 : 1); RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.GetFieldValue(Models.Metadata.Disk.SHA1Key)) ? 0 : 1); } RemoveStatusCount(ItemStatus.BadDump, disk.GetFieldValue(Models.Metadata.Disk.StatusKey) == ItemStatus.BadDump ? 1 : 0); RemoveStatusCount(ItemStatus.Good, disk.GetFieldValue(Models.Metadata.Disk.StatusKey) == ItemStatus.Good ? 1 : 0); RemoveStatusCount(ItemStatus.Nodump, disk.GetFieldValue(Models.Metadata.Disk.StatusKey) == ItemStatus.Nodump ? 1 : 0); RemoveStatusCount(ItemStatus.Verified, disk.GetFieldValue(Models.Metadata.Disk.StatusKey) == ItemStatus.Verified ? 1 : 0); break; case Media media: RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(media.GetFieldValue(Models.Metadata.Media.MD5Key)) ? 0 : 1); RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(media.GetFieldValue(Models.Metadata.Media.SHA1Key)) ? 0 : 1); RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(media.GetFieldValue(Models.Metadata.Media.SHA256Key)) ? 0 : 1); RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.GetFieldValue(Models.Metadata.Media.SpamSumKey)) ? 0 : 1); break; case Rom rom: if (rom.GetFieldValue(Models.Metadata.Rom.StatusKey) != ItemStatus.Nodump) { TotalSize -= NumberHelper.ConvertToInt64(rom.GetFieldValue(Models.Metadata.Rom.SizeKey)) ?? 0; RemoveHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.CRCKey)) ? 0 : 1); RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.MD5Key)) ? 0 : 1); RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SHA1Key)) ? 0 : 1); RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SHA256Key)) ? 0 : 1); RemoveHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SHA384Key)) ? 0 : 1); RemoveHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SHA512Key)) ? 0 : 1); RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.GetFieldValue(Models.Metadata.Rom.SpamSumKey)) ? 0 : 1); } RemoveStatusCount(ItemStatus.BadDump, rom.GetFieldValue(Models.Metadata.Rom.StatusKey) == ItemStatus.BadDump ? 1 : 0); RemoveStatusCount(ItemStatus.Good, rom.GetFieldValue(Models.Metadata.Rom.StatusKey) == ItemStatus.Good ? 1 : 0); RemoveStatusCount(ItemStatus.Nodump, rom.GetFieldValue(Models.Metadata.Rom.StatusKey) == ItemStatus.Nodump ? 1 : 0); RemoveStatusCount(ItemStatus.Verified, rom.GetFieldValue(Models.Metadata.Rom.StatusKey) == ItemStatus.Verified ? 1 : 0); break; } } } /// /// Get the item count for a given hash type, defaulting to 0 if it does not exist /// /// Hash type to retrieve /// The number of items with that hash, if it exists public long GetHashCount(HashType hashType) { lock (HashCounts) { if (!HashCounts.ContainsKey(hashType)) return 0; return HashCounts[hashType]; } } /// /// Get the item count for a given item type, defaulting to 0 if it does not exist /// /// Item type to retrieve /// The number of items of that type, if it exists public long GetItemCount(ItemType itemType) { lock (ItemCounts) { if (!ItemCounts.ContainsKey(itemType)) return 0; return ItemCounts[itemType]; } } /// /// Get the item count for a given item status, defaulting to 0 if it does not exist /// /// Item status to retrieve /// The number of items of that type, if it exists public long GetStatusCount(ItemStatus itemStatus) { lock (StatusCounts) { if (!StatusCounts.ContainsKey(itemStatus)) return 0; return StatusCounts[itemStatus]; } } /// /// Increment the hash count for a given hash type /// /// Hash type to increment /// Amount to increment by, defaults to 1 private void AddHashCount(HashType hashType, long interval = 1) { lock (HashCounts) { if (!HashCounts.ContainsKey(hashType)) HashCounts[hashType] = 0; HashCounts[hashType] += interval; if (HashCounts[hashType] < 0) HashCounts[hashType] = 0; } } /// /// Decrement the hash count for a given hash type /// /// Hash type to increment /// Amount to increment by, defaults to 1 private void RemoveHashCount(HashType hashType, long interval = 1) { lock (HashCounts) { if (!HashCounts.ContainsKey(hashType)) return; HashCounts[hashType] -= interval; if (HashCounts[hashType] < 0) HashCounts[hashType] = 0; } } /// /// Increment the item count for a given item type /// /// Item type to increment /// Amount to increment by, defaults to 1 private void AddItemCount(ItemType itemType, long interval = 1) { lock (ItemCounts) { if (!ItemCounts.ContainsKey(itemType)) ItemCounts[itemType] = 0; ItemCounts[itemType] += interval; if (ItemCounts[itemType] < 0) ItemCounts[itemType] = 0; } } /// /// Decrement the item count for a given item type /// /// Item type to decrement /// Amount to increment by, defaults to 1 private void RemoveItemCount(ItemType itemType, long interval = 1) { lock (ItemCounts) { if (!ItemCounts.ContainsKey(itemType)) return; ItemCounts[itemType] -= interval; if (ItemCounts[itemType] < 0) ItemCounts[itemType] = 0; } } /// /// Increment the item count for a given item status /// /// Item type to increment /// Amount to increment by, defaults to 1 private void AddStatusCount(ItemStatus itemStatus, long interval = 1) { lock (StatusCounts) { if (!StatusCounts.ContainsKey(itemStatus)) StatusCounts[itemStatus] = 0; StatusCounts[itemStatus] += interval; if (StatusCounts[itemStatus] < 0) StatusCounts[itemStatus] = 0; } } /// /// Decrement the item count for a given item status /// /// Item type to decrement /// Amount to increment by, defaults to 1 private void RemoveStatusCount(ItemStatus itemStatus, long interval = 1) { lock (StatusCounts) { if (!StatusCounts.ContainsKey(itemStatus)) return; StatusCounts[itemStatus] -= interval; if (StatusCounts[itemStatus] < 0) StatusCounts[itemStatus] = 0; } } #endregion #region Constructors /// /// Generic constructor /// public ItemDictionary() { bucketedBy = ItemKey.NULL; mergedBy = DedupeType.None; #if NET40_OR_GREATER || NETCOREAPP items = new ConcurrentDictionary?>(); #else items = new Dictionary?>(); #endif logger = new Logger(this); } #endregion #region Custom Functionality /// /// 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 public 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 NET40_OR_GREATER || NETCOREAPP if (items == null || items.IsEmpty) #else if (items == null || items.Count == 0) #endif 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}"); // 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, 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 } // If the merge type isn't the same, we want to merge the dictionary accordingly if (mergedBy != dedupeType) { logger.User($"Deduping roms by {dedupeType}"); // Set the sorted type mergedBy = dedupeType; List keys = [.. Keys]; #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(keys, Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(keys, key => #else foreach (var key in keys) #endif { // Get the possibly unsorted list ConcurrentList? sortedlist = this[key]?.ToConcurrentList(); if (sortedlist == null) #if NET40_OR_GREATER || NETCOREAPP return; #else continue; #endif // Sort the list of items to be consistent DatItem.Sort(ref sortedlist, false); // If we're merging the roms, do so if (dedupeType == DedupeType.Full || (dedupeType == DedupeType.Game && bucketBy == ItemKey.Machine)) sortedlist = DatItem.Merge(sortedlist); // Add the list back to the dictionary Reset(key); AddRange(key, sortedlist); #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } // If the merge type is the same, we want to sort the dictionary to be consistent else { List keys = [.. Keys]; #if NET452_OR_GREATER || NETCOREAPP Parallel.ForEach(keys, Globals.ParallelOptions, key => #elif NET40_OR_GREATER Parallel.ForEach(keys, key => #else foreach (var key in keys) #endif { // Get the possibly unsorted list ConcurrentList? sortedlist = this[key]; // Sort the list of items to be consistent if (sortedlist != null) DatItem.Sort(ref sortedlist, false); #if NET40_OR_GREATER || NETCOREAPP }); #else } #endif } } /// /// Remove any keys that have null or empty values /// public void ClearEmpty() { var keys = items.Keys.Where(k => k != null).ToList(); foreach (string key in keys) { // If the key doesn't exist, skip if (!items.ContainsKey(key)) continue; // If the value is null, remove else if (items[key] == null) #if NET40_OR_GREATER || NETCOREAPP items.TryRemove(key, out _); #else items.Remove(key); #endif // If there are no non-blank items, remove else if (!items[key]!.Any(i => i != null && i is not Blank)) #if NET40_OR_GREATER || NETCOREAPP items.TryRemove(key, out _); #else items.Remove(key); #endif } } /// /// Remove all items marked for removal /// public void ClearMarked() { var keys = items.Keys.ToList(); foreach (string key in keys) { ConcurrentList? oldItemList = items[key]; ConcurrentList? newItemList = oldItemList?.Where(i => i.GetFieldValue(DatItem.RemoveKey) != true)?.ToConcurrentList(); Remove(key); AddRange(key, newItemList); } } /// /// 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 public ConcurrentList GetDuplicates(DatItem datItem, bool sorted = false) { ConcurrentList output = []; // Check for an empty rom list first if (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 ConcurrentList? roms = this[key]; if (roms == null) return output; ConcurrentList left = []; for (int i = 0; i < roms.Count; i++) { DatItem other = roms[i]; if (other.GetFieldValue(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); AddRange(key, output); AddRange(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 public bool HasDuplicates(DatItem datItem, bool sorted = false) { // Check for an empty rom list first if (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 ConcurrentList? roms = this[key]; return roms?.Any(r => datItem.Equals(r)) == true; } /// /// Recalculate the statistics for the Dat /// public void RecalculateStats() { // Wipe out any stats already there 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) { ConcurrentList? datItems = items[key]; if (datItems == null) continue; foreach (DatItem item in datItems) { AddItemStatistics(item); } } } /// /// Reset all statistics /// public void ResetStatistics() { TotalCount = 0; ItemCounts = []; GameCount = 0; TotalSize = 0; HashCounts = []; StatusCounts = []; RemovedCount = 0; } /// /// Get the highest-order Field value that represents the statistics /// private ItemKey GetBestAvailable() { // Get the required counts long diskCount = GetItemCount(ItemType.Disk); long mediaCount = GetItemCount(ItemType.Media); long romCount = GetItemCount(ItemType.Rom); long nodumpCount = GetStatusCount(ItemStatus.Nodump); // If all items are supposed to have a SHA-512, we bucket by that if (diskCount + mediaCount + romCount - nodumpCount == 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 == 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 == 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 == 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 == GetHashCount(HashType.MD5)) return ItemKey.MD5; // Otherwise, we bucket by CRC else return ItemKey.CRC; } /// /// 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 #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 ConcurrentList? 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 } }