Port metadata functionality from ST

This commit is contained in:
Matt Nadareski
2026-03-24 18:03:01 -04:00
parent 5f0fdcfd8d
commit e11a08b587
138 changed files with 37585 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
using SabreTools.Metadata.DatItems;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a Archive.org file list
/// </summary>
public sealed class ArchiveDotOrg : SerializableDatFile<Data.Models.ArchiveDotOrg.Files, Serialization.Readers.ArchiveDotOrg, Serialization.Writers.ArchiveDotOrg, Serialization.CrossModel.ArchiveDotOrg>
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public ArchiveDotOrg(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.ArchiveDotOrg);
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents an AttractMode DAT
/// </summary>
public sealed class AttractMode : SerializableDatFile<Data.Models.AttractMode.MetadataFile, Serialization.Readers.AttractMode, Serialization.Writers.AttractMode, Serialization.CrossModel.AttractMode>
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public AttractMode(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.AttractMode);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
return missingFields;
}
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a ClrMamePro DAT
/// </summary>
public sealed class ClrMamePro : SerializableDatFile<Data.Models.ClrMamePro.MetadataFile, Serialization.Readers.ClrMamePro, Serialization.Writers.ClrMamePro, Serialization.CrossModel.ClrMamePro>
{
#region Fields
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Archive,
ItemType.BiosSet,
ItemType.Chip,
ItemType.DipSwitch,
ItemType.Disk,
ItemType.Display,
ItemType.Driver,
ItemType.Input,
ItemType.Media,
ItemType.Release,
ItemType.Rom,
ItemType.Sample,
ItemType.Sound,
];
#endregion
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public ClrMamePro(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.ClrMamePro);
}
/// <inheritdoc/>
public override void ParseFile(string filename,
int indexId,
bool keep,
bool statsOnly = false,
FilterRunner? filterRunner = null,
bool throwOnError = false)
{
try
{
// Deserialize the input file
var metadataFile = new Serialization.Readers.ClrMamePro().Deserialize(filename, quotes: true);
var metadata = new Serialization.CrossModel.ClrMamePro().Serialize(metadataFile);
// Convert to the internal format
ConvertFromMetadata(metadata, filename, indexId, keep, statsOnly, filterRunner);
}
catch (Exception ex) when (!throwOnError)
{
string message = $"'{filename}' - An error occurred during parsing";
_logger.Error(ex, message);
}
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
#pragma warning disable IDE0010
switch (datItem)
{
case Release release:
if (string.IsNullOrEmpty(release.GetName()))
missingFields.Add(Data.Models.Metadata.Release.NameKey);
if (string.IsNullOrEmpty(release.GetStringFieldValue(Data.Models.Metadata.Release.RegionKey)))
missingFields.Add(Data.Models.Metadata.Release.RegionKey);
break;
case BiosSet biosset:
if (string.IsNullOrEmpty(biosset.GetName()))
missingFields.Add(Data.Models.Metadata.BiosSet.NameKey);
if (string.IsNullOrEmpty(biosset.GetStringFieldValue(Data.Models.Metadata.BiosSet.DescriptionKey)))
missingFields.Add(Data.Models.Metadata.BiosSet.DescriptionKey);
break;
case Rom rom:
if (string.IsNullOrEmpty(rom.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0)
missingFields.Add(Data.Models.Metadata.Rom.SizeKey);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))
&& string.IsNullOrEmpty(rom.GetStringFieldValue("MD2"))
&& string.IsNullOrEmpty(rom.GetStringFieldValue("MD4"))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)))
{
missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
}
break;
case Disk disk:
if (string.IsNullOrEmpty(disk.GetName()))
missingFields.Add(Data.Models.Metadata.Disk.NameKey);
if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key))
&& string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)))
{
missingFields.Add(Data.Models.Metadata.Disk.SHA1Key);
}
break;
case Sample sample:
if (string.IsNullOrEmpty(sample.GetName()))
missingFields.Add(Data.Models.Metadata.Sample.NameKey);
break;
case Archive archive:
if (string.IsNullOrEmpty(archive.GetName()))
missingFields.Add(Data.Models.Metadata.Archive.NameKey);
break;
case Chip chip:
if (chip.GetStringFieldValue(Data.Models.Metadata.Chip.ChipTypeKey).AsChipType() == ChipType.NULL)
missingFields.Add(Data.Models.Metadata.Chip.ChipTypeKey);
if (string.IsNullOrEmpty(chip.GetName()))
missingFields.Add(Data.Models.Metadata.Chip.NameKey);
break;
case Display display:
if (display.GetStringFieldValue(Data.Models.Metadata.Display.DisplayTypeKey).AsDisplayType() == DisplayType.NULL)
missingFields.Add(Data.Models.Metadata.Display.DisplayTypeKey);
if (display.GetInt64FieldValue(Data.Models.Metadata.Display.RotateKey) is null)
missingFields.Add(Data.Models.Metadata.Display.RotateKey);
break;
case Sound sound:
if (sound.GetInt64FieldValue(Data.Models.Metadata.Sound.ChannelsKey) is null)
missingFields.Add(Data.Models.Metadata.Sound.ChannelsKey);
break;
case Input input:
if (input.GetInt64FieldValue(Data.Models.Metadata.Input.PlayersKey) is null)
missingFields.Add(Data.Models.Metadata.Input.PlayersKey);
if (!input.ControlsSpecified)
missingFields.Add(Data.Models.Metadata.Input.ControlKey);
break;
case DipSwitch dipswitch:
if (string.IsNullOrEmpty(dipswitch.GetName()))
missingFields.Add(Data.Models.Metadata.DipSwitch.NameKey);
break;
case Driver driver:
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.StatusKey);
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.EmulationKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
// Serialize the input file
var metadata = ConvertToMetadata(ignoreblanks);
var metadataFile = new Serialization.CrossModel.ClrMamePro().Deserialize(metadata);
if (!new Serialization.Writers.ClrMamePro().SerializeFile(metadataFile, outfile, quotes: true))
{
_logger.Warning($"File '{outfile}' could not be written! See the log for more details.");
return false;
}
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
_logger.User($"'{outfile}' written!{Environment.NewLine}");
return true;
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents parsing and writing of a DosCenter DAT
/// </summary>
public sealed class DosCenter : SerializableDatFile<Data.Models.DosCenter.MetadataFile, Serialization.Readers.DosCenter, Serialization.Writers.DosCenter, Serialization.CrossModel.DosCenter>
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public DosCenter(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.DOSCenter);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0)
missingFields.Add(Data.Models.Metadata.Rom.SizeKey);
// if (string.IsNullOrEmpty(rom.Date))
// missingFields.Add(Data.Models.Metadata.Rom.DateKey);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)))
missingFields.Add(Data.Models.Metadata.Rom.CRCKey);
// if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)))
// missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents parsing and writing of an Everdrive SMDB file
/// </summary>
public sealed class EverdriveSMDB : SerializableDatFile<Data.Models.EverdriveSMDB.MetadataFile, Serialization.Readers.EverdriveSMDB, Serialization.Writers.EverdriveSMDB, Serialization.CrossModel.EverdriveSMDB>
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public EverdriveSMDB(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.EverdriveSMDB);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)))
missingFields.Add(Data.Models.Metadata.Rom.SHA256Key);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)))
missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)))
missingFields.Add(Data.Models.Metadata.Rom.MD5Key);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)))
missingFields.Add(Data.Models.Metadata.Rom.CRCKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}

View File

@@ -0,0 +1,601 @@
using System;
using System.Collections.Generic;
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
using SabreTools.Hashing;
#pragma warning disable IDE0290 // Use primary constructor
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a hashfile such as an SFV, MD5, or SHA-1 file
/// </summary>
public abstract class Hashfile : SerializableDatFile<Data.Models.Hashfile.Hashfile, Serialization.Readers.Hashfile, Serialization.Writers.Hashfile, Serialization.CrossModel.Hashfile>
{
#region Fields
// Private instance variables specific to Hashfile DATs
protected HashType _hash;
#endregion
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Hashfile(DatFile? datFile) : base(datFile)
{
}
/// <inheritdoc/>
public override void ParseFile(string filename,
int indexId,
bool keep,
bool statsOnly = false,
FilterRunner? filterRunner = null,
bool throwOnError = false)
{
try
{
// Deserialize the input file
var hashfile = new Serialization.Readers.Hashfile().Deserialize(filename, _hash);
var metadata = new Serialization.CrossModel.Hashfile().Serialize(hashfile);
// Convert to the internal format
ConvertFromMetadata(metadata, filename, indexId, keep, statsOnly, filterRunner);
}
catch (Exception ex) when (!throwOnError)
{
string message = $"'{filename}' - An error occurred during parsing";
_logger.Error(ex, message);
}
}
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
// Serialize the input file
var metadata = ConvertToMetadata(ignoreblanks);
var hashfile = new Serialization.CrossModel.Hashfile().Deserialize(metadata, _hash);
if (!new Serialization.Writers.Hashfile().SerializeFile(hashfile, outfile, _hash))
{
_logger.Warning($"File '{outfile}' could not be written! See the log for more details.");
return false;
}
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
_logger.User($"'{outfile}' written!{Environment.NewLine}");
return true;
}
}
/// <summary>
/// Represents an SFV (CRC-32) hashfile
/// </summary>
public sealed class SfvFile : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SfvFile(DatFile? datFile) : base(datFile)
{
_hash = HashType.CRC32;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSFV);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)))
missingFields.Add(Data.Models.Metadata.Rom.CRCKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an MD2 hashfile
/// </summary>
public sealed class Md2File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Md2File(DatFile? datFile) : base(datFile)
{
_hash = HashType.MD2;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpMD2);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)))
missingFields.Add(Data.Models.Metadata.Rom.MD2Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an MD4 hashfile
/// </summary>
public sealed class Md4File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Md4File(DatFile? datFile) : base(datFile)
{
_hash = HashType.MD4;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpMD4);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)))
missingFields.Add(Data.Models.Metadata.Rom.MD4Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an MD5 hashfile
/// </summary>
public sealed class Md5File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Disk,
ItemType.Media,
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Md5File(DatFile? datFile) : base(datFile)
{
_hash = HashType.MD5;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpMD5);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Disk disk:
if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)))
missingFields.Add(Data.Models.Metadata.Disk.MD5Key);
break;
case Media medium:
if (string.IsNullOrEmpty(medium.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)))
missingFields.Add(Data.Models.Metadata.Media.MD5Key);
break;
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)))
missingFields.Add(Data.Models.Metadata.Rom.MD5Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an RIPEMD128 hashfile
/// </summary>
public sealed class RipeMD128File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public RipeMD128File(DatFile? datFile) : base(datFile)
{
_hash = HashType.RIPEMD128;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpRIPEMD128);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)))
missingFields.Add(Data.Models.Metadata.Rom.RIPEMD128Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an RIPEMD160 hashfile
/// </summary>
public sealed class RipeMD160File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public RipeMD160File(DatFile? datFile) : base(datFile)
{
_hash = HashType.RIPEMD160;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpRIPEMD160);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)))
missingFields.Add(Data.Models.Metadata.Rom.RIPEMD160Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an SHA-1 hashfile
/// </summary>
public sealed class Sha1File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Disk,
ItemType.Media,
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Sha1File(DatFile? datFile) : base(datFile)
{
_hash = HashType.SHA1;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSHA1);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Disk disk:
if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)))
missingFields.Add(Data.Models.Metadata.Disk.SHA1Key);
break;
case Media medium:
if (string.IsNullOrEmpty(medium.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)))
missingFields.Add(Data.Models.Metadata.Media.SHA1Key);
break;
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)))
missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an SHA-256 hashfile
/// </summary>
public sealed class Sha256File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Media,
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Sha256File(DatFile? datFile) : base(datFile)
{
_hash = HashType.SHA256;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSHA256);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Media medium:
if (string.IsNullOrEmpty(medium.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)))
missingFields.Add(Data.Models.Metadata.Media.SHA256Key);
break;
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)))
missingFields.Add(Data.Models.Metadata.Rom.SHA256Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an SHA-384 hashfile
/// </summary>
public sealed class Sha384File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Sha384File(DatFile? datFile) : base(datFile)
{
_hash = HashType.SHA384;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSHA384);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)))
missingFields.Add(Data.Models.Metadata.Rom.SHA384Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an SHA-512 hashfile
/// </summary>
public sealed class Sha512File : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Sha512File(DatFile? datFile) : base(datFile)
{
_hash = HashType.SHA512;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSHA512);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)))
missingFields.Add(Data.Models.Metadata.Rom.SHA512Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
/// <summary>
/// Represents an SpamSum hashfile
/// </summary>
public sealed class SpamSumFile : Hashfile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Media,
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SpamSumFile(DatFile? datFile) : base(datFile)
{
_hash = HashType.SpamSum;
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSpamSum);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Media medium:
if (string.IsNullOrEmpty(medium.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)))
missingFields.Add(Data.Models.Metadata.Media.SpamSumKey);
break;
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)))
missingFields.Add(Data.Models.Metadata.Rom.SpamSumKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a MAME Listrom file
/// </summary>
public sealed class Listrom : SerializableDatFile<Data.Models.Listrom.MetadataFile, Serialization.Readers.Listrom, Serialization.Writers.Listrom, Serialization.CrossModel.Listrom>
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Disk,
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Listrom(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.Listrom);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Disk disk:
if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key))
&& string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)))
{
missingFields.Add(Data.Models.Metadata.Disk.SHA1Key);
}
break;
case Rom rom:
if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0)
missingFields.Add(Data.Models.Metadata.Rom.SizeKey);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)))
missingFields.Add(Data.Models.Metadata.Rom.CRCKey);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)))
missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}

