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