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,801 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using System.Collections.Concurrent;
#endif
using System.IO;
using System.Text.RegularExpressions;
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using System.Threading.Tasks;
#endif
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
#pragma warning disable IDE0057 // Use range operator
#pragma warning disable IDE0059 // Unnecessary assignment of a value
namespace SabreTools.Metadata.DatFiles
{
public partial class DatFile
{
#region Constants
/// <summary>
/// Scene name Regex pattern
/// </summary>
private const string SceneNamePattern = @"([0-9]{2}\.[0-9]{2}\.[0-9]{2}-)(.*?-.*?)";
#endregion
#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)
{
ExecuteFiltersImpl(filterRunner);
ExecuteFiltersImplDB(filterRunner);
}
/// <summary>
/// Use game descriptions as names, updating cloneof/romof/sampleof
/// </summary>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
public void MachineDescriptionToName(bool throwOnError = false)
{
MachineDescriptionToNameImpl(throwOnError);
MachineDescriptionToNameImplDB(throwOnError);
}
/// <summary>
/// Ensure that all roms are in their own game (or at least try to ensure)
/// </summary>
public void SetOneRomPerGame()
{
SetOneRomPerGameImpl();
SetOneRomPerGameImplDB();
}
/// <summary>
/// Filter a DAT using 1G1R logic given an ordered set of regions
/// </summary>
/// <param name="regionList">List of regions in order of priority</param>
/// <remarks>
/// In the most technical sense, the way that the region list is being used does not
/// confine its values to be just regions. Since it's essentially acting like a
/// specialized version of the machine name filter, anything that is usually encapsulated
/// in parenthesis would be matched on, including disc numbers, languages, editions,
/// and anything else commonly used. Please note that, unlike other existing 1G1R
/// solutions, this does not have the ability to contain custom mappings of parent
/// to clone sets based on name, nor does it have the ability to match on the
/// Release DatItem type.
/// </remarks>
public void SetOneGamePerRegion(List<string> regionList)
{
SetOneGamePerRegionImpl(regionList);
SetOneGamePerRegionImplDB(regionList);
}
/// <summary>
/// Strip the dates from the beginning of scene-style set names
/// </summary>
public void StripSceneDatesFromItems()
{
StripSceneDatesFromItemsImpl();
StripSceneDatesFromItemsImplDB();
}
#endregion
#region Filtering Implementations
/// <summary>
/// Create machine to description mapping dictionary
/// </summary>
/// <remarks>Applies to <see cref="Items"/></remarks>
private IDictionary<string, string> CreateMachineToDescriptionMapping()
{
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
ConcurrentDictionary<string, string> mapping = new();
#else
Dictionary<string, string> mapping = [];
#endif
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(Items.SortedKeys, key =>
#else
foreach (var key in Items.SortedKeys)
#endif
{
var items = GetItemsForBucket(key);
if (items is null)
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
return;
#else
continue;
#endif
foreach (DatItem item in items)
{
// Get the current machine
var machine = item.GetMachine();
if (machine is null)
continue;
// Get the values to check against
string? machineName = machine.GetName();
string? machineDesc = machine.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey);
if (machineName is null || machineDesc is null)
continue;
// Adjust the description
machineDesc = machineDesc.Replace('/', '_').Replace("\"", "''").Replace(":", " -");
if (machineName == machineDesc)
continue;
// If the key mapping doesn't exist, add it
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
mapping.TryAdd(machineName, machineDesc);
#else
if (!mapping.ContainsKey(machineName))
mapping[machineName] = machineDesc;
#endif
}
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
return mapping;
}
/// <summary>
/// Create machine to description mapping dictionary
/// </summary>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private Dictionary<string, string> CreateMachineToDescriptionMappingDB()
{
Dictionary<string, string> mapping = [];
foreach (var machine in GetMachinesDB())
{
// Get the current machine
if (machine.Value is null)
continue;
// Get the values to check against
string? machineName = machine.Value.GetName();
string? machineDesc = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey);
if (machineName is null || machineDesc is null)
continue;
// Adjust the description
machineDesc = machineDesc.Replace('/', '_').Replace("\"", "''").Replace(":", " -");
if (machineName == machineDesc)
continue;
// If the key mapping doesn't exist, add it
if (!mapping.ContainsKey(machineName))
mapping[machineName] = machineDesc;
}
return mapping;
}
/// <summary>
/// Execute all filters in a filter runner on the items in the dictionary
/// </summary>
/// <param name="filterRunner">Preconfigured filter runner to use</param>
/// <remarks>Applies to <see cref="Items"/></remarks>
private void ExecuteFiltersImpl(FilterRunner filterRunner)
{
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(Items.SortedKeys, key =>
#else
foreach (var key in Items.SortedKeys)
#endif
{
ExecuteFilterOnBucket(filterRunner, key);
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Execute all filters in a filter runner on the items in the dictionary
/// </summary>
/// <param name="filterRunner">Preconfigured filter runner to use</param>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private void ExecuteFiltersImplDB(FilterRunner filterRunner)
{
List<string> keys = [.. ItemsDB.SortedKeys];
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(keys, key =>
#else
foreach (var key in keys)
#endif
{
ExecuteFilterOnBucketDB(filterRunner, key);
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Execute all filters in a filter runner on a single bucket
/// </summary>
/// <param name="filterRunner">Preconfigured filter runner to use</param>
/// <param name="bucketName">Name of the bucket to filter on</param>
/// <remarks>Applies to <see cref="Items"/></remarks>
private void ExecuteFilterOnBucket(FilterRunner filterRunner, string bucketName)
{
List<DatItem>? items = GetItemsForBucket(bucketName);
if (items is null)
return;
// Filter all items in the current key
foreach (var item in items)
{
if (!item.PassesFilter(filterRunner))
item.SetFieldValue<bool?>(DatItem.RemoveKey, true);
}
}
/// <summary>
/// Execute all filters in a filter runner on a single bucket
/// </summary>
/// <param name="filterRunner">Preconfigured filter runner to use</param>
/// <param name="bucketName">Name of the bucket to filter on</param>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private void ExecuteFilterOnBucketDB(FilterRunner filterRunner, string bucketName)
{
var items = GetItemsForBucketDB(bucketName);
if (items is null)
return;
// Filter all items in the current key
List<long> newItems = [];
foreach (var item in items)
{
if (!item.Value.PassesFilterDB(filterRunner))
item.Value.SetFieldValue<bool?>(DatItem.RemoveKey, true);
}
}
/// <summary>
/// Use game descriptions as names, updating cloneof/romof/sampleof
/// </summary>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <remarks>Applies to <see cref="Items"/></remarks>
private void MachineDescriptionToNameImpl(bool throwOnError = false)
{
try
{
// First we want to get a mapping for all games to description
var mapping = CreateMachineToDescriptionMapping();
// Now we loop through every item and update accordingly
UpdateMachineNamesFromDescriptions(mapping);
}
catch (Exception ex) when (!throwOnError)
{
_logger.Warning(ex.ToString());
}
}
/// <summary>
/// Use game descriptions as names, updating cloneof/romof/sampleof
/// </summary>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private void MachineDescriptionToNameImplDB(bool throwOnError = false)
{
try
{
// First we want to get a mapping for all games to description
var mapping = CreateMachineToDescriptionMappingDB();
// Now we loop through every item and update accordingly
UpdateMachineNamesFromDescriptionsDB(mapping);
}
catch (Exception ex) when (!throwOnError)
{
_logger.Warning(ex.ToString());
}
}
/// <summary>
/// Filter a DAT using 1G1R logic given an ordered set of regions
/// </summary>
/// <param name="regionList">List of regions in order of priority</param>
/// <remarks>Applies to <see cref="Items"/></remarks>
private void SetOneGamePerRegionImpl(List<string> regionList)
{
// For sake of ease, the first thing we want to do is bucket by game
BucketBy(ItemKey.Machine, norename: true);
// Then we want to get a mapping of all machines to parents
Dictionary<string, List<string>> parents = [];
foreach (string key in Items.SortedKeys)
{
DatItem item = GetItemsForBucket(key)[0];
// Get machine information
Machine? machine = item.GetMachine();
string? machineName = machine?.GetName()?.ToLowerInvariant();
if (machine is null || machineName is null)
continue;
// Get the string values
string? cloneOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey)?.ToLowerInvariant();
string? romOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)?.ToLowerInvariant();
// Match on CloneOf first
if (!string.IsNullOrEmpty(cloneOf))
{
if (!parents.ContainsKey(cloneOf!))
parents.Add(cloneOf!, []);
parents[cloneOf!].Add(machineName);
}
// Then by RomOf
else if (!string.IsNullOrEmpty(romOf))
{
if (!parents.ContainsKey(romOf!))
parents.Add(romOf!, []);
parents[romOf!].Add(machineName);
}
// Otherwise, treat it as a parent
else
{
if (!parents.ContainsKey(machineName))
parents.Add(machineName, []);
parents[machineName].Add(machineName);
}
}
// Once we have the full list of mappings, filter out games to keep
foreach (string key in parents.Keys)
{
// Find the first machine that matches the regions in order, if possible
string? machine = default;
foreach (string region in regionList)
{
machine = parents[key].Find(m => Regex.IsMatch(m, @"\(.*" + region + @".*\)", RegexOptions.IgnoreCase));
if (machine != default)
break;
}
// If we didn't get a match, use the parent
if (machine == default)
machine = key;
// Remove the key from the list
parents[key].Remove(machine);
// Remove the rest of the items from this key
parents[key].ForEach(k => RemoveBucket(k));
}
// Finally, strip out the parent tags
RemoveMachineRelationshipTagsImpl();
}
/// <summary>
/// Filter a DAT using 1G1R logic given an ordered set of regions
/// </summary>
/// <param name="regionList">List of regions in order of priority</param>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private void SetOneGamePerRegionImplDB(List<string> regionList)
{
// Then we want to get a mapping of all machines to parents
Dictionary<string, List<string>> parents = [];
foreach (var machine in GetMachinesDB())
{
if (machine.Value is null)
continue;
// Get machine information
Machine? machineObj = machine.Value;
string? machineName = machineObj?.GetName()?.ToLowerInvariant();
if (machineObj is null || machineName is null)
continue;
// Get the string values
string? cloneOf = machineObj.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey)?.ToLowerInvariant();
string? romOf = machineObj.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)?.ToLowerInvariant();
// Match on CloneOf first
if (!string.IsNullOrEmpty(cloneOf))
{
if (!parents.ContainsKey(cloneOf!))
parents.Add(cloneOf!, []);
parents[cloneOf!].Add(machineName);
}
// Then by RomOf
else if (!string.IsNullOrEmpty(romOf))
{
if (!parents.ContainsKey(romOf!))
parents.Add(romOf!, []);
parents[romOf!].Add(machineName);
}
// Otherwise, treat it as a parent
else
{
if (!parents.ContainsKey(machineName))
parents.Add(machineName, []);
parents[machineName].Add(machineName);
}
}
// Once we have the full list of mappings, filter out games to keep
foreach (string key in parents.Keys)
{
// Find the first machine that matches the regions in order, if possible
string? machine = default;
foreach (string region in regionList)
{
machine = parents[key].Find(m => Regex.IsMatch(m, @"\(.*" + region + @".*\)", RegexOptions.IgnoreCase));
if (machine != default)
break;
}
// If we didn't get a match, use the parent
if (machine == default)
machine = key;
// Remove the key from the list
parents[key].Remove(machine);
// Remove the rest of the items from this key
parents[key].ForEach(k => RemoveMachineDB(k));
}
// Finally, strip out the parent tags
RemoveMachineRelationshipTagsImplDB();
}
/// <summary>
/// Ensure that all roms are in their own game (or at least try to ensure)
/// </summary>
/// <remarks>Applies to <see cref="Items"/></remarks>
private void SetOneRomPerGameImpl()
{
// For each rom, we want to update the game to be "<game name>/<rom name>"
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(Items.SortedKeys, key =>
#else
foreach (var key in Items.SortedKeys)
#endif
{
var items = GetItemsForBucket(key);
if (items is null)
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
return;
#else
continue;
#endif
for (int i = 0; i < items.Count; i++)
{
SetOneRomPerGameImpl(items[i]);
}
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Set internal names to match One Rom Per Game (ORPG) logic
/// </summary>
/// <param name="datItem">DatItem to run logic on</param>
/// <remarks>Applies to <see cref="Items"/></remarks>
private static void SetOneRomPerGameImpl(DatItem datItem)
{
// If the item name is null
string? itemName = datItem.GetName();
if (itemName is null)
return;
// Get the current machine
var machine = datItem.GetMachine();
if (machine is null)
return;
// Clone current machine to avoid conflict
machine = (Machine)machine.Clone();
// Reassign the item to the new machine
datItem.SetFieldValue(DatItem.MachineKey, machine);
// Remove extensions from File and Rom items
if (datItem is DatItems.Formats.File || datItem is Rom)
{
string[] splitname = itemName.Split('.');
itemName = machine.GetName()
+ $"/{string.Join(".", splitname, 0, splitname.Length > 1 ? splitname.Length - 1 : 1)}";
}
else
{
itemName = machine.GetName() + $"/{itemName}";
}
// Strip off "Default" prefix only for ORPG
if (itemName.StartsWith("Default"))
itemName = itemName.Substring("Default".Length + 1);
machine.SetName(itemName);
datItem.SetName(Path.GetFileName(datItem.GetName()));
}
/// <summary>
/// Ensure that all roms are in their own game (or at least try to ensure)
/// </summary>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private void SetOneRomPerGameImplDB()
{
// For each rom, we want to update the game to be "<game name>/<rom name>"
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(ItemsDB.SortedKeys, key =>
#else
foreach (var key in ItemsDB.SortedKeys)
#endif
{
var items = GetItemsForBucketDB(key);
if (items is null)
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
return;
#else
continue;
#endif
foreach (var item in items)
{
SetOneRomPerGameImplDB(item);
}
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Set internal names to match One Rom Per Game (ORPG) logic
/// </summary>
/// <param name="datItem">DatItem to run logic on</param>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private void SetOneRomPerGameImplDB(KeyValuePair<long, DatItem> datItem)
{
// If the item name is null
string? itemName = datItem.Value.GetName();
if (datItem.Key < 0 || itemName is null)
return;
// Get the current machine
var machine = GetMachineForItemDB(datItem.Key);
if (machine.Value is null)
return;
// Clone current machine to avoid conflict
long newMachineIndex = AddMachineDB((Machine)machine.Value.Clone());
machine = new KeyValuePair<long, Machine?>(newMachineIndex, ItemsDB.GetMachine(newMachineIndex));
if (machine.Value is null)
return;
// Reassign the item to the new machine
ItemsDB.RemapDatItemToMachine(datItem.Key, newMachineIndex);
// Remove extensions from File and Rom items
if (datItem.Value is DatItems.Formats.File || datItem.Value is Rom)
{
string[] splitname = itemName.Split('.');
itemName = machine.Value.GetName()
+ $"/{string.Join(".", splitname, 0, splitname.Length > 1 ? splitname.Length - 1 : 1)}";
}
else
{
itemName = machine.Value.GetName() + $"/{itemName}";
}
// Strip off "Default" prefix only for ORPG
if (itemName.StartsWith("Default"))
itemName = itemName.Substring("Default".Length + 1);
machine.Value.SetName(itemName);
datItem.Value.SetName(Path.GetFileName(datItem.Value.GetName()));
}
/// <summary>
/// Strip the dates from the beginning of scene-style set names
/// </summary>
/// <remarks>Applies to <see cref="Items"/></remarks>
private void StripSceneDatesFromItemsImpl()
{
// Now process all of the roms
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(Items.SortedKeys, key =>
#else
foreach (var key in Items.SortedKeys)
#endif
{
var items = GetItemsForBucket(key);
if (items is null)
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
return;
#else
continue;
#endif
foreach (DatItem item in items)
{
// Get the current machine
var machine = item.GetMachine();
if (machine is null)
continue;
// Get the values to check against
string? machineName = machine.GetName();
string? machineDesc = machine.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey);
if (machineName is not null && Regex.IsMatch(machineName, SceneNamePattern))
item.GetMachine()!.SetName(Regex.Replace(machineName, SceneNamePattern, "$2"));
if (machineDesc is not null && Regex.IsMatch(machineDesc, SceneNamePattern))
item.GetMachine()!.SetFieldValue<string?>(Data.Models.Metadata.Machine.DescriptionKey, Regex.Replace(machineDesc, SceneNamePattern, "$2"));
}
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Strip the dates from the beginning of scene-style set names
/// </summary>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private void StripSceneDatesFromItemsImplDB()
{
// Now process all of the machines
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(GetMachinesDB(), machine =>
#else
foreach (var machine in GetMachinesDB())
#endif
{
// Get the current machine
if (machine.Value is null)
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
return;
#else
continue;
#endif
// Get the values to check against
string? machineName = machine.Value.GetName();
string? machineDesc = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey);
if (machineName is not null && Regex.IsMatch(machineName, SceneNamePattern))
machine.Value.SetName(Regex.Replace(machineName, SceneNamePattern, "$2"));
if (machineDesc is not null && Regex.IsMatch(machineDesc, SceneNamePattern))
machine.Value.SetFieldValue<string?>(Data.Models.Metadata.Machine.DescriptionKey, Regex.Replace(machineDesc, SceneNamePattern, "$2"));
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Update machine names from descriptions according to mappings
/// </summary>
/// <remarks>Applies to <see cref="Items"/></remarks>
private void UpdateMachineNamesFromDescriptions(IDictionary<string, string> mapping)
{
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(Items.SortedKeys, key =>
#else
foreach (var key in Items.SortedKeys)
#endif
{
var items = GetItemsForBucket(key);
if (items is null)
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
return;
#else
continue;
#endif
foreach (DatItem item in items)
{
// Get the current machine
var machine = item.GetMachine();
if (machine is null)
continue;
// Get the values to check against
string? machineName = machine.GetName();
string? cloneOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey);
string? romOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey);
string? sampleOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.SampleOfKey);
// Update machine name
if (machineName is not null && mapping.ContainsKey(machineName))
machine.SetName(mapping[machineName]);
// Update cloneof
if (cloneOf is not null && mapping.ContainsKey(cloneOf))
machine.SetFieldValue<string?>(Data.Models.Metadata.Machine.CloneOfKey, mapping[cloneOf]);
// Update romof
if (romOf is not null && mapping.ContainsKey(romOf))
machine.SetFieldValue<string?>(Data.Models.Metadata.Machine.RomOfKey, mapping[romOf]);
// Update sampleof
if (sampleOf is not null && mapping.ContainsKey(sampleOf))
machine.SetFieldValue<string?>(Data.Models.Metadata.Machine.SampleOfKey, mapping[sampleOf]);
}
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Update machine names from descriptions according to mappings
/// </summary>
/// <remarks>Applies to <see cref="ItemsDB"/></remarks>
private void UpdateMachineNamesFromDescriptionsDB(Dictionary<string, string> mapping)
{
foreach (var machine in GetMachinesDB())
{
// Get the current machine
if (machine.Value is null)
continue;
// Get the values to check against
string? machineName = machine.Value.GetName();
string? cloneOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey);
string? romOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey);
string? sampleOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.SampleOfKey);
// Update machine name
if (machineName is not null && mapping.ContainsKey(machineName))
machine.Value.SetName(mapping[machineName]);
// Update cloneof
if (cloneOf is not null && mapping.ContainsKey(cloneOf))
machine.Value.SetFieldValue<string?>(Data.Models.Metadata.Machine.CloneOfKey, mapping[cloneOf]);
// Update romof
if (romOf is not null && mapping.ContainsKey(romOf))
machine.Value.SetFieldValue<string?>(Data.Models.Metadata.Machine.RomOfKey, mapping[romOf]);
// Update sampleof
if (sampleOf is not null && mapping.ContainsKey(sampleOf))
machine.Value.SetFieldValue<string?>(Data.Models.Metadata.Machine.SampleOfKey, mapping[sampleOf]);
}
}
#endregion
}
}

View File

@@ -0,0 +1,715 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using System.Threading.Tasks;
#endif
using SabreTools.Data.Extensions;
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
#pragma warning disable IDE0056 // Use index operator
#pragma warning disable IDE0060 // Remove unused parameter
namespace SabreTools.Metadata.DatFiles
{
public partial class DatFile
{
#region From Metadata
/// <summary>
/// Convert metadata information
/// </summary>
/// <param name="item">Metadata file to convert</param>
/// <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</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
internal void ConvertFromMetadata(Data.Models.Metadata.MetadataFile? item,
string filename,
int indexId,
bool keep,
bool statsOnly,
FilterRunner? filterRunner)
{
// If the metadata file is invalid, we can't do anything
if (item is null || item.Count == 0)
return;
// Create an internal source and add to the dictionary
var source = new Source(indexId, filename);
// long sourceIndex = AddSourceDB(source);
// Get the header from the metadata
var header = item.Read<Data.Models.Metadata.Header>(Data.Models.Metadata.MetadataFile.HeaderKey);
if (header is not null)
ConvertHeader(header, keep);
// Get the machines from the metadata
var machines = item.ReadItemArray<Data.Models.Metadata.Machine>(Data.Models.Metadata.MetadataFile.MachineKey);
if (machines is not null)
ConvertMachines(machines, source, sourceIndex: 0, statsOnly, filterRunner);
}
/// <summary>
/// Convert header information
/// </summary>
/// <param name="item">Header to convert</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise</param>
private void ConvertHeader(Data.Models.Metadata.Header? item, bool keep)
{
// If the header is invalid, we can't do anything
if (item is null || item.Count == 0)
return;
// Create an internal header
var header = new DatHeader(item);
// Convert subheader values
if (item.ContainsKey(Data.Models.Metadata.Header.CanOpenKey))
{
var canOpen = item.Read<Data.Models.OfflineList.CanOpen>(Data.Models.Metadata.Header.CanOpenKey);
if (canOpen?.Extension is not null)
Header.SetFieldValue<string[]?>(Data.Models.Metadata.Header.CanOpenKey, canOpen.Extension);
}
if (item.ContainsKey(Data.Models.Metadata.Header.ImagesKey))
{
var images = item.Read<Data.Models.OfflineList.Images>(Data.Models.Metadata.Header.ImagesKey);
Header.SetFieldValue<Data.Models.OfflineList.Images?>(Data.Models.Metadata.Header.ImagesKey, images);
}
if (item.ContainsKey(Data.Models.Metadata.Header.InfosKey))
{
var infos = item.Read<Data.Models.OfflineList.Infos>(Data.Models.Metadata.Header.InfosKey);
Header.SetFieldValue<Data.Models.OfflineList.Infos?>(Data.Models.Metadata.Header.InfosKey, infos);
}
if (item.ContainsKey(Data.Models.Metadata.Header.NewDatKey))
{
var newDat = item.Read<Data.Models.OfflineList.NewDat>(Data.Models.Metadata.Header.NewDatKey);
Header.SetFieldValue<Data.Models.OfflineList.NewDat?>(Data.Models.Metadata.Header.NewDatKey, newDat);
}
if (item.ContainsKey(Data.Models.Metadata.Header.SearchKey))
{
var search = item.Read<Data.Models.OfflineList.Search>(Data.Models.Metadata.Header.SearchKey);
Header.SetFieldValue<Data.Models.OfflineList.Search?>(Data.Models.Metadata.Header.SearchKey, search);
}
// Selectively set all possible fields -- TODO: Figure out how to make this less manual
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.AuthorKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.AuthorKey, header.GetStringFieldValue(Data.Models.Metadata.Header.AuthorKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.BiosModeKey).AsMergingFlag() == MergingFlag.None)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.BiosModeKey, header.GetStringFieldValue(Data.Models.Metadata.Header.BiosModeKey).AsMergingFlag().AsStringValue());
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.BuildKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.BuildKey, header.GetStringFieldValue(Data.Models.Metadata.Header.BuildKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.CategoryKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.CategoryKey, header.GetStringFieldValue(Data.Models.Metadata.Header.CategoryKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.CommentKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.CommentKey, header.GetStringFieldValue(Data.Models.Metadata.Header.CommentKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.DateKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.DateKey, header.GetStringFieldValue(Data.Models.Metadata.Header.DateKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.DatVersionKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.DatVersionKey, header.GetStringFieldValue(Data.Models.Metadata.Header.DatVersionKey));
if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.DebugKey) is null)
Header.SetFieldValue(Data.Models.Metadata.Header.DebugKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.DebugKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.DescriptionKey, header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.EmailKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.EmailKey, header.GetStringFieldValue(Data.Models.Metadata.Header.EmailKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.EmulatorVersionKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.EmulatorVersionKey, header.GetStringFieldValue(Data.Models.Metadata.Header.EmulatorVersionKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ForceMergingKey).AsMergingFlag() == MergingFlag.None)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.ForceMergingKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ForceMergingKey).AsMergingFlag().AsStringValue());
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ForceNodumpKey).AsNodumpFlag() == NodumpFlag.None)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.ForceNodumpKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ForceNodumpKey).AsNodumpFlag().AsStringValue());
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ForcePackingKey).AsPackingFlag() == PackingFlag.None)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.ForcePackingKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ForcePackingKey).AsPackingFlag().AsStringValue());
if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.ForceZippingKey) is null)
Header.SetFieldValue(Data.Models.Metadata.Header.ForceZippingKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.ForceZippingKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.HeaderKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.HeaderKey, header.GetStringFieldValue(Data.Models.Metadata.Header.HeaderKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.HomepageKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.HomepageKey, header.GetStringFieldValue(Data.Models.Metadata.Header.HomepageKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.IdKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.IdKey, header.GetStringFieldValue(Data.Models.Metadata.Header.IdKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ImFolderKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.ImFolderKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ImFolderKey));
if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.LockBiosModeKey) is null)
Header.SetFieldValue(Data.Models.Metadata.Header.LockBiosModeKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.LockBiosModeKey));
if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.LockRomModeKey) is null)
Header.SetFieldValue(Data.Models.Metadata.Header.LockRomModeKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.LockRomModeKey));
if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.LockSampleModeKey) is null)
Header.SetFieldValue(Data.Models.Metadata.Header.LockSampleModeKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.LockSampleModeKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.MameConfigKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.MameConfigKey, header.GetStringFieldValue(Data.Models.Metadata.Header.MameConfigKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.NameKey, header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.NotesKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.NotesKey, header.GetStringFieldValue(Data.Models.Metadata.Header.NotesKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.PluginKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.PluginKey, header.GetStringFieldValue(Data.Models.Metadata.Header.PluginKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.RefNameKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.RefNameKey, header.GetStringFieldValue(Data.Models.Metadata.Header.RefNameKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.RomModeKey).AsMergingFlag() == MergingFlag.None)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.RomModeKey, header.GetStringFieldValue(Data.Models.Metadata.Header.RomModeKey).AsMergingFlag().AsStringValue());
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.RomTitleKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.RomTitleKey, header.GetStringFieldValue(Data.Models.Metadata.Header.RomTitleKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.RootDirKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.RootDirKey, header.GetStringFieldValue(Data.Models.Metadata.Header.RootDirKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.SampleModeKey).AsMergingFlag() == MergingFlag.None)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.SampleModeKey, header.GetStringFieldValue(Data.Models.Metadata.Header.SampleModeKey).AsMergingFlag().AsStringValue());
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.SchemaLocationKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.SchemaLocationKey, header.GetStringFieldValue(Data.Models.Metadata.Header.SchemaLocationKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsHeightKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.ScreenshotsHeightKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsHeightKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsWidthKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.ScreenshotsWidthKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsWidthKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.SystemKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.SystemKey, header.GetStringFieldValue(Data.Models.Metadata.Header.SystemKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.TimestampKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.TimestampKey, header.GetStringFieldValue(Data.Models.Metadata.Header.TimestampKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.TypeKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.TypeKey, header.GetStringFieldValue(Data.Models.Metadata.Header.TypeKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.UrlKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.UrlKey, header.GetStringFieldValue(Data.Models.Metadata.Header.UrlKey));
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.VersionKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.VersionKey, header.GetStringFieldValue(Data.Models.Metadata.Header.VersionKey));
// Handle implied SuperDAT
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)?.Contains(" - SuperDAT") == true && keep)
{
if (Header.GetStringFieldValue(Data.Models.Metadata.Header.TypeKey) is null)
Header.SetFieldValue<string?>(Data.Models.Metadata.Header.TypeKey, "SuperDAT");
}
}
/// <summary>
/// Convert machines information
/// </summary>
/// <param name="items">Machine array to convert</param>
/// <param name="source">Source to use with the converted items</param>
/// <param name="sourceIndex">Index of the Source to use with the converted items</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ConvertMachines(Data.Models.Metadata.Machine[]? items,
Source source,
long sourceIndex,
bool statsOnly,
FilterRunner? filterRunner)
{
// If the array is invalid, we can't do anything
if (items is null || items.Length == 0)
return;
// Loop through the machines and add
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(items, machine =>
#else
foreach (var machine in items)
#endif
{
ConvertMachine(machine, source, sourceIndex, statsOnly, filterRunner);
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Convert machine information
/// </summary>
/// <param name="item">Machine to convert</param>
/// <param name="source">Source to use with the converted items</param>
/// <param name="sourceIndex">Index of the Source to use with the converted items</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ConvertMachine(Data.Models.Metadata.Machine? item,
Source source,
long sourceIndex,
bool statsOnly,
FilterRunner? filterRunner)
{
// If the machine is invalid, we can't do anything
if (item is null || item.Count == 0)
return;
// If the machine doesn't pass the filter
if (filterRunner is not null && !filterRunner.Run(item))
return;
// Create an internal machine and add to the dictionary
var machine = new Machine(item);
// long machineIndex = AddMachineDB(machine);
// Convert items in the machine
if (item.ContainsKey(Data.Models.Metadata.Machine.AdjusterKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Adjuster>(Data.Models.Metadata.Machine.AdjusterKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Adjuster(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.ArchiveKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Archive>(Data.Models.Metadata.Machine.ArchiveKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Archive(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.BiosSetKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.BiosSet>(Data.Models.Metadata.Machine.BiosSetKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new BiosSet(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.ChipKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Chip>(Data.Models.Metadata.Machine.ChipKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Chip(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.ConfigurationKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Configuration>(Data.Models.Metadata.Machine.ConfigurationKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Configuration(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.DeviceKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Device>(Data.Models.Metadata.Machine.DeviceKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Device(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.DeviceRefKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.DeviceRef>(Data.Models.Metadata.Machine.DeviceRefKey) ?? [];
// Do not filter these due to later use
Array.ForEach(items, item =>
{
var datItem = new DeviceRef(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.DipSwitchKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.DipSwitch>(Data.Models.Metadata.Machine.DipSwitchKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new DipSwitch(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.DiskKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Disk>(Data.Models.Metadata.Machine.DiskKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Disk(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.DisplayKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Display>(Data.Models.Metadata.Machine.DisplayKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Display(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.DriverKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Driver>(Data.Models.Metadata.Machine.DriverKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Driver(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.DumpKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Dump>(Data.Models.Metadata.Machine.DumpKey) ?? [];
for (int i = 0; i < items.Length; i++)
{
var datItem = new Rom(items[i], machine, source, i);
if (datItem.GetName() is not null)
{
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
}
}
}
if (item.ContainsKey(Data.Models.Metadata.Machine.FeatureKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Feature>(Data.Models.Metadata.Machine.FeatureKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Feature(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.InfoKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Info>(Data.Models.Metadata.Machine.InfoKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Info(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.InputKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Input>(Data.Models.Metadata.Machine.InputKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Input(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.MediaKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Media>(Data.Models.Metadata.Machine.MediaKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Media(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.PartKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Part>(Data.Models.Metadata.Machine.PartKey) ?? [];
ProcessItems(items, machine, machineIndex: 0, source, sourceIndex, statsOnly, filterRunner);
}
if (item.ContainsKey(Data.Models.Metadata.Machine.PortKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Port>(Data.Models.Metadata.Machine.PortKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Port(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.RamOptionKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.RamOption>(Data.Models.Metadata.Machine.RamOptionKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new RamOption(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.ReleaseKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Release>(Data.Models.Metadata.Machine.ReleaseKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Release(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.RomKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Rom>(Data.Models.Metadata.Machine.RomKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Rom(item, machine, source);
datItem.SetFieldValue<Source?>(DatItem.SourceKey, source);
datItem.CopyMachineInformation(machine);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.SampleKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Sample>(Data.Models.Metadata.Machine.SampleKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Sample(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.SharedFeatKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.SharedFeat>(Data.Models.Metadata.Machine.SharedFeatKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new SharedFeat(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.SlotKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Slot>(Data.Models.Metadata.Machine.SlotKey) ?? [];
// Do not filter these due to later use
Array.ForEach(items, item =>
{
var datItem = new Slot(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.SoftwareListKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.SoftwareList>(Data.Models.Metadata.Machine.SoftwareListKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new SoftwareList(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.SoundKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Sound>(Data.Models.Metadata.Machine.SoundKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Sound(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
if (item.ContainsKey(Data.Models.Metadata.Machine.VideoKey))
{
var items = item.ReadItemArray<Data.Models.Metadata.Video>(Data.Models.Metadata.Machine.VideoKey) ?? [];
var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item));
Array.ForEach(filtered, item =>
{
var datItem = new Display(item, machine, source);
AddItem(datItem, statsOnly);
// AddItemDB(datItem, machineIndex, sourceIndex, statsOnly);
});
}
}
/// <summary>
/// Convert Part information
/// </summary>
/// <param name="items">Array of internal items to convert</param>
/// <param name="machine">Machine to use with the converted items</param>
/// <param name="machineIndex">Index of the Machine to use with the converted items</param>
/// <param name="source">Source to use with the converted items</param>
/// <param name="sourceIndex">Index of the Source to use with the converted items</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="filterRunner">Optional FilterRunner to filter items on parse</param>
private void ProcessItems(Data.Models.Metadata.Part[] items,
Machine machine,
long machineIndex,
Source source,
long sourceIndex,
bool statsOnly,
FilterRunner? filterRunner)
{
// If the array is null or empty, return without processing
if (items.Length == 0)
return;
// Loop through the items and add
foreach (var item in items)
{
var partItem = new Part(item, machine, source);
// Handle subitems
var dataAreas = item.ReadItemArray<Data.Models.Metadata.DataArea>(Data.Models.Metadata.Part.DataAreaKey);
if (dataAreas is not null)
{
foreach (var dataArea in dataAreas)
{
var dataAreaItem = new DataArea(dataArea, machine, source);
var roms = dataArea.ReadItemArray<Data.Models.Metadata.Rom>(Data.Models.Metadata.DataArea.RomKey);
if (roms is null)
continue;
// Handle "offset" roms
List<Rom> addRoms = [];
foreach (var rom in roms)
{
// If the item doesn't pass the filter
if (filterRunner is not null && !filterRunner.Run(rom))
continue;
// Convert the item
var romItem = new Rom(rom, machine, source);
long? size = romItem.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey);
// If the rom is a continue or ignore
string? loadFlag = rom.ReadString(Data.Models.Metadata.Rom.LoadFlagKey);
if (loadFlag is not null
&& (loadFlag.Equals("continue", StringComparison.OrdinalIgnoreCase)
|| loadFlag.Equals("ignore", StringComparison.OrdinalIgnoreCase)))
{
var lastRom = addRoms[addRoms.Count - 1];
long? lastSize = lastRom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey);
lastRom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, lastSize + size);
continue;
}
romItem.SetFieldValue<DataArea?>(Rom.DataAreaKey, dataAreaItem);
romItem.SetFieldValue<Part?>(Rom.PartKey, partItem);
addRoms.Add(romItem);
}
// Add all of the adjusted roms
foreach (var romItem in addRoms)
{
AddItem(romItem, statsOnly);
// AddItemDB(romItem, machineIndex, sourceIndex, statsOnly);
}
}
}
var diskAreas = item.ReadItemArray<Data.Models.Metadata.DiskArea>(Data.Models.Metadata.Part.DiskAreaKey);
if (diskAreas is not null)
{
foreach (var diskArea in diskAreas)
{
var diskAreaitem = new DiskArea(diskArea, machine, source);
var disks = diskArea.ReadItemArray<Data.Models.Metadata.Disk>(Data.Models.Metadata.DiskArea.DiskKey);
if (disks is null)
continue;
foreach (var disk in disks)
{
// If the item doesn't pass the filter
if (filterRunner is not null && !filterRunner.Run(disk))
continue;
var diskItem = new Disk(disk, machine, source);
diskItem.SetFieldValue<DiskArea?>(Disk.DiskAreaKey, diskAreaitem);
diskItem.SetFieldValue<Part?>(Disk.PartKey, partItem);
AddItem(diskItem, statsOnly);
// AddItemDB(diskItem, machineIndex, sourceIndex, statsOnly);
}
}
}
var dipSwitches = item.ReadItemArray<Data.Models.Metadata.DipSwitch>(Data.Models.Metadata.Part.DipSwitchKey);
if (dipSwitches is not null)
{
foreach (var dipSwitch in dipSwitches)
{
// If the item doesn't pass the filter
if (filterRunner is not null && !filterRunner.Run(dipSwitch))
continue;
var dipSwitchItem = new DipSwitch(dipSwitch, machine, source);
dipSwitchItem.SetFieldValue<Part?>(DipSwitch.PartKey, partItem);
AddItem(dipSwitchItem, statsOnly);
// AddItemDB(dipSwitchItem, machineIndex, sourceIndex, statsOnly);
}
}
var partFeatures = item.ReadItemArray<Data.Models.Metadata.Feature>(Data.Models.Metadata.Part.FeatureKey);
if (partFeatures is not null)
{
foreach (var partFeature in partFeatures)
{
// If the item doesn't pass the filter
if (filterRunner is not null && !filterRunner.Run(partFeature))
continue;
var partFeatureItem = new PartFeature(partFeature);
partFeatureItem.SetFieldValue<Part?>(DipSwitch.PartKey, partItem);
partFeatureItem.SetFieldValue<Source?>(DatItem.SourceKey, source);
partFeatureItem.CopyMachineInformation(machine);
AddItem(partFeatureItem, statsOnly);
// AddItemDB(partFeatureItem, machineIndex, sourceIndex, statsOnly);
}
}
}
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,229 @@
using System;
using System.Xml.Serialization;
using Newtonsoft.Json;
using SabreTools.Metadata.Filter;
using SabreTools.Metadata.Tools;
namespace SabreTools.Metadata.DatFiles
{
/// <summary>
/// Represents all possible DAT header information
/// </summary>
[JsonObject("header"), XmlRoot("header")]
public sealed class DatHeader : ModelBackedItem<Data.Models.Metadata.Header>, ICloneable
{
#region Constants
/// <summary>
/// Read or write format
/// </summary>
public const string DatFormatKey = "DATFORMAT";
/// <summary>
/// External name of the DAT
/// </summary>
public const string FileNameKey = "FILENAME";
#endregion
#region Fields
[JsonIgnore]
public bool CanOpenSpecified
{
get
{
var canOpen = GetStringArrayFieldValue(Data.Models.Metadata.Header.CanOpenKey);
return canOpen is not null && canOpen.Length > 0;
}
}
[JsonIgnore]
public bool ImagesSpecified
{
get
{
return GetFieldValue<Data.Models.OfflineList.Images?>(Data.Models.Metadata.Header.ImagesKey) is not null;
}
}
[JsonIgnore]
public bool InfosSpecified
{
get
{
return GetFieldValue<Data.Models.OfflineList.Infos?>(Data.Models.Metadata.Header.InfosKey) is not null;
}
}
[JsonIgnore]
public bool NewDatSpecified
{
get
{
return GetFieldValue<Data.Models.OfflineList.NewDat?>(Data.Models.Metadata.Header.NewDatKey) is not null;
}
}
[JsonIgnore]
public bool SearchSpecified
{
get
{
return GetFieldValue<Data.Models.OfflineList.Search?>(Data.Models.Metadata.Header.SearchKey) is not null;
}
}
#endregion
#region Constructors
public DatHeader() { }
public DatHeader(Data.Models.Metadata.Header header)
{
// Create a new internal model
_internal = [];
// Get all fields to automatically copy without processing
var nonItemFields = TypeHelper.GetConstants(typeof(Data.Models.Metadata.Header));
if (nonItemFields is not null)
{
// Populate the internal machine from non-filter fields
foreach (string fieldName in nonItemFields)
{
if (header.ContainsKey(fieldName))
_internal[fieldName] = header[fieldName];
}
}
// Get all fields specific to the DatFiles implementation
var nonStandardFields = TypeHelper.GetConstants(typeof(DatHeader));
if (nonStandardFields is not null)
{
// Populate the internal machine from filter fields
foreach (string fieldName in nonStandardFields)
{
if (header.ContainsKey(fieldName))
_internal[fieldName] = header[fieldName];
}
}
// Get all no-filter fields
if (header.ContainsKey(Data.Models.Metadata.Header.CanOpenKey))
_internal[Data.Models.Metadata.Header.CanOpenKey] = header[Data.Models.Metadata.Header.CanOpenKey];
if (header.ContainsKey(Data.Models.Metadata.Header.ImagesKey))
_internal[Data.Models.Metadata.Header.ImagesKey] = header[Data.Models.Metadata.Header.ImagesKey];
if (header.ContainsKey(Data.Models.Metadata.Header.InfosKey))
_internal[Data.Models.Metadata.Header.InfosKey] = header[Data.Models.Metadata.Header.InfosKey];
if (header.ContainsKey(Data.Models.Metadata.Header.NewDatKey))
_internal[Data.Models.Metadata.Header.NewDatKey] = header[Data.Models.Metadata.Header.NewDatKey];
if (header.ContainsKey(Data.Models.Metadata.Header.SearchKey))
_internal[Data.Models.Metadata.Header.SearchKey] = header[Data.Models.Metadata.Header.SearchKey];
}
#endregion
#region Cloning Methods
/// <summary>
/// Clone the current header
/// </summary>
public object Clone() => new DatHeader(GetInternalClone());
/// <summary>
/// Clone just the format from the current header
/// </summary>
public DatHeader CloneFormat()
{
var header = new DatHeader();
header.SetFieldValue(DatFormatKey, GetFieldValue<DatFormat>(DatFormatKey));
return header;
}
/// <summary>
/// Get a clone of the current internal model
/// </summary>
public Data.Models.Metadata.Header GetInternalClone()
{
var header = (_internal.Clone() as Data.Models.Metadata.Header)!;
// Remove fields with default values
if (header.ReadString(Data.Models.Metadata.Header.ForceMergingKey).AsMergingFlag() == MergingFlag.None)
header.Remove(Data.Models.Metadata.Header.ForceMergingKey);
if (header.ReadString(Data.Models.Metadata.Header.ForceNodumpKey).AsNodumpFlag() == NodumpFlag.None)
header.Remove(Data.Models.Metadata.Header.ForceNodumpKey);
if (header.ReadString(Data.Models.Metadata.Header.ForcePackingKey).AsPackingFlag() == PackingFlag.None)
header.Remove(Data.Models.Metadata.Header.ForcePackingKey);
if (header.ReadString(Data.Models.Metadata.Header.BiosModeKey).AsMergingFlag() == MergingFlag.None)
header.Remove(Data.Models.Metadata.Header.BiosModeKey);
if (header.ReadString(Data.Models.Metadata.Header.RomModeKey).AsMergingFlag() == MergingFlag.None)
header.Remove(Data.Models.Metadata.Header.RomModeKey);
if (header.ReadString(Data.Models.Metadata.Header.SampleModeKey).AsMergingFlag() == MergingFlag.None)
header.Remove(Data.Models.Metadata.Header.SampleModeKey);
// Convert subheader values
if (CanOpenSpecified)
header[Data.Models.Metadata.Header.CanOpenKey] = new Data.Models.OfflineList.CanOpen { Extension = GetStringArrayFieldValue(Data.Models.Metadata.Header.CanOpenKey) };
if (ImagesSpecified)
header[Data.Models.Metadata.Header.ImagesKey] = GetFieldValue<Data.Models.OfflineList.Images>(Data.Models.Metadata.Header.ImagesKey);
if (InfosSpecified)
header[Data.Models.Metadata.Header.InfosKey] = GetFieldValue<Data.Models.OfflineList.Infos>(Data.Models.Metadata.Header.InfosKey);
if (NewDatSpecified)
header[Data.Models.Metadata.Header.NewDatKey] = GetFieldValue<Data.Models.OfflineList.NewDat>(Data.Models.Metadata.Header.NewDatKey);
if (SearchSpecified)
header[Data.Models.Metadata.Header.SearchKey] = GetFieldValue<Data.Models.OfflineList.Search>(Data.Models.Metadata.Header.SearchKey);
return header;
}
#endregion
#region Comparision Methods
/// <inheritdoc/>
public override bool Equals(ModelBackedItem? other)
{
// If other is null
if (other is null)
return false;
// If the type is mismatched
if (other is not DatHeader otherItem)
return false;
// Compare internal models
return _internal.EqualTo(otherItem._internal);
}
/// <inheritdoc/>
public override bool Equals(ModelBackedItem<Data.Models.Metadata.Header>? other)
{
// If other is null
if (other is null)
return false;
// If the type is mismatched
if (other is not DatHeader otherItem)
return false;
// Compare internal models
return _internal.EqualTo(otherItem._internal);
}
#endregion
#region Manipulation
/// <summary>
/// Runs a filter and determines if it passes or not
/// </summary>
/// <param name="filterRunner">Filter runner to use for checking</param>
/// <returns>True if the Machine passes the filter, false otherwise</returns>
public bool PassesFilter(FilterRunner filterRunner) => filterRunner.Run(_internal);
#endregion
}
}

View File

@@ -0,0 +1,88 @@
using System;
namespace SabreTools.Metadata.DatFiles
{
/// <summary>
/// Represents various modifiers that can be applied to a DAT
/// </summary>
public sealed class DatModifiers : ICloneable
{
#region Fields
/// <summary>
/// Text to prepend to all outputted lines
/// </summary>
public string? Prefix { get; set; } = null;
/// <summary>
/// Text to append to all outputted lines
/// </summary>
public string? Postfix { get; set; } = null;
/// <summary>
/// Add a new extension to all items
/// </summary>
public string? AddExtension { get; set; } = null;
/// <summary>
/// Remove all item extensions
/// </summary>
public bool RemoveExtension { get; set; } = false;
/// <summary>
/// Replace all item extensions
/// </summary>
public string? ReplaceExtension { get; set; } = null;
/// <summary>
/// Output the machine name before the item name
/// </summary>
public bool GameName { get; set; } = false;
/// <summary>
/// Wrap quotes around the entire line, sans prefix and postfix
/// </summary>
public bool Quotes { get; set; } = false;
/// <summary>
/// Use the item name instead of machine name on output
/// </summary>
public bool UseRomName { get; set; } = false;
/// <summary>
/// Input depot information
/// </summary>
public DepotInformation? InputDepot { get; set; } = null;
/// <summary>
/// Output depot information
/// </summary>
public DepotInformation? OutputDepot { get; set; } = null;
#endregion
#region Cloning Methods
/// <summary>
/// Clone the current modifiers
/// </summary>
public object Clone()
{
return new DatModifiers
{
Prefix = this.Prefix,
Postfix = this.Postfix,
AddExtension = this.AddExtension,
RemoveExtension = this.RemoveExtension,
ReplaceExtension = this.ReplaceExtension,
GameName = this.GameName,
Quotes = this.Quotes,
UseRomName = this.UseRomName,
InputDepot = (DepotInformation?)this.InputDepot?.Clone(),
OutputDepot = (DepotInformation?)this.OutputDepot?.Clone(),
};
}
#endregion
}
}

View File

@@ -0,0 +1,725 @@
using System.Collections.Generic;
using SabreTools.Hashing;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
namespace SabreTools.Metadata.DatFiles
{
/// <summary>
/// Statistics wrapper for outputting
/// </summary>
public class DatStatistics
{
#region Private instance variables
/// <summary>
/// Number of items for each hash type
/// </summary>
private readonly Dictionary<HashType, long> _hashCounts = [];
/// <summary>
/// Number of items for each item type
/// </summary>
private readonly Dictionary<ItemType, long> _itemCounts = [];
/// <summary>
/// Number of items for each item status
/// </summary>
private readonly Dictionary<ItemStatus, long> _statusCounts = [];
/// <summary>
/// Lock for statistics calculation
/// </summary>
private readonly object statsLock = new();
#endregion
#region Fields
/// <summary>
/// Overall item count
/// </summary>
public long TotalCount { get; private set; } = 0;
/// <summary>
/// Number of machines
/// </summary>
/// <remarks>Special count only used by statistics output</remarks>
public long GameCount { get; set; } = 0;
/// <summary>
/// Total uncompressed size
/// </summary>
public long TotalSize { get; private set; } = 0;
/// <summary>
/// Number of items with the remove flag
/// </summary>
public long RemovedCount { get; private set; } = 0;
/// <summary>
/// Name to display on output
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// Total machine count to use on output
/// </summary>
public long MachineCount { get; set; }
/// <summary>
/// Determines if statistics are for a directory or not
/// </summary>
public readonly bool IsDirectory;
#endregion
#region Constructors
/// <summary>
/// Default constructor
/// </summary>
public DatStatistics()
{
DisplayName = null;
MachineCount = 0;
IsDirectory = false;
}
/// <summary>
/// Constructor for aggregate data
/// </summary>
public DatStatistics(string? displayName, bool isDirectory)
{
DisplayName = displayName;
MachineCount = 0;
IsDirectory = isDirectory;
}
#endregion
#region Accessors
/// <summary>
/// Add to the statistics for a given DatItem
/// </summary>
/// <param name="item">Item to add info from</param>
public void AddItemStatistics(DatItem item)
{
lock (statsLock)
{
// No matter what the item is, we increment the count
TotalCount++;
// Increment removal count
if (item.GetBoolFieldValue(DatItem.RemoveKey) == true)
RemovedCount++;
// Increment the item count for the type
AddItemCount(item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType());
// Some item types require special processing
switch (item)
{
case Disk disk:
AddItemStatistics(disk);
break;
case File file:
AddItemStatistics(file);
break;
case Media media:
AddItemStatistics(media);
break;
case Rom rom:
AddItemStatistics(rom);
break;
default:
break;
}
}
}
/// <summary>
/// Add to the statistics for a given DatItem
/// </summary>
/// <param name="item">Item to add info from</param>
public void AddItemStatistics(Data.Models.Metadata.DatItem item)
{
lock (statsLock)
{
// No matter what the item is, we increment the count
TotalCount++;
// Increment removal count
if (item.ReadBool(DatItem.RemoveKey) == true)
RemovedCount++;
// Increment the item count for the type
AddItemCount(item.ReadString(Data.Models.Metadata.DatItem.TypeKey).AsItemType());
#pragma warning disable IDE0010
// Some item types require special processing
switch (item)
{
case Data.Models.Metadata.Disk disk:
AddItemStatistics(disk);
break;
case Data.Models.Metadata.Media media:
AddItemStatistics(media);
break;
case Data.Models.Metadata.Rom rom:
AddItemStatistics(rom);
break;
}
#pragma warning restore IDE0010
}
}
/// <summary>
/// Add statistics from another DatStatistics object
/// </summary>
/// <param name="stats">DatStatistics object to add from</param>
public void AddStatistics(DatStatistics stats)
{
TotalCount += stats.TotalCount;
// Loop through and add stats for all items
foreach (var itemCountKvp in stats._itemCounts)
{
AddItemCount(itemCountKvp.Key, itemCountKvp.Value);
}
GameCount += stats.GameCount;
TotalSize += stats.TotalSize;
// Individual hash counts
foreach (var hashCountKvp in stats._hashCounts)
{
AddHashCount(hashCountKvp.Key, hashCountKvp.Value);
}
// Individual status counts
foreach (var statusCountKvp in stats._statusCounts)
{
AddStatusCount(statusCountKvp.Key, statusCountKvp.Value);
}
RemovedCount += stats.RemovedCount;
}
/// <summary>
/// Get the item count for a given hash type, defaulting to 0 if it does not exist
/// </summary>
/// <param name="hashType">Hash type to retrieve</param>
/// <returns>The number of items with that hash, if it exists</returns>
public long GetHashCount(HashType hashType)
{
lock (_hashCounts)
{
if (!_hashCounts.TryGetValue(hashType, out long value))
return 0;
return value;
}
}
/// <summary>
/// Get the item count for a given item type, defaulting to 0 if it does not exist
/// </summary>
/// <param name="itemType">Item type to retrieve</param>
/// <returns>The number of items of that type, if it exists</returns>
public long GetItemCount(ItemType itemType)
{
lock (_itemCounts)
{
if (!_itemCounts.TryGetValue(itemType, out long value))
return 0;
return value;
}
}
/// <summary>
/// Get the item count for a given item status, defaulting to 0 if it does not exist
/// </summary>
/// <param name="itemStatus">Item status to retrieve</param>
/// <returns>The number of items of that type, if it exists</returns>
public long GetStatusCount(ItemStatus itemStatus)
{
lock (_statusCounts)
{
if (!_statusCounts.TryGetValue(itemStatus, out long value))
return 0;
return value;
}
}
/// <summary>
/// Remove from the statistics given a DatItem
/// </summary>
/// <param name="item">Item to remove info for</param>
public void RemoveItemStatistics(DatItem item)
{
// If we have a null item, we can't do anything
if (item is null)
return;
lock (statsLock)
{
// No matter what the item is, we decrease the count
TotalCount--;
// Decrement removal count
if (item.GetBoolFieldValue(DatItem.RemoveKey) == true)
RemovedCount--;
// Decrement the item count for the type
RemoveItemCount(item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType());
#pragma warning disable IDE0010
// Some item types require special processing
switch (item)
{
case Disk disk:
RemoveItemStatistics(disk);
break;
case File file:
RemoveItemStatistics(file);
break;
case Media media:
RemoveItemStatistics(media);
break;
case Rom rom:
RemoveItemStatistics(rom);
break;
}
#pragma warning restore IDE0010
}
}
/// <summary>
/// Remove from the statistics given a DatItem
/// </summary>
/// <param name="item">Item to remove info for</param>
public void RemoveItemStatistics(Data.Models.Metadata.DatItem item)
{
// If we have a null item, we can't do anything
if (item is null)
return;
lock (statsLock)
{
// No matter what the item is, we decrease the count
TotalCount--;
// Decrement removal count
if (item.ReadBool(DatItem.RemoveKey) == true)
RemovedCount--;
// Decrement the item count for the type
RemoveItemCount(item.ReadString(Data.Models.Metadata.DatItem.TypeKey).AsItemType());
#pragma warning disable IDE0010
// Some item types require special processing
switch (item)
{
case Data.Models.Metadata.Disk disk:
RemoveItemStatistics(disk);
break;
case Data.Models.Metadata.Media media:
RemoveItemStatistics(media);
break;
case Data.Models.Metadata.Rom rom:
RemoveItemStatistics(rom);
break;
}
#pragma warning restore IDE0010
}
}
/// <summary>
/// Reset all statistics
/// </summary>
public void ResetStatistics()
{
_hashCounts.Clear();
_itemCounts.Clear();
_statusCounts.Clear();
TotalCount = 0;
GameCount = 0;
TotalSize = 0;
RemovedCount = 0;
}
/// <summary>
/// Increment the hash count for a given hash type
/// </summary>
/// <param name="hashType">Hash type to increment</param>
/// <param name="interval">Amount to increment by, defaults to 1</param>
private void AddHashCount(HashType hashType, long interval = 1)
{
lock (_hashCounts)
{
if (!_hashCounts.ContainsKey(hashType))
_hashCounts[hashType] = 0;
_hashCounts[hashType] += interval;
if (_hashCounts[hashType] < 0)
_hashCounts[hashType] = 0;
}
}
/// <summary>
/// Increment the item count for a given item type
/// </summary>
/// <param name="itemType">Item type to increment</param>
/// <param name="interval">Amount to increment by, defaults to 1</param>
private void AddItemCount(ItemType itemType, long interval = 1)
{
lock (_itemCounts)
{
if (!_itemCounts.ContainsKey(itemType))
_itemCounts[itemType] = 0;
_itemCounts[itemType] += interval;
if (_itemCounts[itemType] < 0)
_itemCounts[itemType] = 0;
}
}
/// <summary>
/// Add to the statistics for a given Disk
/// </summary>
/// <param name="disk">Item to add info from</param>
private void AddItemStatistics(Disk disk)
{
if (disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump)
{
AddHashCount(HashType.MD5, string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) ? 0 : 1);
AddHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)) ? 0 : 1);
}
AddStatusCount(ItemStatus.BadDump, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0);
AddStatusCount(ItemStatus.Good, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0);
AddStatusCount(ItemStatus.Nodump, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0);
AddStatusCount(ItemStatus.Verified, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0);
}
/// <summary>
/// Add to the statistics for a given Disk
/// </summary>
/// <param name="disk">Item to add info from</param>
private void AddItemStatistics(Data.Models.Metadata.Disk disk)
{
if (disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump)
{
AddHashCount(HashType.MD5, string.IsNullOrEmpty(disk.ReadString(Data.Models.Metadata.Disk.MD5Key)) ? 0 : 1);
AddHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.ReadString(Data.Models.Metadata.Disk.SHA1Key)) ? 0 : 1);
}
AddStatusCount(ItemStatus.BadDump, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0);
AddStatusCount(ItemStatus.Good, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0);
AddStatusCount(ItemStatus.Nodump, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0);
AddStatusCount(ItemStatus.Verified, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0);
}
/// <summary>
/// Add to the statistics for a given File
/// </summary>
/// <param name="file">Item to add info from</param>
private void AddItemStatistics(File file)
{
TotalSize += file.Size ?? 0;
AddHashCount(HashType.CRC32, string.IsNullOrEmpty(file.CRC) ? 0 : 1);
AddHashCount(HashType.MD5, string.IsNullOrEmpty(file.MD5) ? 0 : 1);
AddHashCount(HashType.SHA1, string.IsNullOrEmpty(file.SHA1) ? 0 : 1);
AddHashCount(HashType.SHA256, string.IsNullOrEmpty(file.SHA256) ? 0 : 1);
}
/// <summary>
/// Add to the statistics for a given Media
/// </summary>
/// <param name="media">Item to add info from</param>
private void AddItemStatistics(Media media)
{
AddHashCount(HashType.MD5, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)) ? 0 : 1);
AddHashCount(HashType.SHA1, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)) ? 0 : 1);
AddHashCount(HashType.SHA256, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)) ? 0 : 1);
AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)) ? 0 : 1);
}
/// <summary>
/// Add to the statistics for a given Media
/// </summary>
/// <param name="media">Item to add info from</param>
private void AddItemStatistics(Data.Models.Metadata.Media media)
{
AddHashCount(HashType.MD5, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.MD5Key)) ? 0 : 1);
AddHashCount(HashType.SHA1, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SHA1Key)) ? 0 : 1);
AddHashCount(HashType.SHA256, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SHA256Key)) ? 0 : 1);
AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SpamSumKey)) ? 0 : 1);
}
/// <summary>
/// Add to the statistics for a given Rom
/// </summary>
/// <param name="rom">Item to add info from</param>
private void AddItemStatistics(Rom rom)
{
if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump)
{
TotalSize += rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) ?? 0;
AddHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) ? 0 : 1);
AddHashCount(HashType.MD2, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)) ? 0 : 1);
AddHashCount(HashType.MD4, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)) ? 0 : 1);
AddHashCount(HashType.MD5, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)) ? 0 : 1);
AddHashCount(HashType.RIPEMD128, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)) ? 0 : 1);
AddHashCount(HashType.RIPEMD160, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)) ? 0 : 1);
AddHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)) ? 0 : 1);
AddHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)) ? 0 : 1);
AddHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)) ? 0 : 1);
AddHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)) ? 0 : 1);
AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)) ? 0 : 1);
}
AddStatusCount(ItemStatus.BadDump, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0);
AddStatusCount(ItemStatus.Good, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0);
AddStatusCount(ItemStatus.Nodump, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0);
AddStatusCount(ItemStatus.Verified, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0);
}
/// <summary>
/// Add to the statistics for a given Rom
/// </summary>
/// <param name="rom">Item to add info from</param>
private void AddItemStatistics(Data.Models.Metadata.Rom rom)
{
if (rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump)
{
TotalSize += rom.ReadLong(Data.Models.Metadata.Rom.SizeKey) ?? 0;
AddHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.CRCKey)) ? 0 : 1);
AddHashCount(HashType.MD2, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD2Key)) ? 0 : 1);
AddHashCount(HashType.MD4, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD4Key)) ? 0 : 1);
AddHashCount(HashType.MD5, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD5Key)) ? 0 : 1);
AddHashCount(HashType.RIPEMD128, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.RIPEMD128Key)) ? 0 : 1);
AddHashCount(HashType.RIPEMD160, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.RIPEMD160Key)) ? 0 : 1);
AddHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA1Key)) ? 0 : 1);
AddHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA256Key)) ? 0 : 1);
AddHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA384Key)) ? 0 : 1);
AddHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA512Key)) ? 0 : 1);
AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SpamSumKey)) ? 0 : 1);
}
AddStatusCount(ItemStatus.BadDump, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0);
AddStatusCount(ItemStatus.Good, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0);
AddStatusCount(ItemStatus.Nodump, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0);
AddStatusCount(ItemStatus.Verified, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0);
}
/// <summary>
/// Increment the item count for a given item status
/// </summary>
/// <param name="itemStatus">Item type to increment</param>
/// <param name="interval">Amount to increment by, defaults to 1</param>
private void AddStatusCount(ItemStatus itemStatus, long interval = 1)
{
lock (_statusCounts)
{
if (!_statusCounts.ContainsKey(itemStatus))
_statusCounts[itemStatus] = 0;
_statusCounts[itemStatus] += interval;
if (_statusCounts[itemStatus] < 0)
_statusCounts[itemStatus] = 0;
}
}
/// <summary>
/// Decrement the hash count for a given hash type
/// </summary>
/// <param name="hashType">Hash type to increment</param>
/// <param name="interval">Amount to increment by, defaults to 1</param>
private void RemoveHashCount(HashType hashType, long interval = 1)
{
lock (_hashCounts)
{
if (!_hashCounts.ContainsKey(hashType))
return;
_hashCounts[hashType] -= interval;
if (_hashCounts[hashType] < 0)
_hashCounts[hashType] = 0;
}
}
/// <summary>
/// Decrement the item count for a given item type
/// </summary>
/// <param name="itemType">Item type to decrement</param>
/// <param name="interval">Amount to increment by, defaults to 1</param>
private void RemoveItemCount(ItemType itemType, long interval = 1)
{
lock (_itemCounts)
{
if (!_itemCounts.ContainsKey(itemType))
return;
_itemCounts[itemType] -= interval;
if (_itemCounts[itemType] < 0)
_itemCounts[itemType] = 0;
}
}
/// <summary>
/// Remove from the statistics given a Disk
/// </summary>
/// <param name="disk">Item to remove info for</param>
private void RemoveItemStatistics(Disk disk)
{
if (disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump)
{
RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)) ? 0 : 1);
}
RemoveStatusCount(ItemStatus.BadDump, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0);
RemoveStatusCount(ItemStatus.Good, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0);
RemoveStatusCount(ItemStatus.Nodump, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0);
RemoveStatusCount(ItemStatus.Verified, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0);
}
/// <summary>
/// Remove from the statistics given a Disk
/// </summary>
/// <param name="disk">Item to remove info for</param>
private void RemoveItemStatistics(Data.Models.Metadata.Disk disk)
{
if (disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump)
{
RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(disk.ReadString(Data.Models.Metadata.Disk.MD5Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.ReadString(Data.Models.Metadata.Disk.SHA1Key)) ? 0 : 1);
}
RemoveStatusCount(ItemStatus.BadDump, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0);
RemoveStatusCount(ItemStatus.Good, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0);
RemoveStatusCount(ItemStatus.Nodump, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0);
RemoveStatusCount(ItemStatus.Verified, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0);
}
/// <summary>
/// Remove from the statistics given a File
/// </summary>
/// <param name="file">Item to remove info for</param>
private void RemoveItemStatistics(File file)
{
TotalSize -= file.Size ?? 0;
RemoveHashCount(HashType.CRC32, string.IsNullOrEmpty(file.CRC) ? 0 : 1);
RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(file.MD5) ? 0 : 1);
RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(file.SHA1) ? 0 : 1);
RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(file.SHA256) ? 0 : 1);
}
/// <summary>
/// Remove from the statistics given a Media
/// </summary>
/// <param name="media">Item to remove info for</param>
private void RemoveItemStatistics(Media media)
{
RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)) ? 0 : 1);
RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)) ? 0 : 1);
}
/// <summary>
/// Remove from the statistics given a Media
/// </summary>
/// <param name="media">Item to remove info for</param>
private void RemoveItemStatistics(Data.Models.Metadata.Media media)
{
RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.MD5Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SHA1Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SHA256Key)) ? 0 : 1);
RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SpamSumKey)) ? 0 : 1);
}
/// <summary>
/// Remove from the statistics given a Rom
/// </summary>
/// <param name="rom">Item to remove info for</param>
private void RemoveItemStatistics(Rom rom)
{
if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump)
{
TotalSize -= rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) ?? 0;
RemoveHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) ? 0 : 1);
RemoveHashCount(HashType.MD2, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)) ? 0 : 1);
RemoveHashCount(HashType.MD4, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)) ? 0 : 1);
RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)) ? 0 : 1);
RemoveHashCount(HashType.RIPEMD128, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)) ? 0 : 1);
RemoveHashCount(HashType.RIPEMD160, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)) ? 0 : 1);
RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)) ? 0 : 1);
}
RemoveStatusCount(ItemStatus.BadDump, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0);
RemoveStatusCount(ItemStatus.Good, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0);
RemoveStatusCount(ItemStatus.Nodump, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0);
RemoveStatusCount(ItemStatus.Verified, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0);
}
/// <summary>
/// Remove from the statistics given a Rom
/// </summary>
/// <param name="rom">Item to remove info for</param>
private void RemoveItemStatistics(Data.Models.Metadata.Rom rom)
{
if (rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump)
{
TotalSize -= rom.ReadLong(Data.Models.Metadata.Rom.SizeKey) ?? 0;
RemoveHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.CRCKey)) ? 0 : 1);
RemoveHashCount(HashType.MD2, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD2Key)) ? 0 : 1);
RemoveHashCount(HashType.MD4, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD4Key)) ? 0 : 1);
RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD5Key)) ? 0 : 1);
RemoveHashCount(HashType.RIPEMD128, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.RIPEMD128Key)) ? 0 : 1);
RemoveHashCount(HashType.RIPEMD160, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.RIPEMD160Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA1Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA256Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA384Key)) ? 0 : 1);
RemoveHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA512Key)) ? 0 : 1);
RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SpamSumKey)) ? 0 : 1);
}
RemoveStatusCount(ItemStatus.BadDump, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0);
RemoveStatusCount(ItemStatus.Good, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0);
RemoveStatusCount(ItemStatus.Nodump, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0);
RemoveStatusCount(ItemStatus.Verified, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0);
}
/// <summary>
/// Decrement the item count for a given item status
/// </summary>
/// <param name="itemStatus">Item type to decrement</param>
/// <param name="interval">Amount to increment by, defaults to 1</param>
private void RemoveStatusCount(ItemStatus itemStatus, long interval = 1)
{
lock (_statusCounts)
{
if (!_statusCounts.ContainsKey(itemStatus))
return;
_statusCounts[itemStatus] -= interval;
if (_statusCounts[itemStatus] < 0)
_statusCounts[itemStatus] = 0;
}
}
#endregion
}
}