View File

@@ -0,0 +1,397 @@
using System;
using System.Collections.Generic;
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a MAME/M1 XML DAT
/// </summary>
public sealed class Listxml : SerializableDatFile<Data.Models.Listxml.Mame, Serialization.Readers.Listxml, Serialization.Writers.Listxml, Serialization.CrossModel.Listxml>
{
#region Constants
/// <summary>
/// DTD for original MAME XML DATs
/// </summary>
internal const string MAMEDTD = @"<!DOCTYPE mame [
<!ELEMENT mame (machine+)>
<!ATTLIST mame build CDATA #IMPLIED>
<!ATTLIST mame debug (yes|no) ""no"">
<!ATTLIST mame mameconfig CDATA #REQUIRED>
<!ELEMENT machine (description, year?, manufacturer?, biosset*, rom*, disk*, device_ref*, sample*, chip*, display*, sound?, input?, dipswitch*, configuration*, port*, adjuster*, driver?, feature*, device*, slot*, softwarelist*, ramoption*)>
<!ATTLIST machine name CDATA #REQUIRED>
<!ATTLIST machine sourcefile CDATA #IMPLIED>
<!ATTLIST machine isbios (yes|no) ""no"">
<!ATTLIST machine isdevice (yes|no) ""no"">
<!ATTLIST machine ismechanical (yes|no) ""no"">
<!ATTLIST machine runnable (yes|no) ""yes"">
<!ATTLIST machine cloneof CDATA #IMPLIED>
<!ATTLIST machine romof CDATA #IMPLIED>
<!ATTLIST machine sampleof CDATA #IMPLIED>
<!ELEMENT description (#PCDATA)>
<!ELEMENT year (#PCDATA)>
<!ELEMENT manufacturer (#PCDATA)>
<!ELEMENT biosset EMPTY>
<!ATTLIST biosset name CDATA #REQUIRED>
<!ATTLIST biosset description CDATA #REQUIRED>
<!ATTLIST biosset default (yes|no) ""no"">
<!ELEMENT rom EMPTY>
<!ATTLIST rom name CDATA #REQUIRED>
<!ATTLIST rom bios CDATA #IMPLIED>
<!ATTLIST rom size CDATA #REQUIRED>
<!ATTLIST rom crc CDATA #IMPLIED>
<!ATTLIST rom sha1 CDATA #IMPLIED>
<!ATTLIST rom merge CDATA #IMPLIED>
<!ATTLIST rom region CDATA #IMPLIED>
<!ATTLIST rom offset CDATA #IMPLIED>
<!ATTLIST rom status (baddump|nodump|good) ""good"">
<!ATTLIST rom optional (yes|no) ""no"">
<!ELEMENT disk EMPTY>
<!ATTLIST disk name CDATA #REQUIRED>
<!ATTLIST disk sha1 CDATA #IMPLIED>
<!ATTLIST disk merge CDATA #IMPLIED>
<!ATTLIST disk region CDATA #IMPLIED>
<!ATTLIST disk index CDATA #IMPLIED>
<!ATTLIST disk writable (yes|no) ""no"">
<!ATTLIST disk status (baddump|nodump|good) ""good"">
<!ATTLIST disk optional (yes|no) ""no"">
<!ELEMENT device_ref EMPTY>
<!ATTLIST device_ref name CDATA #REQUIRED>
<!ELEMENT sample EMPTY>
<!ATTLIST sample name CDATA #REQUIRED>
<!ELEMENT chip EMPTY>
<!ATTLIST chip name CDATA #REQUIRED>
<!ATTLIST chip tag CDATA #IMPLIED>
<!ATTLIST chip type (cpu|audio) #REQUIRED>
<!ATTLIST chip clock CDATA #IMPLIED>
<!ELEMENT display EMPTY>
<!ATTLIST display tag CDATA #IMPLIED>
<!ATTLIST display type (raster|vector|lcd|svg|unknown) #REQUIRED>
<!ATTLIST display rotate (0|90|180|270) #IMPLIED>
<!ATTLIST display flipx (yes|no) ""no"">
<!ATTLIST display width CDATA #IMPLIED>
<!ATTLIST display height CDATA #IMPLIED>
<!ATTLIST display refresh CDATA #REQUIRED>
<!ATTLIST display pixclock CDATA #IMPLIED>
<!ATTLIST display htotal CDATA #IMPLIED>
<!ATTLIST display hbend CDATA #IMPLIED>
<!ATTLIST display hbstart CDATA #IMPLIED>
<!ATTLIST display vtotal CDATA #IMPLIED>
<!ATTLIST display vbend CDATA #IMPLIED>
<!ATTLIST display vbstart CDATA #IMPLIED>
<!ELEMENT sound EMPTY>
<!ATTLIST sound channels CDATA #REQUIRED>
<!ELEMENT condition EMPTY>
<!ATTLIST condition tag CDATA #REQUIRED>
<!ATTLIST condition mask CDATA #REQUIRED>
<!ATTLIST condition relation (eq|ne|gt|le|lt|ge) #REQUIRED>
<!ATTLIST condition value CDATA #REQUIRED>
<!ELEMENT input (control*)>
<!ATTLIST input service (yes|no) ""no"">
<!ATTLIST input tilt (yes|no) ""no"">
<!ATTLIST input players CDATA #REQUIRED>
<!ATTLIST input coins CDATA #IMPLIED>
<!ELEMENT control EMPTY>
<!ATTLIST control type CDATA #REQUIRED>
<!ATTLIST control player CDATA #IMPLIED>
<!ATTLIST control buttons CDATA #IMPLIED>
<!ATTLIST control reqbuttons CDATA #IMPLIED>
<!ATTLIST control minimum CDATA #IMPLIED>
<!ATTLIST control maximum CDATA #IMPLIED>
<!ATTLIST control sensitivity CDATA #IMPLIED>
<!ATTLIST control keydelta CDATA #IMPLIED>
<!ATTLIST control reverse (yes|no) ""no"">
<!ATTLIST control ways CDATA #IMPLIED>
<!ATTLIST control ways2 CDATA #IMPLIED>
<!ATTLIST control ways3 CDATA #IMPLIED>
<!ELEMENT dipswitch (condition?, diplocation*, dipvalue*)>
<!ATTLIST dipswitch name CDATA #REQUIRED>
<!ATTLIST dipswitch tag CDATA #REQUIRED>
<!ATTLIST dipswitch mask CDATA #REQUIRED>
<!ELEMENT diplocation EMPTY>
<!ATTLIST diplocation name CDATA #REQUIRED>
<!ATTLIST diplocation number CDATA #REQUIRED>
<!ATTLIST diplocation inverted (yes|no) ""no"">
<!ELEMENT dipvalue (condition?)>
<!ATTLIST dipvalue name CDATA #REQUIRED>
<!ATTLIST dipvalue value CDATA #REQUIRED>
<!ATTLIST dipvalue default (yes|no) ""no"">
<!ELEMENT configuration (condition?, conflocation*, confsetting*)>
<!ATTLIST configuration name CDATA #REQUIRED>
<!ATTLIST configuration tag CDATA #REQUIRED>
<!ATTLIST configuration mask CDATA #REQUIRED>
<!ELEMENT conflocation EMPTY>
<!ATTLIST conflocation name CDATA #REQUIRED>
<!ATTLIST conflocation number CDATA #REQUIRED>
<!ATTLIST conflocation inverted (yes|no) ""no"">
<!ELEMENT confsetting (condition?)>
<!ATTLIST confsetting name CDATA #REQUIRED>
<!ATTLIST confsetting value CDATA #REQUIRED>
<!ATTLIST confsetting default (yes|no) ""no"">
<!ELEMENT port (analog*)>
<!ATTLIST port tag CDATA #REQUIRED>
<!ELEMENT analog EMPTY>
<!ATTLIST analog mask CDATA #REQUIRED>
<!ELEMENT adjuster (condition?)>
<!ATTLIST adjuster name CDATA #REQUIRED>
<!ATTLIST adjuster default CDATA #REQUIRED>
<!ELEMENT driver EMPTY>
<!ATTLIST driver status (good|imperfect|preliminary) #REQUIRED>
<!ATTLIST driver emulation (good|imperfect|preliminary) #REQUIRED>
<!ATTLIST driver cocktail (good|imperfect|preliminary) #IMPLIED>
<!ATTLIST driver savestate (supported|unsupported) #REQUIRED>
<!ATTLIST driver requiresartwork (yes|no) ""no"">
<!ATTLIST driver unofficial (yes|no) ""no"">
<!ATTLIST driver nosoundhardware (yes|no) ""no"">
<!ATTLIST driver incomplete (yes|no) ""no"">
<!ELEMENT feature EMPTY>
<!ATTLIST feature type (protection|timing|graphics|palette|sound|capture|camera|microphone|controls|keyboard|mouse|media|disk|printer|tape|punch|drum|rom|comms|lan|wan) #REQUIRED>
<!ATTLIST feature status (unemulated|imperfect) #IMPLIED>
<!ATTLIST feature overall (unemulated|imperfect) #IMPLIED>
<!ELEMENT device (instance?, extension*)>
<!ATTLIST device type CDATA #REQUIRED>
<!ATTLIST device tag CDATA #IMPLIED>
<!ATTLIST device fixed_image CDATA #IMPLIED>
<!ATTLIST device mandatory CDATA #IMPLIED>
<!ATTLIST device interface CDATA #IMPLIED>
<!ELEMENT instance EMPTY>
<!ATTLIST instance name CDATA #REQUIRED>
<!ATTLIST instance briefname CDATA #REQUIRED>
<!ELEMENT extension EMPTY>
<!ATTLIST extension name CDATA #REQUIRED>
<!ELEMENT slot (slotoption*)>
<!ATTLIST slot name CDATA #REQUIRED>
<!ELEMENT slotoption EMPTY>
<!ATTLIST slotoption name CDATA #REQUIRED>
<!ATTLIST slotoption devname CDATA #REQUIRED>
<!ATTLIST slotoption default (yes|no) ""no"">
<!ELEMENT softwarelist EMPTY>
<!ATTLIST softwarelist tag CDATA #REQUIRED>
<!ATTLIST softwarelist name CDATA #REQUIRED>
<!ATTLIST softwarelist status (original|compatible) #REQUIRED>
<!ATTLIST softwarelist filter CDATA #IMPLIED>
<!ELEMENT ramoption (#PCDATA)>
<!ATTLIST ramoption name CDATA #REQUIRED>
<!ATTLIST ramoption default CDATA #IMPLIED>
]>
";
#endregion
#region Fields
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Adjuster,
ItemType.BiosSet,
ItemType.Chip,
ItemType.Condition,
ItemType.Configuration,
ItemType.Device,
ItemType.DeviceRef,
ItemType.DipSwitch,
ItemType.Disk,
ItemType.Display,
ItemType.Driver,
ItemType.Feature,
ItemType.Input,
ItemType.Port,
ItemType.RamOption,
ItemType.Rom,
ItemType.Sample,
ItemType.Slot,
ItemType.SoftwareList,
ItemType.Sound,
];
#endregion
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Listxml(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.Listxml);
}
/// <inheritdoc/>
public override void ParseFile(string filename,
int indexId,
bool keep,
bool statsOnly = false,
FilterRunner? filterRunner = null,
bool throwOnError = false)
{
try
{
// Deserialize the input file
var mame = new Serialization.Readers.Listxml().Deserialize(filename);
Data.Models.Metadata.MetadataFile? metadata;
if (mame is null)
{
var m1 = new Serialization.Readers.M1().Deserialize(filename);
metadata = new Serialization.CrossModel.M1().Serialize(m1);
}
else
{
metadata = new Serialization.CrossModel.Listxml().Serialize(mame);
}
// Convert to the internal format
ConvertFromMetadata(metadata, filename, indexId, keep, statsOnly, filterRunner);
}
catch (Exception ex) when (!throwOnError)
{
string message = $"'{filename}' - An error occurred during parsing";
_logger.Error(ex, message);
}
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
#pragma warning disable IDE0010
switch (datItem)
{
case BiosSet biosset:
if (string.IsNullOrEmpty(biosset.GetName()))
missingFields.Add(Data.Models.Metadata.BiosSet.NameKey);
if (string.IsNullOrEmpty(biosset.GetStringFieldValue(Data.Models.Metadata.BiosSet.DescriptionKey)))
missingFields.Add(Data.Models.Metadata.BiosSet.DescriptionKey);
break;
case Rom rom:
if (string.IsNullOrEmpty(rom.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0)
missingFields.Add(Data.Models.Metadata.Rom.SizeKey);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)))
{
missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
}
break;
case Disk disk:
if (string.IsNullOrEmpty(disk.GetName()))
missingFields.Add(Data.Models.Metadata.Disk.NameKey);
if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key))
&& string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)))
{
missingFields.Add(Data.Models.Metadata.Disk.SHA1Key);
}
break;
case DeviceRef deviceref:
if (string.IsNullOrEmpty(deviceref.GetName()))
missingFields.Add(Data.Models.Metadata.DeviceRef.NameKey);
break;
case Sample sample:
if (string.IsNullOrEmpty(sample.GetName()))
missingFields.Add(Data.Models.Metadata.Sample.NameKey);
break;
case Chip chip:
if (string.IsNullOrEmpty(chip.GetName()))
missingFields.Add(Data.Models.Metadata.Chip.NameKey);
if (chip.GetStringFieldValue(Data.Models.Metadata.Chip.ChipTypeKey).AsChipType() == ChipType.NULL)
missingFields.Add(Data.Models.Metadata.Chip.ChipTypeKey);
break;
case Display display:
if (display.GetStringFieldValue(Data.Models.Metadata.Display.DisplayTypeKey).AsDisplayType() == DisplayType.NULL)
missingFields.Add(Data.Models.Metadata.Display.DisplayTypeKey);
if (display.GetDoubleFieldValue(Data.Models.Metadata.Display.RefreshKey) is null)
missingFields.Add(Data.Models.Metadata.Display.RefreshKey);
break;
case Sound sound:
if (sound.GetInt64FieldValue(Data.Models.Metadata.Sound.ChannelsKey) is null)
missingFields.Add(Data.Models.Metadata.Sound.ChannelsKey);
break;
case Input input:
if (input.GetInt64FieldValue(Data.Models.Metadata.Input.PlayersKey) is null)
missingFields.Add(Data.Models.Metadata.Input.PlayersKey);
break;
case DipSwitch dipswitch:
if (string.IsNullOrEmpty(dipswitch.GetName()))
missingFields.Add(Data.Models.Metadata.DipSwitch.NameKey);
if (string.IsNullOrEmpty(dipswitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.TagKey)))
missingFields.Add(Data.Models.Metadata.DipSwitch.TagKey);
break;
case Configuration configuration:
if (string.IsNullOrEmpty(configuration.GetName()))
missingFields.Add(Data.Models.Metadata.Configuration.NameKey);
if (string.IsNullOrEmpty(configuration.GetStringFieldValue(Data.Models.Metadata.Configuration.TagKey)))
missingFields.Add(Data.Models.Metadata.Configuration.TagKey);
break;
case Port port:
if (string.IsNullOrEmpty(port.GetStringFieldValue(Data.Models.Metadata.Port.TagKey)))
missingFields.Add(Data.Models.Metadata.Port.TagKey);
break;
case Adjuster adjuster:
if (string.IsNullOrEmpty(adjuster.GetName()))
missingFields.Add(Data.Models.Metadata.Adjuster.NameKey);
break;
case Driver driver:
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.StatusKey);
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.EmulationKey);
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.CocktailKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.CocktailKey);
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.SaveStateKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.SaveStateKey);
break;
case Feature feature:
if (feature.GetStringFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey).AsFeatureType() == FeatureType.NULL)
missingFields.Add(Data.Models.Metadata.Feature.FeatureTypeKey);
break;
case Device device:
if (device.GetStringFieldValue(Data.Models.Metadata.Device.DeviceTypeKey).AsDeviceType() == DeviceType.NULL)
missingFields.Add(Data.Models.Metadata.Device.DeviceTypeKey);
break;
case Slot slot:
if (string.IsNullOrEmpty(slot.GetName()))
missingFields.Add(Data.Models.Metadata.Slot.NameKey);
break;
case DatItems.Formats.SoftwareList softwarelist:
if (string.IsNullOrEmpty(softwarelist.GetStringFieldValue(Data.Models.Metadata.SoftwareList.TagKey)))
missingFields.Add(Data.Models.Metadata.SoftwareList.TagKey);
if (string.IsNullOrEmpty(softwarelist.GetName()))
missingFields.Add(Data.Models.Metadata.SoftwareList.NameKey);
if (softwarelist.GetStringFieldValue(Data.Models.Metadata.SoftwareList.StatusKey).AsSoftwareListStatus() == SoftwareListStatus.None)
missingFields.Add(Data.Models.Metadata.SoftwareList.StatusKey);
break;
case RamOption ramoption:
if (string.IsNullOrEmpty(ramoption.GetName()))
missingFields.Add(Data.Models.Metadata.RamOption.NameKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}

