mirror of
https://github.com/claunia/SabreTools.git
synced 2025-12-16 19:14:27 +00:00
Extract out DatFiles + Reporting namespace
This commit is contained in:
273
SabreTools.DatFiles/DatFiles/AttractMode.cs
Normal file
273
SabreTools.DatFiles/DatFiles/AttractMode.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of an AttractMode DAT
|
||||
/// </summary>
|
||||
internal class AttractMode : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public AttractMode(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an AttractMode DAT and return all found games within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Open a file reader
|
||||
Encoding enc = FileExtensions.GetEncoding(filename);
|
||||
SeparatedValueReader svr = new SeparatedValueReader(File.OpenRead(filename), enc)
|
||||
{
|
||||
Header = true,
|
||||
Quotes = false,
|
||||
Separator = ';',
|
||||
VerifyFieldCount = true
|
||||
};
|
||||
|
||||
// If we're somehow at the end of the stream already, we can't do anything
|
||||
if (svr.EndOfStream)
|
||||
return;
|
||||
|
||||
// Read in the header
|
||||
svr.ReadHeader();
|
||||
|
||||
// Header values should match
|
||||
// #Name;Title;Emulator;CloneOf;Year;Manufacturer;Category;Players;Rotation;Control;Status;DisplayCount;DisplayType;AltRomname;AltTitle;Extra;Buttons
|
||||
|
||||
// Loop through all of the data lines
|
||||
while (!svr.EndOfStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the current line, split and parse
|
||||
svr.ReadNextLine();
|
||||
|
||||
Rom rom = new Rom
|
||||
{
|
||||
Name = "-",
|
||||
Size = Constants.SizeZero,
|
||||
CRC = Constants.CRCZero,
|
||||
MD5 = Constants.MD5Zero,
|
||||
SHA1 = Constants.SHA1Zero,
|
||||
ItemStatus = ItemStatus.None,
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = svr.Line[0], // #Name
|
||||
Description = svr.Line[1], // Title
|
||||
CloneOf = svr.Line[3], // CloneOf
|
||||
Year = svr.Line[4], // Year
|
||||
Manufacturer = svr.Line[5], // Manufacturer
|
||||
Category = svr.Line[6], // Category
|
||||
Players = svr.Line[7], // Players
|
||||
Rotation = svr.Line[8], // Rotation
|
||||
Control = svr.Line[9], // Control
|
||||
Status = svr.Line[10], // Status
|
||||
DisplayCount = svr.Line[11], // DisplayCount
|
||||
DisplayType = svr.Line[12], // DisplayType
|
||||
Comment = svr.Line[15], // Extra
|
||||
Buttons = svr.Line[16], // Buttons
|
||||
},
|
||||
|
||||
AltName = svr.Line[13], // AltRomname
|
||||
AltTitle = svr.Line[14], // AltTitle
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(rom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string message = $"'{filename}' - There was an error parsing line {svr.LineNumber} '{svr.CurrentLine}'";
|
||||
logger.Error(ex, message);
|
||||
if (throwOnError)
|
||||
{
|
||||
svr.Dispose();
|
||||
throw new Exception(message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svr.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false))
|
||||
{
|
||||
Quotes = false,
|
||||
Separator = ';',
|
||||
VerifyFieldCount = true
|
||||
};
|
||||
|
||||
// Write out the header
|
||||
WriteHeader(svw);
|
||||
|
||||
// Use a sorted list of games to output
|
||||
foreach (string key in Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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.Verbose($"File written!{Environment.NewLine}");
|
||||
svw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="svw">SeparatedValueWriter to output to</param>
|
||||
private void WriteHeader(SeparatedValueWriter svw)
|
||||
{
|
||||
string[] headers = new string[]
|
||||
{
|
||||
"#Name",
|
||||
"Title",
|
||||
"Emulator",
|
||||
"CloneOf",
|
||||
"Year",
|
||||
"Manufacturer",
|
||||
"Category",
|
||||
"Players",
|
||||
"Rotation",
|
||||
"Control",
|
||||
"Status",
|
||||
"DisplayCount",
|
||||
"DisplayType",
|
||||
"AltRomname",
|
||||
"AltTitle",
|
||||
"Extra",
|
||||
"Buttons",
|
||||
};
|
||||
|
||||
svw.WriteHeader(headers);
|
||||
|
||||
svw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="svw">SeparatedValueWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
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.Machine.Name,
|
||||
rom.Machine.Description,
|
||||
Header.FileName,
|
||||
rom.Machine.CloneOf,
|
||||
rom.Machine.Year,
|
||||
rom.Machine.Manufacturer,
|
||||
rom.Machine.Category,
|
||||
rom.Machine.Players,
|
||||
rom.Machine.Rotation,
|
||||
rom.Machine.Control,
|
||||
rom.ItemStatus.ToString(),
|
||||
rom.Machine.DisplayCount,
|
||||
rom.Machine.DisplayType,
|
||||
rom.AltName,
|
||||
rom.AltTitle,
|
||||
rom.Machine.Comment,
|
||||
rom.Machine.Buttons,
|
||||
};
|
||||
|
||||
svw.WriteValues(fields);
|
||||
break;
|
||||
}
|
||||
|
||||
svw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
SabreTools.DatFiles/DatFiles/Auxiliary.cs
Normal file
24
SabreTools.DatFiles/DatFiles/Auxiliary.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
/// <summary>
|
||||
/// This holds all of the auxiliary types needed for proper parsing
|
||||
/// </summary>
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
#region DatHeader
|
||||
|
||||
#region OfflineList
|
||||
|
||||
/// <summary>
|
||||
/// Represents one OfflineList infos object
|
||||
/// </summary>
|
||||
public class OfflineListInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool? Visible { get; set; }
|
||||
public bool? InNamingOption { get; set; }
|
||||
public bool? Default { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion // DatHeader
|
||||
}
|
||||
707
SabreTools.DatFiles/DatFiles/ClrMamePro.cs
Normal file
707
SabreTools.DatFiles/DatFiles/ClrMamePro.cs
Normal file
@@ -0,0 +1,707 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.Core.Tools;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a ClrMamePro DAT
|
||||
/// </summary>
|
||||
internal class ClrMamePro : DatFile
|
||||
{
|
||||
#region Fields
|
||||
|
||||
/// <summary>
|
||||
/// Get whether to assume quote usage on read and write or not
|
||||
/// </summary>
|
||||
public bool Quotes { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
/// <param name="quotes">Enable quotes on read and write, false otherwise</param>
|
||||
public ClrMamePro(DatFile datFile, bool quotes)
|
||||
: base(datFile)
|
||||
{
|
||||
Quotes = quotes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a ClrMamePro DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
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 = false,
|
||||
Quotes = Quotes,
|
||||
};
|
||||
|
||||
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 "clrmamepro":
|
||||
case "romvault":
|
||||
ReadHeader(cmpr, keep);
|
||||
break;
|
||||
|
||||
// Sets
|
||||
case "set": // Used by the most ancient DATs
|
||||
case "game": // Used by most CMP DATs
|
||||
case "machine": // Possibly used by MAME CMP DATs
|
||||
ReadSet(cmpr, false, filename, indexId);
|
||||
break;
|
||||
case "resource": // Used by some other DATs to denote a BIOS set
|
||||
ReadSet(cmpr, true, 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read header information
|
||||
/// </summary>
|
||||
/// <param name="cmpr">ClrMameProReader to use to parse the header</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
private void ReadHeader(ClrMameProReader cmpr, bool keep)
|
||||
{
|
||||
bool superdat = false;
|
||||
|
||||
// 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();
|
||||
string itemVal = cmpr.Standalone?.Value;
|
||||
|
||||
// For all other cases
|
||||
switch (itemKey)
|
||||
{
|
||||
case "name":
|
||||
Header.Name = Header.Name ?? itemVal;
|
||||
superdat = superdat || itemVal.Contains(" - SuperDAT");
|
||||
|
||||
if (keep && superdat)
|
||||
Header.Type = Header.Type ?? "SuperDAT";
|
||||
|
||||
break;
|
||||
case "description":
|
||||
Header.Description = Header.Description ?? itemVal;
|
||||
break;
|
||||
case "rootdir":
|
||||
Header.RootDir = Header.RootDir ?? itemVal;
|
||||
break;
|
||||
case "category":
|
||||
Header.Category = Header.Category ?? itemVal;
|
||||
break;
|
||||
case "version":
|
||||
Header.Version = Header.Version ?? itemVal;
|
||||
break;
|
||||
case "date":
|
||||
Header.Date = Header.Date ?? itemVal;
|
||||
break;
|
||||
case "author":
|
||||
Header.Author = Header.Author ?? itemVal;
|
||||
break;
|
||||
case "email":
|
||||
Header.Email = Header.Email ?? itemVal;
|
||||
break;
|
||||
case "homepage":
|
||||
Header.Homepage = Header.Homepage ?? itemVal;
|
||||
break;
|
||||
case "url":
|
||||
Header.Url = Header.Url ?? itemVal;
|
||||
break;
|
||||
case "comment":
|
||||
Header.Comment = Header.Comment ?? itemVal;
|
||||
break;
|
||||
case "header":
|
||||
Header.HeaderSkipper = Header.HeaderSkipper ?? itemVal;
|
||||
break;
|
||||
case "type":
|
||||
Header.Type = Header.Type ?? itemVal;
|
||||
superdat = superdat || itemVal.Contains("SuperDAT");
|
||||
break;
|
||||
case "forcemerging":
|
||||
if (Header.ForceMerging == MergingFlag.None)
|
||||
Header.ForceMerging = itemVal.AsMergingFlag();
|
||||
|
||||
break;
|
||||
case "forcezipping":
|
||||
if (Header.ForcePacking == PackingFlag.None)
|
||||
Header.ForcePacking = itemVal.AsPackingFlag();
|
||||
|
||||
break;
|
||||
case "forcepacking":
|
||||
if (Header.ForcePacking == PackingFlag.None)
|
||||
Header.ForcePacking = itemVal.AsPackingFlag();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read set information
|
||||
/// </summary>
|
||||
/// <param name="cmpr">ClrMameProReader to use to parse the header</param>
|
||||
/// <param name="resource">True if the item is a resource (bios), false otherwise</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadSet(
|
||||
ClrMameProReader cmpr,
|
||||
bool resource,
|
||||
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int indexId)
|
||||
{
|
||||
// Prepare all internal variables
|
||||
bool containsItems = false;
|
||||
Machine machine = new Machine()
|
||||
{
|
||||
MachineType = (resource ? MachineType.Bios : 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;
|
||||
break;
|
||||
case "description":
|
||||
machine.Description = itemVal;
|
||||
break;
|
||||
case "year":
|
||||
machine.Year = itemVal;
|
||||
break;
|
||||
case "manufacturer":
|
||||
machine.Manufacturer = itemVal;
|
||||
break;
|
||||
case "category":
|
||||
machine.Category = itemVal;
|
||||
break;
|
||||
case "cloneof":
|
||||
machine.CloneOf = itemVal;
|
||||
break;
|
||||
case "romof":
|
||||
machine.RomOf = itemVal;
|
||||
break;
|
||||
case "sampleof":
|
||||
machine.SampleOf = itemVal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any internal items
|
||||
else if (cmpr.RowType == CmpRowType.Internal
|
||||
&& !string.IsNullOrWhiteSpace(cmpr.InternalName)
|
||||
&& cmpr.Internal != null)
|
||||
{
|
||||
containsItems = true;
|
||||
string itemKey = cmpr.InternalName;
|
||||
|
||||
// Create the proper DatItem based on the type
|
||||
ItemType itemType = itemKey.AsItemType() ?? ItemType.Rom;
|
||||
DatItem item = DatItem.Create(itemType);
|
||||
|
||||
// Then populate it with information
|
||||
item.CopyMachineInformation(machine);
|
||||
|
||||
item.Source.Index = indexId;
|
||||
item.Source.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.SetFields(new Dictionary<Field, string> { [Field.DatItem_Name] = attrVal } );
|
||||
break;
|
||||
|
||||
case "size":
|
||||
if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).Size = Sanitizer.CleanLong(attrVal);
|
||||
|
||||
break;
|
||||
case "crc":
|
||||
if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).CRC = attrVal;
|
||||
|
||||
break;
|
||||
case "md5":
|
||||
if (item.ItemType == ItemType.Disk)
|
||||
(item as Disk).MD5 = attrVal;
|
||||
else if (item.ItemType == ItemType.Media)
|
||||
(item as Media).MD5 = attrVal;
|
||||
else if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).MD5 = attrVal;
|
||||
|
||||
break;
|
||||
#if NET_FRAMEWORK
|
||||
case "ripemd160":
|
||||
if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).RIPEMD160 = attrVal;
|
||||
|
||||
break;
|
||||
#endif
|
||||
case "sha1":
|
||||
if (item.ItemType == ItemType.Disk)
|
||||
(item as Disk).SHA1 = attrVal;
|
||||
else if (item.ItemType == ItemType.Media)
|
||||
(item as Media).SHA1 = attrVal;
|
||||
else if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).SHA1 = attrVal;
|
||||
|
||||
break;
|
||||
case "sha256":
|
||||
if (item.ItemType == ItemType.Media)
|
||||
(item as Media).SHA256 = attrVal;
|
||||
else if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).SHA256 = attrVal;
|
||||
|
||||
break;
|
||||
case "sha384":
|
||||
if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).SHA384 = attrVal;
|
||||
|
||||
break;
|
||||
case "sha512":
|
||||
if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).SHA512 = attrVal;
|
||||
|
||||
break;
|
||||
case "spamsum":
|
||||
if (item.ItemType == ItemType.Media)
|
||||
(item as Media).SpamSum = attrVal;
|
||||
else if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).SpamSum = attrVal;
|
||||
|
||||
break;
|
||||
case "status":
|
||||
ItemStatus tempFlagStatus = attrVal.AsItemStatus();
|
||||
if (item.ItemType == ItemType.Disk)
|
||||
(item as Disk).ItemStatus = tempFlagStatus;
|
||||
else if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).ItemStatus = tempFlagStatus;
|
||||
|
||||
break;
|
||||
case "date":
|
||||
if (item.ItemType == ItemType.Release)
|
||||
(item as Release).Date = attrVal;
|
||||
else if (item.ItemType == ItemType.Rom)
|
||||
(item as Rom).Date = attrVal;
|
||||
|
||||
break;
|
||||
case "default":
|
||||
if (item.ItemType == ItemType.BiosSet)
|
||||
(item as BiosSet).Default = attrVal.AsYesNo();
|
||||
else if (item.ItemType == ItemType.Release)
|
||||
(item as Release).Default = attrVal.AsYesNo();
|
||||
|
||||
break;
|
||||
case "description":
|
||||
if (item.ItemType == ItemType.BiosSet)
|
||||
(item as BiosSet).Description = attrVal;
|
||||
|
||||
break;
|
||||
case "region":
|
||||
if (item.ItemType == ItemType.Release)
|
||||
(item as Release).Region = attrVal;
|
||||
|
||||
break;
|
||||
case "language":
|
||||
if (item.ItemType == ItemType.Release)
|
||||
(item as Release).Language = 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[]
|
||||
{
|
||||
ItemType.Archive,
|
||||
ItemType.BiosSet,
|
||||
ItemType.Disk,
|
||||
ItemType.Media,
|
||||
ItemType.Release,
|
||||
ItemType.Rom,
|
||||
ItemType.Sample,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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 = Quotes
|
||||
};
|
||||
|
||||
// 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<DatItem> 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];
|
||||
|
||||
// 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, datItem);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
private void WriteHeader(ClrMameProWriter cmpw)
|
||||
{
|
||||
cmpw.WriteStartElement("clrmamepro");
|
||||
|
||||
cmpw.WriteRequiredStandalone("name", Header.Name);
|
||||
cmpw.WriteRequiredStandalone("description", Header.Description);
|
||||
cmpw.WriteOptionalStandalone("category", Header.Category);
|
||||
cmpw.WriteRequiredStandalone("version", Header.Version);
|
||||
cmpw.WriteOptionalStandalone("date", Header.Date);
|
||||
cmpw.WriteRequiredStandalone("author", Header.Author);
|
||||
cmpw.WriteOptionalStandalone("email", Header.Email);
|
||||
cmpw.WriteOptionalStandalone("homepage", Header.Homepage);
|
||||
cmpw.WriteOptionalStandalone("url", Header.Url);
|
||||
cmpw.WriteOptionalStandalone("comment", Header.Comment);
|
||||
cmpw.WriteOptionalStandalone("forcezipping", Header.ForcePacking.FromPackingFlag(true), false);
|
||||
cmpw.WriteOptionalStandalone("forcemerging", Header.ForceMerging.FromMergingFlag(false), false);
|
||||
|
||||
// End clrmamepro
|
||||
cmpw.WriteEndElement();
|
||||
|
||||
cmpw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
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(datItem.Machine.MachineType == MachineType.Bios ? "resource" : "game");
|
||||
|
||||
cmpw.WriteRequiredStandalone("name", datItem.Machine.Name);
|
||||
cmpw.WriteOptionalStandalone("romof", datItem.Machine.RomOf);
|
||||
cmpw.WriteOptionalStandalone("cloneof", datItem.Machine.CloneOf);
|
||||
cmpw.WriteOptionalStandalone("description", datItem.Machine.Description ?? datItem.Machine.Name);
|
||||
cmpw.WriteOptionalStandalone("year", datItem.Machine.Year);
|
||||
cmpw.WriteOptionalStandalone("manufacturer", datItem.Machine.Manufacturer);
|
||||
cmpw.WriteOptionalStandalone("category", datItem.Machine.Category);
|
||||
|
||||
cmpw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game end using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteEndGame(ClrMameProWriter cmpw, DatItem datItem)
|
||||
{
|
||||
// Build the state
|
||||
cmpw.WriteOptionalStandalone("sampleof", datItem.Machine.SampleOf);
|
||||
|
||||
// End game
|
||||
cmpw.WriteEndElement();
|
||||
|
||||
cmpw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="datFile">DatFile to write out from</param>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(ClrMameProWriter cmpw, DatItem datItem)
|
||||
{
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Build the state
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Archive:
|
||||
var archive = datItem as Archive;
|
||||
cmpw.WriteStartElement("archive");
|
||||
cmpw.WriteRequiredAttributeString("name", archive.Name);
|
||||
cmpw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.BiosSet:
|
||||
var biosSet = datItem as BiosSet;
|
||||
cmpw.WriteStartElement("biosset");
|
||||
cmpw.WriteRequiredAttributeString("name", biosSet.Name);
|
||||
cmpw.WriteOptionalAttributeString("description", biosSet.Description);
|
||||
cmpw.WriteOptionalAttributeString("default", biosSet.Default?.ToString().ToLowerInvariant());
|
||||
cmpw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.Disk:
|
||||
var disk = datItem as Disk;
|
||||
cmpw.WriteStartElement("disk");
|
||||
cmpw.WriteRequiredAttributeString("name", disk.Name);
|
||||
cmpw.WriteOptionalAttributeString("md5", disk.MD5?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("sha1", disk.SHA1?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("flags", disk.ItemStatus.FromItemStatus(false));
|
||||
cmpw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.Media:
|
||||
var media = datItem as Media;
|
||||
cmpw.WriteStartElement("media");
|
||||
cmpw.WriteRequiredAttributeString("name", media.Name);
|
||||
cmpw.WriteOptionalAttributeString("md5", media.MD5?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("sha1", media.SHA1?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("sha256", media.SHA256?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("spamsum", media.SpamSum?.ToLowerInvariant());
|
||||
cmpw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.Release:
|
||||
var release = datItem as Release;
|
||||
cmpw.WriteStartElement("release");
|
||||
cmpw.WriteRequiredAttributeString("name", release.Name);
|
||||
cmpw.WriteOptionalAttributeString("region", release.Region);
|
||||
cmpw.WriteOptionalAttributeString("language", release.Language);
|
||||
cmpw.WriteOptionalAttributeString("date", release.Date);
|
||||
cmpw.WriteOptionalAttributeString("default", release.Default?.ToString().ToLowerInvariant());
|
||||
cmpw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
cmpw.WriteStartElement("rom");
|
||||
cmpw.WriteRequiredAttributeString("name", rom.Name);
|
||||
cmpw.WriteOptionalAttributeString("size", rom.Size?.ToString());
|
||||
cmpw.WriteOptionalAttributeString("crc", rom.CRC?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("md5", rom.MD5?.ToLowerInvariant());
|
||||
#if NET_FRAMEWORK
|
||||
cmpw.WriteOptionalAttributeString("ripemd160", rom.RIPEMD160?.ToLowerInvariant());
|
||||
#endif
|
||||
cmpw.WriteOptionalAttributeString("sha1", rom.SHA1?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("sha256", rom.SHA256?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("sha384", rom.SHA384?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("sha512", rom.SHA512?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("spamsum", rom.SpamSum?.ToLowerInvariant());
|
||||
cmpw.WriteOptionalAttributeString("date", rom.Date);
|
||||
cmpw.WriteOptionalAttributeString("flags", rom.ItemStatus.FromItemStatus(false));
|
||||
cmpw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.Sample:
|
||||
var sample = datItem as Sample;
|
||||
cmpw.WriteStartElement("sample");
|
||||
cmpw.WriteRequiredAttributeString("name", sample.Name);
|
||||
cmpw.WriteEndElement();
|
||||
break;
|
||||
}
|
||||
|
||||
cmpw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT footer using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
private void WriteFooter(ClrMameProWriter cmpw)
|
||||
{
|
||||
// End game
|
||||
cmpw.WriteEndElement();
|
||||
|
||||
cmpw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
4088
SabreTools.DatFiles/DatFiles/DatFile.cs
Normal file
4088
SabreTools.DatFiles/DatFiles/DatFile.cs
Normal file
File diff suppressed because it is too large
Load Diff
1087
SabreTools.DatFiles/DatFiles/DatHeader.cs
Normal file
1087
SabreTools.DatFiles/DatFiles/DatHeader.cs
Normal file
File diff suppressed because it is too large
Load Diff
63
SabreTools.DatFiles/DatFiles/DepotInformation.cs
Normal file
63
SabreTools.DatFiles/DatFiles/DepotInformation.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
|
||||
using SabreTools.Core;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Depot information wrapper
|
||||
/// </summary>
|
||||
public class DepotInformation : ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Name or path of the Depot
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use this Depot or not
|
||||
/// </summary>
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Depot byte-depth
|
||||
/// </summary>
|
||||
public int Depth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="isActive">Set active state</param>
|
||||
/// <param name="depth">Set depth between 0 and SHA-1's byte length</param>
|
||||
public DepotInformation(bool isActive = false, int depth = 4)
|
||||
{
|
||||
IsActive = isActive;
|
||||
Depth = depth;
|
||||
|
||||
// Limit depth value
|
||||
if (Depth == Int32.MinValue)
|
||||
Depth = 4;
|
||||
else if (Depth < 0)
|
||||
Depth = 0;
|
||||
else if (Depth > Constants.SHA1Zero.Length)
|
||||
Depth = Constants.SHA1Zero.Length;
|
||||
}
|
||||
|
||||
#region Cloning
|
||||
|
||||
/// <summary>
|
||||
/// Clone the current object
|
||||
/// </summary>
|
||||
public object Clone()
|
||||
{
|
||||
return new DepotInformation
|
||||
{
|
||||
Name = this.Name,
|
||||
IsActive = this.IsActive,
|
||||
Depth = this.Depth,
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
447
SabreTools.DatFiles/DatFiles/DosCenter.cs
Normal file
447
SabreTools.DatFiles/DatFiles/DosCenter.cs
Normal file
@@ -0,0 +1,447 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a DosCenter DAT
|
||||
/// </summary>
|
||||
internal class DosCenter : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public DosCenter(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a DOSCenter DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read header information
|
||||
/// </summary>
|
||||
/// <param name="cmpr">ClrMameProReader to use to parse the header</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read set information
|
||||
/// </summary>
|
||||
/// <param name="cmpr">ClrMameProReader to use to parse the header</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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<DatItem> 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<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game end using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
private void WriteEndGame(ClrMameProWriter cmpw)
|
||||
{
|
||||
// End game
|
||||
cmpw.WriteEndElement();
|
||||
|
||||
cmpw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT footer using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="cmpw">ClrMameProWriter to output to</param>
|
||||
/// <returns>True if the data was written, false on error</returns>
|
||||
private void WriteFooter(ClrMameProWriter cmpw)
|
||||
{
|
||||
// End game
|
||||
cmpw.WriteEndElement();
|
||||
|
||||
cmpw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
219
SabreTools.DatFiles/DatFiles/EverdriveSmdb.cs
Normal file
219
SabreTools.DatFiles/DatFiles/EverdriveSmdb.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of an Everdrive SMDB file
|
||||
/// </summary>
|
||||
internal class EverdriveSMDB : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public EverdriveSMDB(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an Everdrive SMDB file and return all found games within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Open a file reader
|
||||
Encoding enc = FileExtensions.GetEncoding(filename);
|
||||
SeparatedValueReader svr = new SeparatedValueReader(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
|
||||
*/
|
||||
|
||||
string[] fullname = svr.Line[1].Split('/');
|
||||
|
||||
Rom rom = new Rom
|
||||
{
|
||||
Name = svr.Line[1].Substring(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,
|
||||
},
|
||||
};
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(rom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string message = $"'{filename}' - There was an error parsing line {svr.LineNumber} '{svr.CurrentLine}'";
|
||||
logger.Error(ex, message);
|
||||
if (throwOnError)
|
||||
{
|
||||
svr.Dispose();
|
||||
throw new Exception(message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svr.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false))
|
||||
{
|
||||
Quotes = false,
|
||||
Separator = '\t',
|
||||
VerifyFieldCount = true
|
||||
};
|
||||
|
||||
// Use a sorted list of games to output
|
||||
foreach (string key in Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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.Verbose($"File written!{Environment.NewLine}");
|
||||
svw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="svw">SeparatedValueWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
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,
|
||||
};
|
||||
|
||||
svw.WriteValues(fields);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
svw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
371
SabreTools.DatFiles/DatFiles/Hashfile.cs
Normal file
371
SabreTools.DatFiles/DatFiles/Hashfile.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a hashfile such as an SFV, MD5, or SHA-1 file
|
||||
/// </summary>
|
||||
internal class Hashfile : DatFile
|
||||
{
|
||||
// Private instance variables specific to Hashfile DATs
|
||||
private readonly Hash _hash;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
/// <param name="hash">Type of hash that is associated with this DAT</param>
|
||||
public Hashfile(DatFile datFile, Hash hash)
|
||||
: base(datFile)
|
||||
{
|
||||
_hash = hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a hashfile or SFV and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Open a file reader
|
||||
Encoding enc = FileExtensions.GetEncoding(filename);
|
||||
StreamReader sr = new StreamReader(File.OpenRead(filename), enc);
|
||||
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
string line = sr.ReadLine();
|
||||
|
||||
// Split the line and get the name and hash
|
||||
string[] split = line.Split(' ');
|
||||
string name = string.Empty;
|
||||
string hash = string.Empty;
|
||||
|
||||
// If we have CRC, then it's an SFV file and the name is first are
|
||||
if (_hash.HasFlag(Hash.CRC))
|
||||
{
|
||||
name = split[0].Replace("*", String.Empty);
|
||||
hash = split[1];
|
||||
}
|
||||
// Otherwise, the name is second
|
||||
else
|
||||
{
|
||||
name = split[1].Replace("*", String.Empty);
|
||||
hash = split[0];
|
||||
}
|
||||
|
||||
Rom rom = new Rom
|
||||
{
|
||||
Name = name,
|
||||
Size = null,
|
||||
CRC = (_hash.HasFlag(Hash.CRC) ? hash : null),
|
||||
MD5 = (_hash.HasFlag(Hash.MD5) ? hash : null),
|
||||
#if NET_FRAMEWORK
|
||||
RIPEMD160 = (_hash.HasFlag(Hash.RIPEMD160) ? hash : null),
|
||||
#endif
|
||||
SHA1 = (_hash.HasFlag(Hash.SHA1) ? hash : null),
|
||||
SHA256 = (_hash.HasFlag(Hash.SHA256) ? hash : null),
|
||||
SHA384 = (_hash.HasFlag(Hash.SHA384) ? hash : null),
|
||||
SHA512 = (_hash.HasFlag(Hash.SHA512) ? hash : null),
|
||||
SpamSum = (_hash.HasFlag(Hash.SpamSum) ? hash : null),
|
||||
ItemStatus = ItemStatus.None,
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(filename),
|
||||
},
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(rom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string message = $"'{filename}' - There was an error parsing at position {sr.BaseStream.Position}";
|
||||
logger.Error(ex, message);
|
||||
if (throwOnError)
|
||||
{
|
||||
sr.Dispose();
|
||||
throw new Exception(message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sr.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Disk, ItemType.Media, ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false))
|
||||
{
|
||||
Quotes = false,
|
||||
Separator = ' ',
|
||||
VerifyFieldCount = true
|
||||
};
|
||||
|
||||
// Use a sorted list of games to output
|
||||
foreach (string key in Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> datItems = Items[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.Verbose($"File written!{Environment.NewLine}");
|
||||
svw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied SeparatedValueWriter
|
||||
/// </summary>
|
||||
/// <param name="svw">SeparatedValueWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(SeparatedValueWriter svw, DatItem datItem)
|
||||
{
|
||||
// Build the state
|
||||
string[] fields = new string[2];
|
||||
|
||||
// Get the name field
|
||||
string name = string.Empty;
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Disk:
|
||||
var disk = datItem as Disk;
|
||||
if (Header.GameName)
|
||||
name = $"{disk.Machine.Name}{Path.DirectorySeparatorChar}";
|
||||
|
||||
name += disk.Name;
|
||||
break;
|
||||
|
||||
case ItemType.Media:
|
||||
var media = datItem as Media;
|
||||
if (Header.GameName)
|
||||
name = $"{media.Machine.Name}{Path.DirectorySeparatorChar}";
|
||||
|
||||
name += media.Name;
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
if (Header.GameName)
|
||||
name = $"{rom.Machine.Name}{Path.DirectorySeparatorChar}";
|
||||
|
||||
name += rom.Name;
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the hash field and set final fields
|
||||
string hash = string.Empty;
|
||||
switch (_hash)
|
||||
{
|
||||
case Hash.CRC:
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[0] = name;
|
||||
fields[1] = rom.CRC;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Hash.MD5:
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Disk:
|
||||
var disk = datItem as Disk;
|
||||
fields[0] = disk.MD5;
|
||||
fields[1] = name;
|
||||
break;
|
||||
|
||||
case ItemType.Media:
|
||||
var media = datItem as Media;
|
||||
fields[0] = media.MD5;
|
||||
fields[1] = name;
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[0] = rom.MD5;
|
||||
fields[1] = name;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
#if NET_FRAMEWORK
|
||||
case Hash.RIPEMD160:
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[0] = rom.RIPEMD160;
|
||||
fields[1] = name;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
#endif
|
||||
case Hash.SHA1:
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Disk:
|
||||
var disk = datItem as Disk;
|
||||
fields[0] = disk.SHA1;
|
||||
fields[1] = name;
|
||||
break;
|
||||
|
||||
case ItemType.Media:
|
||||
var media = datItem as Media;
|
||||
fields[0] = media.SHA1;
|
||||
fields[1] = name;
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[0] = rom.SHA1;
|
||||
fields[1] = name;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Hash.SHA256:
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Media:
|
||||
var media = datItem as Media;
|
||||
fields[0] = media.SHA256;
|
||||
fields[1] = name;
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[0] = rom.SHA256;
|
||||
fields[1] = name;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Hash.SHA384:
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[0] = rom.SHA384;
|
||||
fields[1] = name;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Hash.SHA512:
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[0] = rom.SHA512;
|
||||
fields[1] = name;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Hash.SpamSum:
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Media:
|
||||
var media = datItem as Media;
|
||||
fields[0] = media.SpamSum;
|
||||
fields[1] = name;
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[0] = rom.SpamSum;
|
||||
fields[1] = name;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// If we had at least one field filled in
|
||||
if (!string.IsNullOrEmpty(fields[0]) || !string.IsNullOrEmpty(fields[1]))
|
||||
svw.WriteValues(fields);
|
||||
|
||||
svw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
1550
SabreTools.DatFiles/DatFiles/ItemDictionary.cs
Normal file
1550
SabreTools.DatFiles/DatFiles/ItemDictionary.cs
Normal file
File diff suppressed because it is too large
Load Diff
460
SabreTools.DatFiles/DatFiles/Listrom.cs
Normal file
460
SabreTools.DatFiles/DatFiles/Listrom.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.Core.Tools;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a MAME Listrom DAT
|
||||
/// </summary>
|
||||
internal class Listrom : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public Listrom(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a MAME Listrom DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <remarks>
|
||||
/// In a new style MAME listrom DAT, each game has the following format:
|
||||
///
|
||||
/// ROMs required for driver "005".
|
||||
/// Name Size Checksum
|
||||
/// 1346b.cpu-u25 2048 CRC(8e68533e) SHA1(a257c556d31691068ed5c991f1fb2b51da4826db)
|
||||
/// 6331.sound-u8 32 BAD CRC(1d298cb0) SHA1(bb0bb62365402543e3154b9a77be9c75010e6abc) BAD_DUMP
|
||||
/// 16v8h-blue.u24 279 NO GOOD DUMP KNOWN
|
||||
/// </remarks>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Open a file reader
|
||||
Encoding enc = FileExtensions.GetEncoding(filename);
|
||||
StreamReader sr = new StreamReader(File.OpenRead(filename), enc);
|
||||
|
||||
string gamename = string.Empty;
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
string line = sr.ReadLine().Trim();
|
||||
|
||||
// If we have a blank line, we just skip it
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have the descriptor line, ignore it
|
||||
else if (line == "Name Size Checksum")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have the beginning of a game, set the name of the game
|
||||
else if (line.StartsWith("ROMs required for"))
|
||||
{
|
||||
gamename = Regex.Match(line, @"^ROMs required for \S*? string.Empty(.*?)string.Empty\.").Groups[1].Value;
|
||||
}
|
||||
|
||||
// If we have a machine with no required roms (usually internal devices), skip it
|
||||
else if (line.StartsWith("No ROMs required for"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, we assume we have a rom that we need to add
|
||||
else
|
||||
{
|
||||
// First, we preprocess the line so that the rom name is consistently correct
|
||||
string[] split = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// If the line doesn't have the 4 spaces of padding, check for 3
|
||||
if (split.Length == 1)
|
||||
split = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// If the split is still unsuccessful, log it and skip
|
||||
if (split.Length == 1)
|
||||
logger.Warning($"Possibly malformed line: '{line}'");
|
||||
|
||||
string romname = split[0];
|
||||
line = line.Substring(romname.Length);
|
||||
|
||||
// Next we separate the ROM into pieces
|
||||
split = line.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Standard Disks have 2 pieces (name, sha1)
|
||||
if (split.Length == 1)
|
||||
{
|
||||
Disk disk = new Disk()
|
||||
{
|
||||
Name = romname,
|
||||
SHA1 = Sanitizer.CleanListromHashData(split[0]),
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = gamename,
|
||||
},
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
ParseAddHelper(disk);
|
||||
}
|
||||
|
||||
// Baddump Disks have 4 pieces (name, BAD, sha1, BAD_DUMP)
|
||||
else if (split.Length == 3 && line.EndsWith("BAD_DUMP"))
|
||||
{
|
||||
Disk disk = new Disk()
|
||||
{
|
||||
Name = romname,
|
||||
SHA1 = Sanitizer.CleanListromHashData(split[1]),
|
||||
ItemStatus = ItemStatus.BadDump,
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = gamename,
|
||||
},
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
ParseAddHelper(disk);
|
||||
}
|
||||
|
||||
// Standard ROMs have 4 pieces (name, size, crc, sha1)
|
||||
else if (split.Length == 3)
|
||||
{
|
||||
Rom rom = new Rom()
|
||||
{
|
||||
Name = romname,
|
||||
Size = Sanitizer.CleanLong(split[0]),
|
||||
CRC = Sanitizer.CleanListromHashData(split[1]),
|
||||
SHA1 = Sanitizer.CleanListromHashData(split[2]),
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = gamename,
|
||||
},
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
ParseAddHelper(rom);
|
||||
}
|
||||
|
||||
// Nodump Disks have 5 pieces (name, NO, GOOD, DUMP, KNOWN)
|
||||
else if (split.Length == 4 && line.EndsWith("NO GOOD DUMP KNOWN"))
|
||||
{
|
||||
Disk disk = new Disk()
|
||||
{
|
||||
Name = romname,
|
||||
ItemStatus = ItemStatus.Nodump,
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = gamename,
|
||||
},
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
ParseAddHelper(disk);
|
||||
}
|
||||
|
||||
// Baddump ROMs have 6 pieces (name, size, BAD, crc, sha1, BAD_DUMP)
|
||||
else if (split.Length == 5 && line.EndsWith("BAD_DUMP"))
|
||||
{
|
||||
Rom rom = new Rom()
|
||||
{
|
||||
Name = romname,
|
||||
Size = Sanitizer.CleanLong(split[0]),
|
||||
CRC = Sanitizer.CleanListromHashData(split[2]),
|
||||
SHA1 = Sanitizer.CleanListromHashData(split[3]),
|
||||
ItemStatus = ItemStatus.BadDump,
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = gamename,
|
||||
},
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
ParseAddHelper(rom);
|
||||
}
|
||||
|
||||
// Nodump ROMs have 6 pieces (name, size, NO, GOOD, DUMP, KNOWN)
|
||||
else if (split.Length == 5 && line.EndsWith("NO GOOD DUMP KNOWN"))
|
||||
{
|
||||
Rom rom = new Rom()
|
||||
{
|
||||
Name = romname,
|
||||
Size = Sanitizer.CleanLong(split[0]),
|
||||
ItemStatus = ItemStatus.Nodump,
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = gamename,
|
||||
},
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
ParseAddHelper(rom);
|
||||
}
|
||||
|
||||
// If we have something else, it's invalid
|
||||
else
|
||||
{
|
||||
logger.Warning($"Invalid line detected: '{romname} {line}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string message = $"'{filename}' - There was an error parsing at position {sr.BaseStream.Position}";
|
||||
logger.Error(ex, message);
|
||||
if (throwOnError)
|
||||
{
|
||||
sr.Dispose();
|
||||
throw new Exception(message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Disk, ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
StreamWriter sw = new StreamWriter(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 Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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];
|
||||
|
||||
// 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(sw);
|
||||
|
||||
// If we have a new game, output the beginning of the new item
|
||||
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
|
||||
WriteStartGame(sw, datItem);
|
||||
|
||||
// Check for a "null" item
|
||||
datItem = ProcessNullifiedItem(datItem);
|
||||
|
||||
// Write out the item if we're not ignoring
|
||||
if (!ShouldIgnore(datItem, ignoreblanks))
|
||||
WriteDatItem(sw, datItem);
|
||||
|
||||
// Set the new data to compare against
|
||||
lastgame = datItem.Machine.Name;
|
||||
}
|
||||
}
|
||||
|
||||
logger.Verbose("File written!" + Environment.NewLine);
|
||||
sw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="sw">StreamWriter to output to</param>
|
||||
/// <param name="rom">DatItem object to be output</param>
|
||||
private void WriteStartGame(StreamWriter sw, DatItem rom)
|
||||
{
|
||||
// No game should start with a path separator
|
||||
rom.Machine.Name = rom.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
|
||||
|
||||
// Build the state
|
||||
sw.Write($"ROMs required for driver \"{rom.Machine.Name}\".\n");
|
||||
sw.Write("Name Size Checksum\n");
|
||||
|
||||
sw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game end using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="sw">StreamWriter to output to</param>
|
||||
private void WriteEndGame(StreamWriter sw)
|
||||
{
|
||||
// End driver
|
||||
sw.Write("\n");
|
||||
|
||||
sw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="sw">StreamWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(StreamWriter sw, DatItem datItem)
|
||||
{
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Build the state
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Disk:
|
||||
var disk = datItem as Disk;
|
||||
|
||||
// The name is padded out to a particular length
|
||||
if (disk.Name.Length < 43)
|
||||
sw.Write(disk.Name.PadRight(43, ' '));
|
||||
else
|
||||
sw.Write($"{disk.Name} ");
|
||||
|
||||
// If we have a baddump, put the first indicator
|
||||
if (disk.ItemStatus == ItemStatus.BadDump)
|
||||
sw.Write(" BAD");
|
||||
|
||||
// If we have a nodump, write out the indicator
|
||||
if (disk.ItemStatus == ItemStatus.Nodump)
|
||||
sw.Write(" NO GOOD DUMP KNOWN");
|
||||
|
||||
// Otherwise, write out the SHA-1 hash
|
||||
else if (!string.IsNullOrWhiteSpace(disk.SHA1))
|
||||
sw.Write($" SHA1({disk.SHA1 ?? string.Empty})");
|
||||
|
||||
// If we have a baddump, put the second indicator
|
||||
if (disk.ItemStatus == ItemStatus.BadDump)
|
||||
sw.Write(" BAD_DUMP");
|
||||
|
||||
sw.Write("\n");
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
|
||||
// The name is padded out to a particular length
|
||||
if (rom.Name.Length < 43)
|
||||
sw.Write(rom.Name.PadRight(43 - rom.Size?.ToString().Length ?? 0, ' '));
|
||||
else
|
||||
sw.Write($"{rom.Name} ");
|
||||
|
||||
// If we don't have a nodump, write out the size
|
||||
if (rom.ItemStatus != ItemStatus.Nodump)
|
||||
sw.Write(rom.Size?.ToString() ?? string.Empty);
|
||||
|
||||
// If we have a baddump, put the first indicator
|
||||
if (rom.ItemStatus == ItemStatus.BadDump)
|
||||
sw.Write(" BAD");
|
||||
|
||||
// If we have a nodump, write out the indicator
|
||||
if (rom.ItemStatus == ItemStatus.Nodump)
|
||||
{
|
||||
sw.Write(" NO GOOD DUMP KNOWN");
|
||||
}
|
||||
// Otherwise, write out the CRC and SHA-1 hashes
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(rom.CRC))
|
||||
sw.Write($" CRC({rom.CRC ?? string.Empty})");
|
||||
if (!string.IsNullOrWhiteSpace(rom.SHA1))
|
||||
sw.Write($" SHA1({rom.SHA1 ?? string.Empty})");
|
||||
}
|
||||
|
||||
// If we have a baddump, put the second indicator
|
||||
if (rom.ItemStatus == ItemStatus.BadDump)
|
||||
sw.Write(" BAD_DUMP");
|
||||
|
||||
sw.Write("\n");
|
||||
break;
|
||||
}
|
||||
|
||||
sw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
1728
SabreTools.DatFiles/DatFiles/Listxml.cs
Normal file
1728
SabreTools.DatFiles/DatFiles/Listxml.cs
Normal file
File diff suppressed because it is too large
Load Diff
1038
SabreTools.DatFiles/DatFiles/Logiqx.cs
Normal file
1038
SabreTools.DatFiles/DatFiles/Logiqx.cs
Normal file
File diff suppressed because it is too large
Load Diff
125
SabreTools.DatFiles/DatFiles/Missfile.cs
Normal file
125
SabreTools.DatFiles/DatFiles/Missfile.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.DatItems;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a Missfile
|
||||
/// </summary>
|
||||
internal class Missfile : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public Missfile(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Missfile and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// There is no consistent way to parse a missfile...
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
StreamWriter sw = new StreamWriter(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 Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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 using machine names or we're not ignoring
|
||||
if (!Header.UseRomName || !ShouldIgnore(datItem, ignoreblanks))
|
||||
WriteDatItem(sw, datItem, lastgame);
|
||||
|
||||
// Set the new data to compare against
|
||||
lastgame = datItem.Machine.Name;
|
||||
}
|
||||
}
|
||||
|
||||
logger.Verbose("File written!" + Environment.NewLine);
|
||||
sw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="sw">StreamWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
/// <param name="lastgame">The name of the last game to be output</param>
|
||||
private void WriteDatItem(StreamWriter sw, DatItem datItem, string lastgame)
|
||||
{
|
||||
// Process the item name
|
||||
ProcessItemName(datItem, false, forceRomName: false);
|
||||
|
||||
// Romba mode automatically uses item name
|
||||
if (Header.OutputDepot?.IsActive == true || Header.UseRomName)
|
||||
sw.Write($"{datItem.GetName() ?? string.Empty}\n");
|
||||
else if (!Header.UseRomName && datItem.Machine.Name != lastgame)
|
||||
sw.Write($"{datItem.Machine.Name}\n");
|
||||
|
||||
sw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
969
SabreTools.DatFiles/DatFiles/OfflineList.cs
Normal file
969
SabreTools.DatFiles/DatFiles/OfflineList.cs
Normal file
@@ -0,0 +1,969 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.Core.Tools;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of an OfflineList XML DAT
|
||||
/// </summary>
|
||||
internal class OfflineList : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public OfflineList(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an OfflineList XML DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
XmlReader xtr = XmlReader.Create(filename, new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
IgnoreComments = true,
|
||||
IgnoreWhitespace = true,
|
||||
ValidationFlags = XmlSchemaValidationFlags.None,
|
||||
ValidationType = ValidationType.None,
|
||||
});
|
||||
|
||||
// If we got a null reader, just return
|
||||
if (xtr == null)
|
||||
return;
|
||||
|
||||
// Otherwise, read the file to the end
|
||||
try
|
||||
{
|
||||
xtr.MoveToContent();
|
||||
while (!xtr.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (xtr.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
xtr.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (xtr.Name)
|
||||
{
|
||||
case "configuration":
|
||||
ReadConfiguration(xtr.ReadSubtree(), keep);
|
||||
|
||||
// Skip the configuration node now that we've processed it
|
||||
xtr.Skip();
|
||||
break;
|
||||
|
||||
case "games":
|
||||
ReadGames(xtr.ReadSubtree(), filename, indexId);
|
||||
|
||||
// Skip the games node now that we've processed it
|
||||
xtr.Skip();
|
||||
break;
|
||||
|
||||
default:
|
||||
xtr.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning(ex, $"Exception found while parsing '{filename}'");
|
||||
if (throwOnError)
|
||||
{
|
||||
xtr.Dispose();
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// For XML errors, just skip the affected node
|
||||
xtr?.Read();
|
||||
}
|
||||
|
||||
xtr.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read configuration information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
private void ReadConfiguration(XmlReader reader, bool keep)
|
||||
{
|
||||
bool superdat = false;
|
||||
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all configuration items (ONLY OVERWRITE IF THERE'S NO DATA)
|
||||
string content;
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "datname":
|
||||
content = reader.ReadElementContentAsString();
|
||||
Header.Name = Header.Name ?? content;
|
||||
superdat = superdat || content.Contains(" - SuperDAT");
|
||||
if (keep && superdat)
|
||||
{
|
||||
Header.Type = Header.Type ?? "SuperDAT";
|
||||
}
|
||||
break;
|
||||
|
||||
case "datversion":
|
||||
content = reader.ReadElementContentAsString();
|
||||
Header.Version = Header.Version ?? content;
|
||||
break;
|
||||
|
||||
case "system":
|
||||
content = reader.ReadElementContentAsString();
|
||||
Header.System = Header.System ?? content;
|
||||
break;
|
||||
|
||||
// TODO: Int32?
|
||||
case "screenshotswidth":
|
||||
content = reader.ReadElementContentAsString();
|
||||
Header.ScreenshotsWidth = Header.ScreenshotsWidth ?? content;
|
||||
break;
|
||||
|
||||
// TODO: Int32?
|
||||
case "screenshotsheight":
|
||||
content = reader.ReadElementContentAsString();
|
||||
Header.ScreenshotsHeight = Header.ScreenshotsHeight ?? content;
|
||||
break;
|
||||
|
||||
case "infos":
|
||||
ReadInfos(reader.ReadSubtree());
|
||||
|
||||
// Skip the infos node now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
case "canopen":
|
||||
ReadCanOpen(reader.ReadSubtree());
|
||||
|
||||
// Skip the canopen node now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
// TODO: Use all header values
|
||||
case "newdat":
|
||||
ReadNewDat(reader.ReadSubtree());
|
||||
|
||||
// Skip the newdat node now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
// TODO: Use header values
|
||||
case "search":
|
||||
ReadSearch(reader.ReadSubtree());
|
||||
|
||||
// Skip the search node now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
case "romtitle":
|
||||
content = reader.ReadElementContentAsString();
|
||||
Header.RomTitle = Header.RomTitle ?? content;
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read infos information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
private void ReadInfos(XmlReader reader)
|
||||
{
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Setup the infos object
|
||||
Header.Infos = new List<OfflineListInfo>();
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add all infos to the info list
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "info":
|
||||
var info = new OfflineListInfo
|
||||
{
|
||||
Name = reader.Name.ToLowerInvariant(),
|
||||
Visible = reader.GetAttribute("visible").AsYesNo(),
|
||||
InNamingOption = reader.GetAttribute("inNamingOption").AsYesNo(),
|
||||
Default = reader.GetAttribute("default").AsYesNo()
|
||||
};
|
||||
|
||||
Header.Infos.Add(info);
|
||||
|
||||
reader.Read();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read canopen information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
private void ReadCanOpen(XmlReader reader)
|
||||
{
|
||||
// Prepare all internal variables
|
||||
Header.CanOpen = new List<string>();
|
||||
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all canopen items
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "extension":
|
||||
Header.CanOpen.Add(reader.ReadElementContentAsString());
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read newdat information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
private void ReadNewDat(XmlReader reader)
|
||||
{
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all newdat items
|
||||
string content;
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "datversionurl":
|
||||
// TODO: Read this into an appropriate field
|
||||
content = reader.ReadElementContentAsString();
|
||||
Header.Url = (string.IsNullOrWhiteSpace(Header.Url) ? content : Header.Url);
|
||||
break;
|
||||
|
||||
case "daturl":
|
||||
// TODO: Read this into an appropriate structure
|
||||
reader.GetAttribute("fileName");
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "imurl":
|
||||
// TODO: Read this into an appropriate field
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read search information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
private void ReadSearch(XmlReader reader)
|
||||
{
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all search items
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "to":
|
||||
// TODO: Read this into an appropriate structure
|
||||
reader.GetAttribute("value");
|
||||
reader.GetAttribute("default"); // (true|false)
|
||||
reader.GetAttribute("auto"); // (true|false)
|
||||
|
||||
ReadTo(reader.ReadSubtree());
|
||||
|
||||
// Skip the to node now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read to information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
private void ReadTo(XmlReader reader)
|
||||
{
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all search items
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "find":
|
||||
// TODO: Read this into an appropriate structure
|
||||
reader.GetAttribute("operation");
|
||||
reader.GetAttribute("value"); // Int32?
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read games information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadGames(XmlReader reader, string filename, int indexId)
|
||||
{
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all games items (ONLY OVERWRITE IF THERE'S NO DATA)
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "game":
|
||||
ReadGame(reader.ReadSubtree(), filename, indexId);
|
||||
|
||||
// Skip the game node now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read game information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadGame(XmlReader reader, string filename, int indexId)
|
||||
{
|
||||
// Prepare all internal variables
|
||||
string releaseNumber = string.Empty, duplicateid;
|
||||
long? size = null;
|
||||
List<Rom> datItems = new List<Rom>();
|
||||
Machine machine = new Machine();
|
||||
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all games items
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "imagenumber":
|
||||
// TODO: Read this into a field
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "releasenumber":
|
||||
// TODO: Read this into a field
|
||||
releaseNumber = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "title":
|
||||
machine.Name = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "savetype":
|
||||
// TODO: Read this into a field
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "romsize":
|
||||
size = Sanitizer.CleanLong(reader.ReadElementContentAsString());
|
||||
break;
|
||||
|
||||
case "publisher":
|
||||
machine.Publisher = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "location":
|
||||
// TODO: Read this into a field
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "sourcerom":
|
||||
// TODO: Read this into a field
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "language":
|
||||
// TODO: Read this into a field
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "files":
|
||||
datItems = ReadFiles(reader.ReadSubtree(), releaseNumber, machine.Name, filename, indexId);
|
||||
|
||||
// Skip the files node now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
case "im1crc":
|
||||
// TODO: Read this into a field
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "im2crc":
|
||||
// TODO: Read this into a field
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "comment":
|
||||
machine.Comment = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "duplicateid":
|
||||
duplicateid = reader.ReadElementContentAsString();
|
||||
if (duplicateid != "0")
|
||||
machine.CloneOf = duplicateid;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add information accordingly for each rom
|
||||
for (int i = 0; i < datItems.Count; i++)
|
||||
{
|
||||
datItems[i].Size = size;
|
||||
datItems[i].CopyMachineInformation(machine);
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(datItems[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read files information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader to use to parse the header</param>
|
||||
/// <param name="releaseNumber">Release number from the parent game</param>
|
||||
/// <param name="machineName">Name of the parent game to use</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private List<Rom> ReadFiles(
|
||||
XmlReader reader,
|
||||
string releaseNumber,
|
||||
string machineName,
|
||||
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int indexId)
|
||||
{
|
||||
// Prepare all internal variables
|
||||
var extensionToCrc = new List<KeyValuePair<string, string>>();
|
||||
var roms = new List<Rom>();
|
||||
|
||||
// If there's no subtree to the configuration, skip it
|
||||
if (reader == null)
|
||||
return roms;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
// Otherwise, read what we can from the header
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all romCRC items
|
||||
switch (reader.Name.ToLowerInvariant())
|
||||
{
|
||||
case "romcrc":
|
||||
extensionToCrc.Add(
|
||||
new KeyValuePair<string, string>(
|
||||
reader.GetAttribute("extension") ?? string.Empty,
|
||||
reader.ReadElementContentAsString().ToLowerInvariant()));
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Now process the roms with the proper information
|
||||
foreach (KeyValuePair<string, string> pair in extensionToCrc)
|
||||
{
|
||||
roms.Add(new Rom()
|
||||
{
|
||||
Name = (releaseNumber != "0" ? releaseNumber + " - " : string.Empty) + machineName + pair.Key,
|
||||
CRC = pair.Value,
|
||||
|
||||
ItemStatus = ItemStatus.None,
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return roms;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
XmlTextWriter xtw = new XmlTextWriter(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 Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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(xtw, datItem);
|
||||
|
||||
// Set the new data to compare against
|
||||
lastgame = datItem.Machine.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the file footer out
|
||||
WriteFooter(xtw);
|
||||
|
||||
logger.Verbose("File written!" + Environment.NewLine);
|
||||
xtw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteHeader(XmlTextWriter xtw)
|
||||
{
|
||||
xtw.WriteStartDocument(false);
|
||||
|
||||
xtw.WriteStartElement("dat");
|
||||
xtw.WriteAttributeString("xsi", "xmlns", "http://www.w3.org/2001/XMLSchema-instance");
|
||||
xtw.WriteAttributeString("noNamespaceSchemaLocation", "xsi", "datas.xsd");
|
||||
|
||||
xtw.WriteStartElement("configuration");
|
||||
xtw.WriteRequiredElementString("datName", Header.Name);
|
||||
xtw.WriteElementString("datVersion", Items.TotalCount.ToString());
|
||||
xtw.WriteRequiredElementString("system", Header.System);
|
||||
xtw.WriteRequiredElementString("screenshotsWidth", Header.ScreenshotsWidth);
|
||||
xtw.WriteRequiredElementString("screenshotsHeight", Header.ScreenshotsHeight);
|
||||
|
||||
if (Header.Infos != null)
|
||||
{
|
||||
xtw.WriteStartElement("infos");
|
||||
|
||||
foreach (var info in Header.Infos)
|
||||
{
|
||||
xtw.WriteStartElement(info.Name);
|
||||
xtw.WriteAttributeString("visible", info.Visible?.ToString());
|
||||
xtw.WriteAttributeString("inNamingOption", info.InNamingOption?.ToString());
|
||||
xtw.WriteAttributeString("default", info.Default?.ToString());
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
|
||||
// End infos
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
|
||||
if (Header.CanOpen != null)
|
||||
{
|
||||
xtw.WriteStartElement("canOpen");
|
||||
|
||||
foreach (string extension in Header.CanOpen)
|
||||
{
|
||||
xtw.WriteElementString("extension", extension);
|
||||
}
|
||||
|
||||
// End canOpen
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
|
||||
xtw.WriteStartElement("newDat");
|
||||
xtw.WriteRequiredElementString("datVersionURL", Header.Url);
|
||||
|
||||
xtw.WriteStartElement("datUrl");
|
||||
xtw.WriteAttributeString("fileName", $"{Header.FileName ?? string.Empty}.zip");
|
||||
xtw.WriteString(Header.Url);
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteRequiredElementString("imURL", Header.Url);
|
||||
|
||||
// End newDat
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("search");
|
||||
|
||||
xtw.WriteStartElement("to");
|
||||
xtw.WriteAttributeString("value", "location");
|
||||
xtw.WriteAttributeString("default", "true");
|
||||
xtw.WriteAttributeString("auto", "true");
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("to");
|
||||
xtw.WriteAttributeString("value", "romSize");
|
||||
xtw.WriteAttributeString("default", "true");
|
||||
xtw.WriteAttributeString("auto", "false");
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("to");
|
||||
xtw.WriteAttributeString("value", "languages");
|
||||
xtw.WriteAttributeString("default", "true");
|
||||
xtw.WriteAttributeString("auto", "true");
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("to");
|
||||
xtw.WriteAttributeString("value", "saveType");
|
||||
xtw.WriteAttributeString("default", "false");
|
||||
xtw.WriteAttributeString("auto", "false");
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("to");
|
||||
xtw.WriteAttributeString("value", "publisher");
|
||||
xtw.WriteAttributeString("default", "false");
|
||||
xtw.WriteAttributeString("auto", "true");
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("to");
|
||||
xtw.WriteAttributeString("value", "sourceRom");
|
||||
xtw.WriteAttributeString("default", "false");
|
||||
xtw.WriteAttributeString("auto", "true");
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End search
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteRequiredElementString("romTitle", Header.RomTitle ?? "%u - %n");
|
||||
|
||||
// End configuration
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("games");
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
/// <returns>True if the data was written, false on error</returns>
|
||||
private void WriteDatItem(XmlTextWriter xtw, DatItem datItem)
|
||||
{
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Build the state
|
||||
xtw.WriteStartElement("game");
|
||||
xtw.WriteElementString("imageNumber", "1");
|
||||
xtw.WriteElementString("releaseNumber", "1");
|
||||
xtw.WriteRequiredElementString("title", datItem.GetName() ?? string.Empty);
|
||||
xtw.WriteElementString("saveType", "None");
|
||||
|
||||
if (datItem.ItemType == ItemType.Rom)
|
||||
{
|
||||
var rom = datItem as Rom;
|
||||
xtw.WriteRequiredElementString("romSize", rom.Size?.ToString());
|
||||
}
|
||||
|
||||
xtw.WriteRequiredElementString("publisher", datItem.Machine.Publisher);
|
||||
xtw.WriteElementString("location", "0");
|
||||
xtw.WriteElementString("sourceRom", "None");
|
||||
xtw.WriteElementString("language", "0");
|
||||
|
||||
if (datItem.ItemType == ItemType.Rom)
|
||||
{
|
||||
var rom = datItem as Rom;
|
||||
string tempext = "." + PathExtensions.GetNormalizedExtension(rom.Name);
|
||||
|
||||
xtw.WriteStartElement("files");
|
||||
if (!string.IsNullOrWhiteSpace(rom.CRC))
|
||||
{
|
||||
xtw.WriteStartElement("romCRC");
|
||||
xtw.WriteRequiredAttributeString("extension", tempext);
|
||||
xtw.WriteString(rom.CRC?.ToUpperInvariant());
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
|
||||
// End files
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
|
||||
xtw.WriteElementString("im1CRC", "00000000");
|
||||
xtw.WriteElementString("im2CRC", "00000000");
|
||||
xtw.WriteRequiredElementString("comment", datItem.Machine.Comment);
|
||||
xtw.WriteRequiredElementString("duplicateID", datItem.Machine.CloneOf);
|
||||
|
||||
// End game
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT footer using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
/// <returns>True if the data was written, false on error</returns>
|
||||
private void WriteFooter(XmlTextWriter xtw)
|
||||
{
|
||||
// End games
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("gui");
|
||||
|
||||
xtw.WriteStartElement("images");
|
||||
xtw.WriteAttributeString("width", "487");
|
||||
xtw.WriteAttributeString("height", "162");
|
||||
|
||||
xtw.WriteStartElement("image");
|
||||
xtw.WriteAttributeString("x", "0");
|
||||
xtw.WriteAttributeString("y", "0");
|
||||
xtw.WriteAttributeString("width", "240");
|
||||
xtw.WriteAttributeString("height", "160");
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.WriteStartElement("image");
|
||||
xtw.WriteAttributeString("x", "245");
|
||||
xtw.WriteAttributeString("y", "0");
|
||||
xtw.WriteAttributeString("width", "240");
|
||||
xtw.WriteAttributeString("height", "160");
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End images
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End gui
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End dat
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
760
SabreTools.DatFiles/DatFiles/OpenMSX.cs
Normal file
760
SabreTools.DatFiles/DatFiles/OpenMSX.cs
Normal file
@@ -0,0 +1,760 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.Core.Tools;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a openMSX softawre list XML DAT
|
||||
/// </summary>
|
||||
internal class OpenMSX : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public OpenMSX(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a openMSX softawre list XML DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Prepare all internal variables
|
||||
XmlReader xtr = XmlReader.Create(filename, new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
IgnoreComments = true,
|
||||
IgnoreWhitespace = true,
|
||||
ValidationFlags = XmlSchemaValidationFlags.None,
|
||||
ValidationType = ValidationType.None,
|
||||
});
|
||||
|
||||
// If we got a null reader, just return
|
||||
if (xtr == null)
|
||||
return;
|
||||
|
||||
// Otherwise, read the file to the end
|
||||
try
|
||||
{
|
||||
xtr.MoveToContent();
|
||||
while (!xtr.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (xtr.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
xtr.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (xtr.Name)
|
||||
{
|
||||
case "softwaredb":
|
||||
Header.Name = Header.Name ?? "openMSX Software List";
|
||||
Header.Description = Header.Description ?? Header.Name;
|
||||
Header.Date = Header.Date ?? xtr.GetAttribute("timestamp");
|
||||
xtr.Read();
|
||||
break;
|
||||
|
||||
// We want to process the entire subtree of the software
|
||||
case "software":
|
||||
ReadSoftware(xtr.ReadSubtree(), filename, indexId);
|
||||
|
||||
// Skip the software now that we've processed it
|
||||
xtr.Skip();
|
||||
break;
|
||||
|
||||
default:
|
||||
xtr.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning(ex, $"Exception found while parsing '{filename}'");
|
||||
if (throwOnError)
|
||||
{
|
||||
xtr.Dispose();
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// For XML errors, just skip the affected node
|
||||
xtr?.Read();
|
||||
}
|
||||
|
||||
xtr.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read software information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a machine block</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadSoftware(XmlReader reader, string filename, int indexId)
|
||||
{
|
||||
// If we have an empty machine, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
int diskno = 0;
|
||||
bool containsItems = false;
|
||||
|
||||
// Create a new machine
|
||||
Machine machine = new Machine();
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the roms from the machine
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "title":
|
||||
machine.Name = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "genmsxid":
|
||||
machine.GenMSXID = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "system":
|
||||
machine.System = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "company":
|
||||
machine.Manufacturer = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "year":
|
||||
machine.Year = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "country":
|
||||
machine.Country = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "dump":
|
||||
containsItems = ReadDump(reader.ReadSubtree(), machine, diskno, filename, indexId);
|
||||
diskno++;
|
||||
|
||||
// Skip the dump now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read dump information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a part block</param>
|
||||
/// <param name="machine">Machine information to pass to contained items</param>
|
||||
/// <param name="diskno">Disk number to use when outputting to other DAT formats</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private bool ReadDump(
|
||||
XmlReader reader,
|
||||
Machine machine,
|
||||
int diskno,
|
||||
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int indexId)
|
||||
{
|
||||
List<DatItem> items = new List<DatItem>();
|
||||
Original original = null;
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the elements from the dump
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "rom":
|
||||
DatItem rom = ReadRom(reader.ReadSubtree(), machine, diskno, filename, indexId);
|
||||
if (rom != null)
|
||||
items.Add(rom);
|
||||
|
||||
// Skip the rom now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
case "megarom":
|
||||
DatItem megarom = ReadMegaRom(reader.ReadSubtree(), machine, diskno, filename, indexId);
|
||||
if (megarom != null)
|
||||
items.Add(megarom);
|
||||
|
||||
// Skip the megarom now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
case "sccpluscart":
|
||||
DatItem sccpluscart = ReadSccPlusCart(reader.ReadSubtree(), machine, diskno, filename, indexId);
|
||||
if (sccpluscart != null)
|
||||
items.Add(sccpluscart);
|
||||
|
||||
// Skip the sccpluscart now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
case "original":
|
||||
original = new Original
|
||||
{
|
||||
Value = reader.GetAttribute("value").AsYesNo(),
|
||||
Content = reader.ReadElementContentAsString()
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have any items, loop through and add them
|
||||
foreach (DatItem item in items)
|
||||
{
|
||||
switch (item.ItemType)
|
||||
{
|
||||
case ItemType.Rom:
|
||||
(item as Rom).Original = original;
|
||||
break;
|
||||
}
|
||||
|
||||
item.CopyMachineInformation(machine);
|
||||
ParseAddHelper(item);
|
||||
}
|
||||
|
||||
return items.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read rom information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a rom block</param>
|
||||
/// <param name="machine">Machine information to pass to contained items</param>
|
||||
/// <param name="diskno">Disk number to use when outputting to other DAT formats</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private DatItem ReadRom(
|
||||
XmlReader reader,
|
||||
Machine machine,
|
||||
int diskno,
|
||||
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int indexId)
|
||||
{
|
||||
string hash = string.Empty,
|
||||
offset = string.Empty,
|
||||
type = string.Empty,
|
||||
remark = string.Empty;
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the elements from the rom
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "hash":
|
||||
hash = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "start":
|
||||
offset = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "type":
|
||||
type = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "remark":
|
||||
remark = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a hash, then create and return the item
|
||||
if (!string.IsNullOrWhiteSpace(hash))
|
||||
{
|
||||
return new Rom
|
||||
{
|
||||
Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty),
|
||||
Offset = offset,
|
||||
Size = null,
|
||||
SHA1 = hash,
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
|
||||
OpenMSXSubType = OpenMSXSubType.Rom,
|
||||
OpenMSXType = type,
|
||||
Remark = remark,
|
||||
};
|
||||
}
|
||||
|
||||
// No valid item means returning null
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read megarom information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a megarom block</param>
|
||||
/// <param name="machine">Machine information to pass to contained items</param>
|
||||
/// <param name="diskno">Disk number to use when outputting to other DAT formats</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private DatItem ReadMegaRom(
|
||||
XmlReader reader,
|
||||
Machine machine,
|
||||
int diskno,
|
||||
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int indexId)
|
||||
{
|
||||
string hash = string.Empty,
|
||||
offset = string.Empty,
|
||||
type = string.Empty,
|
||||
remark = string.Empty;
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the elements from the dump
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "hash":
|
||||
hash = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "start":
|
||||
offset = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "type":
|
||||
reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "remark":
|
||||
remark = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a hash, then create and return the item
|
||||
if (!string.IsNullOrWhiteSpace(hash))
|
||||
{
|
||||
return new Rom
|
||||
{
|
||||
Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty),
|
||||
Offset = offset,
|
||||
Size = null,
|
||||
SHA1 = hash,
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
|
||||
OpenMSXSubType = OpenMSXSubType.MegaRom,
|
||||
OpenMSXType = type,
|
||||
Remark = remark,
|
||||
};
|
||||
}
|
||||
|
||||
// No valid item means returning null
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read sccpluscart information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a sccpluscart block</param>
|
||||
/// <param name="machine">Machine information to pass to contained items</param>
|
||||
/// <param name="diskno">Disk number to use when outputting to other DAT formats</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private DatItem ReadSccPlusCart(
|
||||
XmlReader reader,
|
||||
Machine machine,
|
||||
int diskno,
|
||||
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int indexId)
|
||||
{
|
||||
string boot = string.Empty,
|
||||
hash = string.Empty,
|
||||
remark = string.Empty;
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the elements from the dump
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "boot":
|
||||
boot = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "hash":
|
||||
hash = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "remark":
|
||||
remark = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a hash, then create and return the item
|
||||
if (!string.IsNullOrWhiteSpace(hash))
|
||||
{
|
||||
return new Rom
|
||||
{
|
||||
Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty),
|
||||
Size = null,
|
||||
SHA1 = hash,
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
|
||||
OpenMSXSubType = OpenMSXSubType.SCCPlusCart,
|
||||
Boot = boot,
|
||||
Remark = remark,
|
||||
};
|
||||
}
|
||||
|
||||
// No valid item means returning null
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
XmlTextWriter xtw = new XmlTextWriter(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 Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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];
|
||||
|
||||
// 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(xtw);
|
||||
|
||||
// If we have a new game, output the beginning of the new item
|
||||
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
|
||||
WriteStartGame(xtw, datItem);
|
||||
|
||||
// Check for a "null" item
|
||||
datItem = ProcessNullifiedItem(datItem);
|
||||
|
||||
// Write out the item if we're not ignoring
|
||||
if (!ShouldIgnore(datItem, ignoreblanks))
|
||||
WriteDatItem(xtw, datItem);
|
||||
|
||||
// Set the new data to compare against
|
||||
lastgame = datItem.Machine.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the file footer out
|
||||
WriteFooter(xtw);
|
||||
|
||||
logger.Verbose("File written!" + Environment.NewLine);
|
||||
xtw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteHeader(XmlTextWriter xtw)
|
||||
{
|
||||
xtw.WriteStartDocument();
|
||||
xtw.WriteDocType("softwaredb", null, "softwaredb1.dtd", null);
|
||||
|
||||
xtw.WriteStartElement("softwaredb");
|
||||
xtw.WriteRequiredAttributeString("timestamp", Header.Date);
|
||||
|
||||
//TODO: Figure out how to fix the issue with removed formatting after this point
|
||||
// xtw.WriteComment("Credits");
|
||||
// xtw.WriteCData(@"The softwaredb.xml file contains information about rom mapper types
|
||||
|
||||
//-Copyright 2003 Nicolas Beyaert(Initial Database)
|
||||
//-Copyright 2004 - 2013 BlueMSX Team
|
||||
//-Copyright 2005 - 2020 openMSX Team
|
||||
//-Generation MSXIDs by www.generation - msx.nl
|
||||
|
||||
//- Thanks go out to:
|
||||
//-Generation MSX / Sylvester for the incredible source of information
|
||||
//- p_gimeno and diedel for their help adding and valdiating ROM additions
|
||||
//- GDX for additional ROM info and validations and corrections");
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteStartGame(XmlTextWriter xtw, DatItem datItem)
|
||||
{
|
||||
// No game should start with a path separator
|
||||
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
|
||||
|
||||
// Build the state
|
||||
xtw.WriteStartElement("software");
|
||||
xtw.WriteRequiredElementString("title", datItem.Machine.Name);
|
||||
xtw.WriteRequiredElementString("genmsxid", datItem.Machine.GenMSXID);
|
||||
xtw.WriteRequiredElementString("system", datItem.Machine.System);
|
||||
xtw.WriteRequiredElementString("company", datItem.Machine.Manufacturer);
|
||||
xtw.WriteRequiredElementString("year", datItem.Machine.Year);
|
||||
xtw.WriteRequiredElementString("country", datItem.Machine.Country);
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteEndGame(XmlTextWriter xtw)
|
||||
{
|
||||
// End software
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(XmlTextWriter xtw, DatItem datItem)
|
||||
{
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Build the state
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
xtw.WriteStartElement("dump");
|
||||
|
||||
if (rom.Original != null)
|
||||
{
|
||||
xtw.WriteStartElement("original");
|
||||
xtw.WriteAttributeString("value", rom.Original.Value == true ? "true" : "false");
|
||||
xtw.WriteString(rom.Original.Content);
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
|
||||
switch (rom.OpenMSXSubType)
|
||||
{
|
||||
// Default to Rom for converting from other formats
|
||||
case OpenMSXSubType.Rom:
|
||||
case OpenMSXSubType.NULL:
|
||||
xtw.WriteStartElement(rom.OpenMSXSubType.FromOpenMSXSubType());
|
||||
xtw.WriteRequiredElementString("hash", rom.SHA1?.ToLowerInvariant());
|
||||
xtw.WriteOptionalElementString("start", rom.Offset);
|
||||
xtw.WriteOptionalElementString("type", rom.OpenMSXType);
|
||||
xtw.WriteOptionalElementString("remark", rom.Remark);
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case OpenMSXSubType.MegaRom:
|
||||
xtw.WriteStartElement(rom.OpenMSXSubType.FromOpenMSXSubType());
|
||||
xtw.WriteRequiredElementString("hash", rom.SHA1?.ToLowerInvariant());
|
||||
xtw.WriteOptionalElementString("start", rom.Offset);
|
||||
xtw.WriteOptionalElementString("type", rom.OpenMSXType);
|
||||
xtw.WriteOptionalElementString("remark", rom.Remark);
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case OpenMSXSubType.SCCPlusCart:
|
||||
xtw.WriteStartElement(rom.OpenMSXSubType.FromOpenMSXSubType());
|
||||
xtw.WriteOptionalElementString("boot", rom.Boot);
|
||||
xtw.WriteRequiredElementString("hash", rom.SHA1?.ToLowerInvariant());
|
||||
xtw.WriteOptionalElementString("remark", rom.Remark);
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
}
|
||||
|
||||
// End dump
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
}
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT footer using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteFooter(XmlTextWriter xtw)
|
||||
{
|
||||
// End software
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End softwaredb
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
200
SabreTools.DatFiles/DatFiles/Reports/BaseReport.cs
Normal file
200
SabreTools.DatFiles/DatFiles/Reports/BaseReport.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.DatFiles;
|
||||
|
||||
namespace SabreTools.DatFiles.Reports
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for a report output format
|
||||
/// </summary>
|
||||
/// TODO: Can this be overhauled to have all types write like DatFiles?
|
||||
public abstract class BaseReport
|
||||
{
|
||||
protected string _name;
|
||||
protected long _machineCount;
|
||||
protected ItemDictionary _stats;
|
||||
|
||||
protected StreamWriter _writer;
|
||||
protected bool _baddumpCol;
|
||||
protected bool _nodumpCol;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new report from the filename
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to write out to</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
public BaseReport(string filename, bool baddumpCol = false, bool nodumpCol = false)
|
||||
{
|
||||
var fs = File.Create(filename);
|
||||
if (fs != null)
|
||||
_writer = new StreamWriter(fs);
|
||||
|
||||
_baddumpCol = baddumpCol;
|
||||
_nodumpCol = nodumpCol;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new report from the stream
|
||||
/// </summary>
|
||||
/// <param name="stream">Output stream to write to</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
public BaseReport(Stream stream, bool baddumpCol = false, bool nodumpCol = false)
|
||||
{
|
||||
if (!stream.CanWrite)
|
||||
throw new ArgumentException(nameof(stream));
|
||||
|
||||
_writer = new StreamWriter(stream);
|
||||
_baddumpCol = baddumpCol;
|
||||
_nodumpCol = nodumpCol;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a specific type of BaseReport to be used based on a format and user inputs
|
||||
/// </summary>
|
||||
/// <param name="statReportFormat">Format of the Statistics Report to be created</param>
|
||||
/// <param name="filename">Name of the file to write out to</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
/// <returns>BaseReport of the specific internal type that corresponds to the inputs</returns>
|
||||
public static BaseReport Create(StatReportFormat statReportFormat, string filename, bool baddumpCol, bool nodumpCol)
|
||||
{
|
||||
#if NET_FRAMEWORK
|
||||
switch (statReportFormat)
|
||||
{
|
||||
case StatReportFormat.None:
|
||||
return new Textfile(Console.OpenStandardOutput(), baddumpCol, nodumpCol);
|
||||
|
||||
case StatReportFormat.Textfile:
|
||||
return new Textfile(filename, baddumpCol, nodumpCol);
|
||||
|
||||
case StatReportFormat.CSV:
|
||||
return new SeparatedValue(filename, ',', baddumpCol, nodumpCol);
|
||||
|
||||
case StatReportFormat.HTML:
|
||||
return new Html(filename, baddumpCol, nodumpCol);
|
||||
|
||||
case StatReportFormat.SSV:
|
||||
return new SeparatedValue(filename, ';', baddumpCol, nodumpCol);
|
||||
|
||||
case StatReportFormat.TSV:
|
||||
return new SeparatedValue(filename, '\t', baddumpCol, nodumpCol);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
#else
|
||||
return statReportFormat switch
|
||||
{
|
||||
StatReportFormat.None => new Textfile(Console.OpenStandardOutput(), baddumpCol, nodumpCol),
|
||||
StatReportFormat.Textfile => new Textfile(filename, baddumpCol, nodumpCol),
|
||||
StatReportFormat.CSV => new SeparatedValue(filename, ',', baddumpCol, nodumpCol),
|
||||
StatReportFormat.HTML => new Html(filename, baddumpCol, nodumpCol),
|
||||
StatReportFormat.SSV => new SeparatedValue(filename, ';', baddumpCol, nodumpCol),
|
||||
StatReportFormat.TSV => new SeparatedValue(filename, '\t', baddumpCol, nodumpCol),
|
||||
_ => null,
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the statistics that is being output
|
||||
/// </summary>
|
||||
public void ReplaceStatistics(string datName, long machineCount, ItemDictionary datStats)
|
||||
{
|
||||
_name = datName;
|
||||
_machineCount = machineCount;
|
||||
_stats = datStats;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the report to the output stream
|
||||
/// </summary>
|
||||
public abstract void Write();
|
||||
|
||||
/// <summary>
|
||||
/// Write out the header to the stream, if any exists
|
||||
/// </summary>
|
||||
public abstract void WriteHeader();
|
||||
|
||||
/// <summary>
|
||||
/// Write out the mid-header to the stream, if any exists
|
||||
/// </summary>
|
||||
public abstract void WriteMidHeader();
|
||||
|
||||
/// <summary>
|
||||
/// Write out the separator to the stream, if any exists
|
||||
/// </summary>
|
||||
public abstract void WriteMidSeparator();
|
||||
|
||||
/// <summary>
|
||||
/// Write out the footer-separator to the stream, if any exists
|
||||
/// </summary>
|
||||
public abstract void WriteFooterSeparator();
|
||||
|
||||
/// <summary>
|
||||
/// Write out the footer to the stream, if any exists
|
||||
/// </summary>
|
||||
public abstract void WriteFooter();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the human-readable file size for an arbitrary, 64-bit file size
|
||||
/// The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB"
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns>Human-readable file size</returns>
|
||||
/// <link>http://www.somacon.com/p576.php</link>
|
||||
protected static string GetBytesReadable(long input)
|
||||
{
|
||||
// Get absolute value
|
||||
long absolute_i = (input < 0 ? -input : input);
|
||||
|
||||
// Determine the suffix and readable value
|
||||
string suffix;
|
||||
double readable;
|
||||
if (absolute_i >= 0x1000000000000000) // Exabyte
|
||||
{
|
||||
suffix = "EB";
|
||||
readable = (input >> 50);
|
||||
}
|
||||
else if (absolute_i >= 0x4000000000000) // Petabyte
|
||||
{
|
||||
suffix = "PB";
|
||||
readable = (input >> 40);
|
||||
}
|
||||
else if (absolute_i >= 0x10000000000) // Terabyte
|
||||
{
|
||||
suffix = "TB";
|
||||
readable = (input >> 30);
|
||||
}
|
||||
else if (absolute_i >= 0x40000000) // Gigabyte
|
||||
{
|
||||
suffix = "GB";
|
||||
readable = (input >> 20);
|
||||
}
|
||||
else if (absolute_i >= 0x100000) // Megabyte
|
||||
{
|
||||
suffix = "MB";
|
||||
readable = (input >> 10);
|
||||
}
|
||||
else if (absolute_i >= 0x400) // Kilobyte
|
||||
{
|
||||
suffix = "KB";
|
||||
readable = input;
|
||||
}
|
||||
else
|
||||
{
|
||||
return input.ToString("0 B"); // Byte
|
||||
}
|
||||
|
||||
// Divide by 1024 to get fractional value
|
||||
readable /= 1024;
|
||||
|
||||
// Return formatted number with suffix
|
||||
return readable.ToString("0.### ") + suffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
148
SabreTools.DatFiles/DatFiles/Reports/Html.cs
Normal file
148
SabreTools.DatFiles/DatFiles/Reports/Html.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
|
||||
namespace SabreTools.DatFiles.Reports
|
||||
{
|
||||
/// <summary>
|
||||
/// HTML report format
|
||||
/// </summary>
|
||||
/// TODO: Make output standard width, without making the entire thing a table
|
||||
internal class Html : BaseReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new report from the filename
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to write out to</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
public Html(string filename, bool baddumpCol = false, bool nodumpCol = false)
|
||||
: base(filename, baddumpCol, nodumpCol)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new report from the stream
|
||||
/// </summary>
|
||||
/// <param name="datfile">DatFile to write out statistics for</param>
|
||||
/// <param name="stream">Output stream to write to</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
public Html(Stream stream, bool baddumpCol = false, bool nodumpCol = false)
|
||||
: base(stream, baddumpCol, nodumpCol)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the report to file
|
||||
/// </summary>
|
||||
public override void Write()
|
||||
{
|
||||
string line = "\t\t\t<tr" + (_name.StartsWith("DIR: ")
|
||||
? $" class=\"dir\"><td>{WebUtility.HtmlEncode(_name.Remove(0, 5))}"
|
||||
: $"><td>{WebUtility.HtmlEncode(_name)}") + "</td>"
|
||||
+ $"<td align=\"right\">{GetBytesReadable(_stats.TotalSize)}</td>"
|
||||
+ $"<td align=\"right\">{_machineCount}</td>"
|
||||
+ $"<td align=\"right\">{_stats.RomCount}</td>"
|
||||
+ $"<td align=\"right\">{_stats.DiskCount}</td>"
|
||||
+ $"<td align=\"right\">{_stats.CRCCount}</td>"
|
||||
+ $"<td align=\"right\">{_stats.MD5Count}</td>"
|
||||
#if NET_FRAMEWORK
|
||||
+ $"<td align=\"right\">{_stats.RIPEMD160Count}</td>"
|
||||
#endif
|
||||
+ $"<td align=\"right\">{_stats.SHA1Count}</td>"
|
||||
+ $"<td align=\"right\">{_stats.SHA256Count}</td>"
|
||||
+ (_baddumpCol ? $"<td align=\"right\">{_stats.BaddumpCount}</td>" : string.Empty)
|
||||
+ (_nodumpCol ? $"<td align=\"right\">{_stats.NodumpCount}</td>" : string.Empty)
|
||||
+ "</tr>\n";
|
||||
_writer.Write(line);
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the header to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteHeader()
|
||||
{
|
||||
_writer.Write(@"<!DOCTYPE html>
|
||||
<html>
|
||||
<header>
|
||||
<title>DAT Statistics Report</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.dir {
|
||||
color: #0088FF;
|
||||
}
|
||||
.right {
|
||||
align: right;
|
||||
}
|
||||
</style>
|
||||
</header>
|
||||
<body>
|
||||
<h2>DAT Statistics Report (" + DateTime.Now.ToShortDateString() + @")</h2>
|
||||
<table border=string.Empty1string.Empty cellpadding=string.Empty5string.Empty cellspacing=string.Empty0string.Empty>
|
||||
");
|
||||
_writer.Flush();
|
||||
|
||||
// Now write the mid header for those who need it
|
||||
WriteMidHeader();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the mid-header to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteMidHeader()
|
||||
{
|
||||
_writer.Write(@" <tr bgcolor=string.Emptygraystring.Empty><th>File Name</th><th align=string.Emptyrightstring.Empty>Total Size</th><th align=string.Emptyrightstring.Empty>Games</th><th align=string.Emptyrightstring.Empty>Roms</th>"
|
||||
+ @"<th align=string.Emptyrightstring.Empty>Disks</th><th align=string.Emptyrightstring.Empty># with CRC</th><th align=string.Emptyrightstring.Empty># with MD5</th><th align=string.Emptyrightstring.Empty># with SHA-1</th><th align=string.Emptyrightstring.Empty># with SHA-256</th>"
|
||||
+ (_baddumpCol ? "<th class=\".right\">Baddumps</th>" : string.Empty) + (_nodumpCol ? "<th class=\".right\">Nodumps</th>" : string.Empty) + "</tr>\n");
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the separator to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteMidSeparator()
|
||||
{
|
||||
_writer.Write("<tr><td colspan=\""
|
||||
+ (_baddumpCol && _nodumpCol
|
||||
? "12"
|
||||
: (_baddumpCol ^ _nodumpCol
|
||||
? "11"
|
||||
: "10")
|
||||
)
|
||||
+ "\"></td></tr>\n");
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the footer-separator to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteFooterSeparator()
|
||||
{
|
||||
_writer.Write("<tr border=\"0\"><td colspan=\""
|
||||
+ (_baddumpCol && _nodumpCol
|
||||
? "12"
|
||||
: (_baddumpCol ^ _nodumpCol
|
||||
? "11"
|
||||
: "10")
|
||||
)
|
||||
+ "\"></td></tr>\n");
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the footer to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteFooter()
|
||||
{
|
||||
_writer.Write(@" </table>
|
||||
</body>
|
||||
</html>
|
||||
");
|
||||
_writer.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
108
SabreTools.DatFiles/DatFiles/Reports/SeparatedValue.cs
Normal file
108
SabreTools.DatFiles/DatFiles/Reports/SeparatedValue.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.DatFiles.Reports
|
||||
{
|
||||
/// <summary>
|
||||
/// Separated-Value report format
|
||||
/// </summary>
|
||||
internal class SeparatedValue : BaseReport
|
||||
{
|
||||
private readonly char _separator;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new report from the filename
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to write out to</param>
|
||||
/// <param name="separator">Separator character to use in output</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
public SeparatedValue(string filename, char separator, bool baddumpCol = false, bool nodumpCol = false)
|
||||
: base(filename, baddumpCol, nodumpCol)
|
||||
{
|
||||
_separator = separator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new report from the input DatFile and the stream
|
||||
/// </summary>
|
||||
/// <param name="stream">Output stream to write to</param>
|
||||
/// <param name="separator">Separator character to use in output</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
public SeparatedValue(Stream stream, char separator, bool baddumpCol = false, bool nodumpCol = false)
|
||||
: base(stream, baddumpCol, nodumpCol)
|
||||
{
|
||||
_separator = separator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the report to file
|
||||
/// </summary>
|
||||
public override void Write()
|
||||
{
|
||||
string line = string.Format("\"" + _name + "\"{0}"
|
||||
+ "\"" + _stats.TotalSize + "\"{0}"
|
||||
+ "\"" + _machineCount + "\"{0}"
|
||||
+ "\"" + _stats.RomCount + "\"{0}"
|
||||
+ "\"" + _stats.DiskCount + "\"{0}"
|
||||
+ "\"" + _stats.CRCCount + "\"{0}"
|
||||
+ "\"" + _stats.MD5Count + "\"{0}"
|
||||
#if NET_FRAMEWORK
|
||||
+ "\"" + _stats.RIPEMD160Count + "\"{0}"
|
||||
#endif
|
||||
+ "\"" + _stats.SHA1Count + "\"{0}"
|
||||
+ "\"" + _stats.SHA256Count + "\"{0}"
|
||||
+ "\"" + _stats.SHA384Count + "\"{0}"
|
||||
+ "\"" + _stats.SHA512Count + "\""
|
||||
+ (_baddumpCol ? "{0}\"" + _stats.BaddumpCount + "\"" : string.Empty)
|
||||
+ (_nodumpCol ? "{0}\"" + _stats.NodumpCount + "\"" : string.Empty)
|
||||
+ "\n", _separator);
|
||||
|
||||
_writer.Write(line);
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the header to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteHeader()
|
||||
{
|
||||
_writer.Write(string.Format("\"File Name\"{0}\"Total Size\"{0}\"Games\"{0}\"Roms\"{0}\"Disks\"{0}\"# with CRC\"{0}\"# with MD5\"{0}\"# with SHA-1\"{0}\"# with SHA-256\""
|
||||
+ (_baddumpCol ? "{0}\"BadDumps\"" : string.Empty) + (_nodumpCol ? "{0}\"Nodumps\"" : string.Empty) + "\n", _separator));
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the mid-header to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteMidHeader()
|
||||
{
|
||||
// This call is a no-op for separated value formats
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the separator to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteMidSeparator()
|
||||
{
|
||||
// This call is a no-op for separated value formats
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the footer-separator to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteFooterSeparator()
|
||||
{
|
||||
_writer.Write("\n");
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the footer to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteFooter()
|
||||
{
|
||||
// This call is a no-op for separated value formats
|
||||
}
|
||||
}
|
||||
}
|
||||
109
SabreTools.DatFiles/DatFiles/Reports/Textfile.cs
Normal file
109
SabreTools.DatFiles/DatFiles/Reports/Textfile.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.DatFiles.Reports
|
||||
{
|
||||
/// <summary>
|
||||
/// Textfile report format
|
||||
/// </summary>
|
||||
internal class Textfile : BaseReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new report from the filename
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to write out to</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
public Textfile(string filename, bool baddumpCol = false, bool nodumpCol = false)
|
||||
: base(filename, baddumpCol, nodumpCol)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new report from the stream
|
||||
/// </summary>
|
||||
/// <param name="stream">Output stream to write to</param>
|
||||
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
|
||||
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
|
||||
public Textfile(Stream stream, bool baddumpCol = false, bool nodumpCol = false)
|
||||
: base(stream, baddumpCol, nodumpCol)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the report to file
|
||||
/// </summary>
|
||||
public override void Write()
|
||||
{
|
||||
string line = @"'" + _name + @"':
|
||||
--------------------------------------------------
|
||||
Uncompressed size: " + GetBytesReadable(_stats.TotalSize) + @"
|
||||
Games found: " + _machineCount + @"
|
||||
Roms found: " + _stats.RomCount + @"
|
||||
Disks found: " + _stats.DiskCount + @"
|
||||
Roms with CRC: " + _stats.CRCCount + @"
|
||||
Roms with MD5: " + _stats.MD5Count
|
||||
#if NET_FRAMEWORK
|
||||
+ @"
|
||||
Roms with RIPEMD160: " + _stats.RIPEMD160Count
|
||||
#endif
|
||||
+ @"
|
||||
Roms with SHA-1: " + _stats.SHA1Count + @"
|
||||
Roms with SHA-256: " + _stats.SHA256Count + @"
|
||||
Roms with SHA-384: " + _stats.SHA384Count + @"
|
||||
Roms with SHA-512: " + _stats.SHA512Count + "\n";
|
||||
|
||||
if (_baddumpCol)
|
||||
line += " Roms with BadDump status: " + _stats.BaddumpCount + "\n";
|
||||
|
||||
if (_nodumpCol)
|
||||
line += " Roms with Nodump status: " + _stats.NodumpCount + "\n";
|
||||
|
||||
// For spacing between DATs
|
||||
line += "\n\n";
|
||||
|
||||
_writer.Write(line);
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the header to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteHeader()
|
||||
{
|
||||
// This call is a no-op for textfile output
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the mid-header to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteMidHeader()
|
||||
{
|
||||
// This call is a no-op for textfile output
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the separator to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteMidSeparator()
|
||||
{
|
||||
// This call is a no-op for textfile output
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the footer-separator to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteFooterSeparator()
|
||||
{
|
||||
_writer.Write("\n");
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the footer to the stream, if any exists
|
||||
/// </summary>
|
||||
public override void WriteFooter()
|
||||
{
|
||||
// This call is a no-op for textfile output
|
||||
}
|
||||
}
|
||||
}
|
||||
523
SabreTools.DatFiles/DatFiles/RomCenter.cs
Normal file
523
SabreTools.DatFiles/DatFiles/RomCenter.cs
Normal file
@@ -0,0 +1,523 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.Core.Tools;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a RomCenter DAT
|
||||
/// </summary>
|
||||
internal class RomCenter : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public RomCenter(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a RomCenter DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Prepare all intenral variables
|
||||
IniReader ir = new IniReader(filename) { ValidateRows = false };
|
||||
|
||||
// If we got a null reader, just return
|
||||
if (ir == null)
|
||||
return;
|
||||
|
||||
// Otherwise, read the file to the end
|
||||
try
|
||||
{
|
||||
ir.ReadNextLine();
|
||||
while (!ir.EndOfStream)
|
||||
{
|
||||
// We don't care about whitespace or comments
|
||||
if (ir.RowType == IniRowType.None || ir.RowType == IniRowType.Comment)
|
||||
{
|
||||
ir.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have a section
|
||||
if (ir.RowType == IniRowType.SectionHeader)
|
||||
{
|
||||
switch (ir.Section.ToLowerInvariant())
|
||||
{
|
||||
case "credits":
|
||||
ReadCreditsSection(ir);
|
||||
break;
|
||||
|
||||
case "dat":
|
||||
ReadDatSection(ir);
|
||||
break;
|
||||
|
||||
case "emulator":
|
||||
ReadEmulatorSection(ir);
|
||||
break;
|
||||
|
||||
case "games":
|
||||
ReadGamesSection(ir, filename, indexId);
|
||||
break;
|
||||
|
||||
// Unknown section so we ignore it
|
||||
default:
|
||||
ir.ReadNextLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string message = $"'{filename}' - There was an error parsing line {ir.LineNumber} '{ir.CurrentLine}'";
|
||||
logger.Error(ex, message);
|
||||
if (throwOnError)
|
||||
{
|
||||
ir.Dispose();
|
||||
throw new Exception(message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
ir.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read credits information
|
||||
/// </summary>
|
||||
/// <param name="reader">IniReader to use to parse the credits</param>
|
||||
private void ReadCreditsSection(IniReader reader)
|
||||
{
|
||||
// If the reader is somehow null, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
reader.ReadNextLine();
|
||||
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "credits")
|
||||
{
|
||||
// We don't care about whitespace, comments, or invalid
|
||||
if (reader.RowType != IniRowType.KeyValue)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var kvp = reader.KeyValuePair;
|
||||
|
||||
// If the KeyValuePair is invalid, skip it
|
||||
if (kvp == null)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all credits items
|
||||
switch (kvp?.Key.ToLowerInvariant())
|
||||
{
|
||||
case "author":
|
||||
Header.Author = Header.Author ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "version":
|
||||
Header.Version = Header.Version ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "email":
|
||||
Header.Email = Header.Email ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "homepage":
|
||||
Header.Homepage = Header.Homepage ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "url":
|
||||
Header.Url = Header.Url ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "date":
|
||||
Header.Date = Header.Date ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "comment":
|
||||
Header.Comment = Header.Comment ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
// Unknown value, just skip
|
||||
default:
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read dat information
|
||||
/// </summary>
|
||||
/// <param name="reader">IniReader to use to parse the credits</param>
|
||||
private void ReadDatSection(IniReader reader)
|
||||
{
|
||||
// If the reader is somehow null, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
reader.ReadNextLine();
|
||||
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "dat")
|
||||
{
|
||||
// We don't care about whitespace, comments, or invalid
|
||||
if (reader.RowType != IniRowType.KeyValue)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var kvp = reader.KeyValuePair;
|
||||
|
||||
// If the KeyValuePair is invalid, skip it
|
||||
if (kvp == null)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all dat items
|
||||
switch (kvp?.Key.ToLowerInvariant())
|
||||
{
|
||||
case "version":
|
||||
Header.RomCenterVersion = Header.RomCenterVersion ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "plugin":
|
||||
Header.System = Header.System ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "split":
|
||||
if (Header.ForceMerging == MergingFlag.None && kvp?.Value == "1")
|
||||
Header.ForceMerging = MergingFlag.Split;
|
||||
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "merge":
|
||||
if (Header.ForceMerging == MergingFlag.None && kvp?.Value == "1")
|
||||
Header.ForceMerging = MergingFlag.Merged;
|
||||
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
// Unknown value, just skip
|
||||
default:
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read emulator information
|
||||
/// </summary>
|
||||
/// <param name="reader">IniReader to use to parse the credits</param>
|
||||
private void ReadEmulatorSection(IniReader reader)
|
||||
{
|
||||
// If the reader is somehow null, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
reader.ReadNextLine();
|
||||
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "emulator")
|
||||
{
|
||||
// We don't care about whitespace, comments, or invalid
|
||||
if (reader.RowType != IniRowType.KeyValue)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var kvp = reader.KeyValuePair;
|
||||
|
||||
// If the KeyValuePair is invalid, skip it
|
||||
if (kvp == null)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all emulator items (ONLY OVERWRITE IF THERE'S NO DATA)
|
||||
switch (kvp?.Key.ToLowerInvariant())
|
||||
{
|
||||
case "refname":
|
||||
Header.Name = Header.Name ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "version":
|
||||
Header.Description = Header.Description ?? kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
// Unknown value, just skip
|
||||
default:
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read games information
|
||||
/// </summary>
|
||||
/// <param name="reader">IniReader to use to parse the credits</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadGamesSection(IniReader reader, string filename, int indexId)
|
||||
{
|
||||
// If the reader is somehow null, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
reader.ReadNextLine();
|
||||
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "games")
|
||||
{
|
||||
// We don't care about whitespace or comments
|
||||
// We're keeping keyvalue in case a file has '=' in the row
|
||||
if (reader.RowType != IniRowType.Invalid && reader.RowType != IniRowType.KeyValue)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Roms are not valid row formats, usually
|
||||
string line = reader.CurrentLine;
|
||||
|
||||
// If we don't have a valid game, keep reading
|
||||
if (!line.StartsWith("¬"))
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some old RC DATs have this behavior
|
||||
if (line.Contains("¬N¬O"))
|
||||
line = line.Replace("¬N¬O", string.Empty) + "¬¬";
|
||||
|
||||
/*
|
||||
The rominfo order is as follows:
|
||||
1 - parent name
|
||||
2 - parent description
|
||||
3 - game name
|
||||
4 - game description
|
||||
5 - rom name
|
||||
6 - rom crc
|
||||
7 - rom size
|
||||
8 - romof name
|
||||
9 - merge name
|
||||
*/
|
||||
string[] rominfo = line.Split('¬');
|
||||
Rom rom = new Rom
|
||||
{
|
||||
Name = rominfo[5],
|
||||
Size = Sanitizer.CleanLong(rominfo[7]),
|
||||
CRC = rominfo[6],
|
||||
ItemStatus = ItemStatus.None,
|
||||
|
||||
Machine = new Machine
|
||||
{
|
||||
Name = rominfo[3],
|
||||
Description = rominfo[4],
|
||||
CloneOf = rominfo[1],
|
||||
RomOf = rominfo[8],
|
||||
},
|
||||
|
||||
MergeTag = rominfo[9],
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
};
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(rom);
|
||||
|
||||
reader.ReadNextLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
IniWriter iw = new IniWriter(fs, new UTF8Encoding(false));
|
||||
|
||||
// Write out the header
|
||||
WriteHeader(iw);
|
||||
|
||||
// Write out each of the machines and roms
|
||||
string lastgame = null;
|
||||
List<string> splitpath = new List<string>();
|
||||
|
||||
// Use a sorted list of games to output
|
||||
foreach (string key in Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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(iw, datItem);
|
||||
|
||||
// Set the new data to compare against
|
||||
lastgame = datItem.Machine.Name;
|
||||
}
|
||||
}
|
||||
|
||||
logger.Verbose("File written!" + Environment.NewLine);
|
||||
iw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="iw">IniWriter to output to</param>
|
||||
private void WriteHeader(IniWriter iw)
|
||||
{
|
||||
iw.WriteSection("CREDITS");
|
||||
iw.WriteKeyValuePair("author", Header.Author);
|
||||
iw.WriteKeyValuePair("version", Header.Version);
|
||||
iw.WriteKeyValuePair("comment", Header.Comment);
|
||||
|
||||
iw.WriteSection("DAT");
|
||||
iw.WriteKeyValuePair("version", Header.RomCenterVersion ?? "2.50");
|
||||
iw.WriteKeyValuePair("plugin", Header.System);
|
||||
iw.WriteKeyValuePair("split", Header.ForceMerging == MergingFlag.Split ? "1" : "0");
|
||||
iw.WriteKeyValuePair("merge", Header.ForceMerging == MergingFlag.Full || Header.ForceMerging == MergingFlag.Merged ? "1" : "0");
|
||||
|
||||
iw.WriteSection("EMULATOR");
|
||||
iw.WriteKeyValuePair("refname", Header.Name);
|
||||
iw.WriteKeyValuePair("version", Header.Description);
|
||||
|
||||
iw.WriteSection("GAMES");
|
||||
|
||||
iw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="iw">IniWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(IniWriter iw, DatItem datItem)
|
||||
{
|
||||
/*
|
||||
The rominfo order is as follows:
|
||||
1 - parent name
|
||||
2 - parent description
|
||||
3 - game name
|
||||
4 - game description
|
||||
5 - rom name
|
||||
6 - rom crc
|
||||
7 - rom size
|
||||
8 - romof name
|
||||
9 - merge name
|
||||
*/
|
||||
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Build the state
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
|
||||
iw.WriteString($"¬{rom.Machine.CloneOf ?? string.Empty}");
|
||||
iw.WriteString($"¬{rom.Machine.CloneOf ?? string.Empty}");
|
||||
iw.WriteString($"¬{rom.Machine.Name ?? string.Empty}");
|
||||
if (string.IsNullOrWhiteSpace(rom.Machine.Description ?? string.Empty))
|
||||
iw.WriteString($"¬{rom.Machine.Name ?? string.Empty}");
|
||||
else
|
||||
iw.WriteString($"¬{rom.Machine.Description ?? string.Empty}");
|
||||
iw.WriteString($"¬{rom.Name ?? string.Empty}");
|
||||
iw.WriteString($"¬{rom.CRC ?? string.Empty}");
|
||||
iw.WriteString($"¬{rom.Size?.ToString() ?? string.Empty}");
|
||||
iw.WriteString($"¬{rom.Machine.RomOf ?? string.Empty}");
|
||||
iw.WriteString($"¬{rom.MergeTag ?? string.Empty}");
|
||||
iw.WriteString("¬");
|
||||
iw.WriteLine();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
iw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
524
SabreTools.DatFiles/DatFiles/SabreJSON.cs
Normal file
524
SabreTools.DatFiles/DatFiles/SabreJSON.cs
Normal file
@@ -0,0 +1,524 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.Core.Tools;
|
||||
using SabreTools.DatItems;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a reference SabreDAT JSON
|
||||
/// </summary>
|
||||
internal class SabreJSON : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public SabreJSON(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a reference JSON DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Prepare all internal variables
|
||||
StreamReader sr = new StreamReader(File.OpenRead(filename), new UTF8Encoding(false));
|
||||
JsonTextReader jtr = new JsonTextReader(sr);
|
||||
|
||||
// If we got a null reader, just return
|
||||
if (jtr == null)
|
||||
return;
|
||||
|
||||
// Otherwise, read the file to the end
|
||||
try
|
||||
{
|
||||
jtr.Read();
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
// Skip everything not a property name
|
||||
if (jtr.TokenType != JsonToken.PropertyName)
|
||||
{
|
||||
jtr.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (jtr.Value)
|
||||
{
|
||||
// Header value
|
||||
case "header":
|
||||
ReadHeader(jtr);
|
||||
jtr.Read();
|
||||
break;
|
||||
|
||||
// Machine array
|
||||
case "machines":
|
||||
ReadMachines(jtr, filename, indexId);
|
||||
jtr.Read();
|
||||
break;
|
||||
|
||||
default:
|
||||
jtr.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning($"Exception found while parsing '{filename}': {ex}");
|
||||
if (throwOnError) throw ex;
|
||||
}
|
||||
|
||||
jtr.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read header information
|
||||
/// </summary>
|
||||
/// <param name="jtr">JsonTextReader to use to parse the header</param>
|
||||
private void ReadHeader(JsonTextReader jtr)
|
||||
{
|
||||
// If the reader is invalid, skip
|
||||
if (jtr == null)
|
||||
return;
|
||||
|
||||
// Read in the header and apply any new fields
|
||||
jtr.Read();
|
||||
JsonSerializer js = new JsonSerializer();
|
||||
DatHeader header = js.Deserialize<DatHeader>(jtr);
|
||||
Header.ConditionalCopy(header);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read machine array information
|
||||
/// </summary>
|
||||
/// <param name="itr">JsonTextReader to use to parse the machine</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadMachines(JsonTextReader jtr, string filename, int indexId)
|
||||
{
|
||||
// If the reader is invalid, skip
|
||||
if (jtr == null)
|
||||
return;
|
||||
|
||||
// Read in the machine array
|
||||
jtr.Read();
|
||||
JsonSerializer js = new JsonSerializer();
|
||||
JArray machineArray = js.Deserialize<JArray>(jtr);
|
||||
|
||||
// Loop through each machine object and process
|
||||
foreach (JObject machineObj in machineArray)
|
||||
{
|
||||
ReadMachine(machineObj, filename, indexId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read machine object information
|
||||
/// </summary>
|
||||
/// <param name="machineObj">JObject representing a single machine</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadMachine(JObject machineObj, string filename, int indexId)
|
||||
{
|
||||
// If object is invalid, skip it
|
||||
if (machineObj == null)
|
||||
return;
|
||||
|
||||
// Prepare internal variables
|
||||
Machine machine = null;
|
||||
|
||||
// Read the machine info, if possible
|
||||
if (machineObj.ContainsKey("machine"))
|
||||
machine = machineObj["machine"].ToObject<Machine>();
|
||||
|
||||
// Read items, if possible
|
||||
if (machineObj.ContainsKey("items"))
|
||||
ReadItems(machineObj["items"] as JArray, filename, indexId, machine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read item array information
|
||||
/// </summary>
|
||||
/// <param name="itemsArr">JArray representing the items list</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="machine">Machine information to add to the parsed items</param>
|
||||
private void ReadItems(
|
||||
JArray itemsArr,
|
||||
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int indexId,
|
||||
|
||||
// Miscellaneous
|
||||
Machine machine)
|
||||
{
|
||||
// If the array is invalid, skip
|
||||
if (itemsArr == null)
|
||||
return;
|
||||
|
||||
// Loop through each datitem object and process
|
||||
foreach (JObject itemObj in itemsArr)
|
||||
{
|
||||
ReadItem(itemObj, filename, indexId, machine);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read item information
|
||||
/// </summary>
|
||||
/// <param name="machineObj">JObject representing a single datitem</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="machine">Machine information to add to the parsed items</param>
|
||||
private void ReadItem(
|
||||
JObject itemObj,
|
||||
|
||||
// Standard Dat parsing
|
||||
string filename,
|
||||
int indexId,
|
||||
|
||||
// Miscellaneous
|
||||
Machine machine)
|
||||
{
|
||||
// If we have an empty item, skip it
|
||||
if (itemObj == null)
|
||||
return;
|
||||
|
||||
// Prepare internal variables
|
||||
DatItem datItem = null;
|
||||
|
||||
// Read the datitem info, if possible
|
||||
if (itemObj.ContainsKey("datitem"))
|
||||
{
|
||||
JToken datItemObj = itemObj["datitem"];
|
||||
switch (datItemObj.Value<string>("type").AsItemType())
|
||||
{
|
||||
case ItemType.Adjuster:
|
||||
datItem = datItemObj.ToObject<Adjuster>();
|
||||
break;
|
||||
case ItemType.Analog:
|
||||
datItem = datItemObj.ToObject<Analog>();
|
||||
break;
|
||||
case ItemType.Archive:
|
||||
datItem = datItemObj.ToObject<Archive>();
|
||||
break;
|
||||
case ItemType.BiosSet:
|
||||
datItem = datItemObj.ToObject<BiosSet>();
|
||||
break;
|
||||
case ItemType.Blank:
|
||||
datItem = datItemObj.ToObject<Blank>();
|
||||
break;
|
||||
case ItemType.Chip:
|
||||
datItem = datItemObj.ToObject<Chip>();
|
||||
break;
|
||||
case ItemType.Condition:
|
||||
datItem = datItemObj.ToObject<Condition>();
|
||||
break;
|
||||
case ItemType.Configuration:
|
||||
datItem = datItemObj.ToObject<Configuration>();
|
||||
break;
|
||||
case ItemType.Control:
|
||||
datItem = datItemObj.ToObject<Control>();
|
||||
break;
|
||||
case ItemType.DataArea:
|
||||
datItem = datItemObj.ToObject<DataArea>();
|
||||
break;
|
||||
case ItemType.Device:
|
||||
datItem = datItemObj.ToObject<Device>();
|
||||
break;
|
||||
case ItemType.DeviceReference:
|
||||
datItem = datItemObj.ToObject<DeviceReference>();
|
||||
break;
|
||||
case ItemType.DipSwitch:
|
||||
datItem = datItemObj.ToObject<DipSwitch>();
|
||||
break;
|
||||
case ItemType.Disk:
|
||||
datItem = datItemObj.ToObject<Disk>();
|
||||
break;
|
||||
case ItemType.DiskArea:
|
||||
datItem = datItemObj.ToObject<DiskArea>();
|
||||
break;
|
||||
case ItemType.Display:
|
||||
datItem = datItemObj.ToObject<Display>();
|
||||
break;
|
||||
case ItemType.Driver:
|
||||
datItem = datItemObj.ToObject<Driver>();
|
||||
break;
|
||||
case ItemType.Extension:
|
||||
datItem = datItemObj.ToObject<Extension>();
|
||||
break;
|
||||
case ItemType.Feature:
|
||||
datItem = datItemObj.ToObject<Feature>();
|
||||
break;
|
||||
case ItemType.Info:
|
||||
datItem = datItemObj.ToObject<Info>();
|
||||
break;
|
||||
case ItemType.Input:
|
||||
datItem = datItemObj.ToObject<Input>();
|
||||
break;
|
||||
case ItemType.Instance:
|
||||
datItem = datItemObj.ToObject<Instance>();
|
||||
break;
|
||||
case ItemType.Location:
|
||||
datItem = datItemObj.ToObject<Location>();
|
||||
break;
|
||||
case ItemType.Media:
|
||||
datItem = datItemObj.ToObject<Media>();
|
||||
break;
|
||||
case ItemType.Part:
|
||||
datItem = datItemObj.ToObject<Part>();
|
||||
break;
|
||||
case ItemType.PartFeature:
|
||||
datItem = datItemObj.ToObject<PartFeature>();
|
||||
break;
|
||||
case ItemType.Port:
|
||||
datItem = datItemObj.ToObject<Port>();
|
||||
break;
|
||||
case ItemType.RamOption:
|
||||
datItem = datItemObj.ToObject<RamOption>();
|
||||
break;
|
||||
case ItemType.Release:
|
||||
datItem = datItemObj.ToObject<Release>();
|
||||
break;
|
||||
case ItemType.Rom:
|
||||
datItem = datItemObj.ToObject<Rom>();
|
||||
break;
|
||||
case ItemType.Sample:
|
||||
datItem = datItemObj.ToObject<Sample>();
|
||||
break;
|
||||
case ItemType.Setting:
|
||||
datItem = datItemObj.ToObject<Setting>();
|
||||
break;
|
||||
case ItemType.SharedFeature:
|
||||
datItem = datItemObj.ToObject<SharedFeature>();
|
||||
break;
|
||||
case ItemType.Slot:
|
||||
datItem = datItemObj.ToObject<Slot>();
|
||||
break;
|
||||
case ItemType.SlotOption:
|
||||
datItem = datItemObj.ToObject<SlotOption>();
|
||||
break;
|
||||
case ItemType.SoftwareList:
|
||||
datItem = datItemObj.ToObject<DatItems.SoftwareList>();
|
||||
break;
|
||||
case ItemType.Sound:
|
||||
datItem = datItemObj.ToObject<Sound>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a valid datitem, copy machine info and add
|
||||
if (datItem != null)
|
||||
{
|
||||
datItem.CopyMachineInformation(machine);
|
||||
datItem.Source = new Source { Index = indexId, Name = filename };
|
||||
ParseAddHelper(datItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
StreamWriter sw = new StreamWriter(fs, new UTF8Encoding(false));
|
||||
JsonTextWriter jtw = new JsonTextWriter(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 Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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];
|
||||
|
||||
// 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(jtw);
|
||||
|
||||
// If we have a new game, output the beginning of the new item
|
||||
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
|
||||
WriteStartGame(jtw, datItem);
|
||||
|
||||
// Check for a "null" item
|
||||
datItem = ProcessNullifiedItem(datItem);
|
||||
|
||||
// Write out the item if we're not ignoring
|
||||
if (!ShouldIgnore(datItem, ignoreblanks))
|
||||
WriteDatItem(jtw, datItem);
|
||||
|
||||
// Set the new data to compare against
|
||||
lastgame = datItem.Machine.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the file footer out
|
||||
WriteFooter(jtw);
|
||||
|
||||
logger.Verbose("File written!" + Environment.NewLine);
|
||||
jtw.Close();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied JsonTextWriter
|
||||
/// </summary>
|
||||
/// <param name="jtw">JsonTextWriter to output to</param>
|
||||
private void WriteHeader(JsonTextWriter jtw)
|
||||
{
|
||||
jtw.WriteStartObject();
|
||||
|
||||
// Write the DatHeader
|
||||
jtw.WritePropertyName("header");
|
||||
JsonSerializer js = new JsonSerializer() { Formatting = Formatting.Indented };
|
||||
js.Serialize(jtw, Header);
|
||||
|
||||
jtw.WritePropertyName("machines");
|
||||
jtw.WriteStartArray();
|
||||
|
||||
jtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied JsonTextWriter
|
||||
/// </summary>
|
||||
/// <param name="jtw">JsonTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteStartGame(JsonTextWriter jtw, DatItem datItem)
|
||||
{
|
||||
// No game should start with a path separator
|
||||
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty;
|
||||
|
||||
// Build the state
|
||||
jtw.WriteStartObject();
|
||||
|
||||
// Write the Machine
|
||||
jtw.WritePropertyName("machine");
|
||||
JsonSerializer js = new JsonSerializer() { Formatting = Formatting.Indented };
|
||||
js.Serialize(jtw, datItem.Machine);
|
||||
|
||||
jtw.WritePropertyName("items");
|
||||
jtw.WriteStartArray();
|
||||
|
||||
jtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game end using the supplied JsonTextWriter
|
||||
/// </summary>
|
||||
/// <param name="jtw">JsonTextWriter to output to</param>
|
||||
private void WriteEndGame(JsonTextWriter jtw)
|
||||
{
|
||||
// End items
|
||||
jtw.WriteEndArray();
|
||||
|
||||
// End machine
|
||||
jtw.WriteEndObject();
|
||||
|
||||
jtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied JsonTextWriter
|
||||
/// </summary>
|
||||
/// <param name="jtw">JsonTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(JsonTextWriter jtw, DatItem datItem)
|
||||
{
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Build the state
|
||||
jtw.WriteStartObject();
|
||||
|
||||
// Write the DatItem
|
||||
jtw.WritePropertyName("datitem");
|
||||
JsonSerializer js = new JsonSerializer() { ContractResolver = new BaseFirstContractResolver(), Formatting = Formatting.Indented };
|
||||
js.Serialize(jtw, datItem);
|
||||
|
||||
// End item
|
||||
jtw.WriteEndObject();
|
||||
|
||||
jtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT footer using the supplied JsonTextWriter
|
||||
/// </summary>
|
||||
/// <param name="jtw">JsonTextWriter to output to</param>
|
||||
private void WriteFooter(JsonTextWriter jtw)
|
||||
{
|
||||
// End items
|
||||
jtw.WriteEndArray();
|
||||
|
||||
// End machine
|
||||
jtw.WriteEndObject();
|
||||
|
||||
// End machines
|
||||
jtw.WriteEndArray();
|
||||
|
||||
// End file
|
||||
jtw.WriteEndObject();
|
||||
|
||||
jtw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
373
SabreTools.DatFiles/DatFiles/SabreXML.cs
Normal file
373
SabreTools.DatFiles/DatFiles/SabreXML.cs
Normal file
@@ -0,0 +1,373 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
using SabreTools.DatItems;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a SabreDAT XML
|
||||
/// </summary>
|
||||
internal class SabreXML : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public SabreXML(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an SabreDat XML DAT and return all found directories and files within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Prepare all internal variables
|
||||
XmlReader xtr = XmlReader.Create(filename, new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
IgnoreComments = true,
|
||||
IgnoreWhitespace = true,
|
||||
ValidationFlags = XmlSchemaValidationFlags.None,
|
||||
ValidationType = ValidationType.None,
|
||||
});
|
||||
|
||||
// If we got a null reader, just return
|
||||
if (xtr == null)
|
||||
return;
|
||||
|
||||
// Otherwise, read the file to the end
|
||||
try
|
||||
{
|
||||
xtr.MoveToContent();
|
||||
while (!xtr.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (xtr.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
xtr.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (xtr.Name)
|
||||
{
|
||||
case "header":
|
||||
XmlSerializer xs = new XmlSerializer(typeof(DatHeader));
|
||||
DatHeader header = xs.Deserialize(xtr.ReadSubtree()) as DatHeader;
|
||||
Header.ConditionalCopy(header);
|
||||
xtr.Skip();
|
||||
break;
|
||||
|
||||
case "directory":
|
||||
ReadDirectory(xtr.ReadSubtree(), filename, indexId);
|
||||
|
||||
// Skip the directory node now that we've processed it
|
||||
xtr.Read();
|
||||
break;
|
||||
default:
|
||||
xtr.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning(ex, $"Exception found while parsing '{filename}'");
|
||||
if (throwOnError)
|
||||
{
|
||||
xtr.Dispose();
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// For XML errors, just skip the affected node
|
||||
xtr?.Read();
|
||||
}
|
||||
|
||||
xtr.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read directory information
|
||||
/// </summary>
|
||||
/// <param name="xtr">XmlReader to use to parse the header</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadDirectory(XmlReader xtr, string filename, int indexId)
|
||||
{
|
||||
// If the reader is invalid, skip
|
||||
if (xtr == null)
|
||||
return;
|
||||
|
||||
// Prepare internal variables
|
||||
Machine machine = null;
|
||||
|
||||
// Otherwise, read the directory
|
||||
xtr.MoveToContent();
|
||||
while (!xtr.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (xtr.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
xtr.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (xtr.Name)
|
||||
{
|
||||
case "machine":
|
||||
XmlSerializer xs = new XmlSerializer(typeof(Machine));
|
||||
machine = xs.Deserialize(xtr.ReadSubtree()) as Machine;
|
||||
xtr.Skip();
|
||||
break;
|
||||
|
||||
case "files":
|
||||
ReadFiles(xtr.ReadSubtree(), machine, filename, indexId);
|
||||
|
||||
// Skip the directory node now that we've processed it
|
||||
xtr.Read();
|
||||
break;
|
||||
default:
|
||||
xtr.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read Files information
|
||||
/// </summary>
|
||||
/// <param name="xtr">XmlReader to use to parse the header</param>
|
||||
/// <param name="machine">Machine to copy information from</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadFiles(XmlReader xtr, Machine machine, string filename, int indexId)
|
||||
{
|
||||
// If the reader is invalid, skip
|
||||
if (xtr == null)
|
||||
return;
|
||||
|
||||
// Otherwise, read the items
|
||||
xtr.MoveToContent();
|
||||
while (!xtr.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (xtr.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
xtr.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (xtr.Name)
|
||||
{
|
||||
case "datitem":
|
||||
XmlSerializer xs = new XmlSerializer(typeof(DatItem));
|
||||
DatItem item = xs.Deserialize(xtr.ReadSubtree()) as DatItem;
|
||||
item.CopyMachineInformation(machine);
|
||||
item.Source = new Source { Name = filename, Index = indexId };
|
||||
ParseAddHelper(item);
|
||||
xtr.Skip();
|
||||
break;
|
||||
default:
|
||||
xtr.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
XmlTextWriter xtw = new XmlTextWriter(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 Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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];
|
||||
|
||||
// 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(xtw);
|
||||
|
||||
// If we have a new game, output the beginning of the new item
|
||||
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
|
||||
WriteStartGame(xtw, datItem);
|
||||
|
||||
// Check for a "null" item
|
||||
datItem = ProcessNullifiedItem(datItem);
|
||||
|
||||
// Write out the item if we're not ignoring
|
||||
if (!ShouldIgnore(datItem, ignoreblanks))
|
||||
WriteDatItem(xtw, datItem);
|
||||
|
||||
// Set the new data to compare against
|
||||
lastgame = datItem.Machine.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the file footer out
|
||||
WriteFooter(xtw);
|
||||
|
||||
logger.Verbose("File written!" + Environment.NewLine);
|
||||
xtw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteHeader(XmlTextWriter xtw)
|
||||
{
|
||||
xtw.WriteStartDocument();
|
||||
|
||||
xtw.WriteStartElement("datafile");
|
||||
|
||||
XmlSerializer xs = new XmlSerializer(typeof(DatHeader));
|
||||
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
|
||||
ns.Add("", "");
|
||||
xs.Serialize(xtw, Header, ns);
|
||||
|
||||
xtw.WriteStartElement("data");
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteStartGame(XmlTextWriter xtw, DatItem datItem)
|
||||
{
|
||||
// No game should start with a path separator
|
||||
datItem.Machine.Name = datItem.Machine.Name?.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty;
|
||||
|
||||
// Write the machine
|
||||
xtw.WriteStartElement("directory");
|
||||
XmlSerializer xs = new XmlSerializer(typeof(Machine));
|
||||
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
|
||||
ns.Add("", "");
|
||||
xs.Serialize(xtw, datItem.Machine, ns);
|
||||
|
||||
xtw.WriteStartElement("files");
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteEndGame(XmlTextWriter xtw)
|
||||
{
|
||||
// End files
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End directory
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(XmlTextWriter xtw, DatItem datItem)
|
||||
{
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Write the DatItem
|
||||
XmlSerializer xs = new XmlSerializer(typeof(DatItem));
|
||||
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
|
||||
ns.Add("", "");
|
||||
xs.Serialize(xtw, datItem, ns);
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT footer using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteFooter(XmlTextWriter xtw)
|
||||
{
|
||||
// End files
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End directory
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End data
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End datafile
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
297
SabreTools.DatFiles/DatFiles/SeparatedValue.cs
Normal file
297
SabreTools.DatFiles/DatFiles/SeparatedValue.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.Core.Tools;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a value-separated DAT
|
||||
/// </summary>
|
||||
internal class SeparatedValue : DatFile
|
||||
{
|
||||
// Private instance variables specific to Separated Value DATs
|
||||
private readonly char _delim;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
/// <param name="delim">Delimiter for parsing individual lines</param>
|
||||
public SeparatedValue(DatFile datFile, char delim)
|
||||
: base(datFile)
|
||||
{
|
||||
_delim = delim;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a character-separated value DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Open a file reader
|
||||
Encoding enc = FileExtensions.GetEncoding(filename);
|
||||
SeparatedValueReader svr = new SeparatedValueReader(File.OpenRead(filename), enc)
|
||||
{
|
||||
Header = true,
|
||||
Quotes = true,
|
||||
Separator = _delim,
|
||||
VerifyFieldCount = true,
|
||||
};
|
||||
|
||||
// If we're somehow at the end of the stream already, we can't do anything
|
||||
if (svr.EndOfStream)
|
||||
return;
|
||||
|
||||
// Read in the header
|
||||
svr.ReadHeader();
|
||||
|
||||
// Loop through all of the data lines
|
||||
while (!svr.EndOfStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the current line, split and parse
|
||||
svr.ReadNextLine();
|
||||
|
||||
// Create mapping dictionary
|
||||
var mappings = new Dictionary<Field, string>();
|
||||
|
||||
// Now we loop through and get values for everything
|
||||
for (int i = 0; i < svr.HeaderValues.Count; i++)
|
||||
{
|
||||
Field key = svr.HeaderValues[i].AsField();
|
||||
string value = svr.Line[i];
|
||||
mappings[key] = value;
|
||||
}
|
||||
|
||||
// Set DatHeader fields
|
||||
DatHeader header = new DatHeader();
|
||||
header.SetFields(mappings);
|
||||
Header.ConditionalCopy(header);
|
||||
|
||||
// Set Machine and DatItem fields
|
||||
if (mappings.ContainsKey(Field.DatItem_Type))
|
||||
{
|
||||
DatItem datItem = DatItem.Create(mappings[Field.DatItem_Type].AsItemType());
|
||||
datItem.SetFields(mappings);
|
||||
datItem.Source = new Source(indexId, filename);
|
||||
ParseAddHelper(datItem);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string message = $"'{filename}' - There was an error parsing line {svr.LineNumber} '{svr.CurrentLine}'";
|
||||
logger.Error(ex, message);
|
||||
if (throwOnError)
|
||||
{
|
||||
svr.Dispose();
|
||||
throw new Exception(message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svr.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[] { ItemType.Disk, ItemType.Media, ItemType.Rom };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false))
|
||||
{
|
||||
Quotes = true,
|
||||
Separator = this._delim,
|
||||
VerifyFieldCount = true
|
||||
};
|
||||
|
||||
// Write out the header
|
||||
WriteHeader(svw);
|
||||
|
||||
// Use a sorted list of games to output
|
||||
foreach (string key in Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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.Verbose("File written!" + Environment.NewLine);
|
||||
svw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="svw">SeparatedValueWriter to output to</param>
|
||||
private void WriteHeader(SeparatedValueWriter svw)
|
||||
{
|
||||
string[] headers = new string[]
|
||||
{
|
||||
"File Name",
|
||||
"Internal Name",
|
||||
"Description",
|
||||
"Game Name",
|
||||
"Game Description",
|
||||
"Type",
|
||||
"Rom Name",
|
||||
"Disk Name",
|
||||
"Size",
|
||||
"CRC",
|
||||
"MD5",
|
||||
//"RIPEMD160",
|
||||
"SHA1",
|
||||
"SHA256",
|
||||
//"SHA384",
|
||||
//"SHA512",
|
||||
//"SpamSum",
|
||||
"Nodump",
|
||||
};
|
||||
|
||||
svw.WriteHeader(headers);
|
||||
|
||||
svw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="svw">SeparatedValueWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(SeparatedValueWriter svw, DatItem datItem)
|
||||
{
|
||||
// Separated values should only output Rom and Disk
|
||||
if (datItem.ItemType != ItemType.Disk && datItem.ItemType != ItemType.Rom)
|
||||
return;
|
||||
|
||||
// Build the state
|
||||
// TODO: Can we have some way of saying what fields to write out? Support for read extends to all fields now
|
||||
string[] fields = new string[14]; // 18;
|
||||
fields[0] = Header.FileName;
|
||||
fields[1] = Header.Name;
|
||||
fields[2] = Header.Description;
|
||||
fields[3] = datItem.Machine.Name;
|
||||
fields[4] = datItem.Machine.Description;
|
||||
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Disk:
|
||||
var disk = datItem as Disk;
|
||||
fields[5] = "disk";
|
||||
fields[6] = string.Empty;
|
||||
fields[7] = disk.Name;
|
||||
fields[8] = string.Empty;
|
||||
fields[9] = string.Empty;
|
||||
fields[10] = disk.MD5?.ToLowerInvariant();
|
||||
//fields[11] = string.Empty;
|
||||
fields[11] = disk.SHA1?.ToLowerInvariant();
|
||||
fields[12] = string.Empty;
|
||||
//fields[13] = string.Empty;
|
||||
//fields[14] = string.Empty;
|
||||
//fields[15] = string.Empty;
|
||||
fields[13] = disk.ItemStatus.ToString();
|
||||
break;
|
||||
|
||||
case ItemType.Media:
|
||||
var media = datItem as Media;
|
||||
fields[5] = "media";
|
||||
fields[6] = string.Empty;
|
||||
fields[7] = media.Name;
|
||||
fields[8] = string.Empty;
|
||||
fields[9] = string.Empty;
|
||||
fields[10] = media.MD5?.ToLowerInvariant();
|
||||
//fields[11] = string.Empty;
|
||||
fields[11] = media.SHA1?.ToLowerInvariant();
|
||||
fields[12] = media.SHA256?.ToLowerInvariant();
|
||||
//fields[13] = string.Empty;
|
||||
//fields[14] = string.Empty;
|
||||
//fields[15] = media.SpamSum?.ToLowerInvariant();
|
||||
fields[13] = string.Empty;
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
fields[5] = "rom";
|
||||
fields[6] = rom.Name;
|
||||
fields[7] = string.Empty;
|
||||
fields[8] = rom.Size?.ToString();
|
||||
fields[9] = rom.CRC?.ToLowerInvariant();
|
||||
fields[10] = rom.MD5?.ToLowerInvariant();
|
||||
//fields[11] = rom.RIPEMD160?.ToLowerInvariant();
|
||||
fields[11] = rom.SHA1?.ToLowerInvariant();
|
||||
fields[12] = rom.SHA256?.ToLowerInvariant();
|
||||
//fields[13] = rom.SHA384?.ToLowerInvariant();
|
||||
//fields[14] = rom.SHA512?.ToLowerInvariant();
|
||||
//fields[15] = rom.SpamSum?.ToLowerInvariant();
|
||||
fields[13] = rom.ItemStatus.ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
svw.WriteString(CreatePrefixPostfix(datItem, true));
|
||||
svw.WriteValues(fields, false);
|
||||
svw.WriteString(CreatePrefixPostfix(datItem, false));
|
||||
svw.WriteLine();
|
||||
|
||||
svw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
829
SabreTools.DatFiles/DatFiles/SoftwareList.cs
Normal file
829
SabreTools.DatFiles/DatFiles/SoftwareList.cs
Normal file
@@ -0,0 +1,829 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
|
||||
using SabreTools.Core;
|
||||
using SabreTools.Core.Tools;
|
||||
using SabreTools.DatItems;
|
||||
using SabreTools.IO;
|
||||
|
||||
// TODO: Use softwarelist.dtd and *try* to make this write more correctly
|
||||
namespace SabreTools.DatFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parsing and writing of a SoftwareList
|
||||
/// </summary>
|
||||
internal class SoftwareList : DatFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor designed for casting a base DatFile
|
||||
/// </summary>
|
||||
/// <param name="datFile">Parent DatFile to copy from</param>
|
||||
public SoftwareList(DatFile datFile)
|
||||
: base(datFile)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an SofwareList XML DAT and return all found games and roms within
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
|
||||
{
|
||||
// Prepare all internal variables
|
||||
XmlReader xtr = XmlReader.Create(filename, new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
IgnoreComments = true,
|
||||
IgnoreWhitespace = true,
|
||||
ValidationFlags = XmlSchemaValidationFlags.None,
|
||||
ValidationType = ValidationType.None,
|
||||
});
|
||||
|
||||
// If we got a null reader, just return
|
||||
if (xtr == null)
|
||||
return;
|
||||
|
||||
// Otherwise, read the file to the end
|
||||
try
|
||||
{
|
||||
xtr.MoveToContent();
|
||||
while (!xtr.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (xtr.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
xtr.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (xtr.Name)
|
||||
{
|
||||
case "softwarelist":
|
||||
Header.Name = Header.Name ?? xtr.GetAttribute("name") ?? string.Empty;
|
||||
Header.Description = Header.Description ?? xtr.GetAttribute("description") ?? string.Empty;
|
||||
|
||||
xtr.Read();
|
||||
break;
|
||||
|
||||
// We want to process the entire subtree of the machine
|
||||
case "software":
|
||||
ReadSoftware(xtr.ReadSubtree(), filename, indexId);
|
||||
|
||||
// Skip the software now that we've processed it
|
||||
xtr.Skip();
|
||||
break;
|
||||
|
||||
default:
|
||||
xtr.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning(ex, $"Exception found while parsing '{filename}'");
|
||||
if (throwOnError)
|
||||
{
|
||||
xtr.Dispose();
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// For XML errors, just skip the affected node
|
||||
xtr?.Read();
|
||||
}
|
||||
|
||||
xtr.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read software information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a software block</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private void ReadSoftware(XmlReader reader, string filename, int indexId)
|
||||
{
|
||||
// If we have an empty software, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
bool containsItems = false;
|
||||
|
||||
// Create a new machine
|
||||
Machine machine = new Machine
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
CloneOf = reader.GetAttribute("cloneof"),
|
||||
Supported = reader.GetAttribute("supported").AsSupported(),
|
||||
};
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the elements from the software
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "description":
|
||||
machine.Description = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "year":
|
||||
machine.Year = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "publisher":
|
||||
machine.Publisher = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "info":
|
||||
ParseAddHelper(new Info
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
Value = reader.GetAttribute("value"),
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
});
|
||||
|
||||
reader.Read();
|
||||
break;
|
||||
|
||||
case "sharedfeat":
|
||||
ParseAddHelper(new SharedFeature
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
Value = reader.GetAttribute("value"),
|
||||
|
||||
Source = new Source
|
||||
{
|
||||
Index = indexId,
|
||||
Name = filename,
|
||||
},
|
||||
});
|
||||
|
||||
reader.Read();
|
||||
break;
|
||||
|
||||
case "part": // Contains all rom and disk information
|
||||
var part = new Part()
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
Interface = reader.GetAttribute("interface"),
|
||||
};
|
||||
|
||||
// Now read the internal tags
|
||||
containsItems = ReadPart(reader.ReadSubtree(), machine, part, filename, indexId);
|
||||
|
||||
// Skip the part now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read part information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a part block</param>
|
||||
/// <param name="machine">Machine information to pass to contained items</param>
|
||||
/// <param name="part">Part information to pass to contained items</param>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="indexId">Index ID for the DAT</param>
|
||||
private bool ReadPart(XmlReader reader, Machine machine, Part part, string filename, int indexId)
|
||||
{
|
||||
// If we have an empty port, skip it
|
||||
if (reader == null)
|
||||
return false;
|
||||
|
||||
// Get lists ready
|
||||
part.Features = new List<PartFeature>();
|
||||
List<DatItem> items = new List<DatItem>();
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the elements from the software
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "feature":
|
||||
var feature = new PartFeature()
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
Value = reader.GetAttribute("value"),
|
||||
};
|
||||
|
||||
part.Features.Add(feature);
|
||||
|
||||
reader.Read();
|
||||
break;
|
||||
|
||||
case "dataarea":
|
||||
var dataArea = new DataArea
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
Size = Sanitizer.CleanLong(reader.GetAttribute("size")),
|
||||
Width = Sanitizer.CleanLong(reader.GetAttribute("width")),
|
||||
Endianness = reader.GetAttribute("endianness").AsEndianness(),
|
||||
};
|
||||
|
||||
List<DatItem> roms = ReadDataArea(reader.ReadSubtree(), dataArea);
|
||||
|
||||
// If we got valid roms, add them to the list
|
||||
if (roms != null)
|
||||
items.AddRange(roms);
|
||||
|
||||
// Skip the dataarea now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
case "diskarea":
|
||||
var diskArea = new DiskArea
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
};
|
||||
|
||||
List<DatItem> disks = ReadDiskArea(reader.ReadSubtree(), diskArea);
|
||||
|
||||
// If we got valid disks, add them to the list
|
||||
if (disks != null)
|
||||
items.AddRange(disks);
|
||||
|
||||
// Skip the diskarea now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
case "dipswitch":
|
||||
var dipSwitch = new DipSwitch
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
Tag = reader.GetAttribute("tag"),
|
||||
Mask = reader.GetAttribute("mask"),
|
||||
};
|
||||
|
||||
// Now read the internal tags
|
||||
ReadDipSwitch(reader.ReadSubtree(), dipSwitch);
|
||||
|
||||
items.Add(dipSwitch);
|
||||
|
||||
// Skip the dipswitch now that we've processed it
|
||||
reader.Skip();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop over all of the items, if they exist
|
||||
string key = string.Empty;
|
||||
foreach (DatItem item in items)
|
||||
{
|
||||
// Add all missing information
|
||||
switch (item.ItemType)
|
||||
{
|
||||
case ItemType.DipSwitch:
|
||||
(item as DipSwitch).Part = part;
|
||||
break;
|
||||
case ItemType.Disk:
|
||||
(item as Disk).Part = part;
|
||||
break;
|
||||
case ItemType.Rom:
|
||||
(item as Rom).Part = part;
|
||||
|
||||
// If the rom is continue or ignore, add the size to the previous rom
|
||||
// TODO: Can this be done on write? We technically lose information this way.
|
||||
// Order is not guaranteed, and since these don't tend to have any way
|
||||
// of determining what the "previous" item was after this, that info would
|
||||
// have to be stored *with* the item somehow
|
||||
if ((item as Rom).LoadFlag == LoadFlag.Continue || (item as Rom).LoadFlag == LoadFlag.Ignore)
|
||||
{
|
||||
int index = Items[key].Count - 1;
|
||||
DatItem lastrom = Items[key][index];
|
||||
if (lastrom.ItemType == ItemType.Rom)
|
||||
{
|
||||
(lastrom as Rom).Size += (item as Rom).Size;
|
||||
Items[key].RemoveAt(index);
|
||||
Items[key].Add(lastrom);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
item.Source = new Source(indexId, filename);
|
||||
item.CopyMachineInformation(machine);
|
||||
|
||||
// Finally add each item
|
||||
key = ParseAddHelper(item);
|
||||
}
|
||||
|
||||
return items.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read dataarea information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a dataarea block</param>
|
||||
/// <param name="dataArea">DataArea representing the enclosing area</param>
|
||||
private List<DatItem> ReadDataArea(XmlReader reader, DataArea dataArea)
|
||||
{
|
||||
List<DatItem> items = new List<DatItem>();
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the elements from the software
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "rom":
|
||||
var rom = new Rom
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
Size = Sanitizer.CleanLong(reader.GetAttribute("size")),
|
||||
CRC = reader.GetAttribute("crc"),
|
||||
SHA1 = reader.GetAttribute("sha1"),
|
||||
Offset = reader.GetAttribute("offset"),
|
||||
Value = reader.GetAttribute("value"),
|
||||
ItemStatus = reader.GetAttribute("status").AsItemStatus(),
|
||||
LoadFlag = reader.GetAttribute("loadflag").AsLoadFlag(),
|
||||
|
||||
DataArea = dataArea,
|
||||
};
|
||||
|
||||
items.Add(rom);
|
||||
reader.Read();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read diskarea information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a diskarea block</param>
|
||||
/// <param name="diskArea">DiskArea representing the enclosing area</param>
|
||||
private List<DatItem> ReadDiskArea(XmlReader reader, DiskArea diskArea)
|
||||
{
|
||||
List<DatItem> items = new List<DatItem>();
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the elements from the software
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "disk":
|
||||
DatItem disk = new Disk
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
SHA1 = reader.GetAttribute("sha1"),
|
||||
ItemStatus = reader.GetAttribute("status").AsItemStatus(),
|
||||
Writable = reader.GetAttribute("writable").AsYesNo(),
|
||||
|
||||
DiskArea = diskArea,
|
||||
};
|
||||
|
||||
items.Add(disk);
|
||||
reader.Read();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read DipSwitch DipValues information
|
||||
/// </summary>
|
||||
/// <param name="reader">XmlReader representing a diskarea block</param>
|
||||
/// <param name="dipSwitch">DipSwitch to populate</param>
|
||||
private void ReadDipSwitch(XmlReader reader, DipSwitch dipSwitch)
|
||||
{
|
||||
// If we have an empty dipswitch, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
// Get list ready
|
||||
dipSwitch.Values = new List<Setting>();
|
||||
|
||||
// Otherwise, add what is possible
|
||||
reader.MoveToContent();
|
||||
|
||||
while (!reader.EOF)
|
||||
{
|
||||
// We only want elements
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the information from the dipswitch
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "dipvalue":
|
||||
var dipValue = new Setting
|
||||
{
|
||||
Name = reader.GetAttribute("name"),
|
||||
Value = reader.GetAttribute("value"),
|
||||
Default = reader.GetAttribute("default").AsYesNo(),
|
||||
};
|
||||
|
||||
dipSwitch.Values.Add(dipValue);
|
||||
|
||||
reader.Read();
|
||||
break;
|
||||
|
||||
default:
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ItemType[] GetSupportedTypes()
|
||||
{
|
||||
return new ItemType[]
|
||||
{
|
||||
ItemType.DipSwitch,
|
||||
ItemType.Disk,
|
||||
ItemType.Info,
|
||||
ItemType.Rom,
|
||||
ItemType.SharedFeature,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and open an output file for writing direct from a dictionary
|
||||
/// </summary>
|
||||
/// <param name="outfile">Name of the file to write to</param>
|
||||
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
|
||||
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
|
||||
/// <returns>True if the DAT was written correctly, false otherwise</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
XmlTextWriter xtw = new XmlTextWriter(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 Items.SortedKeys)
|
||||
{
|
||||
List<DatItem> 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];
|
||||
|
||||
// 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(xtw);
|
||||
|
||||
// If we have a new game, output the beginning of the new item
|
||||
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
|
||||
WriteStartGame(xtw, datItem);
|
||||
|
||||
// Check for a "null" item
|
||||
datItem = ProcessNullifiedItem(datItem);
|
||||
|
||||
// Write out the item if we're not ignoring
|
||||
if (!ShouldIgnore(datItem, ignoreblanks))
|
||||
WriteDatItem(xtw, datItem);
|
||||
|
||||
// Set the new data to compare against
|
||||
lastgame = datItem.Machine.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the file footer out
|
||||
WriteFooter(xtw);
|
||||
|
||||
logger.Verbose("File written!" + Environment.NewLine);
|
||||
xtw.Dispose();
|
||||
fs.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
if (throwOnError) throw ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT header using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteHeader(XmlTextWriter xtw)
|
||||
{
|
||||
xtw.WriteStartDocument();
|
||||
xtw.WriteDocType("softwarelist", null, "softwarelist.dtd", null);
|
||||
|
||||
xtw.WriteStartElement("softwarelist");
|
||||
xtw.WriteRequiredAttributeString("name", Header.Name);
|
||||
xtw.WriteRequiredAttributeString("description", Header.Description);
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteStartGame(XmlTextWriter xtw, DatItem datItem)
|
||||
{
|
||||
// No game should start with a path separator
|
||||
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
|
||||
|
||||
// Build the state
|
||||
xtw.WriteStartElement("software");
|
||||
xtw.WriteRequiredAttributeString("name", datItem.Machine.Name);
|
||||
if (!string.Equals(datItem.Machine.Name, datItem.Machine.CloneOf, StringComparison.OrdinalIgnoreCase))
|
||||
xtw.WriteOptionalAttributeString("cloneof", datItem.Machine.CloneOf);
|
||||
xtw.WriteOptionalAttributeString("supported", datItem.Machine.Supported.FromSupported(false));
|
||||
|
||||
xtw.WriteOptionalElementString("description", datItem.Machine.Description);
|
||||
xtw.WriteOptionalElementString("year", datItem.Machine.Year);
|
||||
xtw.WriteOptionalElementString("publisher", datItem.Machine.Publisher);
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out Game start using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteEndGame(XmlTextWriter xtw)
|
||||
{
|
||||
// End software
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DatItem using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
/// <param name="datItem">DatItem object to be output</param>
|
||||
private void WriteDatItem(XmlTextWriter xtw, DatItem datItem)
|
||||
{
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Build the state
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.DipSwitch:
|
||||
var dipSwitch = datItem as DipSwitch;
|
||||
xtw.WriteStartElement("dipswitch");
|
||||
xtw.WriteRequiredAttributeString("name", dipSwitch.Name);
|
||||
xtw.WriteRequiredAttributeString("tag", dipSwitch.Tag);
|
||||
xtw.WriteRequiredAttributeString("mask", dipSwitch.Mask);
|
||||
if (dipSwitch.ValuesSpecified)
|
||||
{
|
||||
foreach (Setting dipValue in dipSwitch.Values)
|
||||
{
|
||||
xtw.WriteStartElement("dipvalue");
|
||||
xtw.WriteRequiredAttributeString("name", dipValue.Name);
|
||||
xtw.WriteOptionalAttributeString("value", dipValue.Value);
|
||||
xtw.WriteOptionalAttributeString("default", dipValue.Default.FromYesNo());
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
}
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.Disk:
|
||||
var disk = datItem as Disk;
|
||||
string diskAreaName = disk.DiskArea?.Name;
|
||||
if (string.IsNullOrWhiteSpace(diskAreaName))
|
||||
diskAreaName = "cdrom";
|
||||
|
||||
xtw.WriteStartElement("part");
|
||||
xtw.WriteRequiredAttributeString("name", disk.Part?.Name);
|
||||
xtw.WriteRequiredAttributeString("interface", disk.Part?.Interface);
|
||||
|
||||
if (disk.Part?.FeaturesSpecified == true)
|
||||
{
|
||||
foreach (PartFeature partFeature in disk.Part.Features)
|
||||
{
|
||||
xtw.WriteStartElement("feature");
|
||||
xtw.WriteRequiredAttributeString("name", partFeature.Name);
|
||||
xtw.WriteRequiredAttributeString("value", partFeature.Value);
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
xtw.WriteStartElement("diskarea");
|
||||
xtw.WriteRequiredAttributeString("name", diskAreaName);
|
||||
|
||||
xtw.WriteStartElement("disk");
|
||||
xtw.WriteRequiredAttributeString("name", disk.Name);
|
||||
xtw.WriteOptionalAttributeString("md5", disk.MD5?.ToLowerInvariant());
|
||||
xtw.WriteOptionalAttributeString("sha1", disk.SHA1?.ToLowerInvariant());
|
||||
xtw.WriteOptionalAttributeString("status", disk.ItemStatus.FromItemStatus(false));
|
||||
xtw.WriteOptionalAttributeString("writable", disk.Writable.FromYesNo());
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End diskarea
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End part
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.Info:
|
||||
var info = datItem as Info;
|
||||
xtw.WriteStartElement("info");
|
||||
xtw.WriteRequiredAttributeString("name", info.Name);
|
||||
xtw.WriteRequiredAttributeString("value", info.Value);
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
string dataAreaName = rom.DataArea?.Name;
|
||||
if (string.IsNullOrWhiteSpace(dataAreaName))
|
||||
dataAreaName = "rom";
|
||||
|
||||
xtw.WriteStartElement("part");
|
||||
xtw.WriteRequiredAttributeString("name", rom.Part?.Name);
|
||||
xtw.WriteRequiredAttributeString("interface", rom.Part?.Interface);
|
||||
|
||||
if (rom.Part?.FeaturesSpecified == true)
|
||||
{
|
||||
foreach (PartFeature kvp in rom.Part.Features)
|
||||
{
|
||||
xtw.WriteStartElement("feature");
|
||||
xtw.WriteRequiredAttributeString("name", kvp.Name);
|
||||
xtw.WriteRequiredAttributeString("value", kvp.Value);
|
||||
xtw.WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
xtw.WriteStartElement("dataarea");
|
||||
xtw.WriteRequiredAttributeString("name", dataAreaName);
|
||||
xtw.WriteOptionalAttributeString("size", rom.DataArea?.Size.ToString());
|
||||
xtw.WriteOptionalAttributeString("width", rom.DataArea?.Width?.ToString());
|
||||
xtw.WriteOptionalAttributeString("endianness", rom.DataArea?.Endianness.FromEndianness());
|
||||
|
||||
xtw.WriteStartElement("rom");
|
||||
xtw.WriteRequiredAttributeString("name", rom.Name);
|
||||
xtw.WriteOptionalAttributeString("size", rom.Size?.ToString());
|
||||
xtw.WriteOptionalAttributeString("crc", rom.CRC?.ToLowerInvariant());
|
||||
xtw.WriteOptionalAttributeString("md5", rom.MD5?.ToLowerInvariant());
|
||||
#if NET_FRAMEWORK
|
||||
xtw.WriteOptionalAttributeString("ripemd160", rom.RIPEMD160?.ToLowerInvariant());
|
||||
#endif
|
||||
xtw.WriteOptionalAttributeString("sha1", rom.SHA1?.ToLowerInvariant());
|
||||
xtw.WriteOptionalAttributeString("sha256", rom.SHA256?.ToLowerInvariant());
|
||||
xtw.WriteOptionalAttributeString("sha384", rom.SHA384?.ToLowerInvariant());
|
||||
xtw.WriteOptionalAttributeString("sha512", rom.SHA512?.ToLowerInvariant());
|
||||
xtw.WriteOptionalAttributeString("offset", rom.Offset);
|
||||
xtw.WriteOptionalAttributeString("value", rom.Value);
|
||||
xtw.WriteOptionalAttributeString("status", rom.ItemStatus.FromItemStatus(false));
|
||||
xtw.WriteOptionalAttributeString("loadflag", rom.LoadFlag.FromLoadFlag());
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End dataarea
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End part
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
|
||||
case ItemType.SharedFeature:
|
||||
var sharedFeature = datItem as SharedFeature;
|
||||
xtw.WriteStartElement("sharedfeat");
|
||||
xtw.WriteRequiredAttributeString("name", sharedFeature.Name);
|
||||
xtw.WriteRequiredAttributeString("value", sharedFeature.Value);
|
||||
xtw.WriteEndElement();
|
||||
break;
|
||||
}
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out DAT footer using the supplied StreamWriter
|
||||
/// </summary>
|
||||
/// <param name="xtw">XmlTextWriter to output to</param>
|
||||
private void WriteFooter(XmlTextWriter xtw)
|
||||
{
|
||||
// End software
|
||||
xtw.WriteEndElement();
|
||||
|
||||
// End softwarelist
|
||||
xtw.WriteEndElement();
|
||||
|
||||
xtw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user