using System; using System.Collections.Generic; using System.IO; using SabreTools.Core.Tools; using SabreTools.DatFiles; using SabreTools.DatItems; using SabreTools.IO.Logging; namespace SabreTools.DatTools { /// /// Represents the cleaning operations that need to be performed on a set of items, usually a DAT /// public class Cleaner { #region Fields /// /// Clean all names to WoD standards /// public bool Clean { get; set; } /// /// Deduplicate items using the given method /// public DedupeType DedupeRoms { get; set; } /// /// Set Machine Description from Machine Name /// public bool DescriptionAsName { get; set; } /// /// Keep machines that don't contain any items /// public bool KeepEmptyGames { get; set; } /// /// Enable "One Rom, One Region (1G1R)" mode /// public bool OneGamePerRegion { get; set; } /// /// Ordered list of regions for "One Rom, One Region (1G1R)" mode /// public List? RegionList { get; set; } /// /// Ensure each rom is in their own game /// public bool OneRomPerGame { get; set; } /// /// Remove all unicode characters /// public bool RemoveUnicode { get; set; } /// /// Include root directory when determing trim sizes /// public string? Root { get; set; } /// /// Remove scene dates from the beginning of machine names /// public bool SceneDateStrip { get; set; } /// /// Change all machine names to "!" /// public bool Single { get; set; } /// /// Trim total machine and item name to not exceed NTFS limits /// public bool Trim { get; set; } #endregion #region Logging /// /// Logging object /// private readonly Logger _logger = new(); #endregion #region Running /// /// Apply cleaning methods to the DatFile /// /// Current DatFile object to run operations on /// True if the error that is thrown should be thrown back to the caller, false otherwise /// True if cleaning was successful, false on error public bool ApplyCleaning(DatFile datFile, bool throwOnError = false) { InternalStopwatch watch = new("Applying cleaning steps to DAT"); try { // Perform item-level cleaning CleanDatItems(datFile); CleanDatItemsDB(datFile); // Bucket and dedupe according to the flag if (DedupeRoms == DedupeType.Full) { datFile.BucketBy(ItemKey.CRC); datFile.Deduplicate(DedupeRoms); } else if (DedupeRoms == DedupeType.Game) { datFile.BucketBy(ItemKey.Machine); datFile.Deduplicate(DedupeRoms); } // Process description to machine name if (DescriptionAsName == true) datFile.MachineDescriptionToName(throwOnError); // If we are removing scene dates, do that now if (SceneDateStrip == true) datFile.StripSceneDatesFromItems(); // Run the one rom per game logic, if required if (OneGamePerRegion == true && RegionList != null) datFile.SetOneGamePerRegion(RegionList); // Run the one rom per game logic, if required if (OneRomPerGame == true) datFile.SetOneRomPerGame(); // Remove all marked items datFile.ClearMarked(); // We remove any blanks, if we aren't supposed to have any if (KeepEmptyGames == false) datFile.ClearEmpty(); } catch (Exception ex) when (!throwOnError) { _logger.Error(ex); return false; } finally { watch.Stop(); } return true; } /// /// Clean individual items based on the current filter /// /// Current DatFile object to run operations on internal void CleanDatItems(DatFile datFile) { foreach (string key in datFile.Items.SortedKeys) { // For every item in the current key var items = datFile.GetItemsForBucket(key); if (items == null) continue; foreach (DatItem item in items) { // If we have a null item, we can't clean it it if (item == null) continue; // Run cleaning per item CleanDatItem(item); } } } /// /// Clean individual items based on the current filter /// /// Current DatFile object to run operations on internal void CleanDatItemsDB(DatFile datFile) { List keys = [.. datFile.ItemsDB.SortedKeys]; foreach (string key in keys) { // For every item in the current key var items = datFile.GetItemsForBucketDB(key); if (items == null) continue; foreach (var item in items) { // If we have a null item, we can't clean it it if (item.Value == null) continue; // Run cleaning per item CleanDatItemDB(datFile.ItemsDB, item); } } } /// /// Clean a DatItem according to the cleaner /// /// DatItem to clean internal void CleanDatItem(DatItem datItem) { // Get the machine associated with the item, if possible var machine = datItem.GetFieldValue(DatItem.MachineKey); if (machine == null) return; // Get the fields for processing string? machineName = machine.GetStringFieldValue(Models.Metadata.Machine.NameKey); string? machineDesc = machine.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey); string? datItemName = datItem.GetName(); // If we're stripping unicode characters, strip machine name and description if (RemoveUnicode) { machineName = TextHelper.RemoveUnicodeCharacters(machineName); machineDesc = TextHelper.RemoveUnicodeCharacters(machineDesc); datItemName = TextHelper.RemoveUnicodeCharacters(datItemName); } // If we're in cleaning mode, sanitize machine name and description if (Clean) { machineName = TextHelper.NormalizeCharacters(machineName); machineDesc = TextHelper.NormalizeCharacters(machineDesc); } // If we are in single game mode, rename the machine if (Single) { machineName = "!"; machineDesc = "!"; } // If we are in NTFS trim mode, trim the item name if (Trim && datItemName != null) { // Windows max name length is 260 int usableLength = 260 - (machineName?.Length ?? 0) - (Root?.Length ?? 0); if (datItemName.Length > usableLength) { string ext = Path.GetExtension(datItemName); datItemName = datItemName.Substring(0, usableLength - ext.Length) + ext; } } // Set the fields back, if necessary machine.SetFieldValue(Models.Metadata.Machine.NameKey, machineName); machine.SetFieldValue(Models.Metadata.Machine.DescriptionKey, machineDesc); datItem.SetName(datItemName); } /// /// Clean a DatItem according to the cleaner /// /// ItemDictionaryDB to get machine information from /// DatItem to clean internal void CleanDatItemDB(ItemDictionaryDB db, KeyValuePair datItem) { // Get the machine associated with the item, if possible var machine = db.GetMachineForItem(datItem.Key); if (machine.Value == null) return; // Get the fields for processing string? machineName = machine.Value.GetStringFieldValue(Models.Metadata.Machine.NameKey); string? machineDesc = machine.Value.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey); string? datItemName = datItem.Value.GetName(); // If we're stripping unicode characters, strip machine name and description if (RemoveUnicode) { machineName = TextHelper.RemoveUnicodeCharacters(machineName); machineDesc = TextHelper.RemoveUnicodeCharacters(machineDesc); datItemName = TextHelper.RemoveUnicodeCharacters(datItemName); } // If we're in cleaning mode, sanitize machine name and description if (Clean) { machineName = TextHelper.NormalizeCharacters(machineName); machineDesc = TextHelper.NormalizeCharacters(machineDesc); } // If we are in single game mode, rename the machine if (Single) { machineName = "!"; machineDesc = "!"; } // If we are in NTFS trim mode, trim the item name if (Trim && datItemName != null) { // Windows max name length is 260 int usableLength = 260 - (machineName?.Length ?? 0) - (Root?.Length ?? 0); if (datItemName.Length > usableLength) { string ext = Path.GetExtension(datItemName); datItemName = datItemName.Substring(0, usableLength - ext.Length) + ext; } } // Set the fields back, if necessary machine.Value.SetFieldValue(Models.Metadata.Machine.NameKey, machineName); machine.Value.SetFieldValue(Models.Metadata.Machine.DescriptionKey, machineDesc); datItem.Value.SetName(datItemName); } #endregion } }