View File

@@ -0,0 +1,404 @@
using System;
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a Logiqx-derived DAT
/// </summary>
public sealed class Logiqx : SerializableDatFile<Data.Models.Logiqx.Datafile, Serialization.Readers.Logiqx, Serialization.Writers.Logiqx, Serialization.CrossModel.Logiqx>
{
#region Constants
/// <summary>
/// DTD for original Logiqx DATs
/// </summary>
/// <remarks>This has been edited to reflect actual current standards</remarks>
internal const string LogiqxDTD = @"<!--
ROM Management Datafile - DTD
For further information, see: http://www.logiqx.com/
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "" -//Logiqx//DTD ROM Management Datafile//EN""
SYSTEM ""http://www.logiqx.com/Dats/datafile.dtd""
$Revision: 1.5 $
$Date: 2008/10/28 21:39:16 $
-->
<!ELEMENT datafile(header?, game*, machine*)>
<!ATTLIST datafile build CDATA #IMPLIED>
<!ATTLIST datafile debug (yes|no) ""no"">
<!ELEMENT header (id?, name, description, rootdir?, type?, category?, version, date?, author, email?, homepage?, url?, comment?, clrmamepro?, romcenter?)>
<!ELEMENT id (#PCDATA)>
<!ELEMENT name(#PCDATA)>
<!ELEMENT description (#PCDATA)>
<!ELEMENT rootdir (#PCDATA)>
<!ELEMENT type (#PCDATA)>
<!ELEMENT category (#PCDATA)>
<!ELEMENT version (#PCDATA)>
<!ELEMENT date (#PCDATA)>
<!ELEMENT author (#PCDATA)>
<!ELEMENT email (#PCDATA)>
<!ELEMENT homepage (#PCDATA)>
<!ELEMENT url (#PCDATA)>
<!ELEMENT comment (#PCDATA)>
<!ELEMENT clrmamepro EMPTY>
<!ATTLIST clrmamepro header CDATA #IMPLIED>
<!ATTLIST clrmamepro forcemerging (none|split|merged|nonmerged|fullmerged|device|full) ""split"">
<!ATTLIST clrmamepro forcenodump(obsolete|required|ignore) ""obsolete"">
<!ATTLIST clrmamepro forcepacking(zip|unzip) ""zip"">
<!ELEMENT romcenter EMPTY>
<!ATTLIST romcenter plugin CDATA #IMPLIED>
<!ATTLIST romcenter rommode (none|split|merged|unmerged|fullmerged|device|full) ""split"">
<!ATTLIST romcenter biosmode (none|split|merged|unmerged|fullmerged|device|full) ""split"">
<!ATTLIST romcenter samplemode (none|split|merged|unmerged|fullmerged|device|full) ""merged"">
<!ATTLIST romcenter lockrommode(yes|no) ""no"">
<!ATTLIST romcenter lockbiosmode(yes|no) ""no"">
<!ATTLIST romcenter locksamplemode(yes|no) ""no"">
<!ELEMENT game (comment*, description, year?, manufacturer?, publisher?, category?, trurip?, release*, biosset*, rom*, disk*, media*, sample*, archive*)>
<!ATTLIST game name CDATA #REQUIRED>
<!ATTLIST game sourcefile CDATA #IMPLIED>
<!ATTLIST game isbios (yes|no) ""no"">
<!ATTLIST game cloneof CDATA #IMPLIED>
<!ATTLIST game romof CDATA #IMPLIED>
<!ATTLIST game sampleof CDATA #IMPLIED>
<!ATTLIST game board CDATA #IMPLIED>
<!ATTLIST game rebuildto CDATA #IMPLIED>
<!ATTLIST game id CDATA #IMPLIED>
<!ATTLIST game cloneofid CDATA #IMPLIED>
<!ATTLIST game runnable (no|partial|yes) ""no"" #IMPLIED>
<!ELEMENT year (#PCDATA)>
<!ELEMENT manufacturer (#PCDATA)>
<!ELEMENT publisher (#PCDATA)>
<!ELEMENT trurip (titleid?, publisher?, developer?, year?, genre?, subgenre?, ratings?, score?, players?, enabled?, crc?, source?, cloneof?, relatedto?)>
<!ELEMENT titleid (#PCDATA)>
<!ELEMENT developer (#PCDATA)>
<!ELEMENT year (#PCDATA)>
<!ELEMENT genre (#PCDATA)>
<!ELEMENT subgenre (#PCDATA)>
<!ELEMENT ratings (#PCDATA)>
<!ELEMENT score (#PCDATA)>
<!ELEMENT players (#PCDATA)>
<!ELEMENT enabled (#PCDATA)>
<!ELEMENT crc (#PCDATA)>
<!ELEMENT source (#PCDATA)>
<!ELEMENT cloneof (#PCDATA)>
<!ELEMENT relatedto (#PCDATA)>
<!ELEMENT release EMPTY>
<!ATTLIST release name CDATA #REQUIRED>
<!ATTLIST release region CDATA #REQUIRED>
<!ATTLIST release language CDATA #IMPLIED>
<!ATTLIST release date CDATA #IMPLIED>
<!ATTLIST release default (yes|no) ""no"">
<!ELEMENT biosset EMPTY>
<!ATTLIST biosset name CDATA #REQUIRED>
<!ATTLIST biosset description CDATA #REQUIRED>
<!ATTLIST biosset default (yes|no) ""no"">
<!ELEMENT rom EMPTY>
<!ATTLIST rom name CDATA #REQUIRED>
<!ATTLIST rom size CDATA #REQUIRED>
<!ATTLIST rom crc CDATA #IMPLIED>
<!ATTLIST rom md5 CDATA #IMPLIED>
<!ATTLIST rom sha1 CDATA #IMPLIED>
<!ATTLIST rom sha256 CDATA #IMPLIED>
<!ATTLIST rom sha384 CDATA #IMPLIED>
<!ATTLIST rom sha512 CDATA #IMPLIED>
<!ATTLIST rom spamsum CDATA #IMPLIED>
<!ATTLIST rom xxh3_64 CDATA #IMPLIED>
<!ATTLIST rom xxh3_128 CDATA #IMPLIED>
<!ATTLIST rom merge CDATA #IMPLIED>
<!ATTLIST rom status (baddump|nodump|good|verified) ""good"">
<!ATTLIST rom serial CDATA #IMPLIED>
<!ATTLIST rom header CDATA #IMPLIED>
<!ATTLIST rom date CDATA #IMPLIED>
<!ATTLIST rom inverted CDATA #IMPLIED>
<!ATTLIST rom mia CDATA #IMPLIED>
<!ELEMENT disk EMPTY>
<!ATTLIST disk name CDATA #REQUIRED>
<!ATTLIST disk md5 CDATA #IMPLIED>
<!ATTLIST disk sha1 CDATA #IMPLIED>
<!ATTLIST disk merge CDATA #IMPLIED>
<!ATTLIST disk status (baddump|nodump|good|verified) ""good"">
<!ELEMENT media EMPTY>
<!ATTLIST media name CDATA #REQUIRED>
<!ATTLIST media md5 CDATA #IMPLIED>
<!ATTLIST media sha1 CDATA #IMPLIED>
<!ATTLIST media sha256 CDATA #IMPLIED>
<!ATTLIST media spamsum CDATA #IMPLIED>
<!ELEMENT sample EMPTY>
<!ATTLIST sample name CDATA #REQUIRED>
<!ELEMENT archive EMPTY>
<!ATTLIST archive name CDATA #REQUIRED>
<!ELEMENT machine (comment*, description, year?, manufacturer?, publisher?, category?, trurip?, release*, biosset*, rom*, disk*, media*, sample*, archive*)>
<!ATTLIST game name CDATA #REQUIRED>
<!ATTLIST game sourcefile CDATA #IMPLIED>
<!ATTLIST game isbios (yes|no) ""no"">
<!ATTLIST game cloneof CDATA #IMPLIED>
<!ATTLIST game romof CDATA #IMPLIED>
<!ATTLIST game sampleof CDATA #IMPLIED>
<!ATTLIST game board CDATA #IMPLIED>
<!ATTLIST game rebuildto CDATA #IMPLIED>
<!ATTLIST game id CDATA #IMPLIED>
<!ATTLIST game cloneofid CDATA #IMPLIED>
<!ATTLIST game runnable (no|partial|yes) ""no"" #IMPLIED>
<!ELEMENT dir (game*, machine*)>
<!ATTLIST dir name CDATA #REQUIRED>
";
/// <summary>
/// XSD for No-Intro Logiqx-derived DATs
/// </summary>
internal const string NoIntroXSD = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<xs:schema attributeFormDefault=""unqualified"" elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
<xs:element name=""datafile"">
<xs:complexType>
<xs:sequence>
<xs:element name=""header"">
<xs:complexType>
<xs:sequence>
<xs:element name=""id"" type=""xs:int""/>
<xs:element name=""name"" type=""xs:string""/>
<xs:element name=""description"" type=""xs:string""/>
<xs:element name=""version"" type=""xs:string""/>
<xs:element name=""author"" type=""xs:string""/>
<xs:element name=""homepage"" type=""xs:string""/>
<xs:element name=""url"" type=""xs:string""/>
<xs:element name=""clrmamepro"">
<xs:complexType>
<xs:attribute name=""forcenodump"" default=""obsolete"" use=""optional"">
<xs:simpleType>
<xs:restriction base=""xs:token"">
<xs:enumeration value=""obsolete""/>
<xs:enumeration value=""required""/>
<xs:enumeration value=""ignore""/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name=""header"" type=""xs:string"" use=""optional""/>
</xs:complexType>
</xs:element>
<xs:element name=""romcenter"" minOccurs=""0"">
<xs:complexType>
<xs:attribute name=""plugin"" type=""xs:string"" use=""optional""/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element maxOccurs=""unbounded"" name=""game"">
<xs:complexType>
<xs:sequence>
<xs:element name=""description"" type=""xs:string""/>
<xs:element name=""rom"">
<xs:complexType>
<xs:attribute name=""name"" type=""xs:string"" use=""required""/>
<xs:attribute name=""size"" type=""xs:unsignedInt"" use=""required""/>
<xs:attribute name=""crc"" type=""xs:string"" use=""required""/>
<xs:attribute name=""md5"" type=""xs:string"" use=""required""/>
<xs:attribute name=""sha1"" type=""xs:string"" use=""required""/>
<xs:attribute name=""sha256"" type=""xs:string"" use=""optional""/>
<xs:attribute name=""status"" type=""xs:string"" use=""optional""/>
<xs:attribute name=""serial"" type=""xs:string"" use=""optional""/>
<xs:attribute name=""header"" type=""xs:string"" use=""optional""/>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name=""name"" type=""xs:string"" use=""required""/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
";
#endregion
#region Fields
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Archive,
ItemType.BiosSet,
ItemType.DeviceRef,
ItemType.Disk,
ItemType.Driver,
ItemType.Media,
ItemType.Release,
ItemType.Rom,
ItemType.Sample,
ItemType.SoftwareList,
];
/// <summary>
/// Indicates if game should be used instead of machine
/// </summary>
private readonly bool _useGame;
#endregion
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
/// <param name="useGame">True if the output uses "game", false if the output uses "machine"</param>
public Logiqx(DatFile? datFile, bool useGame) : base(datFile)
{
_useGame = useGame;
if (useGame)
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.LogiqxDeprecated);
else
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.Logiqx);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
#pragma warning disable IDE0010
switch (datItem)
{
case Release release:
if (string.IsNullOrEmpty(release.GetName()))
missingFields.Add(Data.Models.Metadata.Release.NameKey);
if (string.IsNullOrEmpty(release.GetStringFieldValue(Data.Models.Metadata.Release.RegionKey)))
missingFields.Add(Data.Models.Metadata.Release.RegionKey);
break;
case BiosSet biosset:
if (string.IsNullOrEmpty(biosset.GetName()))
missingFields.Add(Data.Models.Metadata.BiosSet.NameKey);
if (string.IsNullOrEmpty(biosset.GetStringFieldValue(Data.Models.Metadata.BiosSet.DescriptionKey)))
missingFields.Add(Data.Models.Metadata.BiosSet.DescriptionKey);
break;
case Rom rom:
if (string.IsNullOrEmpty(rom.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0)
missingFields.Add(Data.Models.Metadata.Rom.SizeKey);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))
&& string.IsNullOrEmpty(rom.GetStringFieldValue("MD2"))
&& string.IsNullOrEmpty(rom.GetStringFieldValue("MD4"))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)))
{
missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
}
break;
case Disk disk:
if (string.IsNullOrEmpty(disk.GetName()))
missingFields.Add(Data.Models.Metadata.Disk.NameKey);
if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key))
&& string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)))
{
missingFields.Add(Data.Models.Metadata.Disk.SHA1Key);
}
break;
case Media media:
if (string.IsNullOrEmpty(media.GetName()))
missingFields.Add(Data.Models.Metadata.Media.NameKey);
if (string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key))
&& string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key))
&& string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key))
&& string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)))
{
missingFields.Add(Data.Models.Metadata.Media.SHA1Key);
}
break;
case DeviceRef deviceref:
if (string.IsNullOrEmpty(deviceref.GetName()))
missingFields.Add(Data.Models.Metadata.DeviceRef.NameKey);
break;
case Sample sample:
if (string.IsNullOrEmpty(sample.GetName()))
missingFields.Add(Data.Models.Metadata.Sample.NameKey);
break;
case Archive archive:
if (string.IsNullOrEmpty(archive.GetName()))
missingFields.Add(Data.Models.Metadata.Archive.NameKey);
break;
case Driver driver:
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.StatusKey);
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.EmulationKey);
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.CocktailKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.CocktailKey);
if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.SaveStateKey).AsSupportStatus() == SupportStatus.NULL)
missingFields.Add(Data.Models.Metadata.Driver.SaveStateKey);
break;
case DatItems.Formats.SoftwareList softwarelist:
if (string.IsNullOrEmpty(softwarelist.GetStringFieldValue(Data.Models.Metadata.SoftwareList.TagKey)))
missingFields.Add(Data.Models.Metadata.SoftwareList.TagKey);
if (string.IsNullOrEmpty(softwarelist.GetName()))
missingFields.Add(Data.Models.Metadata.SoftwareList.NameKey);
if (softwarelist.GetStringFieldValue(Data.Models.Metadata.SoftwareList.StatusKey).AsSoftwareListStatus() == SoftwareListStatus.None)
missingFields.Add(Data.Models.Metadata.SoftwareList.StatusKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
// Serialize the input file
var metadata = ConvertToMetadata(ignoreblanks);
var datafile = new Serialization.CrossModel.Logiqx().Deserialize(metadata, _useGame);
// TODO: Reenable doctype writing
// Only write the doctype if we don't have No-Intro data
bool success;
if (string.IsNullOrEmpty(Header.GetStringFieldValue(Data.Models.Metadata.Header.IdKey)))
success = new Serialization.Writers.Logiqx().Serialize(datafile, outfile, null, null, null, null);
else
success = new Serialization.Writers.Logiqx().Serialize(datafile, outfile, null, null, null, null);
if (!success)
{
_logger.Warning($"File '{outfile}' could not be written! See the log for more details.");
return false;
}
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
_logger.User($"'{outfile}' written!{Environment.NewLine}");
return true;
}
}
}

View File

@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.DatItems;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a Missfile
/// </summary>
public sealed class Missfile : DatFile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> Enum.GetValues(typeof(ItemType)) as ItemType[] ?? [];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Missfile(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.MissFile);
}
/// <inheritdoc/>
/// <remarks>There is no consistent way to parse a missfile</remarks>
public override void ParseFile(string filename,
int indexId,
bool keep,
bool statsOnly = false,
FilterRunner? filterRunner = null,
bool throwOnError = false)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
// TODO: Check required fields
return null;
}
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
FileStream fs = File.Create(outfile);
// If we get back null for some reason, just log and return
if (fs is null)
{
_logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
return false;
}
StreamWriter sw = new(fs, new UTF8Encoding(false));
// Write out each of the machines and roms
string? lastgame = null;
// Use a sorted list of games to output
foreach (string key in Items.SortedKeys)
{
List<DatItem> datItems = GetItemsForBucket(key, filter: true);
// If this machine doesn't contain any writable items, skip
if (!ContainsWritable(datItems))
continue;
// Resolve the names in the block
datItems = 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 (!Modifiers.UseRomName || !ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(sw, datItem, lastgame);
// Set the new data to compare against
lastgame = datItem.GetMachine()!.GetName();
}
}
_logger.User($"'{outfile}' written!{Environment.NewLine}");
sw.Dispose();
fs.Dispose();
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
return true;
}
/// <inheritdoc/>
public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
FileStream fs = File.Create(outfile);
// If we get back null for some reason, just log and return
if (fs is null)
{
_logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
return false;
}
StreamWriter sw = new(fs, new UTF8Encoding(false));
// Write out each of the machines and roms
string? lastgame = null;
// Use a sorted list of games to output
foreach (string key in ItemsDB.SortedKeys)
{
// If this machine doesn't contain any writable items, skip
var itemsDict = GetItemsForBucketDB(key, filter: true);
if (itemsDict is null || !ContainsWritable([.. itemsDict.Values]))
continue;
// Resolve the names in the block
var items = ResolveNamesDB([.. itemsDict]);
foreach (var kvp in items)
{
// Check for a "null" item
var datItem = new KeyValuePair<long, DatItem>(kvp.Key, ProcessNullifiedItem(kvp.Value));
// Get the machine for the item
var machine = GetMachineForItemDB(datItem.Key);
// Write out the item if we're using machine names or we're not ignoring
if (!Modifiers.UseRomName || !ShouldIgnore(datItem.Value, ignoreblanks))
WriteDatItemDB(sw, datItem, lastgame);
// Set the new data to compare against
lastgame = machine.Value!.GetName();
}
}
_logger.User($"'{outfile}' written!{Environment.NewLine}");
sw.Dispose();
fs.Dispose();
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(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)
{
var machine = datItem.GetMachine();
WriteDatItemImpl(sw, datItem, machine!, lastgame);
}
/// <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 WriteDatItemDB(StreamWriter sw, KeyValuePair<long, DatItem> datItem, string? lastgame)
{
var machine = GetMachineForItemDB(datItem.Key).Value;
WriteDatItemImpl(sw, datItem.Value, machine!, lastgame);
}
/// <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="machine">Machine object representing the set the item is in</param>
/// <param name="lastgame">The name of the last game to be output</param>
private void WriteDatItemImpl(StreamWriter sw, DatItem datItem, Machine machine, string? lastgame)
{
// Process the item name
ProcessItemName(datItem, machine, forceRemoveQuotes: false, forceRomName: false);
// Romba mode automatically uses item name
if (Modifiers.OutputDepot?.IsActive == true || Modifiers.UseRomName)
sw.Write($"{datItem.GetName() ?? string.Empty}\n");
else if (!Modifiers.UseRomName && machine!.GetName() != lastgame)
sw.Write($"{machine!.GetName() ?? string.Empty}\n");
sw.Flush();
}
}
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents an OfflineList XML DAT
/// </summary>
public sealed class OfflineList : SerializableDatFile<Data.Models.OfflineList.Dat, Serialization.Readers.OfflineList, Serialization.Writers.OfflineList, Serialization.CrossModel.OfflineList>
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public OfflineList(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.OfflineList);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0)
missingFields.Add(Data.Models.Metadata.Rom.SizeKey);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)))
missingFields.Add(Data.Models.Metadata.Rom.CRCKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}

