Files
MPF/MPF.Frontend/Tools/SubmissionGenerator.cs
2025-11-25 09:14:46 -05:00

1108 lines
55 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
#if NET35_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Text;
using System.Threading.Tasks;
using BinaryObjectScanner;
using MPF.Processors;
using SabreTools.RedumpLib;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.Data.Sections;
using SabreTools.RedumpLib.Web;
namespace MPF.Frontend.Tools
{
/// <summary>
/// Generator for SubmissionInfo objects
/// </summary>
internal static class SubmissionGenerator
{
#region Constants
private const string RequiredValue = "(REQUIRED)";
private const string RequiredIfExistsValue = "(REQUIRED, IF EXISTS)";
private const string OptionalValue = "(OPTIONAL)";
#endregion
#region Extraction and Filling
/// <summary>
/// Extract all of the possible information from a given input combination
/// </summary>
/// <param name="outputPath">Output path to write to</param>
/// <param name="drive">Drive object representing the current drive</param>
/// <param name="system">Currently selected system</param>
/// <param name="mediaType">Currently selected media type</param>
/// <param name="options">Options object representing user-defined options</param>
/// <param name="processor">Processor object representing how to process the outputs</param>
/// <param name="resultProgress">Optional result progress callback</param>
/// <param name="protectionProgress">Optional protection progress callback</param>
/// <returns>SubmissionInfo populated based on outputs, null on error</returns>
public static async Task<SubmissionInfo?> ExtractOutputInformation(
string outputPath,
Drive? drive,
RedumpSystem? system,
MediaType? mediaType,
Options options,
BaseProcessor processor,
IProgress<ResultEventArgs>? resultProgress = null,
IProgress<ProtectionProgress>? protectionProgress = null)
{
// Split the output path for easier use
var outputDirectory = Path.GetDirectoryName(outputPath);
string outputFilename = Path.GetFileName(outputPath);
// If a standard log zip was provided, replace the suffix with ".tmp" for easier processing
if (outputFilename.EndsWith("_logs.zip", StringComparison.OrdinalIgnoreCase))
{
int zipSuffixIndex = outputFilename.LastIndexOf("_logs.zip", StringComparison.OrdinalIgnoreCase);
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
outputFilename = outputFilename[..zipSuffixIndex] + ".tmp";
#else
outputFilename = outputFilename.Substring(0, zipSuffixIndex) + ".tmp";
#endif
}
// Check that all of the relevant files are there
List<string> missingFiles = processor.FoundAllFiles(mediaType, outputDirectory, outputFilename);
if (missingFiles.Count > 0)
{
resultProgress?.Report(ResultEventArgs.Failure($"There were files missing from the output:\n{string.Join("\n", [.. missingFiles])}\nThis may indicate an issue with the hardware or media, including unsupported devices.\nPlease see dumping program documentation for more details."));
return null;
}
// Extract files from existing log archive, if it exists
#if NET462_OR_GREATER || NETCOREAPP
processor.ExtractFromLogs(mediaType, outputDirectory, outputFilename);
#endif
// Assemble a base path
string basePath = Path.GetFileNameWithoutExtension(outputFilename);
if (!string.IsNullOrEmpty(outputDirectory))
basePath = Path.Combine(outputDirectory, basePath);
// Create the default submission info
SubmissionInfo info = CreateDefaultSubmissionInfo(processor, system, mediaType, options.AddPlaceholders);
// Get specific tool output handling
processor.GenerateSubmissionInfo(info, mediaType, basePath, options.EnableRedumpCompatibility);
if (options.IncludeArtifacts)
info.Artifacts = processor.GenerateArtifacts(mediaType, outputDirectory, outputFilename);
// Get a list of matching IDs for each line in the DAT
if (!string.IsNullOrEmpty(info.TracksAndWriteOffsets.ClrMameProData))
{
bool filledInfo = await FillFromRedump(options, info, resultProgress);
// Add a placeholder for the logs link if not a verification
if (!filledInfo)
info.CommonDiscInfo.CommentsSpecialFields[SiteCode.LogsLink] = "[Please provide a link to your logs here]";
}
// If we have both ClrMamePro and Size and Checksums data, remove the ClrMamePro
if (!string.IsNullOrEmpty(info.SizeAndChecksums.CRC32))
info.TracksAndWriteOffsets.ClrMameProData = null;
// Add the volume label to comments, if possible or necessary
string? volLabels = FormatVolumeLabels(drive?.VolumeLabel, processor.VolumeLabels);
if (volLabels != null)
info.CommonDiscInfo.CommentsSpecialFields[SiteCode.VolumeLabel] = volLabels;
// Extract info based generically on MediaType
ProcessMediaType(info, mediaType, options.AddPlaceholders);
// Extract info based specifically on RedumpSystem
ProcessSystem(info, system, drive, options.AddPlaceholders, processor is DiscImageCreator, basePath);
// Run anti-modchip check, if necessary
if (drive != null && system.SupportsAntiModchipScans() && info.CopyProtection.AntiModchip == YesNo.NULL)
{
resultProgress?.Report(ResultEventArgs.Success("Checking for anti-modchip strings... this might take a while!"));
info.CopyProtection.AntiModchip = await ProtectionTool.GetPlayStationAntiModchipDetected(drive?.Name) ? YesNo.Yes : YesNo.No;
resultProgress?.Report(ResultEventArgs.Success("Anti-modchip string scan complete!"));
}
// Run copy protection, if possible or necessary
if (system.SupportsCopyProtectionScans())
{
resultProgress?.Report(ResultEventArgs.Success("Running copy protection scan... this might take a while!"));
try
{
Dictionary<string, List<string>>? protections = null;
if (options.ScanForProtection)
{
// Explicitly note missing/invalid device paths
if (drive?.Name == null)
resultProgress?.Report(ResultEventArgs.Success("No mounted device path found, protection outputs may be incomplete!"));
protections = await ProtectionTool.RunCombinedProtectionScans(basePath, drive, options, protectionProgress);
}
var protectionString = ProtectionTool.FormatProtections(protections, drive);
info.CopyProtection.Protection += protectionString;
info.CopyProtection.FullProtections = ReformatProtectionDictionary(protections);
resultProgress?.Report(ResultEventArgs.Success("Copy protection scan complete!"));
}
catch (Exception ex)
{
resultProgress?.Report(ResultEventArgs.Failure(ex.ToString()));
}
}
// Set fields that may have automatic filling otherwise
info.CommonDiscInfo.Category ??= DiscCategory.Games;
info.VersionAndEditions.Version ??= options.AddPlaceholders ? RequiredIfExistsValue : string.Empty;
// Comments and contents have odd handling
if (string.IsNullOrEmpty(info.CommonDiscInfo.Comments))
info.CommonDiscInfo.Comments = options.AddPlaceholders ? OptionalValue : string.Empty;
if (string.IsNullOrEmpty(info.CommonDiscInfo.Contents))
info.CommonDiscInfo.Contents = options.AddPlaceholders ? OptionalValue : string.Empty;
// Normalize the disc type with all current information
Validator.NormalizeDiscType(info);
return info;
}
/// <summary>
/// Fill in a SubmissionInfo object from Redump, if possible
/// </summary>
/// <param name="options">Options object representing user-defined options</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="resultProgress">Optional result progress callback</param>
public async static Task<bool> FillFromRedump(Options options,
SubmissionInfo info,
IProgress<ResultEventArgs>? resultProgress = null)
{
// If information should not be pulled at all
if (!options.RetrieveMatchInformation)
return false;
// Set the current dumper based on username
info.DumpersAndStatus.Dumpers = [options.RedumpUsername ?? "Anonymous User"];
info.PartiallyMatchedIDs = [];
// Login to Redump, if possible
var wc = new RedumpClient();
if (!string.IsNullOrEmpty(options.RedumpUsername) && !string.IsNullOrEmpty(options.RedumpPassword))
{
bool? loggedIn = await wc.Login(options.RedumpUsername!, options.RedumpPassword!);
if (loggedIn == null)
{
resultProgress?.Report(ResultEventArgs.Failure("There was an unknown error connecting to Redump, skipping..."));
return false;
}
else if (loggedIn == false)
{
resultProgress?.Report(ResultEventArgs.Failure("Provided Redump credentials were invalid, not using..."));
}
}
// Setup the checks
bool allFound = true;
List<int[]> foundIdSets = [];
// Loop through all of the hashdata to find matching IDs
resultProgress?.Report(ResultEventArgs.Success("Finding disc matches on Redump..."));
var splitData = info.TracksAndWriteOffsets.ClrMameProData?.TrimEnd('\n')?.Split('\n');
int trackCount = splitData?.Length ?? 0;
foreach (string hashData in splitData ?? [])
{
// Catch any errant blank lines
if (string.IsNullOrEmpty(hashData))
{
trackCount--;
resultProgress?.Report(ResultEventArgs.Success("Blank line found, skipping!"));
continue;
}
// If the line ends in a known extra track names, skip them for checking
// TODO: Smarter method to ignore all tracks that start with 0. 00. A. or AA.
if (hashData.Contains(".dmi")
|| hashData.Contains(".pfi")
|| hashData.Contains(".ss")
|| hashData.Contains("(Track 0).bin")
|| hashData.Contains("(Track 0.1).bin")
|| hashData.Contains("(Track 0.2).bin")
|| hashData.Contains("(Track 0.3).bin")
|| hashData.Contains("(Track 0.4).bin")
|| hashData.Contains("(Track 0.5).bin")
|| hashData.Contains("(Track 00).bin")
|| hashData.Contains("(Track 00.1).bin")
|| hashData.Contains("(Track 00.2).bin")
|| hashData.Contains("(Track 00.3).bin")
|| hashData.Contains("(Track 00.4).bin")
|| hashData.Contains("(Track 00.5).bin")
|| hashData.Contains("(Track A).bin")
|| hashData.Contains("(Track A.1).bin")
|| hashData.Contains("(Track A.2).bin")
|| hashData.Contains("(Track A.3).bin")
|| hashData.Contains("(Track A.4).bin")
|| hashData.Contains("(Track A.5).bin")
|| hashData.Contains("(Track AA).bin")
|| hashData.Contains("(Track AA.1).bin")
|| hashData.Contains("(Track AA.2).bin")
|| hashData.Contains("(Track AA.3).bin")
|| hashData.Contains("(Track AA.4).bin")
|| hashData.Contains("(Track AA.5).bin"))
{
trackCount--;
resultProgress?.Report(ResultEventArgs.Success("Extra track found, skipping!"));
continue;
}
// Get the SHA-1 hash
if (!ProcessingTool.GetISOHashValues(hashData, out _, out _, out _, out string? sha1))
{
resultProgress?.Report(ResultEventArgs.Failure($"Line could not be parsed: {hashData}"));
continue;
}
var foundIds = await Validator.ValidateSingleTrack(wc, info, sha1);
if (foundIds != null && foundIds.Count == 1)
resultProgress?.Report(ResultEventArgs.Success($"Single match found for {sha1}"));
else if (foundIds != null && foundIds.Count != 1)
resultProgress?.Report(ResultEventArgs.Success($"Multiple matches found for {sha1}"));
else
resultProgress?.Report(ResultEventArgs.Failure($"No matches found for {sha1}"));
// Add the found IDs to the map
foundIdSets.Add(foundIds?.ToArray() ?? []);
// Ensure that all tracks are found
allFound &= (foundIds != null && foundIds.Count >= 1);
}
// If all tracks were found, check if there are any fully-matched IDs
HashSet<int>? fullyMatchedIdsSet = null;
if (allFound)
{
fullyMatchedIdsSet = null;
foreach (var set in foundIdSets)
{
// First track is always all IDs
if (fullyMatchedIdsSet == null)
{
fullyMatchedIdsSet = [.. set];
continue;
}
// Try to intersect with all known IDs
fullyMatchedIdsSet.IntersectWith(set);
if (fullyMatchedIdsSet.Count == 0)
break;
}
}
// If we don't have any matches but we have a universal hash
if (info.PartiallyMatchedIDs.Count == 0 && info.CommonDiscInfo?.CommentsSpecialFields?.ContainsKey(SiteCode.UniversalHash) == true)
{
string sha1 = info.CommonDiscInfo.CommentsSpecialFields[SiteCode.UniversalHash];
var foundIds = await Validator.ValidateUniversalHash(wc, info);
if (foundIds != null && foundIds.Count == 1)
resultProgress?.Report(ResultEventArgs.Success($"Single match found for universal hash {sha1}"));
else if (foundIds != null && foundIds.Count != 1)
resultProgress?.Report(ResultEventArgs.Success($"Multiple matches found for universal hash {sha1}"));
else
resultProgress?.Report(ResultEventArgs.Failure($"No matches found for universal hash {sha1}"));
// Ensure that the hash is found
allFound = (foundIds != null && foundIds.Count == 1);
// If we found a match, then the disc is a match
if (foundIds != null && foundIds.Count == 1)
fullyMatchedIdsSet = [.. foundIds];
else
fullyMatchedIdsSet = [];
}
// Get a list version of the fully matched IDs
List<int> fullyMatchedIdsList = fullyMatchedIdsSet != null ? [.. fullyMatchedIdsSet] : [];
// Make sure we only have unique IDs
var partiallyMatchedIds = new HashSet<int>();
partiallyMatchedIds.IntersectWith(info.PartiallyMatchedIDs);
info.PartiallyMatchedIDs = [.. partiallyMatchedIds];
info.PartiallyMatchedIDs.Sort();
resultProgress?.Report(ResultEventArgs.Success("Match finding complete! " + (fullyMatchedIdsList != null && fullyMatchedIdsList.Count > 0
? "Fully Matched IDs: " + string.Join(",", [.. fullyMatchedIdsList.ConvertAll(i => i.ToString())])
: "No matches found")));
// Exit early if one failed or there are no matched IDs
if (!allFound || fullyMatchedIdsList == null || fullyMatchedIdsList.Count == 0)
return false;
// Find the first matched ID where the track count matches, we can grab a bunch of info from it
int totalMatchedIDsCount = fullyMatchedIdsList.Count;
for (int i = 0; i < totalMatchedIDsCount; i++)
{
// Skip if the track count doesn't match
if (!await Validator.ValidateTrackCount(wc, fullyMatchedIdsList[i], trackCount))
continue;
// Fill in the fields from the existing ID
resultProgress?.Report(ResultEventArgs.Success($"Filling fields from existing ID {fullyMatchedIdsList[i]}..."));
_ = await Builder.FillFromId(wc, info, fullyMatchedIdsList[i], options.PullAllInformation);
resultProgress?.Report(ResultEventArgs.Success("Information filling complete!"));
// Set the fully matched ID to the current
info.FullyMatchedID = fullyMatchedIdsList[i];
break;
}
// Clear out fully matched IDs from the partial list
if (info.FullyMatchedID.HasValue)
{
if (info.PartiallyMatchedIDs.Count == 1)
info.PartiallyMatchedIDs = null;
else
info.PartiallyMatchedIDs.Remove(info.FullyMatchedID.Value);
}
return true;
}
#endregion
#region Helper Functions
/// <summary>
/// Creates a default SubmissionInfo object based on the current system and media type
/// </summary>
private static SubmissionInfo CreateDefaultSubmissionInfo(BaseProcessor processor, RedumpSystem? system, MediaType? mediaType, bool addPlaceholders)
{
// Create the template object
var info = new SubmissionInfo()
{
CommonDiscInfo = new CommonDiscInfoSection()
{
System = system,
Media = mediaType.ToDiscType(),
Title = addPlaceholders ? RequiredValue : string.Empty,
ForeignTitleNonLatin = addPlaceholders ? OptionalValue : string.Empty,
DiscNumberLetter = addPlaceholders ? OptionalValue : string.Empty,
DiscTitle = addPlaceholders ? OptionalValue : string.Empty,
Category = null,
Region = null,
Languages = null,
Serial = addPlaceholders ? RequiredIfExistsValue : string.Empty,
Barcode = addPlaceholders ? OptionalValue : string.Empty,
Contents = string.Empty,
},
VersionAndEditions = new VersionAndEditionsSection()
{
OtherEditions = addPlaceholders ? "(VERIFY THIS) Original" : string.Empty,
},
DumpingInfo = new DumpingInfoSection()
{
FrontendVersion = FrontendTool.GetCurrentVersion(),
DumpingProgram = GetDumpingProgramFromProcessor(processor),
},
};
return info;
}
/// <summary>
/// Get the dumping program name from the processor
/// </summary>
/// <param name="processor"></param>
/// <returns></returns>
private static string? GetDumpingProgramFromProcessor(BaseProcessor processor)
{
// Map to the internal program
InternalProgram? internalProgram = processor switch
{
Processors.Aaru => InternalProgram.Aaru,
CleanRip => InternalProgram.CleanRip,
DiscImageCreator => InternalProgram.DiscImageCreator,
PS3CFW => InternalProgram.PS3CFW,
Redumper => InternalProgram.Redumper,
UmdImageCreator => InternalProgram.UmdImageCreator,
XboxBackupCreator => InternalProgram.XboxBackupCreator,
_ => null,
};
// Use the internal program to map to the name
return internalProgram.LongName();
}
/// <summary>
/// Simplifies a volume label into uppercase alphanumeric only string
/// </summary>
/// <param name="labels">Volume label to simplify</param>
/// <returns>Simplified volume label, null if empty or invalid</returns>
private static string? SimplifyVolumeLabel(string? label)
{
// Ignore empty labels
if (label == null || label.Length == 0)
return null;
// Take only ASCII alphanumeric characters
var labelBuilder = new StringBuilder();
foreach (char c in label)
{
if (c >= '0' && c <= '9')
labelBuilder.Append(c);
else if (c >= 'A' && c <= 'Z')
labelBuilder.Append(c);
else if (c >= 'a' && c <= 'z')
labelBuilder.Append(char.ToUpper(c));
}
// Ignore non-ASCII labels
string? simpleLabel = labelBuilder.ToString();
if (simpleLabel == null || simpleLabel.Length == 0)
return null;
return simpleLabel;
}
/// <summary>
/// Formats a list of volume labels and their corresponding filesystems
/// </summary>
/// <param name="labels">Dictionary of volume labels and their filesystems</param>
/// <returns>Formatted string of volume labels and their filesystems</returns>
private static string? FormatVolumeLabels(string? driveLabel, Dictionary<string, List<string>>? labels)
{
// Treat empty label as null
if (driveLabel != null && driveLabel.Length == 0)
driveLabel = null;
// Treat "path" labels as null -- Indicates a mounted path
// This can over-match if a label contains a directory separator somehow
if (driveLabel != null && (driveLabel.Contains("/") || driveLabel.Contains("\\")))
driveLabel = null;
// Must have at least one label to format
if (driveLabel == null && (labels == null || labels.Count == 0))
return null;
// If no labels given, use drive label
if (labels == null || labels.Count == 0)
{
// Ignore common volume labels
if (FrontendTool.GetRedumpSystemFromVolumeLabel(driveLabel) != null)
return null;
return driveLabel;
}
// Get the default label to compare against
// TODO: Full pairwise comparison of all labels, not just comparing against drive/UDF label.
string? defaultLabel = null;
if (driveLabel != null && driveLabel.Length != 0)
{
defaultLabel = SimplifyVolumeLabel(driveLabel);
}
#if NET35_OR_GREATER || NETCOREAPP
else
{
defaultLabel = labels
.Where(label => label.Value.Contains("UDF"))
.Select(label => label.Key)
.FirstOrDefault();
}
#endif
// Remove duplicate/useless volume labels
if (defaultLabel != null && defaultLabel.Length != 0)
{
List<string> keys = [.. labels.Keys];
foreach (var label in keys)
{
// Always remove "DVD_ROM" / "CD_ROM" labels
if (label == "DVD_ROM" || label == "CD_ROM")
{
labels.Remove(label);
continue;
}
// Get upper-case ASCII variant of the label
string? tempLabel = SimplifyVolumeLabel(label);
if (tempLabel == null)
continue;
// Remove duplicate volume labels
if (defaultLabel == tempLabel)
labels.Remove(label);
}
}
// If no labels are left, use drive label
if (labels == null || labels.Count == 0)
{
// Ignore common volume labels
if (FrontendTool.GetRedumpSystemFromVolumeLabel(driveLabel) != null)
return null;
return driveLabel;
}
// If only one unique label left, don't mention fs
#if NET20
string[] keyArr = [.. labels.Keys];
string firstLabel = keyArr[0];
#else
string firstLabel = labels.First().Key;
#endif
if (labels.Count == 1 && (firstLabel == driveLabel || driveLabel == null))
{
// Ignore common volume labels
if (FrontendTool.GetRedumpSystemFromVolumeLabel(firstLabel) != null)
return null;
return firstLabel;
}
// Otherwise, state filesystem for each label
List<string> volLabels = [];
// Begin formatted output with the label from Windows, if it is unique and not a common volume label
if (driveLabel != null && !labels.TryGetValue(driveLabel, out List<string>? value) && FrontendTool.GetRedumpSystemFromVolumeLabel(driveLabel) == null)
volLabels.Add(driveLabel);
// Add remaining labels with their corresponding filesystems
foreach (var kvp in labels)
{
// Ignore common volume labels
if (FrontendTool.GetRedumpSystemFromVolumeLabel(kvp.Key) == null)
volLabels.Add($"{kvp.Key} ({string.Join(", ", [.. kvp.Value])})");
}
// Ensure that no labels are empty
volLabels = volLabels.FindAll(label => !string.IsNullOrEmpty(label?.Trim()));
// Print each label separated by a comma and a space
if (volLabels.Count == 0)
return null;
return string.Join(", ", [.. volLabels]);
}
/// <summary>
/// Processes default data based on media type
/// </summary>
private static bool ProcessMediaType(SubmissionInfo info, MediaType? mediaType, bool addPlaceholders)
{
switch (mediaType)
{
case MediaType.CDROM:
case MediaType.GDROM:
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0AdditionalMould = addPlaceholders ? RequiredIfExistsValue : string.Empty;
break;
case MediaType.DVD:
case MediaType.HDDVD:
case MediaType.BluRay:
// If we have a single-layer disc
if (info.SizeAndChecksums.Layerbreak == default)
{
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0AdditionalMould = addPlaceholders ? RequiredIfExistsValue : string.Empty;
}
// If we have a dual-layer disc
else if (info.SizeAndChecksums.Layerbreak2 == default)
{
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0AdditionalMould = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
}
// If we have a triple-layer disc
else if (info.SizeAndChecksums.Layerbreak3 == default)
{
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0AdditionalMould = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer2MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer2MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer2ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
}
// If we have a quad-layer disc
else
{
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0AdditionalMould = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer2MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer2MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer2ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer3MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer3MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer3ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
}
break;
case MediaType.NintendoGameCubeGameDisc:
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0AdditionalMould = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.Extras.BCA ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case MediaType.NintendoWiiOpticalDisc:
case MediaType.NintendoWiiUOpticalDisc:
// If we have a single-layer disc
if (info.SizeAndChecksums.Layerbreak == default)
{
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0AdditionalMould = addPlaceholders ? RequiredIfExistsValue : string.Empty;
}
// If we have a dual-layer disc
else
{
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0AdditionalMould = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
}
info.Extras.DiscKey = addPlaceholders ? RequiredValue : string.Empty;
info.Extras.BCA ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case MediaType.UMD:
// Both single- and dual-layer discs have two "layers" for the ring
info.CommonDiscInfo.Layer0MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer0MouldSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringRing = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1MasteringSID = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.CommonDiscInfo.Layer1ToolstampMasteringCode = addPlaceholders ? RequiredIfExistsValue : string.Empty;
info.SizeAndChecksums.CRC32 ??= (addPlaceholders ? RequiredValue + " [Not automatically generated for UMD]" : string.Empty);
info.SizeAndChecksums.MD5 ??= (addPlaceholders ? RequiredValue + " [Not automatically generated for UMD]" : string.Empty);
info.SizeAndChecksums.SHA1 ??= (addPlaceholders ? RequiredValue + " [Not automatically generated for UMD]" : string.Empty);
info.TracksAndWriteOffsets.ClrMameProData = null;
break;
}
return true;
}
/// <summary>
/// Processes default data based on system type
/// </summary>
private static bool ProcessSystem(SubmissionInfo info, RedumpSystem? system, Drive? drive, bool addPlaceholders, bool isDiscImageCreator, string basePath)
{
// Extract info based specifically on RedumpSystem
switch (system)
{
case RedumpSystem.AcornArchimedes:
info.CommonDiscInfo.Region ??= Region.UnitedKingdom;
break;
case RedumpSystem.AudioCD:
case RedumpSystem.DVDAudio:
case RedumpSystem.EnhancedCD:
case RedumpSystem.SuperAudioCD:
info.CommonDiscInfo.Category ??= DiscCategory.Audio;
break;
case RedumpSystem.BandaiPlaydiaQuickInteractiveSystem:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
info.CommonDiscInfo.Region ??= info.CommonDiscInfo.Region ?? Region.Japan;
break;
case RedumpSystem.BDVideo:
info.CommonDiscInfo.Category ??= DiscCategory.Video;
bool bee = PhysicalTool.GetBusEncryptionEnabled(drive);
if (bee && string.IsNullOrEmpty(info.CopyProtection.Protection))
info.CopyProtection.Protection = "Bus encryption enabled flag set";
else if (bee)
info.CopyProtection.Protection += "\nBus encryption enabled flag set";
else
info.CopyProtection.Protection ??= addPlaceholders ? RequiredIfExistsValue : string.Empty;
break;
case RedumpSystem.DVDVideo:
case RedumpSystem.HDDVDVideo:
info.CommonDiscInfo.Category ??= DiscCategory.Video;
info.CopyProtection.Protection ??= addPlaceholders ? RequiredIfExistsValue : string.Empty;
break;
case RedumpSystem.CommodoreAmigaCD:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.CommodoreAmigaCD32:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
info.CommonDiscInfo.Region ??= Region.Europe;
break;
case RedumpSystem.CommodoreAmigaCDTV:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
info.CommonDiscInfo.Region ??= Region.Europe;
break;
case RedumpSystem.FujitsuFMTownsseries:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
info.CommonDiscInfo.Region ??= Region.Japan;
break;
case RedumpSystem.FujitsuFMTownsMarty:
info.CommonDiscInfo.Region ??= Region.Japan;
break;
case RedumpSystem.HasbroVideoNow:
case RedumpSystem.HasbroVideoNowColor:
case RedumpSystem.HasbroVideoNowJr:
case RedumpSystem.VideoCD:
info.CommonDiscInfo.Category ??= DiscCategory.Video;
break;
case RedumpSystem.HasbroVideoNowXP:
case RedumpSystem.PhotoCD:
case RedumpSystem.SonyElectronicBook:
info.CommonDiscInfo.Category ??= DiscCategory.Multimedia;
break;
case RedumpSystem.IncredibleTechnologiesEagle:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.KonamieAmusement:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.KonamiFireBeat:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.KonamiPython2:
string? kp2Exe = PhysicalTool.GetPlayStationExecutableName(drive);
// TODO: Remove this hack when DIC supports build date output
if (isDiscImageCreator)
info.CommonDiscInfo.EXEDateBuildDate = DiscImageCreator.GetPlayStationEXEDate($"{basePath}_volDesc.txt", kp2Exe);
SetCommentFieldIfNotExists(info, SiteCode.InternalSerialName, drive, PhysicalTool.GetPlayStationSerial);
info.CommonDiscInfo.EXEDateBuildDate ??= PhysicalTool.GetFileDate(drive, kp2Exe, fixTwoDigitYear: true);
if (CommentFieldExists(info, SiteCode.InternalSerialName, out kp2Exe))
info.CommonDiscInfo.Region = ProcessingTool.GetPlayStationRegion(kp2Exe);
SetVersionIfNotExists(info, drive, PhysicalTool.GetPlayStation2Version);
break;
case RedumpSystem.KonamiSystemGV:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.KonamiSystem573:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.KonamiTwinkle:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.MattelHyperScan:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.MicrosoftXboxOne:
info.CommonDiscInfo.CommentsSpecialFields[SiteCode.Filename] = PhysicalTool.GetXboxFilenames(drive) ?? string.Empty;
info.CommonDiscInfo.CommentsSpecialFields[SiteCode.TitleID] = PhysicalTool.GetXboxTitleID(drive) ?? string.Empty;
break;
case RedumpSystem.MicrosoftXboxSeriesXS:
info.CommonDiscInfo.CommentsSpecialFields[SiteCode.Filename] = PhysicalTool.GetXboxFilenames(drive) ?? string.Empty;
info.CommonDiscInfo.CommentsSpecialFields[SiteCode.TitleID] = PhysicalTool.GetXboxTitleID(drive) ?? string.Empty;
break;
case RedumpSystem.NamcoSegaNintendoTriforce:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.NavisoftNaviken21:
info.CommonDiscInfo.EXEDateBuildDate = addPlaceholders ? RequiredValue : string.Empty;
info.CommonDiscInfo.Region ??= Region.Japan;
break;
case RedumpSystem.NECPC88series:
info.CommonDiscInfo.Region ??= Region.Japan;
break;
case RedumpSystem.NECPC98series:
info.CommonDiscInfo.EXEDateBuildDate = addPlaceholders ? RequiredValue : string.Empty;
info.CommonDiscInfo.Region ??= Region.Japan;
break;
case RedumpSystem.NECPCFXPCFXGA:
info.CommonDiscInfo.Region ??= Region.Japan;
break;
case RedumpSystem.SegaChihiro:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.SegaDreamcast:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.SegaNaomi:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.SegaNaomi2:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.SegaTitanVideo:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.SharpX68000:
info.CommonDiscInfo.Region ??= Region.Japan;
break;
case RedumpSystem.SNKNeoGeoCD:
info.CommonDiscInfo.EXEDateBuildDate ??= addPlaceholders ? RequiredValue : string.Empty;
break;
case RedumpSystem.SonyPlayStation:
string? ps1Exe = PhysicalTool.GetPlayStationExecutableName(drive);
// TODO: Remove this hack when DIC supports build date output
if (isDiscImageCreator)
info.CommonDiscInfo.EXEDateBuildDate = DiscImageCreator.GetPlayStationEXEDate($"{basePath}_volDesc.txt", ps1Exe, psx: true);
SetCommentFieldIfNotExists(info, SiteCode.InternalSerialName, drive, PhysicalTool.GetPlayStationSerial);
info.CommonDiscInfo.EXEDateBuildDate ??= PhysicalTool.GetFileDate(drive, ps1Exe, fixTwoDigitYear: true);
if (CommentFieldExists(info, SiteCode.InternalSerialName, out ps1Exe))
info.CommonDiscInfo.Region = ProcessingTool.GetPlayStationRegion(ps1Exe);
break;
case RedumpSystem.SonyPlayStation2:
info.CommonDiscInfo.LanguageSelection ??= [];
string? ps2Exe = PhysicalTool.GetPlayStationExecutableName(drive);
// TODO: Remove this hack when DIC supports build date output
if (isDiscImageCreator)
info.CommonDiscInfo.EXEDateBuildDate = DiscImageCreator.GetPlayStationEXEDate($"{basePath}_volDesc.txt", ps2Exe);
SetCommentFieldIfNotExists(info, SiteCode.InternalSerialName, drive, PhysicalTool.GetPlayStationSerial);
info.CommonDiscInfo.EXEDateBuildDate ??= PhysicalTool.GetFileDate(drive, ps2Exe, fixTwoDigitYear: true);
if (CommentFieldExists(info, SiteCode.InternalSerialName, out ps2Exe))
info.CommonDiscInfo.Region = ProcessingTool.GetPlayStationRegion(ps2Exe);
SetVersionIfNotExists(info, drive, PhysicalTool.GetPlayStation2Version);
break;
case RedumpSystem.SonyPlayStation3:
info.Extras.DiscKey ??= addPlaceholders ? RequiredValue : string.Empty;
info.Extras.DiscID ??= addPlaceholders ? RequiredValue : string.Empty;
SetCommentFieldIfNotExists(info, SiteCode.InternalSerialName, drive, PhysicalTool.GetPlayStation3Serial);
SetVersionIfNotExists(info, drive, PhysicalTool.GetPlayStation3Version);
SetContentFieldIfNotExists(info, SiteCode.Patches, drive, FormatPlayStation3FirmwareVersion);
break;
case RedumpSystem.SonyPlayStation4:
SetCommentFieldIfNotExists(info, SiteCode.InternalSerialName, drive, PhysicalTool.GetPlayStation4Serial);
SetVersionIfNotExists(info, drive, PhysicalTool.GetPlayStation4Version);
SetContentFieldIfNotExists(info, SiteCode.Games, drive, PhysicalTool.GetPlayStation4PkgInfo);
break;
case RedumpSystem.SonyPlayStation5:
SetCommentFieldIfNotExists(info, SiteCode.InternalSerialName, drive, PhysicalTool.GetPlayStation5Serial);
SetVersionIfNotExists(info, drive, PhysicalTool.GetPlayStation5Version);
SetContentFieldIfNotExists(info, SiteCode.Games, drive, PhysicalTool.GetPlayStation5PkgInfo);
break;
case RedumpSystem.TomyKissSite:
info.CommonDiscInfo.Category ??= DiscCategory.Video;
info.CommonDiscInfo.Region ??= Region.Japan;
break;
case RedumpSystem.ZAPiTGamesGameWaveFamilyEntertainmentSystem:
info.CopyProtection.Protection ??= addPlaceholders ? RequiredIfExistsValue : string.Empty;
break;
}
return true;
}
/// <summary>
/// Get a preformatted string for the PS3 firmware version, if possible
/// </summary>
private static string? FormatPlayStation3FirmwareVersion(Drive? drive)
{
string? firmwareVersion = PhysicalTool.GetPlayStation3FirmwareVersion(drive);
if (string.IsNullOrEmpty(firmwareVersion))
return string.Empty;
return $"PS3 Firmware {firmwareVersion}";
}
/// <summary>
/// Determine if a comment field exists based on key
/// </summary>
private static bool CommentFieldExists(SubmissionInfo info, SiteCode key, out string? value)
{
// Ensure the comments fields exist
if (info.CommonDiscInfo.CommentsSpecialFields == null)
info.CommonDiscInfo.CommentsSpecialFields = [];
// Check if the field exists
if (!info.CommonDiscInfo.CommentsSpecialFields.TryGetValue(key, out value))
return false;
if (string.IsNullOrEmpty(value))
return false;
// The value is valid
return true;
}
/// <summary>
/// Set a comment field if it doesn't already have a value
/// </summary>
private static void SetCommentFieldIfNotExists(SubmissionInfo info, SiteCode key, Drive? drive, System.Func<Drive?, string?> valueFunc)
{
// If the field has a valid value, skip
if (CommentFieldExists(info, key, out _))
return;
// Set the value
string? value = valueFunc(drive);
if (value != null)
info.CommonDiscInfo.CommentsSpecialFields[key] = value;
}
/// <summary>
/// Determine if a content field exists based on key
/// </summary>
private static bool ContentFieldExists(SubmissionInfo info, SiteCode key, out string? value)
{
// Ensure the contents fields exist
if (info.CommonDiscInfo.ContentsSpecialFields == null)
info.CommonDiscInfo.ContentsSpecialFields = [];
// Check if the field exists
if (!info.CommonDiscInfo.ContentsSpecialFields.TryGetValue(key, out value))
return false;
if (string.IsNullOrEmpty(value))
return false;
// The value is valid
return true;
}
/// <summary>
/// Set a content field if it doesn't already have a value
/// </summary>
private static void SetContentFieldIfNotExists(SubmissionInfo info, SiteCode key, Drive? drive, Func<Drive?, string?> valueFunc)
{
// If the field has a valid value, skip
if (ContentFieldExists(info, key, out _))
return;
// Set the value
string? value = valueFunc(drive);
if (value != null)
info.CommonDiscInfo.ContentsSpecialFields![key] = value;
}
/// <summary>
/// Set the version if it doesn't already have a value
/// </summary>
private static void SetVersionIfNotExists(SubmissionInfo info, Drive? drive, Func<Drive?, string?> valueFunc)
{
// If the version already exists, skip
if (!string.IsNullOrEmpty(info.VersionAndEditions.Version))
return;
// Set the version
info.VersionAndEditions.Version = valueFunc(drive) ?? string.Empty;
}
/// <summary>
/// Reformat a protection dictionary for submission info
/// </summary>
/// <param name="oldDict">ProtectionDictionary to format</param>
/// <returns>Reformatted dictionary on success, empty on error</returns>
private static Dictionary<string, List<string>?> ReformatProtectionDictionary(Dictionary<string, List<string>>? oldDict)
{
// Null or empty protections return empty
if (oldDict == null || oldDict.Count == 0)
return [];
// Reformat each set into a List
var newDict = new Dictionary<string, List<string>?>();
foreach (string key in oldDict.Keys)
{
newDict[key] = [.. oldDict[key]];
}
return newDict;
}
#endregion
}
}