diff --git a/SabreTools.DatFiles/Formats/AttractMode.Writer.cs b/SabreTools.DatFiles/Formats/AttractMode.Writer.cs index 7b89fa16..fad1070a 100644 --- a/SabreTools.DatFiles/Formats/AttractMode.Writer.cs +++ b/SabreTools.DatFiles/Formats/AttractMode.Writer.cs @@ -104,8 +104,6 @@ namespace SabreTools.DatFiles.Formats } } - // TODO: Populate the games - return rows.ToArray(); } diff --git a/SabreTools.DatFiles/Formats/ClrMamePro.Writer.cs b/SabreTools.DatFiles/Formats/ClrMamePro.Writer.cs index 50e85bda..292ab6a3 100644 --- a/SabreTools.DatFiles/Formats/ClrMamePro.Writer.cs +++ b/SabreTools.DatFiles/Formats/ClrMamePro.Writer.cs @@ -317,8 +317,6 @@ namespace SabreTools.DatFiles.Formats games.Add(game); } - // TODO: Populate the games - return games.ToArray(); } diff --git a/SabreTools.DatFiles/Formats/EverdriveSMDB.Reader.cs b/SabreTools.DatFiles/Formats/EverdriveSMDB.Reader.cs new file mode 100644 index 00000000..34b6c52e --- /dev/null +++ b/SabreTools.DatFiles/Formats/EverdriveSMDB.Reader.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using System.Linq; +using SabreTools.Core; +using SabreTools.Core.Tools; +using SabreTools.DatItems; +using SabreTools.DatItems.Formats; + +namespace SabreTools.DatFiles.Formats +{ + /// + /// Represents parsing and writing of an Everdrive SMDB file + /// + internal partial class EverdriveSMDB : DatFile + { + /// + public override void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false) + { + try + { + // Deserialize the input file + var metadataFile = Serialization.EverdriveSMDB.Deserialize(filename); + + // Convert the row data to the internal format + ConvertRows(metadataFile?.Row, filename, indexId, statsOnly); + } + catch (Exception ex) when (!throwOnError) + { + string message = $"'{filename}' - An error occurred during parsing"; + logger.Error(ex, message); + } + } + + #region Converters + + /// + /// Create a machine from the filename + /// + /// Filename to derive from + /// Filled machine and new filename on success, null on error + private static (Machine?, string?) DeriveMachine(string filename) + { + // If the filename is missing, we can't do anything + if (string.IsNullOrWhiteSpace(filename)) + return (null, null); + + string machineName = Path.GetFileNameWithoutExtension(filename); + if (filename.Contains('/')) + { + string[] split = filename.Split('/'); + machineName = split[0]; + filename = filename[(machineName.Length + 1)..]; + } + else if (filename.Contains('\\')) + { + string[] split = filename.Split('\\'); + machineName = split[0]; + filename = filename[(machineName.Length + 1)..]; + } + + var machine = new Machine { Name = machineName }; + return (machine, filename); + } + + /// + /// Convert rows information + /// + /// Array of deserialized models to convert + /// Name of the file to be parsed + /// Index ID for the DAT + /// True to only add item statistics while parsing, false otherwise + private void ConvertRows(Models.EverdriveSMDB.Row[]? rows, string filename, int indexId, bool statsOnly) + { + // If the rows array is missing, we can't do anything + if (rows == null || !rows.Any()) + return; + + // Loop through the rows and add + foreach (var row in rows) + { + ConvertRow(row, filename, indexId, statsOnly); + } + } + + /// + /// Convert rows information + /// + /// Deserialized model to convert + /// Name of the file to be parsed + /// Index ID for the DAT + /// True to only add item statistics while parsing, false otherwise + private void ConvertRow(Models.EverdriveSMDB.Row? row, string filename, int indexId, bool statsOnly) + { + // If the row is missing, we can't do anything + if (row == null) + return; + + (var machine, string name) = DeriveMachine(row.Name); + if (machine == null) + machine = new Machine { Name = Path.GetFileNameWithoutExtension(row.Name) }; + + var rom = new Rom() + { + Name = name, + Size = Utilities.CleanLong(row.Size), + CRC = row.CRC32, + MD5 = row.MD5, + SHA1 = row.SHA1, + SHA256 = row.SHA256, + ItemStatus = ItemStatus.None, + + Machine = machine, + + Source = new Source + { + Index = indexId, + Name = filename, + }, + }; + + // Now process and add the rom + ParseAddHelper(rom, statsOnly); + } + + #endregion + } +} diff --git a/SabreTools.DatFiles/Formats/EverdriveSMDB.Writer.cs b/SabreTools.DatFiles/Formats/EverdriveSMDB.Writer.cs new file mode 100644 index 00000000..d3cdf667 --- /dev/null +++ b/SabreTools.DatFiles/Formats/EverdriveSMDB.Writer.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using SabreTools.Core; +using SabreTools.DatItems; +using SabreTools.DatItems.Formats; +using SabreTools.IO.Writers; + +namespace SabreTools.DatFiles.Formats +{ + /// + /// Represents parsing and writing of an Everdrive SMDB file + /// + internal partial class EverdriveSMDB : DatFile + { + /// + protected override ItemType[] GetSupportedTypes() + { + return new ItemType[] { ItemType.Rom }; + } + + /// + protected override List GetMissingRequiredFields(DatItem datItem) + { + List missingFields = new(); + + // Check item name + if (string.IsNullOrWhiteSpace(datItem.GetName())) + missingFields.Add(DatItemField.Name); + + switch (datItem) + { + case Rom rom: + if (string.IsNullOrWhiteSpace(rom.SHA256)) + missingFields.Add(DatItemField.SHA256); + if (string.IsNullOrWhiteSpace(rom.SHA1)) + missingFields.Add(DatItemField.SHA1); + if (string.IsNullOrWhiteSpace(rom.MD5)) + missingFields.Add(DatItemField.MD5); + if (string.IsNullOrWhiteSpace(rom.CRC)) + missingFields.Add(DatItemField.CRC); + break; + } + + return missingFields; + } + + /// + public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + logger.User($"Writing to '{outfile}'..."); + + var metadataFile = CreateMetadataFile(ignoreblanks); + if (!Serialization.EverdriveSMDB.SerializeToFile(metadataFile, 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; + } + + return true; + } + + #region Converters + + /// + /// Create a MetadataFile from the current internal information + /// + /// True if blank roms should be skipped on output, false otherwise + private Models.EverdriveSMDB.MetadataFile CreateMetadataFile(bool ignoreblanks) + { + var metadataFile = new Models.EverdriveSMDB.MetadataFile + { + Row = CreateRows(ignoreblanks) + }; + return metadataFile; + } + + /// + /// Create an array of Row from the current internal information + /// + /// True if blank roms should be skipped on output, false otherwise + private Models.EverdriveSMDB.Row[]? CreateRows(bool ignoreblanks) + { + // If we don't have items, we can't do anything + if (this.Items == null || !this.Items.Any()) + return null; + + // Create a list of hold the rows + var rows = new List(); + + // Loop through the sorted items and create games for them + foreach (string key in Items.SortedKeys) + { + var items = Items.FilteredItems(key); + if (items == null || !items.Any()) + continue; + + // Loop through and convert the items to respective lists + foreach (var item in items) + { + // Skip if we're ignoring the item + if (ShouldIgnore(item, ignoreblanks)) + continue; + + switch (item) + { + case Rom rom: + rows.Add(CreateRow(rom)); + break; + } + } + } + + return rows.ToArray(); + } + + /// + /// Create a Row from the current Rom DatItem + /// + private static Models.EverdriveSMDB.Row CreateRow(Rom rom) + { + var row = new Models.EverdriveSMDB.Row + { + SHA256 = rom.SHA256, + Name = $"{rom.Machine.Name}/{rom.Name}", + SHA1 = rom.SHA1, + MD5 = rom.MD5, + CRC32 = rom.CRC, + Size = rom.Size?.ToString(), + }; + return row; + } + + #endregion + } +} diff --git a/SabreTools.DatFiles/Formats/EverdriveSmdb.cs b/SabreTools.DatFiles/Formats/EverdriveSmdb.cs index f20ec5bf..64613e80 100644 --- a/SabreTools.DatFiles/Formats/EverdriveSmdb.cs +++ b/SabreTools.DatFiles/Formats/EverdriveSmdb.cs @@ -1,21 +1,9 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using SabreTools.Core; -using SabreTools.Core.Tools; -using SabreTools.DatItems; -using SabreTools.DatItems.Formats; -using SabreTools.IO; -using SabreTools.IO.Readers; -using SabreTools.IO.Writers; - -namespace SabreTools.DatFiles.Formats +namespace SabreTools.DatFiles.Formats { /// /// Represents parsing and writing of an Everdrive SMDB file /// - internal class EverdriveSMDB : DatFile + internal partial class EverdriveSMDB : DatFile { /// /// Constructor designed for casting a base DatFile @@ -25,192 +13,5 @@ namespace SabreTools.DatFiles.Formats : base(datFile) { } - - /// - public override void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false) - { - // Open a file reader - Encoding enc = filename.GetEncoding(); - SeparatedValueReader svr = new(System.IO.File.OpenRead(filename), enc) - { - Header = false, - Quotes = false, - Separator = '\t', - VerifyFieldCount = false, - }; - - while (!svr.EndOfStream) - { - try - { - // If we can't read the next line, break - if (!svr.ReadNextLine()) - break; - - // If the line returns null somehow, skip - if (svr.Line == null) - continue; - - /* - The gameinfo order is as follows - 0 - SHA-256 - 1 - Machine Name/Filename - 2 - SHA-1 - 3 - MD5 - 4 - CRC32 - 5 - Size (Optional) - */ - - string[] fullname = svr.Line[1].Split('/'); - - Rom rom = new() - { - Name = svr.Line[1][(fullname[0].Length + 1)..], - Size = null, // No size provided, but we don't want the size being 0 - CRC = svr.Line[4], - MD5 = svr.Line[3], - SHA1 = svr.Line[2], - SHA256 = svr.Line[0], - ItemStatus = ItemStatus.None, - - Machine = new Machine - { - Name = fullname[0], - Description = fullname[0], - }, - - Source = new Source - { - Index = indexId, - Name = filename, - }, - }; - - // Size in SMDB files is optional - if (svr.Line.Count > 5) - rom.Size = Utilities.CleanLong(svr.Line[5]); - - // Now process and add the rom - ParseAddHelper(rom, statsOnly); - } - catch (Exception ex) when (!throwOnError) - { - string message = $"'{filename}' - There was an error parsing line {svr.LineNumber} '{svr.CurrentLine}'"; - logger.Error(ex, message); - } - } - - svr.Dispose(); - } - - /// - protected override ItemType[] GetSupportedTypes() - { - return new ItemType[] { ItemType.Rom }; - } - - /// - protected override List GetMissingRequiredFields(DatItem datItem) - { - // TODO: Check required fields - return null; - } - - /// - public override bool WriteToFile(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; - } - - SeparatedValueWriter svw = new(fs, new UTF8Encoding(false)) - { - Quotes = false, - Separator = '\t', - VerifyFieldCount = true - }; - - // Use a sorted list of games to output - foreach (string key in Items.SortedKeys) - { - ConcurrentList datItems = Items.FilteredItems(key); - - // If this machine doesn't contain any writable items, skip - if (!ContainsWritable(datItems)) - continue; - - // Resolve the names in the block - datItems = DatItem.ResolveNames(datItems); - - for (int index = 0; index < datItems.Count; index++) - { - DatItem datItem = datItems[index]; - - // Check for a "null" item - datItem = ProcessNullifiedItem(datItem); - - // Write out the item if we're not ignoring - if (!ShouldIgnore(datItem, ignoreblanks)) - WriteDatItem(svw, datItem); - } - } - - logger.User($"'{outfile}' written!{Environment.NewLine}"); - svw.Dispose(); - fs.Dispose(); - } - catch (Exception ex) when (!throwOnError) - { - logger.Error(ex); - return false; - } - - return true; - } - - /// - /// Write out Game start using the supplied StreamWriter - /// - /// SeparatedValueWriter to output to - /// DatItem object to be output - private void WriteDatItem(SeparatedValueWriter svw, DatItem datItem) - { - // No game should start with a path separator - datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar); - - // Pre-process the item name - ProcessItemName(datItem, true); - - // Build the state - switch (datItem.ItemType) - { - case ItemType.Rom: - var rom = datItem as Rom; - - string[] fields = new string[] - { - rom.SHA256 ?? string.Empty, - $"{rom.Machine.Name ?? string.Empty}/{rom.Name ?? string.Empty}", - rom.SHA1 ?? string.Empty, - rom.MD5 ?? string.Empty, - rom.CRC ?? string.Empty, - rom.Size.ToString() ?? string.Empty, - }; - - svw.WriteValues(fields); - - break; - } - - svw.Flush(); - } } } diff --git a/SabreTools.Serialization/EverdriveSMDB.cs b/SabreTools.Serialization/EverdriveSMDB.Deserializer.cs similarity index 96% rename from SabreTools.Serialization/EverdriveSMDB.cs rename to SabreTools.Serialization/EverdriveSMDB.Deserializer.cs index 27e5756a..f736d9f2 100644 --- a/SabreTools.Serialization/EverdriveSMDB.cs +++ b/SabreTools.Serialization/EverdriveSMDB.Deserializer.cs @@ -8,9 +8,9 @@ using SabreTools.Models.EverdriveSMDB; namespace SabreTools.Serialization { /// - /// Separated value serializer for Everdrive SMDBs + /// Separated value deserializer for Everdrive SMDBs /// - public class EverdriveSMDB + public partial class EverdriveSMDB { /// /// Deserializes an Everdrive SMDB to the defined type diff --git a/SabreTools.Serialization/EverdriveSMDB.Serializer.cs b/SabreTools.Serialization/EverdriveSMDB.Serializer.cs new file mode 100644 index 00000000..ed18821f --- /dev/null +++ b/SabreTools.Serialization/EverdriveSMDB.Serializer.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using SabreTools.IO.Writers; +using SabreTools.Models.EverdriveSMDB; + +namespace SabreTools.Serialization +{ + /// + /// Separated value serializer for Everdrive SMDBs + /// + public partial class EverdriveSMDB + { + /// + /// Serializes the defined type to an Everdrive SMDB file + /// + /// Data to serialize + /// Path to the file to serialize to + /// True on successful serialization, false otherwise + public static bool SerializeToFile(MetadataFile? metadataFile, string path) + { + try + { + using var stream = SerializeToStream(metadataFile); + if (stream == null) + return false; + + using var fs = File.OpenWrite(path); + stream.Seek(0, SeekOrigin.Begin); + stream.CopyTo(fs); + return true; + } + catch + { + // TODO: Handle logging the exception + return false; + } + } + + /// + /// Serializes the defined type to a stream + /// + /// Data to serialize + /// Stream containing serialized data on success, null otherwise + public static Stream? SerializeToStream(MetadataFile? metadataFile) + { + try + { + // If the metadata file is null + if (metadataFile == null) + return null; + + // Setup the writer and output + var stream = new MemoryStream(); + var writer = new SeparatedValueWriter(stream, Encoding.UTF8) { Separator = '\t', Quotes = false }; + + // Write out the rows, if they exist + WriteRows(metadataFile.Row, writer); + + // Return the stream + return stream; + } + catch + { + // TODO: Handle logging the exception + return null; + } + } + + /// + /// Write rows information to the current writer + /// + /// Array of Row objects representing the rows information + /// SeparatedValueWriter representing the output + private static void WriteRows(Row[]? rows, SeparatedValueWriter writer) + { + // If the games information is missing, we can't do anything + if (rows == null || !rows.Any()) + return; + + // Loop through and write out the rows + foreach (var row in rows) + { + var rowArray = new List + { + row.SHA256, + row.Name, + row.SHA1, + row.MD5, + row.CRC32, + }; + + if (row.Size != null) + rowArray.Add(row.Size); + + writer.WriteValues(rowArray.ToArray()); + writer.Flush(); + } + } + } +} \ No newline at end of file