View File

@@ -0,0 +1,88 @@
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents an openMSX softawre list XML DAT
/// </summary>
public sealed class OpenMSX : SerializableDatFile<Data.Models.OpenMSX.SoftwareDb, Serialization.Readers.OpenMSX, Serialization.Writers.OpenMSX, Serialization.CrossModel.OpenMSX>
{
#region Constants
/// <summary>
/// DTD for original openMSX DATs
/// </summary>
internal const string OpenMSXDTD = @"<!ELEMENT softwaredb (person*)>
<!ELEMENT software (title, genmsxid?, system, company,year,country,dump)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT genmsxid (#PCDATA)>
<!ELEMENT system (#PCDATA)>
<!ELEMENT company (#PCDATA)>
<!ELEMENT year (#PCDATA)>
<!ELEMENT country (#PCDATA)>
<!ELEMENT dump (#PCDATA)>
";
internal const string OpenMSXCredits = @"<!-- Credits -->
<![CDATA[
The softwaredb.xml file contains information about rom mapper types
- Copyright 2003 Nicolas Beyaert (Initial Database)
- Copyright 2004-2013 BlueMSX Team
- Copyright 2005-2023 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
]]>";
#endregion
#region Fields
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
#endregion
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public OpenMSX(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.OpenMSX);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)))
missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a RomCenter INI file
/// </summary>
public sealed class RomCenter : SerializableDatFile<Data.Models.RomCenter.MetadataFile, Serialization.Readers.RomCenter, Serialization.Writers.RomCenter, Serialization.CrossModel.RomCenter>
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Rom,
];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public RomCenter(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RomCenter);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Rom rom:
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)))
missingFields.Add(Data.Models.Metadata.Rom.CRCKey);
if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0)
missingFields.Add(Data.Models.Metadata.Rom.SizeKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}

