using System; using System.Collections.Generic; using System.IO; using System.Xml.Serialization; using Newtonsoft.Json; using SabreTools.Core.Filter; using SabreTools.Core.Tools; using SabreTools.DatItems; using SabreTools.DatItems.Formats; using SabreTools.Hashing; using SabreTools.IO.Logging; namespace SabreTools.DatFiles { /// /// Represents a format-agnostic DAT /// [JsonObject("datfile"), XmlRoot("datfile")] public abstract partial class DatFile { #region Fields /// /// Header values /// [JsonProperty("header"), XmlElement("header")] public DatHeader Header { get; private set; } = new DatHeader(); /// /// DatItems and related statistics /// [JsonProperty("items"), XmlElement("items")] public ItemDictionary Items { get; private set; } = []; /// /// DatItems and related statistics /// [JsonProperty("items"), XmlElement("items")] public ItemDictionaryDB ItemsDB { get; private set; } = new ItemDictionaryDB(); /// /// List of supported types for writing /// public abstract ItemType[] SupportedTypes { get; } #endregion #region Logging /// /// Logging object /// [JsonIgnore, XmlIgnore] protected Logger _logger; #endregion #region Constructors /// /// Create a new DatFile from an existing one /// /// DatFile to get the values from public DatFile(DatFile? datFile) { _logger = new Logger(this); if (datFile != null) { Header = (DatHeader)datFile.Header.Clone(); Items = datFile.Items; ItemsDB = datFile.ItemsDB; } } /// /// Fill the header values based on existing Header and path /// /// Path used for creating a name, if necessary /// True if the date should be omitted from name and description, false otherwise public void FillHeaderFromPath(string path, bool bare) { // Get the header strings string? name = Header.GetStringFieldValue(Models.Metadata.Header.NameKey); string? description = Header.GetStringFieldValue(Models.Metadata.Header.DescriptionKey); string? date = Header.GetStringFieldValue(Models.Metadata.Header.DateKey); // If the description is defined but not the name, set the name from the description if (string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(description)) { name = description + (bare ? string.Empty : $" ({date})"); } // If the name is defined but not the description, set the description from the name else if (!string.IsNullOrEmpty(name) && string.IsNullOrEmpty(description)) { description = name + (bare ? string.Empty : $" ({date})"); } // If neither the name or description are defined, set them from the automatic values else if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(description)) { string[] splitpath = path.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar); #if NETFRAMEWORK name = splitpath[splitpath.Length - 1]; description = splitpath[splitpath.Length - 1] + (bare ? string.Empty : $" ({date})"); #else name = splitpath[^1] + (bare ? string.Empty : $" ({date})"); description = splitpath[^1] + (bare ? string.Empty : $" ({date})"); #endif } // Trim both fields name = name?.Trim(); description = description?.Trim(); // Set the fields back Header.SetFieldValue(Models.Metadata.Header.NameKey, name); Header.SetFieldValue(Models.Metadata.Header.DescriptionKey, description); } #endregion #region Accessors /// /// Reset the internal item dictionary /// public void ResetDictionary() { Items.Clear(); ItemsDB = new ItemDictionaryDB(); } /// /// Set the internal header /// /// Replacement header to be used public void SetHeader(DatHeader datHeader) { Header = (DatHeader)datHeader.Clone(); } #endregion #region Filtering /// /// Execute all filters in a filter runner on the items in the dictionary /// /// Preconfigured filter runner to use public void ExecuteFilters(FilterRunner filterRunner) { Items.ExecuteFilters(filterRunner); ItemsDB.ExecuteFilters(filterRunner); } #endregion #region Parsing /// /// Parse DatFile and return all found games and roms within /// /// Name of the file to be parsed /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) /// True to only add item statistics while parsing, false otherwise /// True if the error that is thrown should be thrown back to the caller, false otherwise public abstract void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false); #endregion #region Writing /// /// Create and open an output file for writing direct from a dictionary /// /// Name of the file to write to /// True if blank roms should be skipped on output, false otherwise (default) /// True if the error that is thrown should be thrown back to the caller, false otherwise /// True if the DAT was written correctly, false otherwise public abstract bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false); /// /// Create and open an output file for writing direct from a dictionary /// /// Name of the file to write to /// True if blank roms should be skipped on output, false otherwise (default) /// True if the error that is thrown should be thrown back to the caller, false otherwise /// True if the DAT was written correctly, false otherwise public abstract bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false); /// /// Create a prefix or postfix from inputs /// /// DatItem to create a prefix/postfix for /// True for prefix, false for postfix /// Sanitized string representing the postfix or prefix protected string CreatePrefixPostfix(DatItem item, bool prefix) { // Get machine for the item var machine = item.GetFieldValue(DatItem.MachineKey); if (machine == null) return string.Empty; // Apply the prefix and postfix return CreatePrefixPostfixImpl(item, machine, prefix); } /// /// Create a prefix or postfix from inputs /// /// DatItem to create a prefix/postfix for /// True for prefix, false for postfix /// Sanitized string representing the postfix or prefix protected string CreatePrefixPostfixDB(KeyValuePair item, bool prefix) { // Get machine for the item var machine = ItemsDB.GetMachineForItem(item.Key); if (machine.Value == null) return string.Empty; // Apply the prefix and postfix return CreatePrefixPostfixImpl(item.Value, machine.Value, prefix); } /// /// Create a prefix or postfix from inputs /// /// DatItem to create a prefix/postfix for /// Machine to get information from /// True for prefix, false for postfix /// Sanitized string representing the postfix or prefix private string CreatePrefixPostfixImpl(DatItem item, Machine machine, bool prefix) { // Initialize strings string? type = item.GetStringFieldValue(Models.Metadata.DatItem.TypeKey); string fix, game = machine.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, manufacturer = machine.Value.GetStringFieldValue(Models.Metadata.Machine.ManufacturerKey) ?? string.Empty, publisher = machine.Value.GetStringFieldValue(Models.Metadata.Machine.PublisherKey) ?? string.Empty, category = machine.Value.GetStringFieldValue(Models.Metadata.Machine.CategoryKey) ?? string.Empty, name = item.Value.GetName() ?? type.AsEnumValue().AsStringValue() ?? string.Empty, crc = string.Empty, md2 = string.Empty, md4 = string.Empty, md5 = string.Empty, sha1 = string.Empty, sha256 = string.Empty, sha384 = string.Empty, sha512 = string.Empty, size = string.Empty, spamsum = string.Empty; // Check for quotes bool? quotes = Header.GetBoolFieldValue(DatHeader.QuotesKey); // If we have a prefix if (prefix) { string? prefixString = Header.GetStringFieldValue(DatHeader.PrefixKey); fix = prefixString + (quotes == true ? "\"" : string.Empty); } // If we have a postfix else { string? postfixString = Header.GetStringFieldValue(DatHeader.PostfixKey); fix = (quotes == true ? "\"" : string.Empty) + postfixString; } // Ensure we have the proper values for replacement if (item is Disk disk) { md5 = disk.GetStringFieldValue(Models.Metadata.Disk.MD5Key) ?? string.Empty; sha1 = disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key) ?? string.Empty; } else if (item is Media media) { md5 = media.GetStringFieldValue(Models.Metadata.Media.MD5Key) ?? string.Empty; sha1 = media.GetStringFieldValue(Models.Metadata.Media.SHA1Key) ?? string.Empty; sha256 = media.GetStringFieldValue(Models.Metadata.Media.SHA256Key) ?? string.Empty; spamsum = media.GetStringFieldValue(Models.Metadata.Media.SpamSumKey) ?? string.Empty; } else if (item is Rom rom) { crc = rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) ?? string.Empty; md2 = rom.GetStringFieldValue(Models.Metadata.Rom.MD2Key) ?? string.Empty; md4 = rom.GetStringFieldValue(Models.Metadata.Rom.MD4Key) ?? string.Empty; md5 = rom.GetStringFieldValue(Models.Metadata.Rom.MD5Key) ?? string.Empty; sha1 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key) ?? string.Empty; sha256 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA256Key) ?? string.Empty; sha384 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA384Key) ?? string.Empty; sha512 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA512Key) ?? string.Empty; size = rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey).ToString() ?? string.Empty; spamsum = rom.GetStringFieldValue(Models.Metadata.Rom.SpamSumKey) ?? string.Empty; } // Now do bulk replacement where possible fix = fix .Replace("%game%", game) .Replace("%machine%", game) .Replace("%name%", name) .Replace("%manufacturer%", manufacturer) .Replace("%publisher%", publisher) .Replace("%category%", category) .Replace("%crc%", crc) .Replace("%md2%", md2) .Replace("%md4%", md4) .Replace("%md5%", md5) .Replace("%sha1%", sha1) .Replace("%sha256%", sha256) .Replace("%sha384%", sha384) .Replace("%sha512%", sha512) .Replace("%size%", size) .Replace("%spamsum%", spamsum); return fix; } /// /// Process an item and correctly set the item name /// /// DatItem to update /// True if the Quotes flag should be ignored, false otherwise /// True if the UseRomName should be always on (default), false otherwise protected void ProcessItemName(DatItem item, bool forceRemoveQuotes, bool forceRomName = true) { // Backup relevant values and set new ones accordingly bool? quotesBackup = Header.GetBoolFieldValue(DatHeader.QuotesKey); bool? useRomNameBackup = Header.GetBoolFieldValue(DatHeader.UseRomNameKey); if (forceRemoveQuotes) Header.SetFieldValue(DatHeader.QuotesKey, false); if (forceRomName) Header.SetFieldValue(DatHeader.UseRomNameKey, true); // Get the machine var machine = item.GetFieldValue(DatItem.MachineKey); if (machine == null) return; // Get the name to update string? name = (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true ? item.GetName() : machine.GetStringFieldValue(Models.Metadata.Machine.NameKey)) ?? string.Empty; // Create the proper Prefix and Postfix string pre = CreatePrefixPostfix(item, true); string post = CreatePrefixPostfix(item, false); // If we're in Depot mode, take care of that instead var outputDepot = Header.GetFieldValue(DatHeader.OutputDepotKey); if (outputDepot?.IsActive == true) { if (item is Disk disk) { // We can only write out if there's a SHA-1 string? sha1 = disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key); if (!string.IsNullOrEmpty(sha1)) { name = Utilities.GetDepotPath(sha1, outputDepot.Depth)?.Replace('\\', '/'); item.SetName($"{pre}{name}{post}"); } } else if (item is Media media) { // We can only write out if there's a SHA-1 string? sha1 = media.GetStringFieldValue(Models.Metadata.Media.SHA1Key); if (!string.IsNullOrEmpty(sha1)) { name = Utilities.GetDepotPath(sha1, outputDepot.Depth)?.Replace('\\', '/'); item.SetName($"{pre}{name}{post}"); } } else if (item is Rom rom) { // We can only write out if there's a SHA-1 string? sha1 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key); if (!string.IsNullOrEmpty(sha1)) { name = Utilities.GetDepotPath(sha1, outputDepot.Depth)?.Replace('\\', '/'); item.SetName($"{pre}{name}{post}"); } } return; } string? replaceExtension = Header.GetStringFieldValue(DatHeader.ReplaceExtensionKey); bool? removeExtension = Header.GetBoolFieldValue(DatHeader.RemoveExtensionKey); if (!string.IsNullOrEmpty(replaceExtension) || removeExtension == true) { if (removeExtension == true) Header.SetFieldValue(DatHeader.ReplaceExtensionKey, string.Empty); string? dir = Path.GetDirectoryName(name); if (dir != null) { dir = dir.TrimStart(Path.DirectorySeparatorChar); name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + replaceExtension); } } string? addExtension = Header.GetStringFieldValue(DatHeader.AddExtensionKey); if (!string.IsNullOrEmpty(addExtension)) name += addExtension; if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true && Header.GetBoolFieldValue(DatHeader.GameNameKey) == true) name = Path.Combine(machine.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, name); // Now assign back the formatted name name = $"{pre}{name}{post}"; if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true) item.SetName(name); else machine.SetFieldValue(Models.Metadata.Machine.NameKey, name); // Restore all relevant values if (forceRemoveQuotes) Header.SetFieldValue(DatHeader.QuotesKey, quotesBackup); if (forceRomName) Header.SetFieldValue(DatHeader.UseRomNameKey, useRomNameBackup); } /// /// Process an item and correctly set the item name /// /// DatItem to update /// True if the Quotes flag should be ignored, false otherwise /// True if the UseRomName should be always on (default), false otherwise protected void ProcessItemNameDB(KeyValuePair item, bool forceRemoveQuotes, bool forceRomName = true) { // Backup relevant values and set new ones accordingly bool? quotesBackup = Header.GetBoolFieldValue(DatHeader.QuotesKey); bool? useRomNameBackup = Header.GetBoolFieldValue(DatHeader.UseRomNameKey); if (forceRemoveQuotes) Header.SetFieldValue(DatHeader.QuotesKey, false); if (forceRomName) Header.SetFieldValue(DatHeader.UseRomNameKey, true); // Get machine for the item var machine = ItemsDB.GetMachineForItem(item.Key); // Get the name to update string? name = (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true ? item.Value.GetName() : machine.Value!.GetStringFieldValue(Models.Metadata.Machine.NameKey)) ?? string.Empty; // Create the proper Prefix and Postfix string pre = CreatePrefixPostfixDB(item, true); string post = CreatePrefixPostfixDB(item, false); // If we're in Depot mode, take care of that instead var outputDepot = Header.GetFieldValue(DatHeader.OutputDepotKey); if (outputDepot?.IsActive == true) { if (item.Value is Disk disk) { // We can only write out if there's a SHA-1 string? sha1 = disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key); if (!string.IsNullOrEmpty(sha1)) { name = Utilities.GetDepotPath(sha1, outputDepot.Depth)?.Replace('\\', '/'); item.Value.SetName($"{pre}{name}{post}"); } } else if (item.Value is Media media) { // We can only write out if there's a SHA-1 string? sha1 = media.GetStringFieldValue(Models.Metadata.Media.SHA1Key); if (!string.IsNullOrEmpty(sha1)) { name = Utilities.GetDepotPath(sha1, outputDepot.Depth)?.Replace('\\', '/'); item.Value.SetName($"{pre}{name}{post}"); } } else if (item.Value is Rom rom) { // We can only write out if there's a SHA-1 string? sha1 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key); if (!string.IsNullOrEmpty(sha1)) { name = Utilities.GetDepotPath(sha1, outputDepot.Depth)?.Replace('\\', '/'); item.Value.SetName($"{pre}{name}{post}"); } } return; } string? replaceExtension = Header.GetStringFieldValue(DatHeader.ReplaceExtensionKey); bool? removeExtension = Header.GetBoolFieldValue(DatHeader.RemoveExtensionKey); if (!string.IsNullOrEmpty(replaceExtension) || removeExtension == true) { if (removeExtension == true) Header.SetFieldValue(DatHeader.ReplaceExtensionKey, string.Empty); string? dir = Path.GetDirectoryName(name); if (dir != null) { dir = dir.TrimStart(Path.DirectorySeparatorChar); name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + replaceExtension); } } string? addExtension = Header.GetStringFieldValue(DatHeader.AddExtensionKey); if (!string.IsNullOrEmpty(addExtension)) name += addExtension; if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true && Header.GetBoolFieldValue(DatHeader.GameNameKey) == true) name = Path.Combine(machine.Value!.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, name); // Now assign back the formatted name name = $"{pre}{name}{post}"; if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true) item.Value.SetName(name); else if (machine.Value != null) machine.Value.SetFieldValue(Models.Metadata.Machine.NameKey, name); // Restore all relevant values if (forceRemoveQuotes) Header.SetFieldValue(DatHeader.QuotesKey, quotesBackup); if (forceRomName) Header.SetFieldValue(DatHeader.UseRomNameKey, useRomNameBackup); } /// /// Process any DatItems that are "null", usually created from directory population /// /// DatItem to check for "null" status /// Cleaned DatItem protected DatItem ProcessNullifiedItem(DatItem item) { // If we don't have a Rom, we can ignore it if (item is not Rom rom) return item; // Get machine for the item var machine = item.GetFieldValue(DatItem.MachineKey); if (machine == null) return item; // Process the possibly nullified item return ProcessNullifiedItemImpl(rom, machine); } /// /// Process any DatItems that are "null", usually created from directory population /// /// DatItem to check for "null" status /// Cleaned DatItem protected KeyValuePair ProcessNullifiedItemDB(KeyValuePair item) { // If we don't have a Rom, we can ignore it if (item.Value is not Rom rom) return item; // Get machine for the item var machine = ItemsDB.GetMachineForItem(item.Key); if (machine.Value == null) return item; // Process the possibly nullified item return new KeyValuePair(item.Key, ProcessNullifiedItemImpl(rom, machine.Value)); } /// /// Process any DatItems that are "null", usually created from directory population /// /// Rom to check for "null" status /// Machine for logging /// Cleaned DatItem /// TODO: Investigate what the machine name is really being used for private DatItem ProcessNullifiedItemImpl(Rom rom, Machine machine) { // If the Rom has "null" characteristics, ensure all fields if (rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey) == null && rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) == "null") { _logger.Verbose($"Empty folder found: {machine.GetStringFieldValue(Models.Metadata.Machine.NameKey)}"); rom.SetName(rom.GetName() == "null" ? "-" : rom.GetName()); rom.SetFieldValue(Models.Metadata.Rom.SizeKey, Constants.SizeZero.ToString()); rom.SetFieldValue(Models.Metadata.Rom.CRCKey, rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) == "null" ? ZeroHash.CRC32Str : null); rom.SetFieldValue(Models.Metadata.Rom.MD2Key, rom.GetStringFieldValue(Models.Metadata.Rom.MD2Key) == "null" ? ZeroHash.GetString(HashType.MD2) : null); rom.SetFieldValue(Models.Metadata.Rom.MD4Key, rom.GetStringFieldValue(Models.Metadata.Rom.MD4Key) == "null" ? ZeroHash.GetString(HashType.MD4) : null); rom.SetFieldValue(Models.Metadata.Rom.MD5Key, rom.GetStringFieldValue(Models.Metadata.Rom.MD5Key) == "null" ? ZeroHash.MD5Str : null); rom.SetFieldValue(Models.Metadata.Rom.SHA1Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key) == "null" ? ZeroHash.SHA1Str : null); rom.SetFieldValue(Models.Metadata.Rom.SHA256Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA256Key) == "null" ? ZeroHash.SHA256Str : null); rom.SetFieldValue(Models.Metadata.Rom.SHA384Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA384Key) == "null" ? ZeroHash.SHA384Str : null); rom.SetFieldValue(Models.Metadata.Rom.SHA512Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA512Key) == "null" ? ZeroHash.SHA512Str : null); rom.SetFieldValue(Models.Metadata.Rom.SpamSumKey, rom.GetStringFieldValue(Models.Metadata.Rom.SpamSumKey) == "null" ? ZeroHash.SpamSumStr : null); } return rom; } /// /// Return list of required fields missing from a DatItem /// /// List of missing required fields, null or empty if none were found protected virtual List? GetMissingRequiredFields(DatItem datItem) => null; /// /// Get if a machine contains any writable items /// /// DatItems to check /// True if the machine contains at least one writable item, false otherwise /// Empty machines are kept with this protected bool ContainsWritable(List datItems) { // Empty machines are considered writable if (datItems == null || datItems.Count == 0) return true; foreach (DatItem datItem in datItems) { ItemType itemType = datItem.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); if (Array.Exists(SupportedTypes, t => t == itemType)) return true; } return false; } /// /// Get if a machine contains any writable items /// /// DatItems to check /// True if the machine contains at least one writable item, false otherwise /// Empty machines are kept with this protected bool ContainsWritableDB(Dictionary? datItems) { // Empty machines are considered writable if (datItems == null || datItems.Count == 0) return true; foreach (var datItem in datItems) { ItemType itemType = datItem.Value.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); if (Array.Exists(SupportedTypes, t => t == itemType)) return true; } return false; } /// /// Resolve name duplicates in an arbitrary set of DatItems based on the supplied information /// /// List of DatItem objects representing the items to be merged /// A List of DatItem objects representing the renamed items protected List ResolveNames(List items) { // Create the output list List output = []; // First we want to make sure the list is in alphabetical order DatFileTool.Sort(ref items, true); // Now we want to loop through and check names DatItem? lastItem = null; string? lastrenamed = null; int lastid = 0; for (int i = 0; i < items.Count; i++) { DatItem datItem = items[i]; // If we have the first item, we automatically add it if (lastItem == null) { output.Add(datItem); lastItem = datItem; continue; } // Get the last item name, if applicable string lastItemName = lastItem.GetName() ?? lastItem.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue().AsStringValue() ?? string.Empty; // Get the current item name, if applicable string datItemName = datItem.GetName() ?? datItem.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue().AsStringValue() ?? string.Empty; // If the current item exactly matches the last item, then we don't add it #if NET20 || NET35 if ((datItem.GetDuplicateStatus(lastItem) & DupeType.All) != 0) #else if (datItem.GetDuplicateStatus(lastItem).HasFlag(DupeType.All)) #endif { _logger.Verbose($"Exact duplicate found for '{datItemName}'"); continue; } // If the current name matches the previous name, rename the current item else if (datItemName == lastItemName) { _logger.Verbose($"Name duplicate found for '{datItemName}'"); // Get the duplicate suffix datItemName += datItem.GetDuplicateSuffix(); lastrenamed ??= datItemName; // If we have a conflict with the last renamed item, do the right thing if (datItemName == lastrenamed) { lastrenamed = datItemName; datItemName += (lastid == 0 ? string.Empty : "_" + lastid); lastid++; } // If we have no conflict, then we want to reset the lastrenamed and id else { lastrenamed = null; lastid = 0; } // Set the item name back to the datItem datItem.SetName(datItemName); output.Add(datItem); } // Otherwise, we say that we have a valid named file else { output.Add(datItem); lastItem = datItem; lastrenamed = null; lastid = 0; } } // One last sort to make sure this is ordered DatFileTool.Sort(ref output, true); return output; } /// /// Resolve name duplicates in an arbitrary set of DatItems based on the supplied information /// /// List of item ID to DatItem mappings representing the items to be merged /// A List of DatItem objects representing the renamed items protected List> ResolveNamesDB(List> mappings) { // Create the output dict List> output = []; // First we want to make sure the list is in alphabetical order DatFileTool.SortDB(ref mappings, true); // Now we want to loop through and check names DatItem? lastItem = null; string? lastrenamed = null; int lastid = 0; foreach (var datItem in mappings) { // If we have the first item, we automatically add it if (lastItem == null) { output.Add(datItem); lastItem = datItem.Value; continue; } // Get the last item name, if applicable string lastItemName = lastItem.GetName() ?? lastItem.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue().AsStringValue() ?? string.Empty; // Get the current item name, if applicable string datItemName = datItem.Value.GetName() ?? datItem.Value.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue().AsStringValue() ?? string.Empty; // If the current item exactly matches the last item, then we don't add it #if NET20 || NET35 if ((datItem.Value.GetDuplicateStatus(lastItem) & DupeType.All) != 0) #else if (datItem.Value.GetDuplicateStatus(lastItem).HasFlag(DupeType.All)) #endif { _logger.Verbose($"Exact duplicate found for '{datItemName}'"); continue; } // If the current name matches the previous name, rename the current item else if (datItemName == lastItemName) { _logger.Verbose($"Name duplicate found for '{datItemName}'"); // Get the duplicate suffix datItemName += datItem.Value.GetDuplicateSuffix(); lastrenamed ??= datItemName; // If we have a conflict with the last renamed item, do the right thing if (datItemName == lastrenamed) { lastrenamed = datItemName; datItemName += (lastid == 0 ? string.Empty : "_" + lastid); lastid++; } // If we have no conflict, then we want to reset the lastrenamed and id else { lastrenamed = null; lastid = 0; } // Set the item name back to the datItem datItem.Value.SetName(datItemName); output.Add(datItem); } // Otherwise, we say that we have a valid named file else { output.Add(datItem); lastItem = datItem.Value; lastrenamed = null; lastid = 0; } } // One last sort to make sure this is ordered DatFileTool.SortDB(ref output, true); return output; } /// /// Get if an item should be ignored on write /// /// DatItem to check /// True if blank roms should be skipped on output, false otherwise /// True if the item should be skipped on write, false otherwise protected bool ShouldIgnore(DatItem? datItem, bool ignoreBlanks) { // If this is invoked with a null DatItem, we ignore if (datItem == null) { _logger?.Verbose($"Item was skipped because it was null"); return true; } // If the item is supposed to be removed, we ignore if (datItem.GetBoolFieldValue(DatItem.RemoveKey) == true) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); _logger?.Verbose($"Item '{itemString}' was skipped because it was marked for removal"); return true; } // If we have the Blank dat item, we ignore if (datItem is Blank) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); _logger?.Verbose($"Item '{itemString}' was skipped because it was of type 'Blank'"); return true; } // If we're ignoring blanks and we have a Rom if (ignoreBlanks && datItem is Rom rom) { // If we have a 0-size or blank rom, then we ignore long? size = rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey); if (size == 0 || size == null) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); _logger?.Verbose($"Item '{itemString}' was skipped because it had an invalid size"); return true; } } // If we have an item type not in the list of supported values string datFormat = Header?.GetFieldValue(DatHeader.DatFormatKey).ToString() ?? "Unknown Format"; ItemType itemType = datItem.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); if (!Array.Exists(SupportedTypes, t => t == itemType)) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); _logger?.Verbose($"Item '{itemString}' was skipped because it was not supported in {datFormat}"); return true; } // If we have an item with missing required fields List? missingFields = GetMissingRequiredFields(datItem); if (missingFields != null && missingFields.Count != 0) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); #if NET20 || NET35 _logger?.Verbose($"Item '{itemString}' was skipped because it was missing required fields for {datFormat}: {string.Join(", ", [.. missingFields])}"); #else _logger?.Verbose($"Item '{itemString}' was skipped because it was missing required fields for {datFormat}: {string.Join(", ", missingFields)}"); #endif return true; } return false; } #endregion } }