diff --git a/SabreTools.DatFiles/ItemDictionaryDB.cs b/SabreTools.DatFiles/ItemDictionaryDB.cs index 67491535..f6a165b4 100644 --- a/SabreTools.DatFiles/ItemDictionaryDB.cs +++ b/SabreTools.DatFiles/ItemDictionaryDB.cs @@ -437,6 +437,18 @@ namespace SabreTools.DatFiles return true; } + /// + /// Remove a machine, returning if it could be removed + /// + public bool RemoveMachine(string machineName) + { + if (string.IsNullOrEmpty(machineName)) + return false; + + var machine = _machines.FirstOrDefault(m => m.Value.GetStringFieldValue(Models.Metadata.Machine.NameKey) == machineName); + return RemoveMachine(machine.Key); + } + /// /// Add an item, returning the insert index /// @@ -955,6 +967,96 @@ namespace SabreTools.DatFiles } } + /// + /// Filter a DAT using 1G1R logic given an ordered set of regions + /// + /// List of regions in order of priority + /// + /// In the most technical sense, the way that the region list is being used does not + /// confine its values to be just regions. Since it's essentially acting like a + /// specialized version of the machine name filter, anything that is usually encapsulated + /// in parenthesis would be matched on, including disc numbers, languages, editions, + /// and anything else commonly used. Please note that, unlike other existing 1G1R + /// solutions, this does not have the ability to contain custom mappings of parent + /// to clone sets based on name, nor does it have the ability to match on the + /// Release DatItem type. + /// + public void SetOneGamePerRegion(List regionList) + { + // If we have null region list, make it empty + regionList ??= []; + + // For sake of ease, the first thing we want to do is bucket by game + BucketBy(ItemKey.Machine, DedupeType.None, norename: true); + + // Then we want to get a mapping of all machines to parents + Dictionary> parents = []; + foreach (string key in SortedKeys) + { + var items = GetDatItemsForBucket(key); + if (items == null || items.Length == 0) + continue; + + var item = items[0]; + var machine = GetMachineForItem(item.Item1); + if (machine.Item2 == null) + continue; + + // Match on CloneOf first + if (!string.IsNullOrEmpty(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey))) + { + if (!parents.ContainsKey(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey)!.ToLowerInvariant())) + parents.Add(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey)!.ToLowerInvariant(), []); + + parents[machine.Item2.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey)!.ToLowerInvariant()].Add(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.ToLowerInvariant()); + } + + // Then by RomOf + else if (!string.IsNullOrEmpty(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.RomOfKey))) + { + if (!parents.ContainsKey(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.RomOfKey)!.ToLowerInvariant())) + parents.Add(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.RomOfKey)!.ToLowerInvariant(), []); + + parents[machine.Item2.GetStringFieldValue(Models.Metadata.Machine.RomOfKey)!.ToLowerInvariant()].Add(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.ToLowerInvariant()); + } + + // Otherwise, treat it as a parent + else + { + if (!parents.ContainsKey(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.ToLowerInvariant())) + parents.Add(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.ToLowerInvariant(), []); + + parents[machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.ToLowerInvariant()].Add(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.ToLowerInvariant()); + } + } + + // Once we have the full list of mappings, filter out games to keep + foreach (string key in parents.Keys) + { + // Find the first machine that matches the regions in order, if possible + string? machine = default; + foreach (string region in regionList) + { + machine = parents[key].FirstOrDefault(m => Regex.IsMatch(m, @"\(.*" + region + @".*\)", RegexOptions.IgnoreCase)); + if (machine != default) + break; + } + + // If we didn't get a match, use the parent + if (machine == default) + machine = key; + + // Remove the key from the list + parents[key].Remove(machine); + + // Remove the rest of the items from this key + parents[key].ForEach(k => RemoveMachine(k)); + } + + // Finally, strip out the parent tags + RemoveTagsFromChild(); + } + /// /// Ensure that all roms are in their own game (or at least try to ensure) /// diff --git a/SabreTools.Filtering/Cleaner.cs b/SabreTools.Filtering/Cleaner.cs index 482da8f7..eded4a29 100644 --- a/SabreTools.Filtering/Cleaner.cs +++ b/SabreTools.Filtering/Cleaner.cs @@ -22,7 +22,6 @@ namespace SabreTools.Filtering /// /// Represents the cleaning operations that need to be performed on a set of items, usually a DAT /// - public class Cleaner { #region Fields @@ -133,8 +132,11 @@ namespace SabreTools.Filtering StripSceneDatesFromItems(datFile); // Run the one rom per game logic, if required - if (OneGamePerRegion == true) + if (OneGamePerRegion == true && RegionList != null) + { SetOneGamePerRegion(datFile); + datFile.ItemsDB.SetOneGamePerRegion(RegionList); + } // Run the one rom per game logic, if required if (OneRomPerGame == true)