View File

@@ -0,0 +1,719 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents parsing and writing of a reference SabreDAT JSON
/// </summary>
/// TODO: Transform this into direct serialization and deserialization of the Metadata type
public sealed class SabreJSON : DatFile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> Enum.GetValues(typeof(ItemType)) as ItemType[] ?? [];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SabreJSON(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.SabreJSON);
}
/// <inheritdoc/>
public override void ParseFile(string filename,
int indexId,
bool keep,
bool statsOnly = false,
FilterRunner? filterRunner = null,
bool throwOnError = false)
{
// Prepare all internal variables
var fs = System.IO.File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var sr = new StreamReader(fs, new UTF8Encoding(false));
var jtr = new JsonTextReader(sr);
var source = new Source(indexId, filename);
// long sourceIndex = AddSourceDB(source);
// If we got a null reader, just return
if (jtr is 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, statsOnly, source, sourceIndex: 0, filterRunner);
jtr.Read();
break;
default:
jtr.Read();
break;
}
}
}
catch (Exception ex) when (!throwOnError)
{
_logger.Warning($"Exception found while parsing '{filename}': {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 is null)
return;
// Read in the header and apply any new fields
jtr.Read();
JsonSerializer js = new();
DatHeader? header = js.Deserialize<DatHeader>(jtr);
SetHeader(header);
}
/// <summary>
/// Read machine array information
/// </summary>
/// <param name="jtr">JsonTextReader to use to parse the machine</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="source">Source representing the DAT</param>
/// <param name="sourceIndex">Index of the Source representing the DAT</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ReadMachines(JsonTextReader jtr, bool statsOnly, Source source, long sourceIndex, FilterRunner? filterRunner)
{
// If the reader is invalid, skip
if (jtr is null)
return;
// Read in the machine array
jtr.Read();
var js = new JsonSerializer();
JArray machineArray = js.Deserialize<JArray>(jtr) ?? [];
// Loop through each machine object and process
foreach (JObject machineObj in machineArray.Cast<JObject>())
{
ReadMachine(machineObj, statsOnly, source, sourceIndex, filterRunner);
}
}
/// <summary>
/// Read machine object information
/// </summary>
/// <param name="machineObj">JObject representing a single machine</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="source">Source representing the DAT</param>
/// <param name="sourceIndex">Index of the Source representing the DAT</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ReadMachine(JObject machineObj, bool statsOnly, Source source, long sourceIndex, FilterRunner? filterRunner)
{
// If object is invalid, skip it
if (machineObj is null)
return;
// Prepare internal variables
Machine? machine = null;
// Read the machine info, if possible
if (machineObj.ContainsKey("machine"))
machine = machineObj["machine"]?.ToObject<Machine>();
// If the machine doesn't pass the filter
if (machine is not null && filterRunner is not null && !machine.PassesFilter(filterRunner))
return;
// Add the machine to the dictionary
// long machineIndex = -1;
// if (machine is not null)
// machineIndex = AddMachineDB(machine);
// Read items, if possible
if (machineObj.ContainsKey("items"))
{
ReadItems(machineObj["items"] as JArray,
statsOnly,
source,
sourceIndex,
machine,
machineIndex: 0,
filterRunner);
}
}
/// <summary>
/// Read item array information
/// </summary>
/// <param name="itemsArr">JArray representing the items list</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="source">Source representing the DAT</param>
/// <param name="sourceIndex">Index of the Source representing the DAT</param>
/// <param name="machine">Machine information to add to the parsed items</param>
/// <param name="machineIndex">Index of the Machine to add to the parsed items</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ReadItems(
JArray? itemsArr,
bool statsOnly,
// Standard Dat parsing
Source source,
long sourceIndex,
// Miscellaneous
Machine? machine,
long machineIndex,
FilterRunner? filterRunner)
{
// If the array is invalid, skip
if (itemsArr is null)
return;
// Loop through each datitem object and process
foreach (JObject itemObj in itemsArr.Cast<JObject>())
{
ReadItem(itemObj, statsOnly, source, sourceIndex, machine, machineIndex, filterRunner);
}
}
/// <summary>
/// Read item information
/// </summary>
/// <param name="itemObj">JObject representing a single datitem</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="source">Source representing the DAT</param>
/// <param name="sourceIndex">Index of the Source representing the DAT</param>
/// <param name="machine">Machine information to add to the parsed items</param>
/// <param name="machineIndex">Index of the Machine to add to the parsed items</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ReadItem(
JObject itemObj,
bool statsOnly,
// Standard Dat parsing
Source source,
long sourceIndex,
// Miscellaneous
Machine? machine,
long machineIndex,
FilterRunner? filterRunner)
{
// If we have an empty item, skip it
if (itemObj is null)
return;
// Prepare internal variables
DatItem? datItem = null;
// Read the datitem info, if possible
if (itemObj.ContainsKey("datitem"))
{
JToken? datItemObj = itemObj["datitem"];
if (datItemObj is null)
return;
#pragma warning disable IDE0010
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.ConfLocation:
datItem = datItemObj.ToObject<ConfLocation>();
break;
case ItemType.ConfSetting:
datItem = datItemObj.ToObject<ConfSetting>();
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.DeviceRef:
datItem = datItemObj.ToObject<DeviceRef>();
break;
case ItemType.DipLocation:
datItem = datItemObj.ToObject<DipLocation>();
break;
case ItemType.DipValue:
datItem = datItemObj.ToObject<DipValue>();
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.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.ReleaseDetails:
datItem = datItemObj.ToObject<ReleaseDetails>();
break;
case ItemType.Rom:
datItem = datItemObj.ToObject<Rom>();
break;
case ItemType.Sample:
datItem = datItemObj.ToObject<Sample>();
break;
case ItemType.Serials:
datItem = datItemObj.ToObject<Serials>();
break;
case ItemType.SharedFeat:
datItem = datItemObj.ToObject<SharedFeat>();
break;
case ItemType.Slot:
datItem = datItemObj.ToObject<Slot>();
break;
case ItemType.SlotOption:
datItem = datItemObj.ToObject<SlotOption>();
break;
case ItemType.SoftwareList:
datItem = datItemObj.ToObject<DatItems.Formats.SoftwareList>();
break;
case ItemType.Sound:
datItem = datItemObj.ToObject<Sound>();
break;
case ItemType.SourceDetails:
datItem = datItemObj.ToObject<SourceDetails>();
break;
}
#pragma warning restore IDE0010
}
// If we got a valid datitem, copy machine info and add
if (datItem is not null)
{
// If the item doesn't pass the filter
if (filterRunner is not null && !datItem.PassesFilter(filterRunner))
return;
datItem.CopyMachineInformation(machine);
datItem.SetFieldValue<Source?>(DatItem.SourceKey, source);
AddItem(datItem, statsOnly);
AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
}
}
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
FileStream fs = System.IO.File.Create(outfile);
// If we get back null for some reason, just log and return
if (fs is null)
{
_logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
return false;
}
StreamWriter sw = new(fs, new UTF8Encoding(false));
JsonTextWriter jtw = new(sw)
{
Formatting = Formatting.Indented,
IndentChar = '\t',
Indentation = 1
};
// Write out the header
WriteHeader(jtw);
// Write out each of the machines and roms
string? lastgame = null;
// Use a sorted list of games to output
foreach (string key in Items.SortedKeys)
{
List<DatItem> datItems = GetItemsForBucket(key, filter: true);
// If this machine doesn't contain any writable items, skip
if (!ContainsWritable(datItems))
continue;
// Resolve the names in the block
datItems = 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 is not null && !string.Equals(lastgame, datItem.GetMachine()!.GetName(), StringComparison.OrdinalIgnoreCase))
WriteEndGame(jtw);
// If we have a new game, output the beginning of the new item
if (lastgame is null || !string.Equals(lastgame, datItem.GetMachine()!.GetName(), StringComparison.OrdinalIgnoreCase))
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.GetMachine()!.GetName();
}
}
// Write the file footer out
WriteFooter(jtw);
_logger.User($"'{outfile}' written!{Environment.NewLine}");
jtw.Close();
fs.Dispose();
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
return true;
}
/// <inheritdoc/>
public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
FileStream fs = System.IO.File.Create(outfile);
// If we get back null for some reason, just log and return
if (fs is null)
{
_logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
return false;
}
StreamWriter sw = new(fs, new UTF8Encoding(false));
JsonTextWriter jtw = new(sw)
{
Formatting = Formatting.Indented,
IndentChar = '\t',
Indentation = 1
};
// Write out the header
WriteHeader(jtw);
// Write out each of the machines and roms
string? lastgame = null;
// Use a sorted list of games to output
foreach (string key in ItemsDB.SortedKeys)
{
// If this machine doesn't contain any writable items, skip
var itemsDict = GetItemsForBucketDB(key, filter: true);
if (itemsDict is null || !ContainsWritable([.. itemsDict.Values]))
continue;
// Resolve the names in the block
var items = ResolveNamesDB([.. itemsDict]);
foreach (var kvp in items)
{
// Get the machine for the item
var machine = GetMachineForItemDB(kvp.Key);
// If we have a different game and we're not at the start of the list, output the end of last item
if (lastgame is not null && !string.Equals(lastgame, machine.Value!.GetName(), StringComparison.OrdinalIgnoreCase))
WriteEndGame(jtw);
// If we have a new game, output the beginning of the new item
if (lastgame is null || !string.Equals(lastgame, machine.Value!.GetName(), StringComparison.OrdinalIgnoreCase))
WriteStartGame(jtw, kvp.Value);
// Check for a "null" item
var datItem = new KeyValuePair<long, DatItem>(kvp.Key, ProcessNullifiedItem(kvp.Value));
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem.Value, ignoreblanks))
WriteDatItemDB(jtw, datItem);
// Set the new data to compare against
lastgame = machine.Value!.GetName();
}
}
// Write the file footer out
WriteFooter(jtw);
_logger.User($"'{outfile}' written!{Environment.NewLine}");
jtw.Close();
fs.Dispose();
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(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() { 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 static void WriteStartGame(JsonTextWriter jtw, DatItem datItem)
{
// No game should start with a path separator
if (!string.IsNullOrEmpty(datItem.GetMachine()!.GetName()))
datItem.GetMachine()!.SetName(datItem.GetMachine()!.GetName()!.TrimStart(Path.DirectorySeparatorChar));
// Build the state
jtw.WriteStartObject();
// Write the Machine
jtw.WritePropertyName("machine");
JsonSerializer js = new() { Formatting = Formatting.Indented };
js.Serialize(jtw, datItem.GetMachine()!);
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 static 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)
{
// Get the machine for the item
var machine = datItem.GetMachine();
// Pre-process the item name
ProcessItemName(datItem, machine, forceRemoveQuotes: true, forceRomName: false);
// Build the state
jtw.WriteStartObject();
// Write the DatItem
jtw.WritePropertyName("datitem");
JsonSerializer js = new() { ContractResolver = new BaseFirstContractResolver(), Formatting = Formatting.Indented };
js.Serialize(jtw, datItem);
// End item
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 WriteDatItemDB(JsonTextWriter jtw, KeyValuePair<long, DatItem> datItem)
{
// Get the machine for the item
var machine = GetMachineForItemDB(datItem.Key);
// Pre-process the item name
ProcessItemName(datItem.Value, machine.Value, forceRemoveQuotes: true, forceRomName: false);
// Build the state
jtw.WriteStartObject();
// Write the DatItem
jtw.WritePropertyName("datitem");
JsonSerializer js = new() { 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 static void WriteFooter(JsonTextWriter jtw)
{
// End items
jtw.WriteEndArray();
// End machine
jtw.WriteEndObject();
// End machines
jtw.WriteEndArray();
// End file
jtw.WriteEndObject();
jtw.Flush();
}
// https://github.com/dotnet/runtime/issues/728
private class BaseFirstContractResolver : DefaultContractResolver
{
public BaseFirstContractResolver()
{
NamingStrategy = new CamelCaseNamingStrategy();
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
return [.. base.CreateProperties(type, memberSerialization)
.Where(p => p is not null)
.OrderBy(p => BaseTypesAndSelf(p.DeclaringType).Count())];
static IEnumerable<Type?> BaseTypesAndSelf(Type? t)
{
while (t is not null)
{
yield return t;
t = t.BaseType;
}
}
}
}
}
}

View File

@@ -0,0 +1,522 @@
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.Metadata.Filter;
using SabreTools.Metadata.DatItems;
#pragma warning disable IDE0060 // Remove unused parameter
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents parsing and writing of a SabreDAT XML
/// </summary>
/// TODO: Transform this into direct serialization and deserialization of the Metadata type
public sealed class SabreXML : DatFile
{
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> Enum.GetValues(typeof(ItemType)) as ItemType[] ?? [];
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SabreXML(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.SabreXML);
}
/// <inheritdoc/>
public override void ParseFile(string filename,
int indexId,
bool keep,
bool statsOnly = false,
FilterRunner? filterRunner = null,
bool throwOnError = false)
{
// Prepare all internal variables
XmlReader? xtr = XmlReader.Create(filename, new XmlReaderSettings
{
CheckCharacters = false,
#if NET40_OR_GREATER
DtdProcessing = DtdProcessing.Ignore,
#endif
IgnoreComments = true,
IgnoreWhitespace = true,
ValidationFlags = XmlSchemaValidationFlags.None,
ValidationType = ValidationType.None,
});
var source = new Source(indexId, filename);
long sourceIndex = AddSourceDB(source);
// If we got a null reader, just return
if (xtr is 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(typeof(DatHeader));
DatHeader? header = xs.Deserialize(xtr.ReadSubtree()) as DatHeader;
SetHeader(header);
xtr.Skip();
break;
case "directory":
ReadDirectory(xtr.ReadSubtree(), statsOnly, source, sourceIndex, filterRunner);
// Skip the directory node now that we've processed it
xtr.Read();
break;
default:
xtr.Read();
break;
}
}
}
catch (Exception ex) when (!throwOnError)
{
_logger.Warning(ex, $"Exception found while parsing '{filename}'");
// For XML errors, just skip the affected node
xtr?.Read();
}
#if NET452_OR_GREATER
xtr?.Dispose();
#endif
}
/// <summary>
/// Read directory information
/// </summary>
/// <param name="xtr">XmlReader to use to parse the header</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="source">Source representing the DAT</param>
/// <param name="sourceIndex">Index of the Source representing the DAT</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ReadDirectory(XmlReader xtr,
bool statsOnly,
Source source,
long sourceIndex,
FilterRunner? filterRunner)
{
// If the reader is invalid, skip
if (xtr is null)
return;
// Prepare internal variables
Machine? machine = null;
long machineIndex = -1;
// 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(typeof(Machine));
machine = xs?.Deserialize(xtr.ReadSubtree()) as Machine;
// If the machine doesn't pass the filter
if (machine is not null && filterRunner is not null && !machine.PassesFilter(filterRunner))
machine = null;
if (machine is not null)
machineIndex = AddMachineDB(machine);
xtr.Skip();
break;
case "files":
ReadFiles(xtr.ReadSubtree(),
machine,
machineIndex,
statsOnly,
source,
sourceIndex,
filterRunner);
// 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="machineIndex">Index of the Machine to add to the parsed items</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="source">Source representing the DAT</param>
/// <param name="sourceIndex">Index of the Source representing the DAT</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ReadFiles(XmlReader xtr,
Machine? machine,
long machineIndex,
bool statsOnly,
Source source,
long sourceIndex,
FilterRunner? filterRunner)
{
// If the reader is invalid, skip
if (xtr is 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(typeof(DatItem));
if (xs.Deserialize(xtr.ReadSubtree()) is DatItem item)
{
// If the item doesn't pass the filter
if (filterRunner is not null && !item.PassesFilter(filterRunner))
{
xtr.Skip();
break;
}
item.CopyMachineInformation(machine);
item.SetFieldValue<Source?>(DatItem.SourceKey, source);
AddItem(item, statsOnly);
// AddItemDB(item, machineIndex, sourceIndex, statsOnly);
}
xtr.Skip();
break;
default:
xtr.Read();
break;
}
}
}
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
FileStream fs = File.Create(outfile);
// If we get back null for some reason, just log and return
if (fs is null)
{
_logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
return false;
}
XmlTextWriter xtw = new(fs, new UTF8Encoding(false))
{
Formatting = Formatting.Indented,
IndentChar = '\t',
Indentation = 1,
};
// Write out the header
WriteHeader(xtw);
// Write out each of the machines and roms
string? lastgame = null;
// Use a sorted list of games to output
foreach (string key in Items.SortedKeys)
{
List<DatItem> datItems = GetItemsForBucket(key, filter: true);
// If this machine doesn't contain any writable items, skip
if (!ContainsWritable(datItems))
continue;
// Resolve the names in the block
datItems = 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 is not null && !string.Equals(lastgame, datItem.GetMachine()!.GetName(), StringComparison.OrdinalIgnoreCase))
WriteEndGame(xtw);
// If we have a new game, output the beginning of the new item
if (lastgame is null || !string.Equals(lastgame, datItem.GetMachine()!.GetName(), StringComparison.OrdinalIgnoreCase))
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.GetMachine()!.GetName();
}
}
// Write the file footer out
WriteFooter(xtw);
_logger.User($"'{outfile}' written!{Environment.NewLine}");
#if NET452_OR_GREATER
xtw.Dispose();
#endif
fs.Dispose();
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
return true;
}
/// <inheritdoc/>
public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
FileStream fs = File.Create(outfile);
// If we get back null for some reason, just log and return
if (fs is null)
{
_logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
return false;
}
XmlTextWriter xtw = new(fs, new UTF8Encoding(false))
{
Formatting = Formatting.Indented,
IndentChar = '\t',
Indentation = 1,
};
// Write out the header
WriteHeader(xtw);
// Write out each of the machines and roms
string? lastgame = null;
// Use a sorted list of games to output
foreach (string key in ItemsDB.SortedKeys)
{
// If this machine doesn't contain any writable items, skip
var itemsDict = GetItemsForBucketDB(key, filter: true);
if (itemsDict is null || !ContainsWritable([.. itemsDict.Values]))
continue;
// Resolve the names in the block
var items = ResolveNamesDB([.. itemsDict]);
foreach (var kvp in items)
{
// Get the machine for the item
var machine = GetMachineForItemDB(kvp.Key);
// If we have a different game and we're not at the start of the list, output the end of last item
if (lastgame is not null && !string.Equals(lastgame, machine.Value!.GetName(), StringComparison.OrdinalIgnoreCase))
WriteEndGame(xtw);
// If we have a new game, output the beginning of the new item
if (lastgame is null || !string.Equals(lastgame, machine.Value!.GetName(), StringComparison.OrdinalIgnoreCase))
WriteStartGame(xtw, kvp.Value);
// Check for a "null" item
var datItem = new KeyValuePair<long, DatItem>(kvp.Key, ProcessNullifiedItem(kvp.Value));
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem.Value, ignoreblanks))
WriteDatItemDB(xtw, datItem);
// Set the new data to compare against
lastgame = machine.Value!.GetName();
}
}
// Write the file footer out
WriteFooter(xtw);
_logger.User($"'{outfile}' written!{Environment.NewLine}");
#if NET452_OR_GREATER
xtw.Dispose();
#endif
fs.Dispose();
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
return true;
}
/// <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(typeof(DatHeader));
XmlSerializerNamespaces ns = new();
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 static void WriteStartGame(XmlTextWriter xtw, DatItem datItem)
{
// No game should start with a path separator
datItem.GetMachine()!.SetName(datItem.GetMachine()!.GetName()?.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty);
// Write the machine
xtw.WriteStartElement("directory");
XmlSerializer xs = new(typeof(Machine));
XmlSerializerNamespaces ns = new();
ns.Add("", "");
xs.Serialize(xtw, datItem.GetMachine(), 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 static 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)
{
// Get the machine for the item
var machine = datItem.GetMachine();
// Pre-process the item name
ProcessItemName(datItem, machine, forceRemoveQuotes: true, forceRomName: false);
// Write the DatItem
XmlSerializer xs = new(typeof(DatItem));
XmlSerializerNamespaces ns = new();
ns.Add("", "");
xs.Serialize(xtw, datItem, ns);
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 WriteDatItemDB(XmlTextWriter xtw, KeyValuePair<long, DatItem> datItem)
{
// Get the machine for the item
var machine = GetMachineForItemDB(datItem.Key);
// Pre-process the item name
ProcessItemName(datItem.Value, machine.Value, forceRemoveQuotes: true, forceRomName: false);
// Write the DatItem
XmlSerializer xs = new(typeof(DatItem));
XmlSerializerNamespaces ns = new();
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 static void WriteFooter(XmlTextWriter xtw)
{
// End files
xtw.WriteEndElement();
// End directory
xtw.WriteEndElement();
// End data
xtw.WriteEndElement();
// End datafile
xtw.WriteEndElement();
xtw.Flush();
}
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
#pragma warning disable IDE0290 // Use primary constructor
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a value-separated DAT
/// </summary>
public abstract class SeparatedValue : SerializableDatFile<Data.Models.SeparatedValue.MetadataFile, Serialization.Readers.SeparatedValue, Serialization.Writers.SeparatedValue, Serialization.CrossModel.SeparatedValue>
{
#region Fields
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.Disk,
ItemType.Media,
ItemType.Rom,
];
/// <summary>
/// Represents the delimiter between fields
/// </summary>
protected char _delim;
#endregion
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SeparatedValue(DatFile? datFile) : base(datFile)
{
}
/// <inheritdoc/>
public override void ParseFile(string filename,
int indexId,
bool keep,
bool statsOnly = false,
FilterRunner? filterRunner = null,
bool throwOnError = false)
{
try
{
// Deserialize the input file
var metadataFile = new Serialization.Readers.SeparatedValue().Deserialize(filename, _delim);
var metadata = new Serialization.CrossModel.SeparatedValue().Serialize(metadataFile);
// Convert to the internal format
ConvertFromMetadata(metadata, filename, indexId, keep, statsOnly, filterRunner);
}
catch (Exception ex) when (!throwOnError)
{
string message = $"'{filename}' - An error occurred during parsing";
_logger.Error(ex, message);
}
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
// Check item name
if (string.IsNullOrEmpty(datItem.GetName()))
missingFields.Add(Data.Models.Metadata.Rom.NameKey);
#pragma warning disable IDE0010
switch (datItem)
{
case Disk disk:
if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key))
&& string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)))
{
missingFields.Add(Data.Models.Metadata.Disk.SHA1Key);
}
break;
case Media media:
if (string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key))
&& string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key))
&& string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key))
&& string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)))
{
missingFields.Add(Data.Models.Metadata.Media.SHA1Key);
}
break;
case Rom rom:
if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0)
missingFields.Add(Data.Models.Metadata.Rom.SizeKey);
if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key))
&& string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)))
{
missingFields.Add(Data.Models.Metadata.Rom.SHA1Key);
}
break;
}
#pragma warning restore IDE0010
return missingFields;
}
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
// Serialize the input file
var metadata = ConvertToMetadata(ignoreblanks);
var metadataFile = new Serialization.CrossModel.SeparatedValue().Deserialize(metadata);
if (!new Serialization.Writers.SeparatedValue().SerializeFile(metadataFile, outfile, _delim, longHeader: false))
{
_logger.Warning($"File '{outfile}' could not be written! See the log for more details.");
return false;
}
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
_logger.User($"'{outfile}' written!{Environment.NewLine}");
return true;
}
}
/// <summary>
/// Represents a comma-separated value file
/// </summary>
public sealed class CommaSeparatedValue : SeparatedValue
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public CommaSeparatedValue(DatFile? datFile) : base(datFile)
{
_delim = ',';
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.CSV);
}
}
/// <summary>
/// Represents a semicolon-separated value file
/// </summary>
public sealed class SemicolonSeparatedValue : SeparatedValue
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SemicolonSeparatedValue(DatFile? datFile) : base(datFile)
{
_delim = ';';
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.SSV);
}
}
/// <summary>
/// Represents a tab-separated value file
/// </summary>
public sealed class TabSeparatedValue : SeparatedValue
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public TabSeparatedValue(DatFile? datFile) : base(datFile)
{
_delim = '\t';
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.TSV);
}
}
}

