diff --git a/SabreTools.DatFiles/ItemDictionaryDB.cs b/SabreTools.DatFiles/ItemDictionaryDB.cs
new file mode 100644
index 00000000..32e30240
--- /dev/null
+++ b/SabreTools.DatFiles/ItemDictionaryDB.cs
@@ -0,0 +1,1477 @@
+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 object();
+
+ ///
+ /// 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 null;
+
+ // Open the database connection
+ dbc.Open();
+
+ string query = $"SELECT key FROM keys";
+ SqliteCommand slc = new SqliteCommand(query, dbc);
+ SqliteDataReader sldr = slc.ExecuteReader();
+
+ List keys = new List();
+ if (sldr.HasRows)
+ {
+ while (sldr.Read())
+ {
+ keys.Add(sldr.GetString(0));
+ }
+ }
+
+ // Dispose of database objects
+ slc.Dispose();
+ sldr.Dispose();
+ dbc.Close();
+
+ return 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 = 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 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 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 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 SqliteCommand(query, dbc);
+ SqliteDataReader sldr = slc.ExecuteReader();
+
+ ConcurrentList items = new ConcurrentList();
+ if (sldr.HasRows)
+ {
+ while (sldr.Read())
+ {
+ string itemString = sldr.GetString(0);
+ DatItem datItem = JsonConvert.DeserializeObject(itemString);
+ 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 SqliteCommand(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 SqliteCommand(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.ItemType)
+ {
+ case ItemType.Adjuster:
+ AdjusterCount++;
+ break;
+ case ItemType.Analog:
+ AnalogCount++;
+ break;
+ case ItemType.Archive:
+ ArchiveCount++;
+ break;
+ case ItemType.BiosSet:
+ BiosSetCount++;
+ break;
+ case ItemType.Chip:
+ ChipCount++;
+ break;
+ case ItemType.Condition:
+ ConditionCount++;
+ break;
+ case ItemType.Configuration:
+ ConfigurationCount++;
+ break;
+ case ItemType.DataArea:
+ DataAreaCount++;
+ break;
+ case ItemType.Device:
+ DeviceCount++;
+ break;
+ case ItemType.DeviceReference:
+ DeviceReferenceCount++;
+ break;
+ case ItemType.DipSwitch:
+ DipSwitchCount++;
+ break;
+ case ItemType.Disk:
+ DiskCount++;
+ if ((item as Disk).ItemStatus != ItemStatus.Nodump)
+ {
+ MD5Count += (string.IsNullOrWhiteSpace((item as Disk).MD5) ? 0 : 1);
+ SHA1Count += (string.IsNullOrWhiteSpace((item as Disk).SHA1) ? 0 : 1);
+ }
+
+ BaddumpCount += ((item as Disk).ItemStatus == ItemStatus.BadDump ? 1 : 0);
+ GoodCount += ((item as Disk).ItemStatus == ItemStatus.Good ? 1 : 0);
+ NodumpCount += ((item as Disk).ItemStatus == ItemStatus.Nodump ? 1 : 0);
+ VerifiedCount += ((item as Disk).ItemStatus == ItemStatus.Verified ? 1 : 0);
+ break;
+ case ItemType.DiskArea:
+ DiskAreaCount++;
+ break;
+ case ItemType.Display:
+ DisplayCount++;
+ break;
+ case ItemType.Driver:
+ DriverCount++;
+ break;
+ case ItemType.Feature:
+ FeatureCount++;
+ break;
+ case ItemType.Info:
+ InfoCount++;
+ break;
+ case ItemType.Input:
+ InputCount++;
+ break;
+ case ItemType.Media:
+ MediaCount++;
+ MD5Count += (string.IsNullOrWhiteSpace((item as Media).MD5) ? 0 : 1);
+ SHA1Count += (string.IsNullOrWhiteSpace((item as Media).SHA1) ? 0 : 1);
+ SHA256Count += (string.IsNullOrWhiteSpace((item as Media).SHA256) ? 0 : 1);
+ SpamSumCount += (string.IsNullOrWhiteSpace((item as Media).SpamSum) ? 0 : 1);
+ break;
+ case ItemType.Part:
+ PartCount++;
+ break;
+ case ItemType.PartFeature:
+ PartFeatureCount++;
+ break;
+ case ItemType.Port:
+ PortCount++;
+ break;
+ case ItemType.RamOption:
+ RamOptionCount++;
+ break;
+ case ItemType.Release:
+ ReleaseCount++;
+ break;
+ case ItemType.Rom:
+ RomCount++;
+ if ((item as Rom).ItemStatus != ItemStatus.Nodump)
+ {
+ TotalSize += (item as Rom).Size ?? 0;
+ CRCCount += (string.IsNullOrWhiteSpace((item as Rom).CRC) ? 0 : 1);
+ MD5Count += (string.IsNullOrWhiteSpace((item as Rom).MD5) ? 0 : 1);
+ SHA1Count += (string.IsNullOrWhiteSpace((item as Rom).SHA1) ? 0 : 1);
+ SHA256Count += (string.IsNullOrWhiteSpace((item as Rom).SHA256) ? 0 : 1);
+ SHA384Count += (string.IsNullOrWhiteSpace((item as Rom).SHA384) ? 0 : 1);
+ SHA512Count += (string.IsNullOrWhiteSpace((item as Rom).SHA512) ? 0 : 1);
+ SpamSumCount += (string.IsNullOrWhiteSpace((item as Rom).SpamSum) ? 0 : 1);
+ }
+
+ BaddumpCount += ((item as Rom).ItemStatus == ItemStatus.BadDump ? 1 : 0);
+ GoodCount += ((item as Rom).ItemStatus == ItemStatus.Good ? 1 : 0);
+ NodumpCount += ((item as Rom).ItemStatus == ItemStatus.Nodump ? 1 : 0);
+ VerifiedCount += ((item as Rom).ItemStatus == ItemStatus.Verified ? 1 : 0);
+ break;
+ case ItemType.Sample:
+ SampleCount++;
+ break;
+ case ItemType.SharedFeature:
+ SharedFeatureCount++;
+ break;
+ case ItemType.Slot:
+ SlotCount++;
+ break;
+ case ItemType.SoftwareList:
+ SoftwareListCount++;
+ break;
+ case ItemType.Sound:
+ SoundCount++;
+ 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);
+ }
+ }
+
+ ///
+ /// 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))
+ return false;
+
+ return this[key].Contains(value);
+ }
+ }
+
+ ///
+ /// 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))
+ 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 SqliteCommand(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))
+ 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 SqliteCommand(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))
+ 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.ItemType)
+ {
+ case ItemType.Adjuster:
+ AdjusterCount--;
+ break;
+ case ItemType.Analog:
+ AnalogCount--;
+ break;
+ case ItemType.Archive:
+ ArchiveCount--;
+ break;
+ case ItemType.BiosSet:
+ BiosSetCount--;
+ break;
+ case ItemType.Chip:
+ ChipCount--;
+ break;
+ case ItemType.Condition:
+ ConditionCount--;
+ break;
+ case ItemType.Configuration:
+ ConfigurationCount--;
+ break;
+ case ItemType.DataArea:
+ DataAreaCount--;
+ break;
+ case ItemType.Device:
+ DeviceCount--;
+ break;
+ case ItemType.DeviceReference:
+ DeviceReferenceCount--;
+ break;
+ case ItemType.DipSwitch:
+ DipSwitchCount--;
+ break;
+ case ItemType.Disk:
+ DiskCount--;
+ if ((item as Disk).ItemStatus != ItemStatus.Nodump)
+ {
+ MD5Count -= (string.IsNullOrWhiteSpace((item as Disk).MD5) ? 0 : 1);
+ SHA1Count -= (string.IsNullOrWhiteSpace((item as Disk).SHA1) ? 0 : 1);
+ }
+
+ BaddumpCount -= ((item as Disk).ItemStatus == ItemStatus.BadDump ? 1 : 0);
+ GoodCount -= ((item as Disk).ItemStatus == ItemStatus.Good ? 1 : 0);
+ NodumpCount -= ((item as Disk).ItemStatus == ItemStatus.Nodump ? 1 : 0);
+ VerifiedCount -= ((item as Disk).ItemStatus == ItemStatus.Verified ? 1 : 0);
+ break;
+ case ItemType.DiskArea:
+ DiskAreaCount--;
+ break;
+ case ItemType.Display:
+ DisplayCount--;
+ break;
+ case ItemType.Driver:
+ DriverCount--;
+ break;
+ case ItemType.Feature:
+ FeatureCount--;
+ break;
+ case ItemType.Info:
+ InfoCount--;
+ break;
+ case ItemType.Input:
+ InputCount--;
+ break;
+ case ItemType.Media:
+ MediaCount--;
+ MD5Count -= (string.IsNullOrWhiteSpace((item as Media).MD5) ? 0 : 1);
+ SHA1Count -= (string.IsNullOrWhiteSpace((item as Media).SHA1) ? 0 : 1);
+ SHA256Count -= (string.IsNullOrWhiteSpace((item as Media).SHA256) ? 0 : 1);
+ break;
+ case ItemType.Part:
+ PartCount--;
+ break;
+ case ItemType.PartFeature:
+ PartFeatureCount--;
+ break;
+ case ItemType.Port:
+ PortCount--;
+ break;
+ case ItemType.RamOption:
+ RamOptionCount--;
+ break;
+ case ItemType.Release:
+ ReleaseCount--;
+ break;
+ case ItemType.Rom:
+ RomCount--;
+ if ((item as Rom).ItemStatus != ItemStatus.Nodump)
+ {
+ TotalSize -= (item as Rom).Size ?? 0;
+ CRCCount -= (string.IsNullOrWhiteSpace((item as Rom).CRC) ? 0 : 1);
+ MD5Count -= (string.IsNullOrWhiteSpace((item as Rom).MD5) ? 0 : 1);
+ SHA1Count -= (string.IsNullOrWhiteSpace((item as Rom).SHA1) ? 0 : 1);
+ SHA256Count -= (string.IsNullOrWhiteSpace((item as Rom).SHA256) ? 0 : 1);
+ SHA384Count -= (string.IsNullOrWhiteSpace((item as Rom).SHA384) ? 0 : 1);
+ SHA512Count -= (string.IsNullOrWhiteSpace((item as Rom).SHA512) ? 0 : 1);
+ }
+
+ BaddumpCount -= ((item as Rom).ItemStatus == ItemStatus.BadDump ? 1 : 0);
+ GoodCount -= ((item as Rom).ItemStatus == ItemStatus.Good ? 1 : 0);
+ NodumpCount -= ((item as Rom).ItemStatus == ItemStatus.Nodump ? 1 : 0);
+ VerifiedCount -= ((item as Rom).ItemStatus == ItemStatus.Verified ? 1 : 0);
+ break;
+ case ItemType.Sample:
+ SampleCount--;
+ break;
+ case ItemType.SharedFeature:
+ SharedFeatureCount--;
+ break;
+ case ItemType.Slot:
+ SlotCount--;
+ break;
+ case ItemType.SoftwareList:
+ SoftwareListCount--;
+ break;
+ case ItemType.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 (!File.Exists(ItemDictionaryFileName))
+ 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 SqliteCommand(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];
+
+ // 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
+ 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].Count(i => i != null && i.ItemType != ItemType.Blank) == 0)
+ this[key] = null;
+ }
+ }
+
+ ///
+ /// Remove all items marked for removal
+ ///
+ public void ClearMarked()
+ {
+ 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 ConcurrentList();
+
+ // 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];
+ ConcurrentList left = new ConcurrentList();
+ 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));
+ }
+
+ ///
+ /// 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)
+ return;
+
+ // Loop through and add
+ foreach (string key in Keys)
+ {
+ ConcurrentList datItems = this[key];
+ 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
+ }
+}