View File

@@ -0,0 +1,66 @@
using System;
using SabreTools.Hashing;
namespace SabreTools.Metadata.DatFiles
{
/// <summary>
/// Depot information wrapper
/// </summary>
public class DepotInformation : ICloneable
{
/// <summary>
/// Name or path of the Depot
/// </summary>
public string? Name { get; }
/// <summary>
/// Whether to use this Depot or not
/// </summary>
public bool IsActive { get; }
/// <summary>
/// Depot byte-depth
/// </summary>
public int Depth { get; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="isActive">Set active state</param>
/// <param name="depth">Set depth between 0 and SHA-1's byte length</param>
public DepotInformation(bool isActive, int depth)
: this(null, isActive, depth)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="name">Identifier for the depot</param>
/// <param name="isActive">Set active state</param>
/// <param name="depth">Set depth between 0 and SHA-1's byte length</param>
public DepotInformation(string? name, bool isActive, int depth)
{
Name = name;
IsActive = isActive;
Depth = depth;
// Limit depth value
if (Depth == int.MinValue)
Depth = 4;
else if (Depth < 0)
Depth = 0;
else if (Depth > HashType.SHA1.ZeroBytes.Length)
Depth = HashType.SHA1.ZeroBytes.Length;
}
#region Cloning
/// <summary>
/// Clone the current object
/// </summary>
public object Clone() => new DepotInformation(Name, IsActive, Depth);
#endregion
}
}

View File

@@ -0,0 +1,267 @@
using System;
namespace SabreTools.Metadata.DatFiles
{
/// <summary>
/// DAT output formats
/// </summary>
[Flags]
public enum DatFormat : ulong
{
#region XML Formats
/// <summary>
/// Logiqx XML (using machine)
/// </summary>
Logiqx = 1 << 0,
/// <summary>
/// Logiqx XML (using game)
/// </summary>
LogiqxDeprecated = 1 << 1,
/// <summary>
/// MAME Softare List XML
/// </summary>
SoftwareList = 1 << 2,
/// <summary>
/// MAME Listxml output
/// </summary>
Listxml = 1 << 3,
/// <summary>
/// OfflineList XML
/// </summary>
OfflineList = 1 << 4,
/// <summary>
/// SabreDAT XML
/// </summary>
SabreXML = 1 << 5,
/// <summary>
/// openMSX Software List XML
/// </summary>
OpenMSX = 1 << 6,
/// <summary>
/// Archive.org file list XML
/// </summary>
ArchiveDotOrg = 1 << 7,
#endregion
#region Propietary Formats
/// <summary>
/// ClrMamePro custom
/// </summary>
ClrMamePro = 1 << 8,
/// <summary>
/// RomCenter INI-based
/// </summary>
RomCenter = 1 << 9,
/// <summary>
/// DOSCenter custom
/// </summary>
DOSCenter = 1 << 10,
/// <summary>
/// AttractMode custom
/// </summary>
AttractMode = 1 << 11,
#endregion
#region Standardized Text Formats
/// <summary>
/// ClrMamePro missfile
/// </summary>
MissFile = 1 << 12,
/// <summary>
/// Comma-Separated Values (standardized)
/// </summary>
CSV = 1 << 13,
/// <summary>
/// Semicolon-Separated Values (standardized)
/// </summary>
SSV = 1 << 14,
/// <summary>
/// Tab-Separated Values (standardized)
/// </summary>
TSV = 1 << 15,
/// <summary>
/// MAME Listrom output
/// </summary>
Listrom = 1 << 16,
/// <summary>
/// Everdrive Packs SMDB
/// </summary>
EverdriveSMDB = 1 << 17,
/// <summary>
/// SabreJSON
/// </summary>
SabreJSON = 1 << 18,
#endregion
#region SFV-similar Formats
/// <summary>
/// CRC32 hash list
/// </summary>
RedumpSFV = 1 << 19,
/// <summary>
/// MD2 hash list
/// </summary>
RedumpMD2 = 1 << 20,
/// <summary>
/// MD4 hash list
/// </summary>
RedumpMD4 = 1 << 21,
/// <summary>
/// MD5 hash list
/// </summary>
RedumpMD5 = 1 << 22,
/// <summary>
/// RIPEMD128 hash list
/// </summary>
RedumpRIPEMD128 = 1 << 23,
/// <summary>
/// RIPEMD160 hash list
/// </summary>
RedumpRIPEMD160 = 1 << 24,
/// <summary>
/// SHA-1 hash list
/// </summary>
RedumpSHA1 = 1 << 25,
/// <summary>
/// SHA-256 hash list
/// </summary>
RedumpSHA256 = 1 << 26,
/// <summary>
/// SHA-384 hash list
/// </summary>
RedumpSHA384 = 1 << 27,
/// <summary>
/// SHA-512 hash list
/// </summary>
RedumpSHA512 = 1 << 28,
/// <summary>
/// SpamSum hash list
/// </summary>
RedumpSpamSum = 1 << 29,
#endregion
// Specialty combinations
ALL = ulong.MaxValue,
}
/// <summary>
/// Determines merging tag handling for DAT output
/// </summary>
public enum MergingFlag
{
[Mapping("none")]
None = 0,
[Mapping("split")]
Split,
[Mapping("merged")]
Merged,
[Mapping("nonmerged", "unmerged")]
NonMerged,
/// <remarks>This is not usually defined for Merging flags</remarks>
[Mapping("fullmerged")]
FullMerged,
/// <remarks>This is not usually defined for Merging flags</remarks>
[Mapping("device", "deviceunmerged", "devicenonmerged")]
DeviceNonMerged,
/// <remarks>This is not usually defined for Merging flags</remarks>
[Mapping("full", "fullunmerged", "fullnonmerged")]
FullNonMerged,
}
/// <summary>
/// Determines nodump tag handling for DAT output
/// </summary>
public enum NodumpFlag
{
[Mapping("none")]
None = 0,
[Mapping("obsolete")]
Obsolete,
[Mapping("required")]
Required,
[Mapping("ignore")]
Ignore,
}
/// <summary>
/// Determines packing tag handling for DAT output
/// </summary>
public enum PackingFlag
{
[Mapping("none")]
None = 0,
/// <summary>
/// Force all sets to be in archives, except disk and media
/// </summary>
[Mapping("zip", "yes")]
Zip,
/// <summary>
/// Force all sets to be extracted into subfolders
/// </summary>
[Mapping("unzip", "no")]
Unzip,
/// <summary>
/// Force sets with single items to be extracted to the parent folder
/// </summary>
[Mapping("partial")]
Partial,
/// <summary>
/// Force all sets to be extracted to the parent folder
/// </summary>
[Mapping("flat")]
Flat,
/// <summary>
/// Force all sets to have all archives treated as files
/// </summary>
[Mapping("fileonly")]
FileOnly,
}
}

View File

@@ -0,0 +1,172 @@
using System.Collections.Generic;
using SabreTools.Metadata.Tools;
namespace SabreTools.Metadata.DatFiles
{
public static class Extensions
{
#region Private Maps
/// <summary>
/// Set of enum to string mappings for MergingFlag
/// </summary>
private static readonly Dictionary<string, MergingFlag> _toMergingFlagMap = Converters.GenerateToEnum<MergingFlag>();
/// <summary>
/// Set of string to enum mappings for MergingFlag
/// </summary>
private static readonly Dictionary<MergingFlag, string> _fromMergingFlagMap = Converters.GenerateToString<MergingFlag>(useSecond: false);
/// <summary>
/// Set of string to enum mappings for MergingFlag (secondary)
/// </summary>
private static readonly Dictionary<MergingFlag, string> _fromMergingFlagSecondaryMap = Converters.GenerateToString<MergingFlag>(useSecond: true);
/// <summary>
/// Set of enum to string mappings for NodumpFlag
/// </summary>
private static readonly Dictionary<string, NodumpFlag> _toNodumpFlagMap = Converters.GenerateToEnum<NodumpFlag>();
/// <summary>
/// Set of string to enum mappings for NodumpFlag
/// </summary>
private static readonly Dictionary<NodumpFlag, string> _fromNodumpFlagMap = Converters.GenerateToString<NodumpFlag>(useSecond: false);
/// <summary>
/// Set of enum to string mappings for PackingFlag
/// </summary>
private static readonly Dictionary<string, PackingFlag> _toPackingFlagMap = Converters.GenerateToEnum<PackingFlag>();
/// <summary>
/// Set of string to enum mappings for PackingFlag
/// </summary>
private static readonly Dictionary<PackingFlag, string> _fromPackingFlagMap = Converters.GenerateToString<PackingFlag>(useSecond: false);
/// <summary>
/// Set of string to enum mappings for PackingFlag (secondary)
/// </summary>
private static readonly Dictionary<PackingFlag, string> _fromPackingFlagSecondaryMap = Converters.GenerateToString<PackingFlag>(useSecond: true);
#endregion
#region String to Enum
/// <summary>
/// Get the enum value for an input string, if possible
/// </summary>
/// <param name="value">String value to parse/param>
/// <returns>Enum value representing the input, default on error</returns>
public static MergingFlag AsMergingFlag(this string? value)
{
// Normalize the input value
value = value?.ToLowerInvariant();
if (value is null)
return default;
// Try to get the value from the mappings
if (_toMergingFlagMap.ContainsKey(value))
return _toMergingFlagMap[value];
// Otherwise, return the default value for the enum
return default;
}
/// <summary>
/// Get the enum value for an input string, if possible
/// </summary>
/// <param name="value">String value to parse/param>
/// <returns>Enum value representing the input, default on error</returns>
public static NodumpFlag AsNodumpFlag(this string? value)
{
// Normalize the input value
value = value?.ToLowerInvariant();
if (value is null)
return default;
// Try to get the value from the mappings
if (_toNodumpFlagMap.ContainsKey(value))
return _toNodumpFlagMap[value];
// Otherwise, return the default value for the enum
return default;
}
/// <summary>
/// Get the enum value for an input string, if possible
/// </summary>
/// <param name="value">String value to parse/param>
/// <returns>Enum value representing the input, default on error</returns>
public static PackingFlag AsPackingFlag(this string? value)
{
// Normalize the input value
value = value?.ToLowerInvariant();
if (value is null)
return default;
// Try to get the value from the mappings
if (_toPackingFlagMap.ContainsKey(value))
return _toPackingFlagMap[value];
// Otherwise, return the default value for the enum
return default;
}
#endregion
#region Enum to String
/// <summary>
/// Get the string value for an input enum, if possible
/// </summary>
/// <param name="value">Enum value to parse/param>
/// <param name="useSecond">True to use the second mapping option, if it exists</param>
/// <returns>String value representing the input, default on error</returns>
public static string? AsStringValue(this MergingFlag value, bool useSecond = false)
{
// Try to get the value from the mappings
if (!useSecond && _fromMergingFlagMap.ContainsKey(value))
return _fromMergingFlagMap[value];
else if (useSecond && _fromMergingFlagSecondaryMap.ContainsKey(value))
return _fromMergingFlagSecondaryMap[value];
// Otherwise, return null
return null;
}
/// <summary>
/// Get the string value for an input enum, if possible
/// </summary>
/// <param name="value">Enum value to parse/param>
/// <param name="useSecond">True to use the second mapping option, if it exists</param>
/// <returns>String value representing the input, default on error</returns>
public static string? AsStringValue(this NodumpFlag value)
{
// Try to get the value from the mappings
if (_fromNodumpFlagMap.ContainsKey(value))
return _fromNodumpFlagMap[value];
// Otherwise, return null
return null;
}
/// <summary>
/// Get the string value for an input enum, if possible
/// </summary>
/// <param name="value">Enum value to parse/param>
/// <param name="useSecond">True to use the second mapping option, if it exists</param>
/// <returns>String value representing the input, default on error</returns>
public static string? AsStringValue(this PackingFlag value, bool useSecond = false)
{
// Try to get the value from the mappings
if (!useSecond && _fromPackingFlagMap.ContainsKey(value))
return _fromPackingFlagMap[value];
else if (useSecond && _fromPackingFlagSecondaryMap.ContainsKey(value))
return _fromPackingFlagSecondaryMap[value];
// Otherwise, return null
return null;
}
#endregion
}
}

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;
}
}
}