View File

@@ -0,0 +1,121 @@
using System;
using SabreTools.Metadata.Filter;
using SabreTools.Data.Models.Metadata;
using SabreTools.Serialization.CrossModel;
using SabreTools.Serialization.Readers;
using SabreTools.Serialization.Writers;
#pragma warning disable IDE0290 // Use primary constructor
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents a DAT that can be serialized
/// </summary>
/// <typeparam name="TModel">Base internal model for the DAT type</typeparam>
/// <typeparam name="TFileReader">IFileReader type to use for conversion</typeparam>
/// <typeparam name="TFileWriter">IFileWriter type to use for conversion</typeparam>
/// <typeparam name="TCrossModel">ICrossModel for cross-model serialization</typeparam>
public abstract class SerializableDatFile<TModel, TFileReader, TFileWriter, TCrossModel> : DatFile
where TFileReader : IFileReader<TModel>
where TFileWriter : IFileWriter<TModel>
where TCrossModel : ICrossModel<TModel, MetadataFile>
{
#region Static Serialization Instances
/// <summary>
/// File deserializer instance
/// </summary>
private static readonly TFileReader FileDeserializer = Activator.CreateInstance<TFileReader>();
/// <summary>
/// File serializer instance
/// </summary>
private static readonly TFileWriter FileSerializer = Activator.CreateInstance<TFileWriter>();
/// <summary>
/// Cross-model serializer instance
/// </summary>
private static readonly TCrossModel CrossModelSerializer = Activator.CreateInstance<TCrossModel>();
#endregion
/// <inheritdoc/>
protected SerializableDatFile(DatFile? datFile) : base(datFile) { }
/// <inheritdoc/>
public override void ParseFile(string filename,
int indexId,
bool keep,
bool statsOnly = false,
FilterRunner? filterRunner = null,
bool throwOnError = false)
{
try
{
// Deserialize the input file in two steps
var specificFormat = FileDeserializer.Deserialize(filename);
var internalFormat = CrossModelSerializer.Serialize(specificFormat);
// Convert to the internal format
ConvertFromMetadata(internalFormat, filename, indexId, keep, statsOnly, filterRunner);
}
catch (Exception ex) when (!throwOnError)
{
string message = $"'{filename}' - An error occurred during parsing";
_logger.Error(ex, message);
}
}
/// <inheritdoc/>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
// Serialize the input file in two steps
var internalFormat = ConvertToMetadata(ignoreblanks);
var specificFormat = CrossModelSerializer.Deserialize(internalFormat);
if (!FileSerializer.SerializeFile(specificFormat, outfile))
{
_logger.Warning($"File '{outfile}' could not be written! See the log for more details.");
return false;
}
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
_logger.User($"'{outfile}' written!{Environment.NewLine}");
return true;
}
/// <inheritdoc/>
public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
_logger.User($"Writing to '{outfile}'...");
// Serialize the input file in two steps
var internalFormat = ConvertToMetadataDB(ignoreblanks);
var specificFormat = CrossModelSerializer.Deserialize(internalFormat);
if (!FileSerializer.SerializeFile(specificFormat, outfile))
{
_logger.Warning($"File '{outfile}' could not be written! See the log for more details.");
return false;
}
}
catch (Exception ex) when (!throwOnError)
{
_logger.Error(ex);
return false;
}
_logger.User($"'{outfile}' written!{Environment.NewLine}");
return true;
}
}
}

