using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Xml.Serialization; using Microsoft.Data.Sqlite; using NaturalSort; using Newtonsoft.Json; using SabreTools.Core; using SabreTools.DatItems; using SabreTools.DatItems.Formats; using SabreTools.IO; using SabreTools.Logging; namespace SabreTools.DatFiles { /// /// Item dictionary with statistics, bucketing, and sorting /// [JsonObject("items"), XmlRoot("items")] public class ItemDictionaryDB : 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 database connection for the class /// private readonly SqliteConnection? dbc = null; /// /// Internal item dictionary name /// private string? itemDictionaryFileName = null; /// /// Lock for statistics calculation /// private readonly object statsLock = new(); /// /// Logging object /// private readonly Logger logger; #endregion #region Publically available fields #region Database /// /// Item dictionary file name /// public string ItemDictionaryFileName { get { if (string.IsNullOrWhiteSpace(itemDictionaryFileName)) itemDictionaryFileName = Path.Combine(PathTool.GetRuntimeDirectory(), $"itemDictionary{Guid.NewGuid()}.sqlite"); return itemDictionaryFileName; } } /// /// Item dictionary connection string /// public string ItemDictionaryConnectionString => $"Data Source={ItemDictionaryFileName};"; #endregion #region Keys /// /// Get the keys from the file database /// /// List of the keys [JsonIgnore, XmlIgnore] public ICollection Keys { get { // If we have no database connection, we can't do anything if (dbc == null) return Array.Empty(); // Open the database connection dbc.Open(); string query = $"SELECT key FROM keys"; SqliteCommand slc = new(query, dbc); SqliteDataReader sldr = slc.ExecuteReader(); List keys = GetKeys(); if (sldr.HasRows) { while (sldr.Read()) { keys.Add(sldr.GetString(0)); } } // Dispose of database objects slc.Dispose(); sldr.Dispose(); dbc.Close(); return keys; } } private static List GetKeys() { return new List(); } /// /// Get the keys in sorted order from the file dictionary /// /// List of the keys in sorted order [JsonIgnore, XmlIgnore] public List? SortedKeys { get { if (Keys == null) return null; var keys = 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 Adjuster items /// [JsonIgnore, XmlIgnore] public long AdjusterCount { get; private set; } = 0; /// /// Number of Analog items /// [JsonIgnore, XmlIgnore] public long AnalogCount { get; private set; } = 0; /// /// Number of Archive items /// [JsonIgnore, XmlIgnore] public long ArchiveCount { get; private set; } = 0; /// /// Number of BiosSet items /// [JsonIgnore, XmlIgnore] public long BiosSetCount { get; private set; } = 0; /// /// Number of Chip items /// [JsonIgnore, XmlIgnore] public long ChipCount { get; private set; } = 0; /// /// Number of top-level Condition items /// [JsonIgnore, XmlIgnore] public long ConditionCount { get; private set; } = 0; /// /// Number of Configuration items /// [JsonIgnore, XmlIgnore] public long ConfigurationCount { get; private set; } = 0; /// /// Number of DataArea items /// [JsonIgnore, XmlIgnore] public long DataAreaCount { get; private set; } = 0; /// /// Number of Device items /// [JsonIgnore, XmlIgnore] public long DeviceCount { get; private set; } = 0; /// /// Number of Device Reference items /// [JsonIgnore, XmlIgnore] public long DeviceReferenceCount { get; private set; } = 0; /// /// Number of DIP Switch items /// [JsonIgnore, XmlIgnore] public long DipSwitchCount { get; private set; } = 0; /// /// Number of Disk items /// [JsonIgnore, XmlIgnore] public long DiskCount { get; private set; } = 0; /// /// Number of DiskArea items /// [JsonIgnore, XmlIgnore] public long DiskAreaCount { get; private set; } = 0; /// /// Number of Display items /// [JsonIgnore, XmlIgnore] public long DisplayCount { get; private set; } = 0; /// /// Number of Driver items /// [JsonIgnore, XmlIgnore] public long DriverCount { get; private set; } = 0; /// /// Number of Feature items /// [JsonIgnore, XmlIgnore] public long FeatureCount { get; private set; } = 0; /// /// Number of Info items /// [JsonIgnore, XmlIgnore] public long InfoCount { get; private set; } = 0; /// /// Number of Input items /// [JsonIgnore, XmlIgnore] public long InputCount { get; private set; } = 0; /// /// Number of Media items /// [JsonIgnore, XmlIgnore] public long MediaCount { get; private set; } = 0; /// /// Number of Part items /// [JsonIgnore, XmlIgnore] public long PartCount { get; private set; } = 0; /// /// Number of PartFeature items /// [JsonIgnore, XmlIgnore] public long PartFeatureCount { get; private set; } = 0; /// /// Number of Port items /// [JsonIgnore, XmlIgnore] public long PortCount { get; private set; } = 0; /// /// Number of RamOption items /// [JsonIgnore, XmlIgnore] public long RamOptionCount { get; private set; } = 0; /// /// Number of Release items /// [JsonIgnore, XmlIgnore] public long ReleaseCount { get; private set; } = 0; /// /// Number of ReleaseDetails items /// [JsonIgnore, XmlIgnore] public long ReleaseDetailsCount { get; private set; } = 0; /// /// Number of Rom items /// [JsonIgnore, XmlIgnore] public long RomCount { get; private set; } = 0; /// /// Number of Sample items /// [JsonIgnore, XmlIgnore] public long SampleCount { get; private set; } = 0; /// /// Number of Serials items /// [JsonIgnore, XmlIgnore] public long SerialsCount { get; private set; } = 0; /// /// Number of SharedFeature items /// [JsonIgnore, XmlIgnore] public long SharedFeatureCount { get; private set; } = 0; /// /// Number of Slot items /// [JsonIgnore, XmlIgnore] public long SlotCount { get; private set; } = 0; /// /// Number of SoftwareList items /// [JsonIgnore, XmlIgnore] public long SoftwareListCount { get; private set; } = 0; /// /// Number of Sound items /// [JsonIgnore, XmlIgnore] public long SoundCount { get; private set; } = 0; /// /// Number of SourceDetails items /// [JsonIgnore, XmlIgnore] public long SourceDetailsCount { get; private set; } = 0; /// /// 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 with a CRC hash /// [JsonIgnore, XmlIgnore] public long CRCCount { get; private set; } = 0; /// /// Number of items with an MD5 hash /// [JsonIgnore, XmlIgnore] public long MD5Count { get; private set; } = 0; /// /// Number of items with a SHA-1 hash /// [JsonIgnore, XmlIgnore] public long SHA1Count { get; private set; } = 0; /// /// Number of items with a SHA-256 hash /// [JsonIgnore, XmlIgnore] public long SHA256Count { get; private set; } = 0; /// /// Number of items with a SHA-384 hash /// [JsonIgnore, XmlIgnore] public long SHA384Count { get; private set; } = 0; /// /// Number of items with a SHA-512 hash /// [JsonIgnore, XmlIgnore] public long SHA512Count { get; private set; } = 0; /// /// Number of items with a SpamSum fuzzy hash /// [JsonIgnore, XmlIgnore] public long SpamSumCount { get; private set; } = 0; /// /// Number of items with the baddump status /// [JsonIgnore, XmlIgnore] public long BaddumpCount { get; private set; } = 0; /// /// Number of items with the good status /// [JsonIgnore, XmlIgnore] public long GoodCount { get; private set; } = 0; /// /// Number of items with the nodump status /// [JsonIgnore, XmlIgnore] public long NodumpCount { get; private set; } = 0; /// /// Number of items with the remove flag /// [JsonIgnore, XmlIgnore] public long RemovedCount { get; private set; } = 0; /// /// Number of items with the verified status /// [JsonIgnore, XmlIgnore] public long VerifiedCount { 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); // If we have no database connection, we can't do anything if (dbc == null) return null; // Open the database connection dbc.Open(); string query = $"SELECT item FROM groups WHERE key='{key}'"; SqliteCommand slc = new(query, dbc); SqliteDataReader sldr = slc.ExecuteReader(); ConcurrentList items = new(); if (sldr.HasRows) { while (sldr.Read()) { string itemString = sldr.GetString(0); DatItem? datItem = JsonConvert.DeserializeObject(itemString); if (datItem != null) items.Add(datItem); } } // Dispose of database objects slc.Dispose(); sldr.Dispose(); dbc.Close(); // Now return the value return items; } } set { Remove(key); if (value == null) { // If we have no database connection, we can't do anything if (dbc == null) return; // Open the database connection dbc.Open(); // Now remove the value string itemString = JsonConvert.SerializeObject(value); string query = $"DELETE FROM groups WHERE key='{key}'"; SqliteCommand slc = new(query, dbc); slc.ExecuteNonQuery(); // Dispose of database objects slc.Dispose(); dbc.Close(); } 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; // If we have no database connection, we can't do anything if (dbc == null) return; // Open the database connection dbc.Open(); // Now add the value string itemString = JsonConvert.SerializeObject(value); string query = $"INSERT INTO groups (key, item) VALUES ('{key}', '{itemString}')"; SqliteCommand slc = new(query, dbc); slc.ExecuteNonQuery(); // Dispose of database objects slc.Dispose(); dbc.Close(); // 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.Remove) RemovedCount++; // Now we do different things for each item type switch (item) { case Adjuster: AdjusterCount++; break; case Analog: AnalogCount++; break; case Archive: ArchiveCount++; break; case BiosSet: BiosSetCount++; break; case Chip: ChipCount++; break; case Condition: ConditionCount++; break; case Configuration: ConfigurationCount++; break; case DataArea: DataAreaCount++; break; case Device: DeviceCount++; break; case DeviceReference: DeviceReferenceCount++; break; case DipSwitch: DipSwitchCount++; break; case Disk disk: DiskCount++; if (disk.ItemStatus != ItemStatus.Nodump) { MD5Count += (string.IsNullOrWhiteSpace(disk.MD5) ? 0 : 1); SHA1Count += (string.IsNullOrWhiteSpace(disk.SHA1) ? 0 : 1); } BaddumpCount += (disk.ItemStatus == ItemStatus.BadDump ? 1 : 0); GoodCount += (disk.ItemStatus == ItemStatus.Good ? 1 : 0); NodumpCount += (disk.ItemStatus == ItemStatus.Nodump ? 1 : 0); VerifiedCount += (disk.ItemStatus == ItemStatus.Verified ? 1 : 0); break; case DiskArea: DiskAreaCount++; break; case Display: DisplayCount++; break; case Driver: DriverCount++; break; case Feature: FeatureCount++; break; case Info: InfoCount++; break; case Input: InputCount++; break; case Media media: MediaCount++; MD5Count += (string.IsNullOrWhiteSpace(media.MD5) ? 0 : 1); SHA1Count += (string.IsNullOrWhiteSpace(media.SHA1) ? 0 : 1); SHA256Count += (string.IsNullOrWhiteSpace(media.SHA256) ? 0 : 1); SpamSumCount += (string.IsNullOrWhiteSpace(media.SpamSum) ? 0 : 1); break; case Part: PartCount++; break; case PartFeature: PartFeatureCount++; break; case Port: PortCount++; break; case RamOption: RamOptionCount++; break; case Release: ReleaseCount++; break; case ReleaseDetails: ReleaseDetailsCount++; break; case Rom rom: RomCount++; if (rom.ItemStatus != ItemStatus.Nodump) { TotalSize += rom.Size ?? 0; CRCCount += (string.IsNullOrWhiteSpace(rom.CRC) ? 0 : 1); MD5Count += (string.IsNullOrWhiteSpace(rom.MD5) ? 0 : 1); SHA1Count += (string.IsNullOrWhiteSpace(rom.SHA1) ? 0 : 1); SHA256Count += (string.IsNullOrWhiteSpace(rom.SHA256) ? 0 : 1); SHA384Count += (string.IsNullOrWhiteSpace(rom.SHA384) ? 0 : 1); SHA512Count += (string.IsNullOrWhiteSpace(rom.SHA512) ? 0 : 1); SpamSumCount += (string.IsNullOrWhiteSpace(rom.SpamSum) ? 0 : 1); } BaddumpCount += (rom.ItemStatus == ItemStatus.BadDump ? 1 : 0); GoodCount += (rom.ItemStatus == ItemStatus.Good ? 1 : 0); NodumpCount += (rom.ItemStatus == ItemStatus.Nodump ? 1 : 0); VerifiedCount += (rom.ItemStatus == ItemStatus.Verified ? 1 : 0); break; case Sample: SampleCount++; break; case Serials: SerialsCount++; break; case SharedFeature: SharedFeatureCount++; break; case Slot: SlotCount++; break; case SoftwareList: SoftwareListCount++; break; case Sound: SoundCount++; break; case SourceDetails: SourceDetailsCount++; 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 foreach (DatItem item in value) { Add(key, item); } // 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; ArchiveCount += stats.ArchiveCount; BiosSetCount += stats.BiosSetCount; ChipCount += stats.ChipCount; DiskCount += stats.DiskCount; MediaCount += stats.MediaCount; ReleaseCount += stats.ReleaseCount; RomCount += stats.RomCount; SampleCount += stats.SampleCount; GameCount += stats.GameCount; TotalSize += stats.TotalSize; // Individual hash counts CRCCount += stats.CRCCount; MD5Count += stats.MD5Count; SHA1Count += stats.SHA1Count; SHA256Count += stats.SHA256Count; SHA384Count += stats.SHA384Count; SHA512Count += stats.SHA512Count; SpamSumCount += stats.SpamSumCount; // Individual status counts BaddumpCount += stats.BaddumpCount; GoodCount += stats.GoodCount; NodumpCount += stats.NodumpCount; RemovedCount += stats.RemovedCount; VerifiedCount += stats.VerifiedCount; } /// /// 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 Keys?.Contains(key) == true; } } /// /// 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 (Keys?.Contains(key) != true) return false; return this[key]?.Contains(value) == true; } } /// /// 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 (Keys?.Contains(key) == true) return; // If we have no database connection, we can't do anything if (dbc == null) return; // Open the database connection dbc.Open(); string query = $"INSERT INTO keys (key) VALUES ('{key}')"; SqliteCommand slc = new(query, dbc); slc.ExecuteNonQuery(); // Dispose of database objects slc.Dispose(); dbc.Close(); } /// /// 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 = this[key]; if (fi == null) return new ConcurrentList(); // Filter the list return fi.Where(i => i != null) .Where(i => !i.Remove) .Where(i => i.Machine?.Name != 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) || this[key] == null) return false; // Remove the statistics first foreach (DatItem item in this[key]!) { RemoveItemStatistics(item); } // Remove the key from the dictionary this[key] = null; return true; } } /// /// 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)) return false; // Remove the statistics first RemoveItemStatistics(value); // If we have no database connection, we can't do anything if (dbc == null) return false; // Open the database connection dbc.Open(); // Now remove the value string itemString = JsonConvert.SerializeObject(value); string query = $"DELETE FROM groups WHERE key='{key}' AND item='{itemString}'"; SqliteCommand slc = new(query, dbc); slc.ExecuteNonQuery(); // Dispose of database objects slc.Dispose(); dbc.Close(); return true; } } /// /// 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) || this[key] == null) return false; // Remove the statistics first foreach (DatItem item in this[key]!) { RemoveItemStatistics(item); } // Remove the key from the dictionary this[key] = null; 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.Remove) RemovedCount--; // Now we do different things for each item type switch (item) { case Adjuster: AdjusterCount--; break; case Analog: AnalogCount--; break; case Archive: ArchiveCount--; break; case BiosSet: BiosSetCount--; break; case Chip: ChipCount--; break; case Condition: ConditionCount--; break; case Configuration: ConfigurationCount--; break; case DataArea: DataAreaCount--; break; case Device: DeviceCount--; break; case DeviceReference: DeviceReferenceCount--; break; case DipSwitch: DipSwitchCount--; break; case Disk disk: DiskCount--; if (disk.ItemStatus != ItemStatus.Nodump) { MD5Count -= (string.IsNullOrWhiteSpace(disk.MD5) ? 0 : 1); SHA1Count -= (string.IsNullOrWhiteSpace(disk.SHA1) ? 0 : 1); } BaddumpCount -= (disk.ItemStatus == ItemStatus.BadDump ? 1 : 0); GoodCount -= (disk.ItemStatus == ItemStatus.Good ? 1 : 0); NodumpCount -= (disk.ItemStatus == ItemStatus.Nodump ? 1 : 0); VerifiedCount -= (disk.ItemStatus == ItemStatus.Verified ? 1 : 0); break; case DiskArea: DiskAreaCount--; break; case Display: DisplayCount--; break; case Driver: DriverCount--; break; case Feature: FeatureCount--; break; case Info: InfoCount--; break; case Input: InputCount--; break; case Media media: MediaCount--; MD5Count -= (string.IsNullOrWhiteSpace(media.MD5) ? 0 : 1); SHA1Count -= (string.IsNullOrWhiteSpace(media.SHA1) ? 0 : 1); SHA256Count -= (string.IsNullOrWhiteSpace(media.SHA256) ? 0 : 1); break; case Part: PartCount--; break; case PartFeature: PartFeatureCount--; break; case Port: PortCount--; break; case RamOption: RamOptionCount--; break; case Release: ReleaseCount--; break; case Rom rom: RomCount--; if (rom.ItemStatus != ItemStatus.Nodump) { TotalSize -= rom.Size ?? 0; CRCCount -= (string.IsNullOrWhiteSpace(rom.CRC) ? 0 : 1); MD5Count -= (string.IsNullOrWhiteSpace(rom.MD5) ? 0 : 1); SHA1Count -= (string.IsNullOrWhiteSpace(rom.SHA1) ? 0 : 1); SHA256Count -= (string.IsNullOrWhiteSpace(rom.SHA256) ? 0 : 1); SHA384Count -= (string.IsNullOrWhiteSpace(rom.SHA384) ? 0 : 1); SHA512Count -= (string.IsNullOrWhiteSpace(rom.SHA512) ? 0 : 1); } BaddumpCount -= (rom.ItemStatus == ItemStatus.BadDump ? 1 : 0); GoodCount -= (rom.ItemStatus == ItemStatus.Good ? 1 : 0); NodumpCount -= (rom.ItemStatus == ItemStatus.Nodump ? 1 : 0); VerifiedCount -= (rom.ItemStatus == ItemStatus.Verified ? 1 : 0); break; case Sample: SampleCount--; break; case SharedFeature: SharedFeatureCount--; break; case Slot: SlotCount--; break; case SoftwareList: SoftwareListCount--; break; case Sound: SoundCount--; break; } } } #endregion #region Constructors /// /// Generic constructor /// public ItemDictionaryDB() { bucketedBy = ItemKey.NULL; mergedBy = DedupeType.None; dbc = new SqliteConnection(ItemDictionaryConnectionString); logger = new Logger(this); } #endregion #region Database /// /// Ensure that the database exists and has the proper schema /// protected void EnsureDatabase() { // Make sure the file exists if (!System.IO.File.Exists(ItemDictionaryFileName)) System.IO.File.Create(ItemDictionaryFileName); // If we have no database connection, we can't do anything if (dbc == null) return; // Open the database connection dbc.Open(); // Make sure the database has the correct schema string query = @" CREATE TABLE IF NOT EXISTS keys ( 'key' TEXT NOT NULL PRIMARY KEY (key) )"; SqliteCommand slc = new(query, dbc); slc.ExecuteNonQuery(); query = @" CREATE TABLE IF NOT EXISTS groups ( 'key' TEXT NOT NULL 'item` TEXT NOT NULL )"; slc = new SqliteCommand(query, dbc); slc.ExecuteNonQuery(); slc.Dispose(); dbc.Close(); } #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 database or no keys at all, we skip if (dbc == null || Keys.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}"); // 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.ToList(); Parallel.For(0, oldkeys.Count, Globals.ParallelOptions, k => { string key = oldkeys[k]; if (this[key] == null) return; // 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 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.ToList(); Parallel.ForEach(keys, Globals.ParallelOptions, key => { // Get the possibly unsorted list ConcurrentList sortedlist = this[key].ToConcurrentList(); // 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 the merge type is the same, we want to sort the dictionary to be consistent else { List keys = Keys.ToList(); Parallel.ForEach(keys, Globals.ParallelOptions, key => { // 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); }); } } /// /// Remove any keys that have null or empty values /// public void ClearEmpty() { var keys = Keys.Where(k => k != null).ToList(); foreach (string key in keys) { // If the key doesn't exist, skip if (!Keys.Contains(key)) continue; // If the value is null, remove else if (this[key] == null) this[key] = null; // If there are no non-blank items, remove else if (!this[key]!.Any(i => i != null && i.ItemType != ItemType.Blank)) this[key] = null; } } /// /// Remove all items marked for removal /// public void ClearMarked() { if (Keys == null) return; var keys = Keys.ToList(); foreach (string key in keys) { ConcurrentList? oldItemList = this[key]; ConcurrentList? newItemList = oldItemList?.Where(i => !i.Remove)?.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 = new(); // 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) || this[key] == null) return output; // Try to find duplicates ConcurrentList roms = this[key]!; ConcurrentList left = new(); for (int i = 0; i < roms.Count; i++) { DatItem other = roms[i]; if (other.Remove) continue; if (datItem.Equals(other)) { other.Remove = 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 (dbc == null || Keys == null) return; // Loop through and add foreach (string key in Keys) { ConcurrentList? datItems = this[key]; if (datItems == null) continue; foreach (DatItem item in datItems) { AddItemStatistics(item); } } } /// /// Reset all statistics /// public void ResetStatistics() { TotalCount = 0; ArchiveCount = 0; BiosSetCount = 0; ChipCount = 0; DiskCount = 0; MediaCount = 0; ReleaseCount = 0; RomCount = 0; SampleCount = 0; GameCount = 0; TotalSize = 0; CRCCount = 0; MD5Count = 0; SHA1Count = 0; SHA256Count = 0; SHA384Count = 0; SHA512Count = 0; SpamSumCount = 0; BaddumpCount = 0; GoodCount = 0; NodumpCount = 0; RemovedCount = 0; VerifiedCount = 0; } /// /// Get the highest-order Field value that represents the statistics /// private ItemKey GetBestAvailable() { // If all items are supposed to have a SHA-512, we bucket by that if (DiskCount + MediaCount + RomCount - NodumpCount == SHA512Count) return ItemKey.SHA512; // If all items are supposed to have a SHA-384, we bucket by that else if (DiskCount + MediaCount + RomCount - NodumpCount == SHA384Count) return ItemKey.SHA384; // If all items are supposed to have a SHA-256, we bucket by that else if (DiskCount + MediaCount + RomCount - NodumpCount == SHA256Count) return ItemKey.SHA256; // If all items are supposed to have a SHA-1, we bucket by that else if (DiskCount + MediaCount + RomCount - NodumpCount == SHA1Count) return ItemKey.SHA1; // If all items are supposed to have a MD5, we bucket by that else if (DiskCount + MediaCount + RomCount - NodumpCount == MD5Count) 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 => throw new NotImplementedException(); public int Count => throw new NotImplementedException(); public bool IsReadOnly => throw new NotImplementedException(); public bool TryGetValue(string key, out ConcurrentList? value) => throw new NotImplementedException(); public void Add(KeyValuePair?> item) => throw new NotImplementedException(); public void Clear() => throw new NotImplementedException(); public bool Contains(KeyValuePair?> item) => throw new NotImplementedException(); public void CopyTo(KeyValuePair?>[] array, int arrayIndex) => throw new NotImplementedException(); public bool Remove(KeyValuePair?> item) => throw new NotImplementedException(); public IEnumerator?>> GetEnumerator() => throw new NotImplementedException(); IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); #endregion } }