diff --git a/SabreTools.DatFiles/DatFile.cs b/SabreTools.DatFiles/DatFile.cs index c8dc1e10..e7db10f1 100644 --- a/SabreTools.DatFiles/DatFile.cs +++ b/SabreTools.DatFiles/DatFile.cs @@ -226,6 +226,15 @@ namespace SabreTools.DatFiles /// 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 /// @@ -234,9 +243,15 @@ namespace SabreTools.DatFiles /// 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); + // Initialize strings string fix, game = item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, + manufacturer = machine!.GetStringFieldValue(Models.Metadata.Machine.ManufacturerKey) ?? string.Empty, + publisher = machine!.GetStringFieldValue(Models.Metadata.Machine.PublisherKey) ?? string.Empty, + category = machine!.GetStringFieldValue(Models.Metadata.Machine.CategoryKey) ?? string.Empty, name = item.GetName() ?? item.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue().AsStringValue() ?? string.Empty, crc = string.Empty, md5 = string.Empty, @@ -281,14 +296,93 @@ namespace SabreTools.DatFiles } // Now do bulk replacement where possible - var machine = item.GetFieldValue(DatItem.MachineKey); fix = fix .Replace("%game%", game) .Replace("%machine%", game) .Replace("%name%", name) - .Replace("%manufacturer%", machine!.GetStringFieldValue(Models.Metadata.Machine.ManufacturerKey) ?? string.Empty) - .Replace("%publisher%", machine!.GetStringFieldValue(Models.Metadata.Machine.PublisherKey) ?? string.Empty) - .Replace("%category%", machine!.GetStringFieldValue(Models.Metadata.Machine.CategoryKey) ?? string.Empty) + .Replace("%manufacturer%", manufacturer) + .Replace("%publisher%", publisher) + .Replace("%category%", category) + .Replace("%crc%", crc) + .Replace("%md5%", md5) + .Replace("%sha1%", sha1) + .Replace("%sha256%", sha256) + .Replace("%sha384%", sha384) + .Replace("%sha512%", sha512) + .Replace("%size%", size) + .Replace("%spamsum%", spamsum); + + return fix; + } + + /// + /// 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((long, DatItem) item, bool prefix) + { + // Get machine for the item + var machine = ItemsDB.GetMachineForItem(item.Item1); + + // Initialize strings + string fix, + game = machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, + manufacturer = machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.ManufacturerKey) ?? string.Empty, + publisher = machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.PublisherKey) ?? string.Empty, + category = machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.CategoryKey) ?? string.Empty, + name = item.Item2.GetName() ?? item.Item2.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue().AsStringValue() ?? string.Empty, + crc = string.Empty, + md5 = string.Empty, + sha1 = string.Empty, + sha256 = string.Empty, + sha384 = string.Empty, + sha512 = string.Empty, + size = string.Empty, + spamsum = string.Empty; + + // If we have a prefix + if (prefix) + fix = Header.GetStringFieldValue(DatHeader.PrefixKey) + (Header.GetBoolFieldValue(DatHeader.QuotesKey) == true ? "\"" : string.Empty); + + // If we have a postfix + else + fix = (Header.GetBoolFieldValue(DatHeader.QuotesKey) == true ? "\"" : string.Empty) + Header.GetStringFieldValue(DatHeader.PostfixKey); + + // Ensure we have the proper values for replacement + if (item.Item2 is Disk disk) + { + md5 = disk.GetStringFieldValue(Models.Metadata.Disk.MD5Key) ?? string.Empty; + sha1 = disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key) ?? string.Empty; + } + else if (item.Item2 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.Item2 is Rom rom) + { + crc = rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) ?? 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("%md5%", md5) .Replace("%sha1%", sha1) @@ -394,21 +488,117 @@ namespace SabreTools.DatFiles 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((long, 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 machine for the item + var machine = ItemsDB.GetMachineForItem(item.Item1); + + // Get the name to update + string? name = (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true + ? item.Item2.GetName() + : machine.Item2!.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 + if (Header.GetFieldValue(DatHeader.OutputDepotKey)?.IsActive == true) + { + if (item.Item2 is Disk disk) + { + // We can only write out if there's a SHA-1 + if (!string.IsNullOrEmpty(disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key))) + { + name = Utilities.GetDepotPath(disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key), Header.GetFieldValue(DatHeader.OutputDepotKey)!.Depth)?.Replace('\\', '/'); + item.Item2.SetName($"{pre}{name}{post}"); + } + } + else if (item.Item2 is Media media) + { + // We can only write out if there's a SHA-1 + if (!string.IsNullOrEmpty(media.GetStringFieldValue(Models.Metadata.Media.SHA1Key))) + { + name = Utilities.GetDepotPath(media.GetStringFieldValue(Models.Metadata.Media.SHA1Key), Header.GetFieldValue(DatHeader.OutputDepotKey)!.Depth)?.Replace('\\', '/'); + item.Item2.SetName($"{pre}{name}{post}"); + } + } + else if (item.Item2 is Rom rom) + { + // We can only write out if there's a SHA-1 + if (!string.IsNullOrEmpty(rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key))) + { + name = Utilities.GetDepotPath(rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key), Header.GetFieldValue(DatHeader.OutputDepotKey)!.Depth)?.Replace('\\', '/'); + item.Item2.SetName($"{pre}{name}{post}"); + } + } + + return; + } + + if (!string.IsNullOrEmpty(Header.GetStringFieldValue(DatHeader.ReplaceExtensionKey)) || Header.GetBoolFieldValue(DatHeader.RemoveExtensionKey) == true) + { + if (Header.GetBoolFieldValue(DatHeader.RemoveExtensionKey) == 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) + Header.GetStringFieldValue(DatHeader.ReplaceExtensionKey)); + } + } + + if (!string.IsNullOrEmpty(Header.GetStringFieldValue(DatHeader.AddExtensionKey))) + name += Header.GetStringFieldValue(DatHeader.AddExtensionKey); + + if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true && Header.GetBoolFieldValue(DatHeader.GameNameKey) == true) + name = Path.Combine(machine.Item2!.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.Item2.SetName(name); + else if (machine.Item2 != null) + machine.Item2.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 + /// DatItem to check for "null" status /// Cleaned DatItem - protected DatItem ProcessNullifiedItem(DatItem datItem) + protected DatItem ProcessNullifiedItem(DatItem item) { // If we don't have a Rom, we can ignore it - if (datItem is not Rom rom) - return datItem; + if (item is not Rom rom) + return item; // 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: {datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)}"); + logger.Verbose($"Empty folder found: {item.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)}"); rom.SetName(rom.GetName() == "null" ? "-" : rom.GetName()); rom.SetFieldValue(Models.Metadata.Rom.SizeKey, Constants.SizeZero.ToString()); @@ -427,18 +617,21 @@ namespace SabreTools.DatFiles /// /// Process any DatItems that are "null", usually created from directory population /// - /// DatItem to check for "null" status + /// DatItem to check for "null" status /// Cleaned DatItem - protected (long, DatItem) ProcessNullifiedItem((long, DatItem) datItem) + protected (long, DatItem) ProcessNullifiedItem((long, DatItem) item) { // If we don't have a Rom, we can ignore it - if (datItem.Item2 is not Rom rom) - return datItem; + if (item.Item2 is not Rom rom) + return item; + + // Get machine for the item + var machine = ItemsDB.GetMachineForItem(item.Item1); // 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: {datItem.Item2.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)}"); + logger.Verbose($"Empty folder found: {machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey)}"); rom.SetName(rom.GetName() == "null" ? "-" : rom.GetName()); rom.SetFieldValue(Models.Metadata.Rom.SizeKey, Constants.SizeZero.ToString()); @@ -451,7 +644,7 @@ namespace SabreTools.DatFiles rom.SetFieldValue(Models.Metadata.Rom.SpamSumKey, rom.GetStringFieldValue(Models.Metadata.Rom.SpamSumKey) == "null" ? Constants.SpamSumZero : null); } - return (datItem.Item1, rom); + return (item.Item1, rom); } /// diff --git a/SabreTools.DatFiles/Formats/Missfile.cs b/SabreTools.DatFiles/Formats/Missfile.cs index 40b4e950..539b05b3 100644 --- a/SabreTools.DatFiles/Formats/Missfile.cs +++ b/SabreTools.DatFiles/Formats/Missfile.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using SabreTools.Core; using SabreTools.DatItems; @@ -96,6 +97,68 @@ namespace SabreTools.DatFiles.Formats return true; } + /// + public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + logger.User($"Writing to '{outfile}'..."); + FileStream fs = File.Create(outfile); + + // If we get back null for some reason, just log and return + if (fs == null) + { + logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + StreamWriter sw = new(fs, new UTF8Encoding(false)); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in ItemsDB.SortedKeys) + { + // If this machine doesn't contain any writable items, skip + var items = ItemsDB.GetItemsForBucket(key, filter: true); + if (items == null || !ContainsWritable(items)) + continue; + + // Resolve the names in the block + items = DatItem.ResolveNamesDB(items.ToConcurrentList()).ToArray(); + + for (int index = 0; index < items.Length; index++) + { + // Check for a "null" item + var datItem = items[index]; + datItem = ProcessNullifiedItem(datItem); + + // Get the machine for the item + var machine = ItemsDB.GetMachineForItem(datItem.Item1); + + // Write out the item if we're using machine names or we're not ignoring + if (!Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true || !ShouldIgnore(datItem, ignoreblanks)) + WriteDatItemDB(sw, datItem, lastgame); + + // Set the new data to compare against + lastgame = machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey); + } + } + + logger.User($"'{outfile}' written!{Environment.NewLine}"); + sw.Dispose(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex); + return false; + } + + return true; + } + /// /// Write out DatItem using the supplied StreamWriter /// @@ -115,5 +178,28 @@ namespace SabreTools.DatFiles.Formats sw.Flush(); } + + /// + /// Write out DatItem using the supplied StreamWriter + /// + /// StreamWriter to output to + /// DatItem object to be output + /// The name of the last game to be output + private void WriteDatItemDB(StreamWriter sw, (long, DatItem) datItem, string? lastgame) + { + // Get the machine for the item + var machine = ItemsDB.GetMachineForItem(datItem.Item1); + + // Process the item name + ProcessItemNameDB(datItem, false, forceRomName: false); + + // Romba mode automatically uses item name + if (Header.GetFieldValue(DatHeader.OutputDepotKey)?.IsActive == true || Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true) + sw.Write($"{datItem.Item2.GetName() ?? string.Empty}\n"); + else if (!Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true && machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey) != lastgame) + sw.Write($"{machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty}\n"); + + sw.Flush(); + } } } diff --git a/SabreTools.DatFiles/Formats/SabreJSON.cs b/SabreTools.DatFiles/Formats/SabreJSON.cs index ad1df6a3..3a8dad46 100644 --- a/SabreTools.DatFiles/Formats/SabreJSON.cs +++ b/SabreTools.DatFiles/Formats/SabreJSON.cs @@ -442,6 +442,89 @@ namespace SabreTools.DatFiles.Formats return true; } + /// + public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + logger.User($"Writing to '{outfile}'..."); + FileStream fs = System.IO.File.Create(outfile); + + // If we get back null for some reason, just log and return + if (fs == null) + { + logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + StreamWriter sw = new(fs, new UTF8Encoding(false)); + JsonTextWriter jtw = new(sw) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; + + // Write out the header + WriteHeader(jtw); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in ItemsDB.SortedKeys) + { + // If this machine doesn't contain any writable items, skip + var items = ItemsDB.GetItemsForBucket(key, filter: true); + if (items == null || !ContainsWritable(items)) + continue; + + // Resolve the names in the block + items = DatItem.ResolveNamesDB(items.ToConcurrentList()).ToArray(); + + for (int index = 0; index < items.Length; index++) + { + var datItem = items[index]; + + // Get the machine for the item + var machine = ItemsDB.GetMachineForItem(datItem.Item1); + + // If we have a different game and we're not at the start of the list, output the end of last item + if (lastgame != null && !string.Equals(lastgame, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase)) + SabreJSON.WriteEndGame(jtw); + + // If we have a new game, output the beginning of the new item + if (lastgame == null || !string.Equals(lastgame, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase)) + WriteStartGameDB(jtw, datItem); + + // Check for a "null" item + datItem = ProcessNullifiedItem(datItem); + + // Write out the item if we're not ignoring + if (!ShouldIgnore(datItem, ignoreblanks)) + WriteDatItemDB(jtw, datItem); + + // Set the new data to compare against + lastgame = machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey); + } + } + + // Write the file footer out + SabreJSON.WriteFooter(jtw); + + logger.User($"'{outfile}' written!{Environment.NewLine}"); + jtw.Close(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex); + return false; + } + + return true; + } + /// /// Write out DAT header using the supplied JsonTextWriter /// @@ -486,6 +569,34 @@ namespace SabreTools.DatFiles.Formats jtw.Flush(); } + /// + /// Write out Game start using the supplied JsonTextWriter + /// + /// JsonTextWriter to output to + /// DatItem object to be output + private void WriteStartGameDB(JsonTextWriter jtw, (long, DatItem) datItem) + { + // Get the machine for the item + var machine = ItemsDB.GetMachineForItem(datItem.Item1); + + // No game should start with a path separator + if (!string.IsNullOrEmpty(machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey))) + machine.Item2!.SetFieldValue(Models.Metadata.Machine.NameKey, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.TrimStart(Path.DirectorySeparatorChar)); + + // Build the state + jtw.WriteStartObject(); + + // Write the Machine + jtw.WritePropertyName("machine"); + JsonSerializer js = new() { Formatting = Formatting.Indented }; + js.Serialize(jtw, machine.Item2!); + + jtw.WritePropertyName("items"); + jtw.WriteStartArray(); + + jtw.Flush(); + } + /// /// Write out Game end using the supplied JsonTextWriter /// @@ -525,6 +636,30 @@ namespace SabreTools.DatFiles.Formats jtw.Flush(); } + /// + /// Write out DatItem using the supplied JsonTextWriter + /// + /// JsonTextWriter to output to + /// DatItem object to be output + private void WriteDatItemDB(JsonTextWriter jtw, (long, DatItem) datItem) + { + // Pre-process the item name + ProcessItemNameDB(datItem, true); + + // Build the state + jtw.WriteStartObject(); + + // Write the DatItem + jtw.WritePropertyName("datitem"); + JsonSerializer js = new() { ContractResolver = new BaseFirstContractResolver(), Formatting = Formatting.Indented }; + js.Serialize(jtw, datItem.Item2); + + // End item + jtw.WriteEndObject(); + + jtw.Flush(); + } + /// /// Write out DAT footer using the supplied JsonTextWriter /// diff --git a/SabreTools.DatFiles/Formats/SabreXML.cs b/SabreTools.DatFiles/Formats/SabreXML.cs index 1a7ab9a5..1bc09a5a 100644 --- a/SabreTools.DatFiles/Formats/SabreXML.cs +++ b/SabreTools.DatFiles/Formats/SabreXML.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Text; using System.Xml; using System.Xml.Schema; @@ -272,6 +273,90 @@ namespace SabreTools.DatFiles.Formats return true; } + /// + public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + logger.User($"Writing to '{outfile}'..."); + FileStream fs = File.Create(outfile); + + // If we get back null for some reason, just log and return + if (fs == null) + { + logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + XmlTextWriter xtw = new(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1, + }; + + // Write out the header + WriteHeader(xtw); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in ItemsDB.SortedKeys) + { + // If this machine doesn't contain any writable items, skip + var items = ItemsDB.GetItemsForBucket(key, filter: true); + if (items == null || !ContainsWritable(items)) + continue; + + // Resolve the names in the block + items = DatItem.ResolveNamesDB(items.ToConcurrentList()).ToArray(); + + for (int index = 0; index < items.Length; index++) + { + var datItem = items[index]; + + // Get the machine for the item + var machine = ItemsDB.GetMachineForItem(datItem.Item1); + + // If we have a different game and we're not at the start of the list, output the end of last item + if (lastgame != null && !string.Equals(lastgame, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase)) + WriteEndGame(xtw); + + // If we have a new game, output the beginning of the new item + if (lastgame == null || !string.Equals(lastgame, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase)) + WriteStartGameDB(xtw, datItem); + + // Check for a "null" item + datItem = ProcessNullifiedItem(datItem); + + // Write out the item if we're not ignoring + if (!ShouldIgnore(datItem, ignoreblanks)) + WriteDatItemDB(xtw, datItem); + + // Set the new data to compare against + lastgame = machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey); + } + } + + // Write the file footer out + WriteFooter(xtw); + + logger.User($"'{outfile}' written!{Environment.NewLine}"); +#if NET452_OR_GREATER + xtw.Dispose(); +#endif + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex); + return false; + } + + return true; + } + /// /// Write out DAT header using the supplied StreamWriter /// @@ -314,6 +399,31 @@ namespace SabreTools.DatFiles.Formats xtw.Flush(); } + /// + /// Write out Game start using the supplied StreamWriter + /// + /// XmlTextWriter to output to + /// DatItem object to be output + private void WriteStartGameDB(XmlTextWriter xtw, (long, DatItem) datItem) + { + // Get the machine for the item + var machine = ItemsDB.GetMachineForItem(datItem.Item1); + + // No game should start with a path separator + machine.Item2!.SetFieldValue(Models.Metadata.Machine.NameKey, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey)?.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty); + + // Write the machine + xtw.WriteStartElement("directory"); + XmlSerializer xs = new(typeof(Machine)); + XmlSerializerNamespaces ns = new(); + ns.Add("", ""); + xs.Serialize(xtw, machine.Item2, ns); + + xtw.WriteStartElement("files"); + + xtw.Flush(); + } + /// /// Write out Game start using the supplied StreamWriter /// @@ -348,6 +458,25 @@ namespace SabreTools.DatFiles.Formats xtw.Flush(); } + /// + /// Write out DatItem using the supplied StreamWriter + /// + /// XmlTextWriter to output to + /// DatItem object to be output + private void WriteDatItemDB(XmlTextWriter xtw, (long, DatItem) datItem) + { + // Pre-process the item name + ProcessItemNameDB(datItem, true); + + // Write the DatItem + XmlSerializer xs = new(typeof(DatItem)); + XmlSerializerNamespaces ns = new(); + ns.Add("", ""); + xs.Serialize(xtw, datItem.Item2, ns); + + xtw.Flush(); + } + /// /// Write out DAT footer using the supplied StreamWriter /// diff --git a/SabreTools.DatFiles/SerializableDatFile.cs b/SabreTools.DatFiles/SerializableDatFile.cs index 84062029..aa4fa3f9 100644 --- a/SabreTools.DatFiles/SerializableDatFile.cs +++ b/SabreTools.DatFiles/SerializableDatFile.cs @@ -61,5 +61,31 @@ namespace SabreTools.DatFiles logger.User($"'{outfile}' written!{Environment.NewLine}"); return true; } + + /// + public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + logger.User($"Writing to '{outfile}'..."); + + // Serialize the input file in two steps + var internalFormat = ConvertMetadata(ignoreblanks); + var specificFormat = Activator.CreateInstance().Deserialize(internalFormat); + if (!Activator.CreateInstance().Serialize(specificFormat, outfile)) + { + logger.Warning($"File '{outfile}' could not be written! See the log for more details."); + return false; + } + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex); + return false; + } + + logger.User($"'{outfile}' written!{Environment.NewLine}"); + return true; + } } } diff --git a/SabreTools.DatItems/DatItem.cs b/SabreTools.DatItems/DatItem.cs index 3660cba2..d539d1e0 100644 --- a/SabreTools.DatItems/DatItem.cs +++ b/SabreTools.DatItems/DatItem.cs @@ -168,7 +168,7 @@ namespace SabreTools.DatItems if (item?.GetFieldValue(DatItem.MachineKey) == null) return; - if (item.GetFieldValue(DatItem.MachineKey)!.Clone() is Machine cloned) + if (item!.GetFieldValue(DatItem.MachineKey)!.Clone() is Machine cloned) SetFieldValue(DatItem.MachineKey, cloned); } @@ -704,6 +704,103 @@ namespace SabreTools.DatItems return output; } + /// + /// Resolve name duplicates in an arbitrary set of ROMs based on the supplied information + /// + /// List of File objects representing the roms to be merged + /// A List of DatItem objects representing the renamed roms + public static ConcurrentList<(long, DatItem)> ResolveNamesDB(ConcurrentList<(long, DatItem)> infiles) + { + // Create the output list + ConcurrentList<(long, DatItem)> output = []; + + // First we want to make sure the list is in alphabetical order + Sort(ref infiles, true); + + // Now we want to loop through and check names + (long, DatItem?) lastItem = (-1, null); + string? lastrenamed = null; + int lastid = 0; + for (int i = 0; i < infiles.Count; i++) + { + var datItem = infiles[i]; + + // If we have the first item, we automatically add it + if (lastItem.Item2 == null) + { + output.Add(datItem); + lastItem = datItem; + continue; + } + + // Get the last item name, if applicable + string lastItemName = lastItem.Item2.GetName() + ?? lastItem.Item2.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue().AsStringValue() + ?? string.Empty; + + // Get the current item name, if applicable + string datItemName = datItem.Item2.GetName() + ?? datItem.Item2.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 NETFRAMEWORK + if ((datItem.Item2.GetDuplicateStatus(lastItem.Item2) & DupeType.All) != 0) +#else + if (datItem.Item2.GetDuplicateStatus(lastItem.Item2).HasFlag(DupeType.All)) +#endif + { + staticLogger.Verbose($"Exact duplicate found for '{datItemName}'"); + continue; + } + + // If the current name matches the previous name, rename the current item + else if (datItemName == lastItemName) + { + staticLogger.Verbose($"Name duplicate found for '{datItemName}'"); + + if (datItem.Item2 is Disk || datItem.Item2 is Formats.File || datItem.Item2 is Media || datItem.Item2 is Rom) + { + datItemName += GetDuplicateSuffix(datItem.Item2); + 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.Item2.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 + Sort(ref output, true); + + return output; + } + /// /// Get duplicate suffix based on the item type /// @@ -772,6 +869,59 @@ namespace SabreTools.DatItems return true; } + /// + /// Sort a list of File objects by SourceID, Game, and Name (in order) + /// + /// List of File objects representing the roms to be sorted + /// True if files are not renamed, false otherwise + /// True if it sorted correctly, false otherwise + public static bool Sort(ref ConcurrentList<(long, DatItem)> roms, bool norename) + { + roms.Sort(delegate ((long, DatItem) x, (long, DatItem) y) + { + try + { + var nc = new NaturalComparer(); + + // If machine names don't match + string? xMachineName = x.Item2.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey); + string? yMachineName = y.Item2.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey); + if (xMachineName != yMachineName) + return nc.Compare(xMachineName, yMachineName); + + // If types don't match + string? xType = x.Item2.GetStringFieldValue(Models.Metadata.DatItem.TypeKey); + string? yType = y.Item2.GetStringFieldValue(Models.Metadata.DatItem.TypeKey); + if (xType != yType) + return xType.AsEnumValue() - yType.AsEnumValue(); + + // If directory names don't match + string? xDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(x.Item2.GetName() ?? string.Empty)); + string? yDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(y.Item2.GetName() ?? string.Empty)); + if (xDirectoryName != yDirectoryName) + return nc.Compare(xDirectoryName, yDirectoryName); + + // If item names don't match + string? xName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(x.Item2.GetName() ?? string.Empty)); + string? yName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(y.Item2.GetName() ?? string.Empty)); + if (xName != yName) + return nc.Compare(xName, yName); + + // Otherwise, compare on machine or source, depending on the flag + int? xSourceIndex = x.Item2.GetFieldValue(DatItem.SourceKey)?.Index; + int? ySourceIndex = y.Item2.GetFieldValue(DatItem.SourceKey)?.Index; + return (norename ? nc.Compare(xMachineName, yMachineName) : (xSourceIndex - ySourceIndex) ?? 0); + } + catch + { + // Absorb the error + return 0; + } + }); + + return true; + } + #endregion #endregion // Static Methods