mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-21 00:05:19 +00:00
Port metadata functionality from ST
This commit is contained in:
801
SabreTools.Metadata.DatFiles/DatFile.Filtering.cs
Normal file
801
SabreTools.Metadata.DatFiles/DatFile.Filtering.cs
Normal 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
|
||||
}
|
||||
}
|
||||
715
SabreTools.Metadata.DatFiles/DatFile.FromMetadata.cs
Normal file
715
SabreTools.Metadata.DatFiles/DatFile.FromMetadata.cs
Normal 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
|
||||
}
|
||||
}
|
||||
1506
SabreTools.Metadata.DatFiles/DatFile.Splitting.cs
Normal file
1506
SabreTools.Metadata.DatFiles/DatFile.Splitting.cs
Normal file
File diff suppressed because it is too large
Load Diff
1107
SabreTools.Metadata.DatFiles/DatFile.ToMetadata.cs
Normal file
1107
SabreTools.Metadata.DatFiles/DatFile.ToMetadata.cs
Normal file
File diff suppressed because it is too large
Load Diff
1252
SabreTools.Metadata.DatFiles/DatFile.cs
Normal file
1252
SabreTools.Metadata.DatFiles/DatFile.cs
Normal file
File diff suppressed because it is too large
Load Diff
229
SabreTools.Metadata.DatFiles/DatHeader.cs
Normal file
229
SabreTools.Metadata.DatFiles/DatHeader.cs
Normal 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
|
||||
}
|
||||
}
|
||||
88
SabreTools.Metadata.DatFiles/DatModifiers.cs
Normal file
88
SabreTools.Metadata.DatFiles/DatModifiers.cs
Normal 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
|
||||
}
|
||||
}
|
||||
725
SabreTools.Metadata.DatFiles/DatStatistics.cs
Normal file
725
SabreTools.Metadata.DatFiles/DatStatistics.cs
Normal 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
|
||||
}
|
||||
}
|
||||
66
SabreTools.Metadata.DatFiles/DepotInformation.cs
Normal file
66
SabreTools.Metadata.DatFiles/DepotInformation.cs
Normal 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
|
||||
}
|
||||
}
|
||||
267
SabreTools.Metadata.DatFiles/Enums.cs
Normal file
267
SabreTools.Metadata.DatFiles/Enums.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
172
SabreTools.Metadata.DatFiles/Extensions.cs
Normal file
172
SabreTools.Metadata.DatFiles/Extensions.cs
Normal 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
|
||||
}
|
||||
}
|
||||
25
SabreTools.Metadata.DatFiles/Formats/ArchiveDotOrg.cs
Normal file
25
SabreTools.Metadata.DatFiles/Formats/ArchiveDotOrg.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
SabreTools.Metadata.DatFiles/Formats/AttractMode.cs
Normal file
38
SabreTools.Metadata.DatFiles/Formats/AttractMode.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
203
SabreTools.Metadata.DatFiles/Formats/ClrMamePro.cs
Normal file
203
SabreTools.Metadata.DatFiles/Formats/ClrMamePro.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
SabreTools.Metadata.DatFiles/Formats/DosCenter.cs
Normal file
55
SabreTools.Metadata.DatFiles/Formats/DosCenter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
SabreTools.Metadata.DatFiles/Formats/EverdriveSmdb.cs
Normal file
55
SabreTools.Metadata.DatFiles/Formats/EverdriveSmdb.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
601
SabreTools.Metadata.DatFiles/Formats/Hashfile.cs
Normal file
601
SabreTools.Metadata.DatFiles/Formats/Hashfile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
SabreTools.Metadata.DatFiles/Formats/Listrom.cs
Normal file
63
SabreTools.Metadata.DatFiles/Formats/Listrom.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
397
SabreTools.Metadata.DatFiles/Formats/Listxml.cs
Normal file
397
SabreTools.Metadata.DatFiles/Formats/Listxml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
404
SabreTools.Metadata.DatFiles/Formats/Logiqx.cs
Normal file
404
SabreTools.Metadata.DatFiles/Formats/Logiqx.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
214
SabreTools.Metadata.DatFiles/Formats/Missfile.cs
Normal file
214
SabreTools.Metadata.DatFiles/Formats/Missfile.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
47
SabreTools.Metadata.DatFiles/Formats/OfflineList.cs
Normal file
47
SabreTools.Metadata.DatFiles/Formats/OfflineList.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
SabreTools.Metadata.DatFiles/Formats/OpenMSX.cs
Normal file
88
SabreTools.Metadata.DatFiles/Formats/OpenMSX.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
SabreTools.Metadata.DatFiles/Formats/RomCenter.cs
Normal file
51
SabreTools.Metadata.DatFiles/Formats/RomCenter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
719
SabreTools.Metadata.DatFiles/Formats/SabreJSON.cs
Normal file
719
SabreTools.Metadata.DatFiles/Formats/SabreJSON.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
522
SabreTools.Metadata.DatFiles/Formats/SabreXML.cs
Normal file
522
SabreTools.Metadata.DatFiles/Formats/SabreXML.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
191
SabreTools.Metadata.DatFiles/Formats/SeparatedValue.cs
Normal file
191
SabreTools.Metadata.DatFiles/Formats/SeparatedValue.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
121
SabreTools.Metadata.DatFiles/Formats/SerializableDatFile.cs
Normal file
121
SabreTools.Metadata.DatFiles/Formats/SerializableDatFile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
213
SabreTools.Metadata.DatFiles/Formats/SoftwareList.cs
Normal file
213
SabreTools.Metadata.DatFiles/Formats/SoftwareList.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
931
SabreTools.Metadata.DatFiles/ItemDictionary.cs
Normal file
931
SabreTools.Metadata.DatFiles/ItemDictionary.cs
Normal 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
|
||||
}
|
||||
}
|
||||
1263
SabreTools.Metadata.DatFiles/ItemDictionaryDB.cs
Normal file
1263
SabreTools.Metadata.DatFiles/ItemDictionaryDB.cs
Normal file
File diff suppressed because it is too large
Load Diff
12
SabreTools.Metadata.DatFiles/ItemMappings.cs
Normal file
12
SabreTools.Metadata.DatFiles/ItemMappings.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user