View File

@@ -0,0 +1,931 @@
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using System.Collections.Concurrent;
#endif
using System.Collections.Generic;
using System.IO;
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using System.Threading.Tasks;
#endif
using System.Xml.Serialization;
using Newtonsoft.Json;
using SabreTools.Metadata.Tools;
using SabreTools.Metadata.DatItems;
using SabreTools.Metadata.DatItems.Formats;
using SabreTools.Hashing;
using SabreTools.IO.Logging;
using SabreTools.Text.Compare;
namespace SabreTools.Metadata.DatFiles
{
/// <summary>
/// Item dictionary with statistics, bucketing, and sorting
/// </summary>
[JsonObject("items"), XmlRoot("items")]
public class ItemDictionary
{
#region Private instance variables
/// <summary>
/// Determine the bucketing key for all items
/// </summary>
private ItemKey _bucketedBy = ItemKey.NULL;
/// <summary>
/// Internal dictionary for the class
/// </summary>
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
private readonly ConcurrentDictionary<string, List<DatItem>?> _items = [];
#else
private readonly Dictionary<string, List<DatItem>?> _items = [];
#endif
/// <summary>
/// Logging object
/// </summary>
private readonly Logger _logger;
#endregion
#region Fields
/// <summary>
/// Get the keys in sorted order from the file dictionary
/// </summary>
/// <returns>List of the keys in sorted order</returns>
[JsonIgnore, XmlIgnore]
public string[] SortedKeys
{
get
{
List<string> keys = [.. _items.Keys];
keys.Sort(new NaturalComparer());
return [.. keys];
}
}
/// <summary>
/// DAT statistics
/// </summary>
[JsonIgnore, XmlIgnore]
public DatStatistics DatStatistics { get; } = new DatStatistics();
#endregion
#region Constructors
/// <summary>
/// Generic constructor
/// </summary>
public ItemDictionary()
{
_logger = new Logger(this);
}
#endregion
#region Accessors
/// <summary>
/// Add a DatItem to the dictionary after checking
/// </summary>
/// <param name="item">Item data to check against</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <returns>The key for the item</returns>
public string AddItem(DatItem item, bool statsOnly)
{
// If we have a Disk, File, Media, or Rom, clean the hash data
if (item is Disk disk)
{
// If the file has aboslutely no hashes, skip and log
if (disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump
&& string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key))
&& string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)))
{
_logger.Verbose($"Incomplete entry for '{disk.GetName()}' will be output as nodump");
disk.SetFieldValue<string?>(Data.Models.Metadata.Disk.StatusKey, ItemStatus.Nodump.AsStringValue());
}
item = disk;
}
else if (item is DatItems.Formats.File file)
{
// If the file has aboslutely no hashes, skip and log
if (string.IsNullOrEmpty(file.CRC)
&& string.IsNullOrEmpty(file.MD5)
&& string.IsNullOrEmpty(file.SHA1)
&& string.IsNullOrEmpty(file.SHA256))
{
_logger.Verbose($"Incomplete entry for '{file.GetName()}' will be output as nodump");
}
item = file;
}
else if (item is Media media)
{
// If the file has aboslutely no hashes, skip and log
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)))
{
_logger.Verbose($"Incomplete entry for '{media.GetName()}' will be output as nodump");
}
item = media;
}
else if (item is Rom rom)
{
long? size = rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey);
// If we have the case where there is SHA-1 and nothing else, we don't fill in any other part of the data
if (size is null && !string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)))
{
// No-op, just catch it so it doesn't go further
//logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Entry with only SHA-1 found - '{rom.GetName()}'");
}
// 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
else if ((size == 0 || size is null)
&& (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) || rom.HasZeroHash()))
{
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.SizeKey, "0");
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.CRCKey, HashType.CRC32.ZeroString);
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.MD2Key, null); // HashType.MD2.ZeroString
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.MD4Key, null); // HashType.MD4.ZeroString
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.MD5Key, HashType.MD5.ZeroString);
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.RIPEMD128Key, null); // HashType.RIPEMD128.ZeroString
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.RIPEMD160Key, null); // HashType.RIPEMD160.ZeroString
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.SHA1Key, HashType.SHA1.ZeroString);
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.SHA256Key, null); // HashType.SHA256.ZeroString;
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.SHA384Key, null); // HashType.SHA384.ZeroString;
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.SHA512Key, null); // HashType.SHA512.ZeroString;
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.SpamSumKey, null); // HashType.SpamSum.ZeroString;
}
// If the file has no size and it's not the above case, skip and log
else if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump && (size == 0 || size is null))
{
//logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Incomplete entry for '{rom.GetName()}' will be output as nodump");
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.StatusKey, ItemStatus.Nodump.AsStringValue());
}
// If the file has a size but aboslutely no hashes, skip and log
else if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump
&& size is not null && size > 0
&& !rom.HasHashes())
{
//logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Incomplete entry for '{rom.GetName()}' will be output as nodump");
rom.SetFieldValue<string?>(Data.Models.Metadata.Rom.StatusKey, ItemStatus.Nodump.AsStringValue());
}
item = rom;
}
// Get the key and add the file
string key = GetBucketKey(item, _bucketedBy, lower: true, norename: true);
// If only adding statistics, we add an empty key for games and then just item stats
if (statsOnly)
{
EnsureBucketingKey(key);
DatStatistics.AddItemStatistics(item);
}
else
{
AddItem(key, item);
}
return key;
}
/// <summary>
/// Remove all items marked for removal
/// </summary>
public void ClearMarked()
{
string[] keys = [.. SortedKeys];
#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(keys, Core.Globals.ParallelOptions, key =>
#elif NET40_OR_GREATER
Parallel.ForEach(keys, key =>
#else
foreach (var key in keys)
#endif
{
var list = GetItemsForBucket(key, filter: true);
RemoveBucket(key);
list.ForEach(item => AddItem(key, item));
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Get the items associated with a bucket name
/// </summary>
/// <param name="bucketName">Name of the bucket to retrive items for</param>
/// <param name="filter">Indicates if RemoveKey filtering is performed</param>
/// <returns>List representing the bucket items, empty on missing</returns>
public List<DatItem> GetItemsForBucket(string? bucketName, bool filter = false)
{
if (bucketName is null)
return [];
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
if (!_items.TryGetValue(bucketName, out var items))
return [];
#else
if (!_items.ContainsKey(bucketName))
return [];
var items = _items[bucketName];
#endif
if (items is null || !filter)
return [.. items ?? []];
var datItems = new List<DatItem>();
foreach (DatItem item in items)
{
if (item.GetBoolFieldValue(DatItem.RemoveKey) != true)
datItems.Add(item);
}
return datItems;
}
/// <summary>
/// Remove a key from the file dictionary if it exists
/// </summary>
/// <param name="key">Key in the dictionary to remove</param>
public bool RemoveBucket(string key)
{
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
bool removed = _items.TryRemove(key, out var list);
#else
if (!_items.ContainsKey(key))
return false;
bool removed = true;
var list = _items[key];
_items.Remove(key);
#endif
if (list is null)
return removed;
foreach (var item in list)
{
DatStatistics.RemoveItemStatistics(item);
}
return removed;
}
/// <summary>
/// Remove the indexed instance of a value from the file dictionary if it exists
/// </summary>
/// <param name="key">Key in the dictionary to remove from</param>
/// <param name="value">Value to remove from the dictionary</param>
/// <param name="index">Index of the item to be removed</param>
public bool RemoveItem(string key, DatItem value, int index)
{
// Explicit lock for some weird corner cases
lock (key)
{
// If the key doesn't exist, return
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
if (!_items.TryGetValue(key, out var list) || list is null)
return false;
#else
if (!_items.ContainsKey(key))
return false;
var list = _items[key];
if (list is null)
return false;
#endif
// If the value doesn't exist in the key, assume it has been removed
if (index < 0)
return false;
// Remove the statistics first
DatStatistics.RemoveItemStatistics(value);
list.RemoveAt(index);
return true;
}
}
/// <summary>
/// Override the internal ItemKey value
/// </summary>
/// <param name="newBucket"></param>
public void SetBucketedBy(ItemKey newBucket)
{
_bucketedBy = newBucket;
}
/// <summary>
/// Add a value to the file dictionary
/// </summary>
/// <param name="key">Key in the dictionary to add to</param>
/// <param name="value">Value to add to the dictionary</param>
internal void AddItem(string key, DatItem value)
{
// Explicit lock for some weird corner cases
lock (key)
{
// Ensure the key exists
EnsureBucketingKey(key);
// If item is null, don't add it
if (value is null)
return;
// Now add the value
_items[key]!.Add(value);
// Now update the statistics
DatStatistics.AddItemStatistics(value);
}
}
#endregion
#region Bucketing
/// <summary>
/// Take the arbitrarily bucketed Files Dictionary and convert to one bucketed by a user-defined method
/// </summary>
/// <param name="bucketBy">ItemKey enum representing how to bucket the individual items</param>
/// <param name="lower">True if the key should be lowercased (default), false otherwise</param>
/// <param name="norename">True if games should only be compared on game and file name, false if system and source are counted</param>
public void BucketBy(ItemKey bucketBy, bool lower = true, bool norename = true)
{
// If we have a situation where there's no dictionary or no keys at all, we skip
if (_items is null || _items.Count == 0)
return;
// If the sorted type isn't the same, we want to sort the dictionary accordingly
if (_bucketedBy != bucketBy && bucketBy != ItemKey.NULL)
{
_logger.User($"Organizing roms by {bucketBy}");
PerformBucketing(bucketBy, lower, norename);
}
// Sort the dictionary to be consistent
_logger.User($"Sorting roms by {bucketBy}");
PerformSorting(norename);
}
/// <summary>
/// Perform deduplication on the current sorted dictionary
/// </summary>
public void Deduplicate()
{
#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(SortedKeys, Core.Globals.ParallelOptions, key =>
#elif NET40_OR_GREATER
Parallel.ForEach(SortedKeys, key =>
#else
foreach (var key in SortedKeys)
#endif
{
// Get the possibly unsorted list
List<DatItem> sortedList = GetItemsForBucket(key);
// Sort and merge the list
Sort(ref sortedList, norename: false);
sortedList = Merge(sortedList);
// Add the list back to the dictionary
RemoveBucket(key);
sortedList.ForEach(item => AddItem(key, item));
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Return the duplicate status of two items
/// </summary>
/// <param name="self">Current DatItem</param>
/// <param name="last">DatItem to check against</param>
/// <returns>The DupeType corresponding to the relationship between the two</returns>
public DupeType GetDuplicateStatus(DatItem? self, DatItem? last)
{
DupeType output = 0x00;
// If either item is null
if (self is null || last is null)
return output;
// If we don't have a duplicate at all, return none
if (!self.Equals(last))
return output;
// Get the sources for comparison
var selfSource = self.GetFieldValue<Source?>(DatItem.SourceKey);
var lastSource = last.GetFieldValue<Source?>(DatItem.SourceKey);
// Get the machines for comparison
var selfMachine = self.GetMachine();
string? selfMachineName = selfMachine?.GetName();
var lastMachine = last.GetMachine();
string? lastMachineName = lastMachine?.GetName();
// If the duplicate is external already
#if NET20 || NET35
if ((last.GetFieldValue<DupeType>(DatItem.DupeTypeKey) & DupeType.External) != 0)
#else
if (last.GetFieldValue<DupeType>(DatItem.DupeTypeKey).HasFlag(DupeType.External))
#endif
output |= DupeType.External;
// If the duplicate should be external
else if (lastSource?.Index != selfSource?.Index)
output |= DupeType.External;
// Otherwise, it's considered an internal dupe
else
output |= DupeType.Internal;
// If the item and machine names match
if (lastMachineName == selfMachineName && last.GetName() == self.GetName())
output |= DupeType.All;
// Otherwise, hash match is assumed
else
output |= DupeType.Hash;
return output;
}
/// <summary>
/// Merge an arbitrary set of DatItems based on the supplied information
/// </summary>
/// <param name="items">List of DatItem objects representing the items to be merged</param>
/// <returns>A List of DatItem objects representing the merged items</returns>
/// TODO: Make this internal like the DB counterpart
public static List<DatItem> Merge(List<DatItem>? items)
{
// Check for null or blank inputs first
if (items is null || items.Count == 0)
return [];
// Create placeholder object for checking duplicates
var dupDict = new ItemDictionary();
// Create output list
List<DatItem> output = [];
// Then deduplicate them by checking to see if data matches previous saved roms
int nodumpCount = 0;
foreach (DatItem datItem in items)
{
// If we don't have a Disk, File, Media, or Rom, we skip checking for duplicates
if (datItem is not Disk && datItem is not DatItems.Formats.File && datItem is not Media && datItem is not Rom)
continue;
// If it's a nodump, add and skip
if (datItem is Rom rom && rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump)
{
output.Add(datItem);
nodumpCount++;
continue;
}
else if (datItem is Disk disk && disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump)
{
output.Add(datItem);
nodumpCount++;
continue;
}
// If it's the first non-nodump item in the list, don't touch it
if (output.Count == nodumpCount)
{
output.Add(datItem);
continue;
}
// Find the index of the first duplicate, if one exists
int pos = output.FindIndex(lastItem => dupDict.GetDuplicateStatus(datItem, lastItem) != 0x00);
if (pos < 0)
{
output.Add(datItem);
continue;
}
// Get the duplicate item
DatItem savedItem = output[pos];
DupeType dupetype = dupDict.GetDuplicateStatus(datItem, savedItem);
// Disks, File, Media, and Roms have more information to fill
if (datItem is Disk diskItem && savedItem is Disk savedDisk)
savedDisk.FillMissingInformation(diskItem);
else if (datItem is DatItems.Formats.File fileItem && savedItem is DatItems.Formats.File savedFile)
savedFile.FillMissingInformation(fileItem);
else if (datItem is Media mediaItem && savedItem is Media savedMedia)
savedMedia.FillMissingInformation(mediaItem);
else if (datItem is Rom romItem && savedItem is Rom savedRom)
savedRom.FillMissingInformation(romItem);
// Set the duplicate type on the saved item
savedItem.SetFieldValue(DatItem.DupeTypeKey, dupetype);
// Get the sources associated with the items
var savedSource = savedItem.GetFieldValue<Source?>(DatItem.SourceKey);
var itemSource = datItem.GetFieldValue<Source?>(DatItem.SourceKey);
// Get the machines associated with the items
var savedMachine = savedItem.GetMachine();
var itemMachine = datItem.GetMachine();
// If the current source has a lower ID than the saved, use the saved source
if (itemSource?.Index < savedSource?.Index)
{
datItem.SetFieldValue<Source?>(DatItem.SourceKey, savedSource.Clone() as Source);
savedItem.CopyMachineInformation(datItem);
savedItem.SetName(datItem.GetName());
}
// If the saved machine is a child of the current machine, use the current machine instead
if (savedMachine?.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey) == itemMachine?.GetName()
|| savedMachine?.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey) == itemMachine?.GetName())
{
savedItem.CopyMachineInformation(datItem);
savedItem.SetName(datItem.GetName());
}
// Replace the original item in the list
output.RemoveAt(pos);
output.Insert(pos, savedItem);
}
// Then return the result
return output;
}
/// <summary>
/// List all duplicates found in a DAT based on a DatItem
/// </summary>
/// <param name="datItem">Item to try to match</param>
/// <param name="sorted">True if the DAT is already sorted accordingly, false otherwise (default)</param>
/// <returns>List of matched DatItem objects</returns>
/// <remarks>This also sets the remove flag on any duplicates found</remarks>
/// TODO: Figure out if removal should be a flag or just removed entirely
internal List<DatItem> GetDuplicates(DatItem datItem, bool sorted = false)
{
// Check for an empty rom list first
if (DatStatistics.TotalCount == 0)
return [];
// We want to get the proper key for the DatItem
string key = SortAndGetKey(datItem, sorted);
// Get the items for the current key, if possible
List<DatItem> items = GetItemsForBucket(key, filter: false);
if (items.Count == 0)
return [];
// Try to find duplicates
List<DatItem> output = [];
foreach (DatItem other in items)
{
// Skip items marked for removal
if (other.GetBoolFieldValue(DatItem.RemoveKey) == true)
continue;
// Mark duplicates for future removal
if (datItem.Equals(other))
{
other.SetFieldValue<bool?>(DatItem.RemoveKey, true);
output.Add(other);
}
}
// Return any matching items
return output;
}
/// <summary>
/// Check if a DAT contains the given DatItem
/// </summary>
/// <param name="datItem">Item to try to match</param>
/// <param name="sorted">True if the DAT is already sorted accordingly, false otherwise (default)</param>
/// <returns>True if it contains the rom, false otherwise</returns>
internal bool HasDuplicates(DatItem datItem, bool sorted = false)
{
// Check for an empty rom list first
if (DatStatistics.TotalCount == 0)
return false;
// We want to get the proper key for the DatItem
string key = SortAndGetKey(datItem, sorted);
// Try to find duplicates
List<DatItem> roms = GetItemsForBucket(key);
if (roms.Count == 0)
return false;
return roms.FindIndex(datItem.Equals) > -1;
}
/// <summary>
/// Ensure the key exists in the items dictionary
/// </summary>
/// <param name="key">Key to ensure</param>
private void EnsureBucketingKey(string key)
{
// If the key is missing from the dictionary, add it
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
_items.GetOrAdd(key, []);
#else
if (!_items.ContainsKey(key))
_items[key] = [];
#endif
}
/// <summary>
/// Get the highest-order Field value that represents the statistics
/// </summary>
private ItemKey GetBestAvailable()
{
// Get the required counts
long diskCount = DatStatistics.GetItemCount(ItemType.Disk);
long mediaCount = DatStatistics.GetItemCount(ItemType.Media);
long romCount = DatStatistics.GetItemCount(ItemType.Rom);
long nodumpCount = DatStatistics.GetStatusCount(ItemStatus.Nodump);
// If all items are supposed to have a SHA-512, we bucket by that
if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA512))
return ItemKey.SHA512;
// If all items are supposed to have a SHA-384, we bucket by that
else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA384))
return ItemKey.SHA384;
// If all items are supposed to have a SHA-256, we bucket by that
else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA256))
return ItemKey.SHA256;
// If all items are supposed to have a SHA-1, we bucket by that
else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA1))
return ItemKey.SHA1;
// If all items are supposed to have a RIPEMD160, we bucket by that
else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.RIPEMD160))
return ItemKey.RIPEMD160;
// If all items are supposed to have a RIPEMD128, we bucket by that
else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.RIPEMD128))
return ItemKey.RIPEMD128;
// If all items are supposed to have a MD5, we bucket by that
else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD5))
return ItemKey.MD5;
// If all items are supposed to have a MD4, we bucket by that
else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD4))
return ItemKey.MD4;
// If all items are supposed to have a MD2, we bucket by that
else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD2))
return ItemKey.MD2;
// Otherwise, we bucket by CRC
else
return ItemKey.CRC;
}
/// <summary>
/// Get the bucketing key for a given item
/// <param name="datItem">The current item</param>
/// <param name="bucketBy">ItemKey value representing what key to get</param>
/// <param name="lower">True if the key should be lowercased, false otherwise</param>
/// <param name="norename">True if games should only be compared on game and file name, false if system and source are counted</param>
/// </summary>
private static string GetBucketKey(DatItem datItem, ItemKey bucketBy, bool lower, bool norename)
{
if (datItem is null)
return string.Empty;
// Treat NULL like machine
if (bucketBy == ItemKey.NULL)
bucketBy = ItemKey.Machine;
// Get the machine and source
var machine = datItem.GetMachine();
var source = datItem.GetFieldValue<Source?>(DatItem.SourceKey);
// Get the bucket key
return datItem.GetKey(bucketBy, machine, source, lower, norename);
}
/// <summary>
/// Perform bucketing based on the item key provided
/// </summary>
/// <param name="bucketBy">ItemKey enum representing how to bucket the individual items</param>
/// <param name="lower">True if the key should be lowercased, false otherwise</param>
/// <param name="norename">True if games should only be compared on game and file name, false if system and source are counted</param>
private void PerformBucketing(ItemKey bucketBy, bool lower, bool norename)
{
// Set the sorted type
_bucketedBy = bucketBy;
// First do the initial sort of all of the roms inplace
List<string> oldkeys = [.. SortedKeys];
#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.For(0, oldkeys.Count, Core.Globals.ParallelOptions, k =>
#elif NET40_OR_GREATER
Parallel.For(0, oldkeys.Count, k =>
#else
for (int k = 0; k < oldkeys.Count; k++)
#endif
{
string key = oldkeys[k];
if (GetItemsForBucket(key, filter: true).Count == 0)
RemoveBucket(key);
// Now add each of the roms to their respective keys
for (int i = 0; i < GetItemsForBucket(key).Count; i++)
{
DatItem item = GetItemsForBucket(key)[i];
if (item is null || item.GetBoolFieldValue(DatItem.RemoveKey) == true)
continue;
// Get the machine and source
var machine = item.GetMachine();
var source = item.GetFieldValue<Source?>(DatItem.SourceKey);
// We want to get the key most appropriate for the given sorting type
string newkey = item.GetKey(bucketBy, machine, source, lower, norename);
// If the key is different, move the item to the new key
if (newkey != key)
{
AddItem(newkey, item);
bool removed = RemoveItem(key, item, i);
if (!removed)
continue;
i--; // This make sure that the pointer stays on the correct since one was removed
}
}
// If the key is now empty, remove it
if (GetItemsForBucket(key, filter: true).Count == 0)
RemoveBucket(key);
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Perform inplace sorting of the dictionary
/// </summary>
private void PerformSorting(bool norename)
{
#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
Parallel.ForEach(SortedKeys, Core.Globals.ParallelOptions, key =>
#elif NET40_OR_GREATER
Parallel.ForEach(SortedKeys, key =>
#else
foreach (var key in SortedKeys)
#endif
{
// Get the possibly unsorted list
List<DatItem> sortedList = GetItemsForBucket(key);
// Sort the list of items to be consistent
Sort(ref sortedList, norename);
// Add the list back to the dictionary
RemoveBucket(key);
sortedList.ForEach(item => AddItem(key, item));
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
});
#else
}
#endif
}
/// <summary>
/// Sort a list of DatItem objects by SourceID, Game, and Name (in order)
/// </summary>
/// <param name="items">List of DatItem objects representing the items to be sorted</param>
/// <param name="norename">True if files are not renamed, false otherwise</param>
/// <returns>True if it sorted correctly, false otherwise</returns>
private bool Sort(ref List<DatItem> items, bool norename)
{
// Create the comparer extenal to the delegate
var nc = new NaturalComparer();
// Sort by machine, type, item name, and source
items.Sort(delegate (DatItem x, DatItem y)
{
try
{
// Compare on source if renaming
if (!norename)
{
int xSourceIndex = x.GetFieldValue<Source?>(DatItem.SourceKey)?.Index ?? 0;
int ySourceIndex = y.GetFieldValue<Source?>(DatItem.SourceKey)?.Index ?? 0;
if (xSourceIndex != ySourceIndex)
return xSourceIndex - ySourceIndex;
}
// Get the machines
Machine? xMachine = x.GetMachine();
Machine? yMachine = y.GetMachine();
// If machine names don't match
string? xMachineName = xMachine?.GetName();
string? yMachineName = yMachine?.GetName();
if (xMachineName != yMachineName)
return nc.Compare(xMachineName, yMachineName);
// If types don't match
string? xType = x.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey);
string? yType = y.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey);
if (xType != yType)
return xType.AsItemType() - yType.AsItemType();
// If directory names don't match
string? xDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(x.GetName() ?? string.Empty));
string? yDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(y.GetName() ?? string.Empty));
if (xDirectoryName != yDirectoryName)
return nc.Compare(xDirectoryName, yDirectoryName);
// If item names don't match
string? xName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(x.GetName() ?? string.Empty));
string? yName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(y.GetName() ?? string.Empty));
return nc.Compare(xName, yName);
}
catch
{
// Absorb the error
return 0;
}
});
return true;
}
/// <summary>
/// Sort the input DAT and get the key to be used by the item
/// </summary>
/// <param name="datItem">Item to try to match</param>
/// <param name="sorted">True if the DAT is already sorted accordingly, false otherwise (default)</param>
/// <returns>Key to try to use</returns>
private string SortAndGetKey(DatItem datItem, bool sorted = false)
{
// If we're not already sorted, take care of it
if (!sorted)
BucketBy(GetBestAvailable());
// Now that we have the sorted type, we get the proper key
return GetBucketKey(datItem, _bucketedBy, lower: true, norename: true);
}
#endregion
#region Statistics
/// <summary>
/// Recalculate the statistics for the Dat
/// </summary>
public void RecalculateStats()
{
// Wipe out any stats already there
DatStatistics.ResetStatistics();
// If we have a blank Dat in any way, return
if (_items is null || _items.Count == 0)
return;
// Loop through and add
foreach (string key in _items.Keys)
{
List<DatItem>? datItems = _items[key];
if (datItems is null)
continue;
foreach (DatItem item in datItems)
{
DatStatistics.AddItemStatistics(item);
}
}
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
namespace SabreTools.Metadata.DatFiles
{
/// <summary>
/// Class used during deduplication
/// </summary>
public struct ItemMappings(DatItems.DatItem item, long machineId, long sourceId)
{
public DatItems.DatItem Item = item;
public long MachineId = machineId;
public long SourceId = sourceId;
}
}

View File

@@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<IncludeSymbols>true</IncludeSymbols>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>2.3.0</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>DatFile specific functionality for metadata file processing</Description>
<Copyright>Copyright (c) Matt Nadareski 2016-2026</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Serialization</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>metadata dat datfile</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="SabreTools.Metadata.DatFiles.Test" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.Data.Extensions\SabreTools.Data.Extensions.csproj" />
<ProjectReference Include="..\SabreTools.Metadata\SabreTools.Metadata.csproj" />
<ProjectReference Include="..\SabreTools.Metadata.DatItems\SabreTools.Metadata.DatItems.csproj" />
<ProjectReference Include="..\SabreTools.Metadata.Filter\SabreTools.Metadata.Filter.csproj" />
<ProjectReference Include="..\SabreTools.Serialization.CrossModel\SabreTools.Serialization.CrossModel.csproj" />
<ProjectReference Include="..\SabreTools.Serialization.Readers\SabreTools.Serialization.Readers.csproj" />
<ProjectReference Include="..\SabreTools.Serialization.Writers\SabreTools.Serialization.Writers.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
</ItemGroup>
</Project>