2020-12-10 10:58:00 -08:00
using System ;
2022-11-03 16:46:53 -07:00
using System.Collections.Generic ;
2020-12-10 10:58:00 -08:00
using System.IO ;
2017-09-25 12:21:52 -07:00
using System.Linq ;
2024-03-05 03:04:47 -05:00
#if NET40_OR_GREATER | | NETCOREAPP
2024-03-05 02:56:50 -05:00
using System.Threading.Tasks ;
2024-03-05 03:04:47 -05:00
#endif
2020-09-07 14:47:27 -07:00
using System.Xml.Serialization ;
2022-11-03 16:05:07 -07:00
using Newtonsoft.Json ;
2020-12-08 13:23:59 -08:00
using SabreTools.Core ;
2020-12-10 22:16:53 -08:00
using SabreTools.Core.Tools ;
2020-12-09 22:11:35 -08:00
using SabreTools.DatFiles.Formats ;
2020-12-10 10:58:00 -08:00
using SabreTools.DatItems ;
2021-02-02 10:23:43 -08:00
using SabreTools.DatItems.Formats ;
2024-03-05 02:52:53 -05:00
using SabreTools.Filter ;
2024-03-04 23:56:05 -05:00
using SabreTools.Hashing ;
2020-12-07 14:29:45 -08:00
using SabreTools.Logging ;
2016-10-26 22:10:47 -07:00
2020-12-08 16:37:08 -08:00
namespace SabreTools.DatFiles
2016-04-19 01:11:23 -07:00
{
2019-01-08 11:49:31 -08:00
/// <summary>
/// Represents a format-agnostic DAT
/// </summary>
2020-09-08 10:12:41 -07:00
[JsonObject("datfile"), XmlRoot("datfile")]
2024-03-10 00:20:56 -05:00
public abstract partial class DatFile
2019-01-08 11:49:31 -08:00
{
2020-07-31 14:04:10 -07:00
#region Fields
2019-01-08 11:49:31 -08:00
/// <summary>
2020-07-27 10:26:08 -07:00
/// Header values
2019-01-08 11:49:31 -08:00
/// </summary>
2020-10-07 15:42:30 -07:00
[JsonProperty("header"), XmlElement("header")]
2020-07-27 10:26:08 -07:00
public DatHeader Header { get ; set ; } = new DatHeader ( ) ;
2019-01-08 11:49:31 -08:00
/// <summary>
2020-07-27 10:26:08 -07:00
/// DatItems and related statistics
2019-01-08 11:49:31 -08:00
/// </summary>
2020-10-07 15:42:30 -07:00
[JsonProperty("items"), XmlElement("items")]
2024-02-28 19:19:50 -05:00
public ItemDictionary Items { get ; set ; } = [ ] ;
2019-01-08 11:49:31 -08:00
#endregion
2020-10-07 15:42:30 -07:00
#region Logging
/// <summary>
/// Logging object
/// </summary>
[JsonIgnore, XmlIgnore]
2020-10-07 16:37:10 -07:00
protected Logger logger ;
2020-10-07 15:42:30 -07:00
#endregion
2020-07-15 09:41:59 -07:00
#region Constructors
2019-01-08 11:49:31 -08:00
/// <summary>
2020-07-15 09:41:59 -07:00
/// Create a new DatFile from an existing one
2019-01-08 11:49:31 -08:00
/// </summary>
2020-07-15 09:41:59 -07:00
/// <param name="datFile">DatFile to get the values from</param>
2023-08-10 23:22:14 -04:00
public DatFile ( DatFile ? datFile )
2019-01-08 11:49:31 -08:00
{
2020-10-07 16:37:10 -07:00
logger = new Logger ( this ) ;
2020-07-15 09:41:59 -07:00
if ( datFile ! = null )
2019-01-08 11:49:31 -08:00
{
2020-07-27 10:26:08 -07:00
Header = datFile . Header ;
2020-07-31 14:04:10 -07:00
Items = datFile . Items ;
2019-01-08 11:49:31 -08:00
}
}
/// <summary>
2020-07-15 09:41:59 -07:00
/// Create a specific type of DatFile to be used based on a format and a base DAT
2019-01-08 11:49:31 -08:00
/// </summary>
2020-07-15 09:41:59 -07:00
/// <param name="datFormat">Format of the DAT to be created</param>
/// <param name="baseDat">DatFile containing the information to use in specific operations</param>
2020-09-20 21:12:57 -07:00
/// <param name="quotes">For relevant types, assume the usage of quotes</param>
2020-07-15 09:41:59 -07:00
/// <returns>DatFile of the specific internal type that corresponds to the inputs</returns>
2023-08-10 23:22:14 -04:00
public static DatFile Create ( DatFormat ? datFormat = null , DatFile ? baseDat = null , bool quotes = true )
2019-01-08 11:49:31 -08:00
{
2020-12-14 16:01:28 -08:00
return datFormat switch
2019-01-08 11:49:31 -08:00
{
2021-07-19 10:39:21 -07:00
DatFormat . ArchiveDotOrg = > new ArchiveDotOrg ( baseDat ) ,
2020-12-14 16:01:28 -08:00
DatFormat . AttractMode = > new AttractMode ( baseDat ) ,
DatFormat . ClrMamePro = > new ClrMamePro ( baseDat , quotes ) ,
DatFormat . CSV = > new SeparatedValue ( baseDat , ',' ) ,
DatFormat . DOSCenter = > new DosCenter ( baseDat ) ,
DatFormat . EverdriveSMDB = > new EverdriveSMDB ( baseDat ) ,
DatFormat . Listrom = > new Listrom ( baseDat ) ,
DatFormat . Listxml = > new Listxml ( baseDat ) ,
DatFormat . Logiqx = > new Logiqx ( baseDat , false ) ,
DatFormat . LogiqxDeprecated = > new Logiqx ( baseDat , true ) ,
DatFormat . MissFile = > new Missfile ( baseDat ) ,
DatFormat . OfflineList = > new OfflineList ( baseDat ) ,
DatFormat . OpenMSX = > new OpenMSX ( baseDat ) ,
2024-03-04 23:56:05 -05:00
DatFormat . RedumpMD5 = > new Hashfile ( baseDat , HashType . MD5 ) ,
DatFormat . RedumpSFV = > new Hashfile ( baseDat , HashType . CRC32 ) ,
DatFormat . RedumpSHA1 = > new Hashfile ( baseDat , HashType . SHA1 ) ,
DatFormat . RedumpSHA256 = > new Hashfile ( baseDat , HashType . SHA256 ) ,
DatFormat . RedumpSHA384 = > new Hashfile ( baseDat , HashType . SHA384 ) ,
DatFormat . RedumpSHA512 = > new Hashfile ( baseDat , HashType . SHA512 ) ,
DatFormat . RedumpSpamSum = > new Hashfile ( baseDat , HashType . SpamSum ) ,
2020-12-14 16:01:28 -08:00
DatFormat . RomCenter = > new RomCenter ( baseDat ) ,
DatFormat . SabreJSON = > new SabreJSON ( baseDat ) ,
DatFormat . SabreXML = > new SabreXML ( baseDat ) ,
DatFormat . SoftwareList = > new Formats . SoftwareList ( baseDat ) ,
DatFormat . SSV = > new SeparatedValue ( baseDat , ';' ) ,
DatFormat . TSV = > new SeparatedValue ( baseDat , '\t' ) ,
2023-08-10 23:22:14 -04:00
2020-07-15 09:41:59 -07:00
// We use new-style Logiqx as a backup for generic DatFile
2020-12-14 16:01:28 -08:00
_ = > new Logiqx ( baseDat , false ) ,
} ;
2019-01-08 11:49:31 -08:00
}
/// <summary>
2020-07-15 09:41:59 -07:00
/// Create a new DatFile from an existing DatHeader
2019-01-08 11:49:31 -08:00
/// </summary>
2020-07-15 09:41:59 -07:00
/// <param name="datHeader">DatHeader to get the values from</param>
public static DatFile Create ( DatHeader datHeader )
2019-01-08 11:49:31 -08:00
{
2020-07-15 09:41:59 -07:00
DatFile datFile = Create ( datHeader . DatFormat ) ;
2020-07-27 10:26:08 -07:00
datFile . Header = ( DatHeader ) datHeader . Clone ( ) ;
2020-07-15 09:41:59 -07:00
return datFile ;
2019-01-08 11:49:31 -08:00
}
/// <summary>
2020-08-27 20:56:50 -07:00
/// Fill the header values based on existing Header and path
/// </summary>
/// <param name="path">Path used for creating a name, if necessary</param>
/// <param name="bare">True if the date should be omitted from name and description, false otherwise</param>
public void FillHeaderFromPath ( string path , bool bare )
{
// If the description is defined but not the name, set the name from the description
2024-03-10 04:10:37 -04:00
if ( string . IsNullOrEmpty ( Header . GetFieldValue < string? > ( Models . Metadata . Header . NameKey ) ) & & ! string . IsNullOrEmpty ( Header . GetFieldValue < string? > ( Models . Metadata . Header . DescriptionKey ) ) )
2020-08-27 20:56:50 -07:00
{
2024-03-10 04:10:37 -04:00
Header . SetFieldValue < string? > ( Models . Metadata . Header . NameKey , Header . GetFieldValue < string? > ( Models . Metadata . Header . DescriptionKey ) ) ;
2020-08-27 20:56:50 -07:00
}
// If the name is defined but not the description, set the description from the name
2024-03-10 04:10:37 -04:00
else if ( ! string . IsNullOrEmpty ( Header . GetFieldValue < string? > ( Models . Metadata . Header . NameKey ) ) & & string . IsNullOrEmpty ( Header . GetFieldValue < string? > ( Models . Metadata . Header . DescriptionKey ) ) )
2020-08-27 20:56:50 -07:00
{
2024-03-10 04:10:37 -04:00
Header . SetFieldValue < string? > ( Models . Metadata . Header . DescriptionKey , Header . GetFieldValue < string? > ( Models . Metadata . Header . NameKey ) + ( bare ? string . Empty : $" ({Header.GetFieldValue<string?>(Models.Metadata.Header.DateKey)})" ) ) ;
2020-08-27 20:56:50 -07:00
}
// If neither the name or description are defined, set them from the automatic values
2024-03-10 04:10:37 -04:00
else if ( string . IsNullOrEmpty ( Header . GetFieldValue < string? > ( Models . Metadata . Header . NameKey ) ) & & string . IsNullOrEmpty ( Header . GetFieldValue < string? > ( Models . Metadata . Header . DescriptionKey ) ) )
2020-08-27 20:56:50 -07:00
{
string [ ] splitpath = path . TrimEnd ( Path . DirectorySeparatorChar ) . Split ( Path . DirectorySeparatorChar ) ;
2024-03-10 04:10:37 -04:00
Header . SetFieldValue < string? > ( Models . Metadata . Header . NameKey , splitpath . Last ( ) ) ;
Header . SetFieldValue < string? > ( Models . Metadata . Header . DescriptionKey , Header . GetFieldValue < string? > ( Models . Metadata . Header . NameKey ) + ( bare ? string . Empty : $" ({Header.GetFieldValue<string?>(Models.Metadata.Header.DateKey)})" ) ) ;
2020-08-27 20:56:50 -07:00
}
}
2020-07-15 09:41:59 -07:00
#endregion
2023-08-10 23:22:14 -04:00
2024-03-05 02:52:53 -05:00
#region Filtering
/// <summary>
/// Execute all filters in a filter runner on the items in the dictionary
/// </summary>
/// <param name="filterRunner">Preconfigured filter runner to use</param>
public void ExecuteFilters ( FilterRunner filterRunner )
{
List < string > keys = [ . . Items . Keys ] ;
#if NET452_OR_GREATER | | NETCOREAPP
Parallel . ForEach ( keys , Globals . ParallelOptions , key = >
#elif NET40_OR_GREATER
Parallel . ForEach ( keys , key = >
#else
foreach ( var key in keys )
#endif
{
ConcurrentList < DatItem > ? items = Items [ key ] ;
if ( items = = null )
#if NET40_OR_GREATER | | NETCOREAPP
return ;
#else
continue ;
#endif
2024-03-05 03:04:47 -05:00
// Filter all items in the current key
var newItems = new ConcurrentList < DatItem > ( ) ;
foreach ( var item in items )
{
if ( item . PassesFilter ( filterRunner ) )
newItems . Add ( item ) ;
}
// Set the value in the key to the new set
Items [ key ] = newItems ;
2024-03-05 02:52:53 -05:00
#if NET40_OR_GREATER | | NETCOREAPP
} ) ;
#else
}
#endif
}
#endregion
2020-12-10 10:58:00 -08:00
#region Parsing
/// <summary>
/// Parse DatFile and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
2020-12-23 13:55:09 -08:00
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
2020-12-10 10:58:00 -08:00
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
2020-12-23 13:55:09 -08:00
public abstract void ParseFile ( string filename , int indexId , bool keep , bool statsOnly = false , bool throwOnError = false ) ;
2020-12-10 10:58:00 -08:00
/// <summary>
/// Add a rom to the Dat after checking
/// </summary>
/// <param name="item">Item data to check against</param>
2020-12-23 13:55:09 -08:00
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
2020-12-10 10:58:00 -08:00
/// <returns>The key for the item</returns>
2020-12-23 13:55:09 -08:00
protected string ParseAddHelper ( DatItem item , bool statsOnly )
2020-12-10 10:58:00 -08:00
{
2020-12-14 16:01:28 -08:00
string key ;
2020-12-10 10:58:00 -08:00
// If we have a Disk, Media, or Rom, clean the hash data
2023-08-10 23:22:14 -04:00
if ( item . ItemType = = ItemType . Disk & & item is Disk disk )
2020-12-10 10:58:00 -08:00
{
// If the file has aboslutely no hashes, skip and log
2024-03-09 21:34:26 -05:00
if ( disk . GetFieldValue < ItemStatus > ( Models . Metadata . Disk . StatusKey ) ! = ItemStatus . Nodump
& & string . IsNullOrEmpty ( disk . GetFieldValue < string? > ( Models . Metadata . Disk . MD5Key ) )
& & string . IsNullOrEmpty ( disk . GetFieldValue < string? > ( Models . Metadata . Disk . SHA1Key ) ) )
2020-12-10 10:58:00 -08:00
{
2024-03-08 20:42:24 -05:00
logger . Verbose ( $"Incomplete entry for '{disk.GetName()}' will be output as nodump" ) ;
2024-03-09 21:34:26 -05:00
disk . SetFieldValue < ItemStatus > ( Models . Metadata . Disk . StatusKey , ItemStatus . Nodump ) ;
2020-12-10 10:58:00 -08:00
}
item = disk ;
}
2023-08-10 23:22:14 -04:00
if ( item . ItemType = = ItemType . Media & & item is Media media )
2020-12-10 10:58:00 -08:00
{
2023-08-10 23:22:14 -04:00
// If the file has aboslutely no hashes, skip and log
2024-03-09 21:34:26 -05:00
if ( string . IsNullOrEmpty ( media . GetFieldValue < string? > ( Models . Metadata . Media . MD5Key ) )
& & string . IsNullOrEmpty ( media . GetFieldValue < string? > ( Models . Metadata . Media . SHA1Key ) )
& & string . IsNullOrEmpty ( media . GetFieldValue < string? > ( Models . Metadata . Media . SHA256Key ) )
& & string . IsNullOrEmpty ( media . GetFieldValue < string? > ( Models . Metadata . Media . SpamSumKey ) ) )
2023-08-10 23:22:14 -04:00
{
2024-03-08 20:42:24 -05:00
logger . Verbose ( $"Incomplete entry for '{media.GetName()}' will be output as nodump" ) ;
2023-08-10 23:22:14 -04:00
}
2020-12-10 10:58:00 -08:00
2023-08-10 23:22:14 -04:00
item = media ;
}
else if ( item . ItemType = = ItemType . Rom & & item is Rom rom )
{
2020-12-10 10:58:00 -08:00
// If we have the case where there is SHA-1 and nothing else, we don't fill in any other part of the data
2024-03-09 21:34:26 -05:00
if ( rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) = = null & & ! rom . HasHashes ( ) )
2020-12-10 10:58:00 -08:00
{
// No-op, just catch it so it doesn't go further
2024-03-08 20:42:24 -05:00
logger . Verbose ( $"{Header.FileName}: Entry with only SHA-1 found - '{rom.GetName()}'" ) ;
2020-12-10 10:58:00 -08:00
}
// If we have a rom and it's missing size AND the hashes match a 0-byte file, fill in the rest of the info
2024-03-09 21:34:26 -05:00
else if ( ( rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) = = 0 | | rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) = = null )
& & ( string . IsNullOrEmpty ( rom . GetFieldValue < string? > ( Models . Metadata . Rom . CRCKey ) ) | | rom . HasZeroHash ( ) ) )
2020-12-10 10:58:00 -08:00
{
// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually
2024-03-09 21:34:26 -05:00
rom . SetFieldValue < long? > ( Models . Metadata . Rom . SizeKey , Constants . SizeZero ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . CRCKey , Constants . CRCZero ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . MD5Key , Constants . MD5Zero ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SHA1Key , Constants . SHA1Zero ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SHA256Key , null ) ; // Constants.SHA256Zero;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SHA384Key , null ) ; // Constants.SHA384Zero;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SHA512Key , null ) ; // Constants.SHA512Zero;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SpamSumKey , null ) ; // Constants.SpamSumZero;
2020-12-10 10:58:00 -08:00
}
// If the file has no size and it's not the above case, skip and log
2024-03-09 21:34:26 -05:00
else if ( rom . GetFieldValue < ItemStatus > ( Models . Metadata . Rom . StatusKey ) ! = ItemStatus . Nodump & & ( rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) = = 0 | | rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) = = null ) )
2020-12-10 10:58:00 -08:00
{
2024-03-08 20:42:24 -05:00
logger . Verbose ( $"{Header.FileName}: Incomplete entry for '{rom.GetName()}' will be output as nodump" ) ;
2024-03-09 21:34:26 -05:00
rom . SetFieldValue < ItemStatus > ( Models . Metadata . Rom . StatusKey , ItemStatus . Nodump ) ;
2020-12-10 10:58:00 -08:00
}
// If the file has a size but aboslutely no hashes, skip and log
2024-03-09 21:34:26 -05:00
else if ( rom . GetFieldValue < ItemStatus > ( Models . Metadata . Rom . StatusKey ) ! = ItemStatus . Nodump
& & rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) ! = null & & rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) > 0
2020-12-10 10:58:00 -08:00
& & ! rom . HasHashes ( ) )
{
2024-03-08 20:42:24 -05:00
logger . Verbose ( $"{Header.FileName}: Incomplete entry for '{rom.GetName()}' will be output as nodump" ) ;
2024-03-09 21:34:26 -05:00
rom . SetFieldValue < ItemStatus > ( Models . Metadata . Rom . StatusKey , ItemStatus . Nodump ) ;
2020-12-10 10:58:00 -08:00
}
item = rom ;
}
// Get the key and add the file
2020-12-14 15:31:28 -08:00
key = item . GetKey ( ItemKey . Machine ) ;
2020-12-23 13:22:06 -08:00
2020-12-23 13:59:00 -08:00
// If only adding statistics, we add an empty key for games and then just item stats
2020-12-23 13:22:06 -08:00
if ( statsOnly )
2020-12-23 13:59:00 -08:00
{
2020-12-23 14:06:48 -08:00
Items . EnsureKey ( key ) ;
2020-12-23 13:22:06 -08:00
Items . AddItemStatistics ( item ) ;
2020-12-23 13:59:00 -08:00
}
2020-12-23 13:22:06 -08:00
else
2020-12-23 13:59:00 -08:00
{
2020-12-23 13:22:06 -08:00
Items . Add ( key , item ) ;
2020-12-23 13:59:00 -08:00
}
2020-12-10 10:58:00 -08:00
return key ;
}
/// <summary>
/// Get a sanitized Date from an input string
/// </summary>
/// <param name="input">String to get value from</param>
/// <returns>Date as a string, if possible</returns>
2023-08-10 23:22:14 -04:00
protected static string? CleanDate ( string? input )
2020-12-10 10:58:00 -08:00
{
// Null in, null out
2024-02-28 22:54:56 -05:00
if ( string . IsNullOrEmpty ( input ) )
2020-12-10 10:58:00 -08:00
return null ;
string date = string . Empty ;
if ( input ! = null )
{
if ( DateTime . TryParse ( input , out DateTime dateTime ) )
date = dateTime . ToString ( ) ;
else
date = input ;
}
return date ;
}
2023-08-10 23:22:14 -04:00
2020-12-10 10:58:00 -08:00
/// <summary>
/// Clean a hash string from a Listrom DAT
/// </summary>
/// <param name="hash">Hash string to sanitize</param>
/// <returns>Cleaned string</returns>
2022-11-03 16:05:07 -07:00
protected static string CleanListromHashData ( string hash )
2020-12-10 10:58:00 -08:00
{
if ( hash . StartsWith ( "CRC" ) )
return hash . Substring ( 4 , 8 ) . ToLowerInvariant ( ) ;
else if ( hash . StartsWith ( "SHA1" ) )
return hash . Substring ( 5 , 40 ) . ToLowerInvariant ( ) ;
return hash ;
}
#endregion
2023-08-10 23:22:14 -04:00
2020-12-10 11:28:11 -08:00
#region Writing
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public abstract bool WriteToFile ( string outfile , bool ignoreblanks = false , bool throwOnError = false ) ;
/// <summary>
/// Create a prefix or postfix from inputs
/// </summary>
/// <param name="item">DatItem to create a prefix/postfix for</param>
/// <param name="prefix">True for prefix, false for postfix</param>
/// <returns>Sanitized string representing the postfix or prefix</returns>
protected string CreatePrefixPostfix ( DatItem item , bool prefix )
{
// Initialize strings
2020-12-14 16:01:28 -08:00
string fix ,
2024-03-09 23:43:43 -05:00
game = item . Machine . GetFieldValue < string? > ( Models . Metadata . Machine . NameKey ) ? ? string . Empty ,
2020-12-10 11:28:11 -08:00
name = item . GetName ( ) ? ? item . ItemType . ToString ( ) ,
crc = string . Empty ,
md5 = string . Empty ,
sha1 = string . Empty ,
sha256 = string . Empty ,
sha384 = string . Empty ,
sha512 = string . Empty ,
size = string . Empty ,
spamsum = string . Empty ;
// If we have a prefix
if ( prefix )
fix = Header . Prefix + ( Header . Quotes ? "\"" : string . Empty ) ;
// If we have a postfix
else
fix = ( Header . Quotes ? "\"" : string . Empty ) + Header . Postfix ;
// Ensure we have the proper values for replacement
2023-08-10 23:22:14 -04:00
if ( item . ItemType = = ItemType . Disk & & item is Disk disk )
2020-12-10 11:28:11 -08:00
{
2024-03-09 21:34:26 -05:00
md5 = disk . GetFieldValue < string? > ( Models . Metadata . Disk . MD5Key ) ? ? string . Empty ;
sha1 = disk . GetFieldValue < string? > ( Models . Metadata . Disk . SHA1Key ) ? ? string . Empty ;
2020-12-10 11:28:11 -08:00
}
2023-08-10 23:22:14 -04:00
else if ( item . ItemType = = ItemType . Media & & item is Media media )
2020-12-10 11:28:11 -08:00
{
2024-03-09 21:34:26 -05:00
md5 = media . GetFieldValue < string? > ( Models . Metadata . Media . MD5Key ) ? ? string . Empty ;
sha1 = media . GetFieldValue < string? > ( Models . Metadata . Media . SHA1Key ) ? ? string . Empty ;
sha256 = media . GetFieldValue < string? > ( Models . Metadata . Media . SHA256Key ) ? ? string . Empty ;
spamsum = media . GetFieldValue < string? > ( Models . Metadata . Media . SpamSumKey ) ? ? string . Empty ;
2020-12-10 11:28:11 -08:00
}
2023-08-10 23:22:14 -04:00
else if ( item . ItemType = = ItemType . Rom & & item is Rom rom )
2020-12-10 11:28:11 -08:00
{
2024-03-09 21:34:26 -05:00
crc = rom . GetFieldValue < string? > ( Models . Metadata . Rom . CRCKey ) ? ? string . Empty ;
md5 = rom . GetFieldValue < string? > ( Models . Metadata . Rom . MD5Key ) ? ? string . Empty ;
sha1 = rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA1Key ) ? ? string . Empty ;
sha256 = rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA256Key ) ? ? string . Empty ;
sha384 = rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA384Key ) ? ? string . Empty ;
sha512 = rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA512Key ) ? ? string . Empty ;
size = rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) ? . ToString ( ) ? ? string . Empty ;
spamsum = rom . GetFieldValue < string? > ( Models . Metadata . Rom . SpamSumKey ) ? ? string . Empty ;
2020-12-10 11:28:11 -08:00
}
// Now do bulk replacement where possible
fix = fix
. Replace ( "%game%" , game )
. Replace ( "%machine%" , game )
. Replace ( "%name%" , name )
2024-03-09 23:43:43 -05:00
. Replace ( "%manufacturer%" , item . Machine . GetFieldValue < string? > ( Models . Metadata . Machine . ManufacturerKey ) ? ? string . Empty )
. Replace ( "%publisher%" , item . Machine . GetFieldValue < string? > ( Models . Metadata . Machine . PublisherKey ) ? ? string . Empty )
. Replace ( "%category%" , item . Machine . GetFieldValue < string? > ( Models . Metadata . Machine . CategoryKey ) ? ? string . Empty )
2020-12-10 11:28:11 -08:00
. Replace ( "%crc%" , crc )
. Replace ( "%md5%" , md5 )
. Replace ( "%sha1%" , sha1 )
. Replace ( "%sha256%" , sha256 )
. Replace ( "%sha384%" , sha384 )
. Replace ( "%sha512%" , sha512 )
. Replace ( "%size%" , size )
. Replace ( "%spamsum%" , spamsum ) ;
return fix ;
}
/// <summary>
/// Process an item and correctly set the item name
/// </summary>
/// <param name="item">DatItem to update</param>
/// <param name="forceRemoveQuotes">True if the Quotes flag should be ignored, false otherwise</param>
/// <param name="forceRomName">True if the UseRomName should be always on (default), false otherwise</param>
protected void ProcessItemName ( DatItem item , bool forceRemoveQuotes , bool forceRomName = true )
{
// Backup relevant values and set new ones accordingly
bool quotesBackup = Header . Quotes ;
bool useRomNameBackup = Header . UseRomName ;
if ( forceRemoveQuotes )
Header . Quotes = false ;
if ( forceRomName )
Header . UseRomName = true ;
2021-03-19 20:56:12 -07:00
// Get the name to update
2024-03-09 23:43:43 -05:00
string? name = ( Header . UseRomName ? item . GetName ( ) : item . Machine . GetFieldValue < string? > ( Models . Metadata . Machine . NameKey ) ) ? ? string . Empty ;
2021-03-19 20:56:12 -07:00
2020-12-10 11:28:11 -08:00
// Create the proper Prefix and Postfix
string pre = CreatePrefixPostfix ( item , true ) ;
string post = CreatePrefixPostfix ( item , false ) ;
// If we're in Depot mode, take care of that instead
if ( Header . OutputDepot ? . IsActive = = true )
{
2023-08-10 23:22:14 -04:00
if ( item . ItemType = = ItemType . Disk & & item is Disk disk )
2020-12-10 11:28:11 -08:00
{
// We can only write out if there's a SHA-1
2024-03-09 21:34:26 -05:00
if ( ! string . IsNullOrEmpty ( disk . GetFieldValue < string? > ( Models . Metadata . Disk . SHA1Key ) ) )
2020-12-10 11:28:11 -08:00
{
2024-03-09 21:34:26 -05:00
name = Utilities . GetDepotPath ( disk . GetFieldValue < string? > ( Models . Metadata . Disk . SHA1Key ) , Header . OutputDepot . Depth ) ? . Replace ( '\\' , '/' ) ;
2020-12-14 10:11:20 -08:00
item . SetName ( $"{pre}{name}{post}" ) ;
2020-12-10 11:28:11 -08:00
}
}
2023-08-10 23:22:14 -04:00
else if ( item . ItemType = = ItemType . Media & & item is Media media )
2020-12-10 11:28:11 -08:00
{
// We can only write out if there's a SHA-1
2024-03-09 21:34:26 -05:00
if ( ! string . IsNullOrEmpty ( media . GetFieldValue < string? > ( Models . Metadata . Media . SHA1Key ) ) )
2020-12-10 11:28:11 -08:00
{
2024-03-09 21:34:26 -05:00
name = Utilities . GetDepotPath ( media . GetFieldValue < string? > ( Models . Metadata . Media . SHA1Key ) , Header . OutputDepot . Depth ) ? . Replace ( '\\' , '/' ) ;
2020-12-14 10:11:20 -08:00
item . SetName ( $"{pre}{name}{post}" ) ;
2020-12-10 11:28:11 -08:00
}
}
2023-08-10 23:22:14 -04:00
else if ( item . ItemType = = ItemType . Rom & & item is Rom rom )
2020-12-10 11:28:11 -08:00
{
// We can only write out if there's a SHA-1
2024-03-09 21:34:26 -05:00
if ( ! string . IsNullOrEmpty ( rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA1Key ) ) )
2020-12-10 11:28:11 -08:00
{
2024-03-09 21:34:26 -05:00
name = Utilities . GetDepotPath ( rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA1Key ) , Header . OutputDepot . Depth ) ? . Replace ( '\\' , '/' ) ;
2020-12-14 10:11:20 -08:00
item . SetName ( $"{pre}{name}{post}" ) ;
2020-12-10 11:28:11 -08:00
}
}
return ;
}
2024-02-28 22:54:56 -05:00
if ( ! string . IsNullOrEmpty ( Header . ReplaceExtension ) | | Header . RemoveExtension )
2020-12-10 11:28:11 -08:00
{
if ( Header . RemoveExtension )
Header . ReplaceExtension = string . Empty ;
2023-08-10 23:22:14 -04:00
string? dir = Path . GetDirectoryName ( name ) ;
if ( dir ! = null )
{
dir = dir . TrimStart ( Path . DirectorySeparatorChar ) ;
name = Path . Combine ( dir , Path . GetFileNameWithoutExtension ( name ) + Header . ReplaceExtension ) ;
}
2020-12-10 11:28:11 -08:00
}
2024-02-28 22:54:56 -05:00
if ( ! string . IsNullOrEmpty ( Header . AddExtension ) )
2020-12-10 11:28:11 -08:00
name + = Header . AddExtension ;
if ( Header . UseRomName & & Header . GameName )
2024-03-09 23:43:43 -05:00
name = Path . Combine ( item . Machine . GetFieldValue < string? > ( Models . Metadata . Machine . NameKey ) ? ? string . Empty , name ) ;
2020-12-10 11:28:11 -08:00
2021-03-19 20:56:12 -07:00
// Now assign back the formatted name
name = $"{pre}{name}{post}" ;
if ( Header . UseRomName )
item . SetName ( name ) ;
2023-08-14 13:17:51 -04:00
else if ( item . Machine ! = null )
2024-03-09 23:43:43 -05:00
item . Machine . SetFieldValue < string? > ( Models . Metadata . Machine . NameKey , name ) ;
2020-12-10 11:28:11 -08:00
// Restore all relevant values
if ( forceRemoveQuotes )
Header . Quotes = quotesBackup ;
if ( forceRomName )
Header . UseRomName = useRomNameBackup ;
}
/// <summary>
/// Process any DatItems that are "null", usually created from directory population
/// </summary>
/// <param name="datItem">DatItem to check for "null" status</param>
/// <returns>Cleaned DatItem</returns>
protected DatItem ProcessNullifiedItem ( DatItem datItem )
{
// If we don't have a Rom, we can ignore it
if ( datItem . ItemType ! = ItemType . Rom )
return datItem ;
// Cast for easier parsing
2023-08-10 23:22:14 -04:00
if ( datItem is not Rom rom )
return datItem ;
2020-12-10 11:28:11 -08:00
// If the Rom has "null" characteristics, ensure all fields
2024-03-09 21:34:26 -05:00
if ( rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) = = null & & rom . GetFieldValue < string? > ( Models . Metadata . Rom . CRCKey ) = = "null" )
2020-12-10 11:28:11 -08:00
{
2024-03-09 23:43:43 -05:00
logger . Verbose ( $"Empty folder found: {datItem.Machine.GetFieldValue<string?>(Models.Metadata.Machine.NameKey)}" ) ;
2020-12-10 11:28:11 -08:00
2024-03-08 20:42:24 -05:00
rom . SetName ( rom . GetName ( ) = = "null" ? "-" : rom . GetName ( ) ) ;
2024-03-09 21:34:26 -05:00
rom . SetFieldValue < long? > ( Models . Metadata . Rom . SizeKey , Constants . SizeZero ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . CRCKey , rom . GetFieldValue < string? > ( Models . Metadata . Rom . CRCKey ) = = "null" ? Constants . CRCZero : null ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . MD5Key , rom . GetFieldValue < string? > ( Models . Metadata . Rom . MD5Key ) = = "null" ? Constants . MD5Zero : null ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SHA1Key , rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA1Key ) = = "null" ? Constants . SHA1Zero : null ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SHA256Key , rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA256Key ) = = "null" ? Constants . SHA256Zero : null ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SHA384Key , rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA384Key ) = = "null" ? Constants . SHA384Zero : null ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SHA512Key , rom . GetFieldValue < string? > ( Models . Metadata . Rom . SHA512Key ) = = "null" ? Constants . SHA512Zero : null ) ;
rom . SetFieldValue < string? > ( Models . Metadata . Rom . SpamSumKey , rom . GetFieldValue < string? > ( Models . Metadata . Rom . SpamSumKey ) = = "null" ? Constants . SpamSumZero : null ) ;
2020-12-10 11:28:11 -08:00
}
return rom ;
}
/// <summary>
/// Get supported types for write
/// </summary>
/// <returns>List of supported types for writing</returns>
protected virtual ItemType [ ] GetSupportedTypes ( )
{
2024-02-28 19:19:50 -05:00
return Enum . GetValues ( typeof ( ItemType ) ) as ItemType [ ] ? ? [ ] ;
2020-12-10 11:28:11 -08:00
}
2022-11-03 16:14:42 -07:00
/// <summary>
2022-11-03 16:46:53 -07:00
/// Return list of required fields missing from a DatItem
2022-11-03 16:14:42 -07:00
/// </summary>
2022-11-03 16:46:53 -07:00
/// <returns>List of missing required fields, null or empty if none were found</returns>
2024-03-05 23:41:00 -05:00
protected virtual List < string > ? GetMissingRequiredFields ( DatItem datItem ) = > null ;
2022-11-03 16:14:42 -07:00
2020-12-10 11:28:11 -08:00
/// <summary>
/// Get if a machine contains any writable items
/// </summary>
/// <param name="datItems">DatItems to check</param>
/// <returns>True if the machine contains at least one writable item, false otherwise</returns>
/// <remarks>Empty machines are kept with this</remarks>
2021-07-18 21:00:01 -07:00
protected bool ContainsWritable ( ConcurrentList < DatItem > datItems )
2020-12-10 11:28:11 -08:00
{
// Empty machines are considered writable
if ( datItems = = null | | datItems . Count = = 0 )
return true ;
foreach ( DatItem datItem in datItems )
{
if ( GetSupportedTypes ( ) . Contains ( datItem . ItemType ) )
return true ;
}
return false ;
}
/// <summary>
/// Get if an item should be ignored on write
/// </summary>
/// <param name="datItem">DatItem to check</param>
2022-11-03 16:05:07 -07:00
/// <param name="ignoreBlanks">True if blank roms should be skipped on output, false otherwise</param>
2020-12-10 11:28:11 -08:00
/// <returns>True if the item should be skipped on write, false otherwise</returns>
2023-08-10 23:22:14 -04:00
protected bool ShouldIgnore ( DatItem ? datItem , bool ignoreBlanks )
2020-12-10 11:28:11 -08:00
{
2022-11-03 16:29:06 -07:00
// If this is invoked with a null DatItem, we ignore
if ( datItem = = null )
{
logger ? . Verbose ( $"Item was skipped because it was null" ) ;
return true ;
}
2020-12-10 11:28:11 -08:00
// If the item is supposed to be removed, we ignore
if ( datItem . Remove )
2022-11-03 16:27:58 -07:00
{
string itemString = JsonConvert . SerializeObject ( datItem , Formatting . None ) ;
logger ? . Verbose ( $"Item '{itemString}' was skipped because it was marked for removal" ) ;
2020-12-10 11:28:11 -08:00
return true ;
2022-11-03 16:27:58 -07:00
}
2020-12-10 11:28:11 -08:00
// If we have the Blank dat item, we ignore
if ( datItem . ItemType = = ItemType . Blank )
2022-11-03 16:27:58 -07:00
{
string itemString = JsonConvert . SerializeObject ( datItem , Formatting . None ) ;
logger ? . Verbose ( $"Item '{itemString}' was skipped because it was of type 'Blank'" ) ;
2020-12-10 11:28:11 -08:00
return true ;
2022-11-03 16:27:58 -07:00
}
2020-12-10 11:28:11 -08:00
// If we're ignoring blanks and we have a Rom
2023-08-10 23:22:14 -04:00
if ( ignoreBlanks & & datItem . ItemType = = ItemType . Rom & & datItem is Rom rom )
2020-12-10 11:28:11 -08:00
{
// If we have a 0-size or blank rom, then we ignore
2024-03-09 21:34:26 -05:00
if ( rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) = = 0 | | rom . GetFieldValue < long? > ( Models . Metadata . Rom . SizeKey ) = = null )
2022-11-03 16:27:58 -07:00
{
string itemString = JsonConvert . SerializeObject ( datItem , Formatting . None ) ;
logger ? . Verbose ( $"Item '{itemString}' was skipped because it had an invalid size" ) ;
2020-12-10 11:28:11 -08:00
return true ;
2022-11-03 16:27:58 -07:00
}
2020-12-10 11:28:11 -08:00
}
// If we have an item type not in the list of supported values
if ( ! GetSupportedTypes ( ) . Contains ( datItem . ItemType ) )
2022-11-03 16:27:58 -07:00
{
string itemString = JsonConvert . SerializeObject ( datItem , Formatting . None ) ;
logger ? . Verbose ( $"Item '{itemString}' was skipped because it was not supported in {Header?.DatFormat}" ) ;
2020-12-10 11:28:11 -08:00
return true ;
2022-11-03 16:27:58 -07:00
}
2020-12-10 11:28:11 -08:00
2022-11-03 16:05:07 -07:00
// If we have an item with missing required fields
2024-03-05 23:41:00 -05:00
List < string > ? missingFields = GetMissingRequiredFields ( datItem ) ;
2022-12-22 09:27:02 -08:00
if ( missingFields ! = null & & missingFields . Count ! = 0 )
2022-11-03 16:27:58 -07:00
{
string itemString = JsonConvert . SerializeObject ( datItem , Formatting . None ) ;
2024-02-28 22:54:56 -05:00
#if NET20 | | NET35
2024-03-05 23:41:00 -05:00
logger ? . Verbose ( $"Item '{itemString}' was skipped because it was missing required fields for {Header?.DatFormat}: {string.Join(" , ", [.. missingFields])}" ) ;
2024-02-28 22:54:56 -05:00
#else
2022-11-03 16:46:53 -07:00
logger ? . Verbose ( $"Item '{itemString}' was skipped because it was missing required fields for {Header?.DatFormat}: {string.Join(" , ", missingFields)}" ) ;
2024-02-28 22:54:56 -05:00
#endif
2022-11-03 16:05:07 -07:00
return true ;
2022-11-03 16:27:58 -07:00
}
2022-11-03 16:01:03 -07:00
2020-12-10 11:28:11 -08:00
return false ;
}
#endregion
2019-01-08 11:49:31 -08:00
}
2016-04-19 01:11:23 -07:00
}