View File

@@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles.Formats
{
/// <summary>
/// Represents parsing and writing of a SoftwareList
/// </summary>
public sealed class SoftwareList : SerializableDatFile<Data.Models.SoftwareList.SoftwareList, Serialization.Readers.SoftwareList, Serialization.Writers.SoftwareList, Serialization.CrossModel.SoftwareList>
{
#region Constants
/// <summary>
/// DTD for original MAME Software List DATs
/// </summary>
/// <remarks>
/// TODO: See if there's an updated DTD and then check for required fields
/// </remarks>
internal const string SoftwareListDTD = @"<!ELEMENT softwarelist (notes?, software+)>
<!ATTLIST softwarelist name CDATA #REQUIRED>
<!ATTLIST softwarelist description CDATA #IMPLIED>
<!ELEMENT notes (#PCDATA)>
<!ELEMENT software (description, year, publisher, notes?, info*, sharedfeat*, part*)>
<!ATTLIST software name CDATA #REQUIRED>
<!ATTLIST software cloneof CDATA #IMPLIED>
<!ATTLIST software supported (yes|partial|no) ""yes"">
<!ELEMENT description (#PCDATA)>
<!ELEMENT year (#PCDATA)>
<!ELEMENT publisher (#PCDATA)>
<!ELEMENT info EMPTY>
<!ATTLIST info name CDATA #REQUIRED>
<!ATTLIST info value CDATA #IMPLIED>
<!ELEMENT sharedfeat EMPTY>
<!ATTLIST sharedfeat name CDATA #REQUIRED>
<!ATTLIST sharedfeat value CDATA #IMPLIED>
<!ELEMENT part (feature*, dataarea*, diskarea*, dipswitch*)>
<!ATTLIST part name CDATA #REQUIRED>
<!ATTLIST part interface CDATA #REQUIRED>
<!-- feature is used to store things like pcb-type, mapper type, etc. Specific values depend on the system. -->
<!ELEMENT feature EMPTY>
<!ATTLIST feature name CDATA #REQUIRED>
<!ATTLIST feature value CDATA #IMPLIED>
<!ELEMENT dataarea (rom*)>
<!ATTLIST dataarea name CDATA #REQUIRED>
<!ATTLIST dataarea size CDATA #REQUIRED>
<!ATTLIST dataarea width (8|16|32|64) ""8"">
<!ATTLIST dataarea endianness (big|little) ""little"">
<!ELEMENT rom EMPTY>
<!ATTLIST rom name CDATA #IMPLIED>
<!ATTLIST rom size CDATA #IMPLIED>
<!ATTLIST rom crc CDATA #IMPLIED>
<!ATTLIST rom sha1 CDATA #IMPLIED>
<!ATTLIST rom offset CDATA #IMPLIED>
<!ATTLIST rom value CDATA #IMPLIED>
<!ATTLIST rom status (baddump|nodump|good) ""good"">
<!ATTLIST rom loadflag (load16_byte|load16_word|load16_word_swap|load32_byte|load32_word|load32_word_swap|load32_dword|load64_word|load64_word_swap|reload|fill|continue|reload_plain|ignore) #IMPLIED>
<!ELEMENT diskarea (disk*)>
<!ATTLIST diskarea name CDATA #REQUIRED>
<!ELEMENT disk EMPTY>
<!ATTLIST disk name CDATA #REQUIRED>
<!ATTLIST disk sha1 CDATA #IMPLIED>
<!ATTLIST disk status (baddump|nodump|good) ""good"">
<!ATTLIST disk writeable (yes|no) ""no"">
<!ELEMENT dipswitch (dipvalue*)>
<!ATTLIST dipswitch name CDATA #REQUIRED>
<!ATTLIST dipswitch tag CDATA #REQUIRED>
<!ATTLIST dipswitch mask CDATA #REQUIRED>
<!ELEMENT dipvalue EMPTY>
<!ATTLIST dipvalue name CDATA #REQUIRED>
<!ATTLIST dipvalue value CDATA #REQUIRED>
<!ATTLIST dipvalue default (yes|no) ""no"">
";
#endregion
#region Fields
/// <inheritdoc/>
public override ItemType[] SupportedTypes
=> [
ItemType.DipSwitch,
ItemType.Disk,
ItemType.Info,
ItemType.PartFeature,
ItemType.Rom,
ItemType.SharedFeat,
];
#endregion
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SoftwareList(DatFile? datFile) : base(datFile)
{
Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.SoftwareList);
}
/// <inheritdoc/>
protected internal override List<string>? GetMissingRequiredFields(DatItem datItem)
{
List<string> missingFields = [];
#pragma warning disable IDE0010
switch (datItem)
{
case DipSwitch dipSwitch:
if (!dipSwitch.PartSpecified)
{
missingFields.Add(Data.Models.Metadata.Part.NameKey);
missingFields.Add(Data.Models.Metadata.Part.InterfaceKey);
}
else
{
if (string.IsNullOrEmpty(dipSwitch.GetFieldValue<Part?>(DipSwitch.PartKey)!.GetName()))
missingFields.Add(Data.Models.Metadata.Part.NameKey);
if (string.IsNullOrEmpty(dipSwitch.GetFieldValue<Part?>(DipSwitch.PartKey)!.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey)))
missingFields.Add(Data.Models.Metadata.Part.InterfaceKey);
}
if (string.IsNullOrEmpty(dipSwitch.GetName()))
missingFields.Add(Data.Models.Metadata.DipSwitch.NameKey);
if (string.IsNullOrEmpty(dipSwitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.TagKey)))
missingFields.Add(Data.Models.Metadata.DipSwitch.TagKey);
if (string.IsNullOrEmpty(dipSwitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.MaskKey)))
missingFields.Add(Data.Models.Metadata.DipSwitch.MaskKey);
if (dipSwitch.ValuesSpecified)
{
var dipValues = dipSwitch.GetFieldValue<DipValue[]?>(Data.Models.Metadata.DipSwitch.DipValueKey);
if (Array.Find(dipValues!, dv => string.IsNullOrEmpty(dv.GetName())) is not null)
missingFields.Add(Data.Models.Metadata.DipValue.NameKey);
if (Array.Find(dipValues!, dv => string.IsNullOrEmpty(dv.GetStringFieldValue(Data.Models.Metadata.DipValue.ValueKey))) is not null)
missingFields.Add(Data.Models.Metadata.DipValue.ValueKey);
}
break;
case Disk disk:
if (!disk.PartSpecified)
{
missingFields.Add(Data.Models.Metadata.Part.NameKey);
missingFields.Add(Data.Models.Metadata.Part.InterfaceKey);
}
else
{
if (string.IsNullOrEmpty(disk.GetFieldValue<Part?>(Disk.PartKey)!.GetName()))
missingFields.Add(Data.Models.Metadata.Part.NameKey);
if (string.IsNullOrEmpty(disk.GetFieldValue<Part?>(Disk.PartKey)!.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey)))
missingFields.Add(Data.Models.Metadata.Part.InterfaceKey);
}
if (!disk.DiskAreaSpecified)
{
missingFields.Add(Data.Models.Metadata.DiskArea.NameKey);
}
else
{
if (string.IsNullOrEmpty(disk.GetFieldValue<DiskArea?>(Disk.DiskAreaKey)!.GetName()))
missingFields.Add(Data.Models.Metadata.DiskArea.NameKey);
}
if (string.IsNullOrEmpty(disk.GetName()))
missingFields.Add(Data.Models.Metadata.Disk.NameKey);
break;
case Info info:
if (string.IsNullOrEmpty(info.GetName()))
missingFields.Add(Data.Models.Metadata.Info.NameKey);
break;
case Rom rom:
if (!rom.PartSpecified)
{
missingFields.Add(Data.Models.Metadata.Part.NameKey);
missingFields.Add(Data.Models.Metadata.Part.InterfaceKey);
}
else
{
if (string.IsNullOrEmpty(rom.GetFieldValue<Part?>(Rom.PartKey)!.GetName()))
missingFields.Add(Data.Models.Metadata.Part.NameKey);
if (string.IsNullOrEmpty(rom.GetFieldValue<Part?>(Rom.PartKey)!.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey)))
missingFields.Add(Data.Models.Metadata.Part.InterfaceKey);
}
if (!rom.DataAreaSpecified)
{
missingFields.Add(Data.Models.Metadata.DataArea.NameKey);
missingFields.Add(Data.Models.Metadata.DataArea.SizeKey);
}
else
{
if (string.IsNullOrEmpty(rom.GetFieldValue<DataArea?>(Rom.DataAreaKey)!.GetName()))
missingFields.Add(Data.Models.Metadata.DataArea.NameKey);
if (rom.GetFieldValue<DataArea?>(Rom.DataAreaKey)!.GetInt64FieldValue(Data.Models.Metadata.DataArea.SizeKey) is null)
missingFields.Add(Data.Models.Metadata.DataArea.SizeKey);
}
break;
case SharedFeat sharedFeat:
if (string.IsNullOrEmpty(sharedFeat.GetName()))
missingFields.Add(Data.Models.Metadata.SharedFeat.NameKey);
break;
}
#pragma warning restore IDE0010
return missingFields;
}
}
}