using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using SabreTools.Core; using SabreTools.Core.Tools; using SabreTools.DatItems; using SabreTools.IO; namespace SabreTools.DatFiles.Formats { /// /// Represents parsing and writing of a DosCenter DAT /// internal class DosCenter : DatFile { /// /// Constructor designed for casting a base DatFile /// /// Parent DatFile to copy from public DosCenter(DatFile datFile) : base(datFile) { } /// /// Parse a DOSCenter DAT and return all found games and roms within /// /// Name of the file to be parsed /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) /// True if the error that is thrown should be thrown back to the caller, false otherwise protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false) { // Open a file reader Encoding enc = FileExtensions.GetEncoding(filename); ClrMameProReader cmpr = new ClrMameProReader(File.OpenRead(filename), enc) { DosCenter = true }; while (!cmpr.EndOfStream) { try { cmpr.ReadNextLine(); // Ignore everything not top-level if (cmpr.RowType != CmpRowType.TopLevel) continue; // Switch on the top-level name switch (cmpr.TopLevel.ToLowerInvariant()) { // Header values case "doscenter": ReadHeader(cmpr); break; // Sets case "game": ReadGame(cmpr, filename, indexId); break; default: break; } } catch (Exception ex) { string message = $"'{filename}' - There was an error parsing line {cmpr.LineNumber} '{cmpr.CurrentLine}'"; logger.Error(ex, message); if (throwOnError) { cmpr.Dispose(); throw new Exception(message, ex); } } } cmpr.Dispose(); } /// /// Read header information /// /// ClrMameProReader to use to parse the header private void ReadHeader(ClrMameProReader cmpr) { // If there's no subtree to the header, skip it if (cmpr == null || cmpr.EndOfStream) return; // While we don't hit an end element or end of stream while (!cmpr.EndOfStream) { cmpr.ReadNextLine(); // Ignore comments, internal items, and nothingness if (cmpr.RowType == CmpRowType.None || cmpr.RowType == CmpRowType.Comment || cmpr.RowType == CmpRowType.Internal) continue; // If we reached the end of a section, break if (cmpr.RowType == CmpRowType.EndTopLevel) break; // If the standalone value is null, we skip if (cmpr.Standalone == null) continue; string itemKey = cmpr.Standalone?.Key.ToLowerInvariant().TrimEnd(':'); string itemVal = cmpr.Standalone?.Value; // For all other cases switch (itemKey) { case "name": Header.Name = Header.Name ?? itemVal; break; case "description": Header.Description = Header.Description ?? itemVal; break; case "dersion": Header.Version = Header.Version ?? itemVal; break; case "date": Header.Date = Header.Date ?? itemVal; break; case "author": Header.Author = Header.Author ?? itemVal; break; case "homepage": Header.Homepage = Header.Homepage ?? itemVal; break; case "comment": Header.Comment = Header.Comment ?? itemVal; break; } } } /// /// Read set information /// /// ClrMameProReader to use to parse the header /// Name of the file to be parsed /// Index ID for the DAT private void ReadGame(ClrMameProReader cmpr, string filename, int indexId) { // Prepare all internal variables bool containsItems = false; Machine machine = new Machine() { MachineType = MachineType.NULL, }; // If there's no subtree to the header, skip it if (cmpr == null || cmpr.EndOfStream) return; // While we don't hit an end element or end of stream while (!cmpr.EndOfStream) { cmpr.ReadNextLine(); // Ignore comments and nothingness if (cmpr.RowType == CmpRowType.None || cmpr.RowType == CmpRowType.Comment) continue; // If we reached the end of a section, break if (cmpr.RowType == CmpRowType.EndTopLevel) break; // Handle any standalone items if (cmpr.RowType == CmpRowType.Standalone && cmpr.Standalone != null) { string itemKey = cmpr.Standalone?.Key.ToLowerInvariant(); string itemVal = cmpr.Standalone?.Value; switch (itemKey) { case "name": machine.Name = (itemVal.ToLowerInvariant().EndsWith(".zip") ? itemVal.Remove(itemVal.Length - 4) : itemVal); machine.Description = (itemVal.ToLowerInvariant().EndsWith(".zip") ? itemVal.Remove(itemVal.Length - 4) : itemVal); break; } } // Handle any internal items else if (cmpr.RowType == CmpRowType.Internal && string.Equals(cmpr.InternalName, "file", StringComparison.OrdinalIgnoreCase) && cmpr.Internal != null) { containsItems = true; // Create the proper DatItem based on the type Rom item = DatItem.Create(ItemType.Rom) as Rom; // Then populate it with information item.CopyMachineInformation(machine); item.Source = new Source { Index = indexId, Name = filename, }; // Loop through all of the attributes foreach (var kvp in cmpr.Internal) { string attrKey = kvp.Key; string attrVal = kvp.Value; switch (attrKey) { //If the item is empty, we automatically skip it because it's a fluke case "": continue; // Regular attributes case "name": item.Name = attrVal; break; case "size": item.Size = Sanitizer.CleanLong(attrVal); break; case "crc": item.CRC = attrVal; break; case "date": item.Date = attrVal; break; } } // Now process and add the rom ParseAddHelper(item); } } // If no items were found for this machine, add a Blank placeholder if (!containsItems) { Blank blank = new Blank() { Source = new Source { Index = indexId, Name = filename, }, }; blank.CopyMachineInformation(machine); // Now process and add the rom ParseAddHelper(blank); } } /// protected override ItemType[] GetSupportedTypes() { return new ItemType[] { ItemType.Rom }; } /// /// 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 override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) { try { logger.User($"Opening file for writing: {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; } ClrMameProWriter cmpw = new ClrMameProWriter(fs, new UTF8Encoding(false)) { Quotes = false }; // Write out the header WriteHeader(cmpw); // Write out each of the machines and roms string lastgame = null; // Use a sorted list of games to output foreach (string key in Items.SortedKeys) { List 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]; List newsplit = datItem.Machine.Name.Split('\\').ToList(); // 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 && lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant()) WriteEndGame(cmpw); // If we have a new game, output the beginning of the new item if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant()) WriteStartGame(cmpw, datItem); // Check for a "null" item datItem = ProcessNullifiedItem(datItem); // Write out the item if we're not ignoring if (!ShouldIgnore(datItem, ignoreblanks)) WriteDatItem(cmpw, datItem); // Set the new data to compare against lastgame = datItem.Machine.Name; } } // Write the file footer out WriteFooter(cmpw); logger.Verbose($"File written!{Environment.NewLine}"); cmpw.Dispose(); fs.Dispose(); } catch (Exception ex) { logger.Error(ex); if (throwOnError) throw ex; return false; } return true; } /// /// Write out DAT header using the supplied StreamWriter /// /// ClrMameProWriter to output to private void WriteHeader(ClrMameProWriter cmpw) { // Build the state cmpw.WriteStartElement("DOSCenter"); cmpw.WriteRequiredStandalone("Name:", Header.Name, false); cmpw.WriteRequiredStandalone("Description:", Header.Description, false); cmpw.WriteRequiredStandalone("Version:", Header.Version, false); cmpw.WriteRequiredStandalone("Date:", Header.Date, false); cmpw.WriteRequiredStandalone("Author:", Header.Author, false); cmpw.WriteRequiredStandalone("Homepage:", Header.Homepage, false); cmpw.WriteRequiredStandalone("Comment:", Header.Comment, false); cmpw.WriteEndElement(); cmpw.Flush(); } /// /// Write out Game start using the supplied StreamWriter /// /// ClrMameProWriter to output to /// DatItem object to be output private void WriteStartGame(ClrMameProWriter cmpw, DatItem datItem) { // No game should start with a path separator datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar); // Build the state cmpw.WriteStartElement("game"); cmpw.WriteRequiredStandalone("name", $"{datItem.Machine.Name}.zip", true); cmpw.Flush(); } /// /// Write out Game end using the supplied StreamWriter /// /// ClrMameProWriter to output to private void WriteEndGame(ClrMameProWriter cmpw) { // End game cmpw.WriteEndElement(); cmpw.Flush(); } /// /// Write out DatItem using the supplied StreamWriter /// /// ClrMameProWriter to output to /// DatItem object to be output private void WriteDatItem(ClrMameProWriter cmpw, DatItem datItem) { // Pre-process the item name ProcessItemName(datItem, true); // Build the state switch (datItem.ItemType) { case ItemType.Rom: var rom = datItem as Rom; cmpw.WriteStartElement("file"); cmpw.WriteRequiredAttributeString("name", rom.Name); cmpw.WriteOptionalAttributeString("size", rom.Size?.ToString()); cmpw.WriteOptionalAttributeString("date", rom.Date); cmpw.WriteOptionalAttributeString("crc", rom.CRC?.ToLowerInvariant()); cmpw.WriteEndElement(); break; } cmpw.Flush(); } /// /// Write out DAT footer using the supplied StreamWriter /// /// ClrMameProWriter to output to /// True if the data was written, false on error private void WriteFooter(ClrMameProWriter cmpw) { // End game cmpw.WriteEndElement(); cmpw.Flush(); } } }