Files
MPF/MPF.Core/InfoTool.cs
2023-10-18 22:19:25 -04:00

2819 lines
126 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using BurnOutSharp;
using MPF.Core.Data;
using MPF.Core.Hashing;
using MPF.Core.Modules;
using MPF.Core.Utilities;
using Newtonsoft.Json;
using SabreTools.Models.PIC;
using SabreTools.RedumpLib.Data;
using Formatting = Newtonsoft.Json.Formatting;
namespace MPF.Core
{
public static class InfoTool
{
#region Information Extraction
/// <summary>
/// Ensures that all required output files have been created
/// </summary>
/// <param name="outputDirectory">Output folder to write to</param>
/// <param name="outputFilename">Output filename to use as the base path</param>
/// <param name="parameters">Parameters object representing what to send to the internal program</param>
/// <param name="preCheck">True if this is a check done before a dump, false if done after</param>
/// <returns>Tuple of true if all required files exist, false otherwise and a list representing missing files</returns>
#if NET48
internal static (bool, List<string>) FoundAllFiles(string outputDirectory, string outputFilename, BaseParameters parameters, bool preCheck)
#else
internal static (bool, List<string>) FoundAllFiles(string? outputDirectory, string outputFilename, BaseParameters? parameters, bool preCheck)
#endif
{
// If there are no parameters set
if (parameters == null)
return (false, new List<string>());
// First, sanitized the output filename to strip off any potential extension
outputFilename = Path.GetFileNameWithoutExtension(outputFilename);
// Then get the base path for all checking
string basePath;
if (string.IsNullOrWhiteSpace(outputDirectory))
basePath = outputFilename;
else
basePath = Path.Combine(outputDirectory, outputFilename);
// Finally, let the parameters say if all files exist
return parameters.CheckAllOutputFilesExist(basePath, preCheck);
}
/// <summary>
/// Generate the proper datfile from the input Datafile, if possible
/// </summary>
/// <param name="datafile">.dat file location</param>
/// <returns>Relevant pieces of the datfile, null on error</returns>
#if NET48
internal static string GenerateDatfile(Datafile datafile)
#else
internal static string? GenerateDatfile(Datafile? datafile)
#endif
{
// If we don't have a valid datafile, we can't do anything
if (datafile?.Games == null || datafile.Games.Length == 0)
return null;
var roms = datafile.Games[0].Roms;
if (roms == null || roms.Length == 0)
return null;
// Otherwise, reconstruct the hash data with only the required info
try
{
string datString = string.Empty;
for (int i = 0; i < roms.Length; i++)
{
var rom = roms[i];
datString += $"<rom name=\"{rom.Name}\" size=\"{rom.Size}\" crc=\"{rom.Crc}\" md5=\"{rom.Md5}\" sha1=\"{rom.Sha1}\" />\n";
}
return datString.TrimEnd('\n');
}
catch
{
// We don't care what the exception is right now
return null;
}
}
/// <summary>
/// Get the existence of an anti-modchip string from a PlayStation disc, if possible
/// </summary>
/// <param name="drive">Drive object representing the current drive</param>
/// <returns>Anti-modchip existence if possible, false on error</returns>
internal static async Task<bool> GetAntiModchipDetected(Drive drive)
=> await Protection.GetPlayStationAntiModchipDetected(drive.Name);
/// <summary>
/// Get the current detected copy protection(s), if possible
/// </summary>
/// <param name="drive">Drive object representing the current drive</param>
/// <param name="options">Options object that determines what to scan</param>
/// <param name="progress">Optional progress callback</param>
/// <returns>Detected copy protection(s) if possible, null on error</returns>
#if NET48
internal static async Task<(string, Dictionary<string, List<string>>)> GetCopyProtection(Drive drive, Data.Options options, IProgress<ProtectionProgress> progress = null)
#else
internal static async Task<(string?, Dictionary<string, List<string>>?)> GetCopyProtection(Drive? drive, Data.Options options, IProgress<ProtectionProgress>? progress = null)
#endif
{
if (options.ScanForProtection && drive?.Name != null)
{
(var protection, _) = await Protection.RunProtectionScanOnPath(drive.Name, options, progress);
return (Protection.FormatProtections(protection), protection);
}
return ("(CHECK WITH PROTECTIONID)", null);
}
/// <summary>
/// Get Datafile from a standard DAT
/// </summary>
/// <param name="dat">Path to the DAT file to parse</param>
/// <returns>Filled Datafile on success, null on error</returns>
#if NET48
internal static Datafile GetDatafile(string dat)
#else
internal static Datafile? GetDatafile(string? dat)
#endif
{
// If there's no path, we can't read the file
if (string.IsNullOrWhiteSpace(dat))
return null;
// If the file doesn't exist, we can't read it
if (!File.Exists(dat))
return null;
try
{
// Open and read in the XML file
XmlReader xtr = XmlReader.Create(dat, new XmlReaderSettings
{
CheckCharacters = false,
DtdProcessing = DtdProcessing.Ignore,
IgnoreComments = true,
IgnoreWhitespace = true,
ValidationFlags = XmlSchemaValidationFlags.None,
ValidationType = ValidationType.None,
});
// If the reader is null for some reason, we can't do anything
if (xtr == null)
return null;
var serializer = new XmlSerializer(typeof(Datafile));
return serializer.Deserialize(xtr) as Datafile;
}
catch
{
// We don't care what the exception is right now
return null;
}
}
/// <summary>
/// Gets disc information from a PIC file
/// </summary>
/// <param name="pic">Path to a PIC.bin file</param>
/// <returns>Filled DiscInformation on success, null on error</returns>
/// <remarks>This omits the emergency brake information, if it exists</remarks>
#if NET48
internal static DiscInformation GetDiscInformation(string pic)
#else
internal static DiscInformation? GetDiscInformation(string pic)
#endif
{
try
{
return new SabreTools.Serialization.Files.PIC().Deserialize(pic);
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get hashes from an input file path
/// </summary>
/// <param name="filename">Path to the input file</param>
/// <returns>True if hashing was successful, false otherwise</returns>
#if NET48
internal static bool GetFileHashes(string filename, out long size, out string crc32, out string md5, out string sha1)
#else
internal static bool GetFileHashes(string filename, out long size, out string? crc32, out string? md5, out string? sha1)
#endif
{
// Set all initial values
size = -1; crc32 = null; md5 = null; sha1 = null;
// If the file doesn't exist, we can't do anything
if (!File.Exists(filename))
return false;
// Set the file size
size = new FileInfo(filename).Length;
// Open the input file
var input = File.OpenRead(filename);
try
{
// Get a list of hashers to run over the buffer
var hashers = new List<Hasher>
{
new Hasher(Hash.CRC32),
new Hasher(Hash.MD5),
new Hasher(Hash.SHA1),
new Hasher(Hash.SHA256),
new Hasher(Hash.SHA384),
new Hasher(Hash.SHA512),
};
// Initialize the hashing helpers
var loadBuffer = new ThreadLoadBuffer(input);
int buffersize = 3 * 1024 * 1024;
byte[] buffer0 = new byte[buffersize];
byte[] buffer1 = new byte[buffersize];
/*
Please note that some of the following code is adapted from
RomVault. This is a modified version of how RomVault does
threaded hashing. As such, some of the terminology and code
is the same, though variable names and comments may have
been tweaked to better fit this code base.
*/
// Pre load the first buffer
long refsize = size;
int next = refsize > buffersize ? buffersize : (int)refsize;
input.Read(buffer0, 0, next);
int current = next;
refsize -= next;
bool bufferSelect = true;
while (current > 0)
{
// Trigger the buffer load on the second buffer
next = refsize > buffersize ? buffersize : (int)refsize;
if (next > 0)
loadBuffer.Trigger(bufferSelect ? buffer1 : buffer0, next);
byte[] buffer = bufferSelect ? buffer0 : buffer1;
// Run hashes in parallel
Parallel.ForEach(hashers, h => h.Process(buffer, current));
// Wait for the load buffer worker, if needed
if (next > 0)
loadBuffer.Wait();
// Setup for the next hashing step
current = next;
refsize -= next;
bufferSelect = !bufferSelect;
}
// Finalize all hashing helpers
loadBuffer.Finish();
Parallel.ForEach(hashers, h => h.Terminate());
// Get the results
crc32 = hashers.First(h => h.HashType == Hash.CRC32).GetHashString();
md5 = hashers.First(h => h.HashType == Hash.MD5).GetHashString();
sha1 = hashers.First(h => h.HashType == Hash.SHA1).GetHashString();
//sha256 = hashers.First(h => h.HashType == Hash.SHA256).GetHashString();
//sha384 = hashers.First(h => h.HashType == Hash.SHA384).GetHashString();
//sha512 = hashers.First(h => h.HashType == Hash.SHA512).GetHashString();
// Dispose of the hashers
loadBuffer.Dispose();
hashers.ForEach(h => h.Dispose());
return true;
}
catch (IOException)
{
return false;
}
finally
{
input.Dispose();
}
}
/// <summary>
/// Get the last modified date from a file path, if possible
/// </summary>
/// <param name="filename">Path to the input file</param>
/// <returns>Filled DateTime on success, null on failure</returns>
#if NET48
internal static DateTime? GetFileModifiedDate(string filename, bool fallback = false)
#else
internal static DateTime? GetFileModifiedDate(string? filename, bool fallback = false)
#endif
{
if (string.IsNullOrWhiteSpace(filename))
return fallback ? (DateTime?)DateTime.UtcNow : null;
else if (!File.Exists(filename))
return fallback ? (DateTime?)DateTime.UtcNow : null;
var fi = new FileInfo(filename);
return fi.LastWriteTimeUtc;
}
/// <summary>
/// Get the full lines from the input file, if possible
/// </summary>
/// <param name="filename">file location</param>
/// <param name="binary">True if should read as binary, false otherwise (default)</param>
/// <returns>Full text of the file, null on error</returns>
#if NET48
internal static string GetFullFile(string filename, bool binary = false)
#else
internal static string? GetFullFile(string filename, bool binary = false)
#endif
{
// If the file doesn't exist, we can't get info from it
if (!File.Exists(filename))
return null;
// If we're reading as binary
if (binary)
{
byte[] bytes = File.ReadAllBytes(filename);
return BitConverter.ToString(bytes).Replace("-", string.Empty);
}
return File.ReadAllText(filename);
}
/// <summary>
/// Get the split values for ISO-based media
/// </summary>
/// <param name="datafile">Datafile represenging the hash data</param>
/// <returns>True if extraction was successful, false otherwise</returns>
#if NET48
internal static bool GetISOHashValues(Datafile datafile, out long size, out string crc32, out string md5, out string sha1)
#else
internal static bool GetISOHashValues(Datafile? datafile, out long size, out string? crc32, out string? md5, out string? sha1)
#endif
{
size = -1; crc32 = null; md5 = null; sha1 = null;
if (datafile?.Games == null || datafile.Games.Length == 0)
return false;
var roms = datafile.Games[0].Roms;
if (roms == null || roms.Length == 0)
return false;
var rom = roms[0];
_ = Int64.TryParse(rom.Size, out size);
crc32 = rom.Crc;
md5 = rom.Md5;
sha1 = rom.Sha1;
return true;
}
/// <summary>
/// Get the split values for ISO-based media
/// </summary>
/// <param name="hashData">String representing the combined hash data</param>
/// <returns>True if extraction was successful, false otherwise</returns>
#if NET48
internal static bool GetISOHashValues(string hashData, out long size, out string crc32, out string md5, out string sha1)
#else
internal static bool GetISOHashValues(string? hashData, out long size, out string? crc32, out string? md5, out string? sha1)
#endif
{
size = -1; crc32 = null; md5 = null; sha1 = null;
if (string.IsNullOrWhiteSpace(hashData))
return false;
var hashreg = new Regex(@"<rom name="".*?"" size=""(.*?)"" crc=""(.*?)"" md5=""(.*?)"" sha1=""(.*?)""");
Match m = hashreg.Match(hashData);
if (m.Success)
{
_ = Int64.TryParse(m.Groups[1].Value, out size);
crc32 = m.Groups[2].Value;
md5 = m.Groups[3].Value;
sha1 = m.Groups[4].Value;
return true;
}
else
{
return false;
}
}
/// <summary>
/// Get the layerbreak info associated from the disc information
/// </summary>
/// <param name="di">Disc information containing unformatted data</param>
/// <returns>True if layerbreak info was set, false otherwise</returns>
#if NET48
internal static bool GetLayerbreaks(DiscInformation di, out long? layerbreak1, out long? layerbreak2, out long? layerbreak3)
#else
internal static bool GetLayerbreaks(DiscInformation? di, out long? layerbreak1, out long? layerbreak2, out long? layerbreak3)
#endif
{
// Set the default values
layerbreak1 = null; layerbreak2 = null; layerbreak3 = null;
// If we don't have valid disc information, we can't do anything
if (di?.Units == null || di.Units.Length <= 1)
return false;
#if NET48
int ReadFromArrayBigEndian(byte[] bytes, int offset)
#else
static int ReadFromArrayBigEndian(byte[]? bytes, int offset)
#endif
{
if (bytes == null)
return default;
var span = new ReadOnlySpan<byte>(bytes, offset, 0x04);
byte[] rev = span.ToArray();
Array.Reverse(rev);
return BitConverter.ToInt32(rev, 0);
}
// Layerbreak 1 (2+ layers)
if (di.Units.Length >= 2)
{
long offset = ReadFromArrayBigEndian(di.Units[0]?.Body?.FormatDependentContents, 0x0C);
long value = ReadFromArrayBigEndian(di.Units[0]?.Body?.FormatDependentContents, 0x10);
layerbreak1 = value - offset + 2;
}
// Layerbreak 2 (3+ layers)
if (di.Units.Length >= 3)
{
long offset = ReadFromArrayBigEndian(di.Units[1]?.Body?.FormatDependentContents, 0x0C);
long value = ReadFromArrayBigEndian(di.Units[1]?.Body?.FormatDependentContents, 0x10);
layerbreak2 = layerbreak1 + value - offset + 2;
}
// Layerbreak 3 (4 layers)
if (di.Units.Length >= 4)
{
long offset = ReadFromArrayBigEndian(di.Units[2]?.Body?.FormatDependentContents, 0x0C);
long value = ReadFromArrayBigEndian(di.Units[2]?.Body?.FormatDependentContents, 0x10);
layerbreak3 = layerbreak2 + value - offset + 2;
}
return true;
}
/// <summary>
/// Get if LibCrypt data is detected in the subchannel file, if possible
/// </summary>
/// <param name="info">Base submission info to fill in specifics for</param>
/// <param name="basePath">Base filename and path to use for checking</param>
/// <returns>Status of the LibCrypt data, if possible</returns>
internal static void GetLibCryptDetected(SubmissionInfo info, string basePath)
{
#if NET48
if (info.CopyProtection == null) info.CopyProtection = new CopyProtectionSection();
#else
info.CopyProtection ??= new CopyProtectionSection();
#endif
bool? psLibCryptStatus = Protection.GetLibCryptDetected(basePath + ".sub");
if (psLibCryptStatus == true)
{
// Guard against false positives
if (File.Exists(basePath + "_subIntention.txt"))
{
string libCryptData = GetFullFile(basePath + "_subIntention.txt") ?? "";
if (string.IsNullOrEmpty(libCryptData))
{
info.CopyProtection.LibCrypt = YesNo.No;
}
else
{
info.CopyProtection.LibCrypt = YesNo.Yes;
info.CopyProtection.LibCryptData = libCryptData;
}
}
else
{
info.CopyProtection.LibCrypt = YesNo.No;
}
}
else if (psLibCryptStatus == false)
{
info.CopyProtection.LibCrypt = YesNo.No;
}
else
{
info.CopyProtection.LibCrypt = YesNo.NULL;
info.CopyProtection.LibCryptData = "LibCrypt could not be detected because subchannel file is missing";
}
}
/// <summary>
/// Get the PIC identifier from the first disc information unit, if possible
/// </summary>
/// <param name="di">Disc information containing the data</param>
/// <returns>String representing the PIC identifier, null on error</returns>
#if NET48
internal static string GetPICIdentifier(DiscInformation di)
#else
internal static string? GetPICIdentifier(DiscInformation? di)
#endif
{
// If we don't have valid disc information, we can't do anything
if (di?.Units == null || di.Units.Length <= 1)
return null;
// We assume the identifier is consistent across all units
return di.Units[0]?.Body?.DiscTypeIdentifier;
}
/// <summary>
/// Get the EXE date from a PlayStation disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <param name="serial">Internal disc serial, if possible</param>
/// <param name="region">Output region, if possible</param>
/// <param name="date">Output EXE date in "yyyy-mm-dd" format if possible, null on error</param>
/// <returns></returns>
#if NET48
internal static bool GetPlayStationExecutableInfo(char? driveLetter, out string serial, out Region? region, out string date)
#else
internal static bool GetPlayStationExecutableInfo(char? driveLetter, out string? serial, out Region? region, out string? date)
#endif
{
serial = null; region = null; date = null;
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return false;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
return GetPlayStationExecutableInfo(drivePath, out serial, out region, out date);
}
/// <summary>
/// Get the EXE date from a PlayStation disc, if possible
/// </summary>
/// <param name="drivePath">Drive path to use to check</param>
/// <param name="serial">Internal disc serial, if possible</param>
/// <param name="region">Output region, if possible</param>
/// <param name="date">Output EXE date in "yyyy-mm-dd" format if possible, null on error</param>
/// <returns></returns>
#if NET48
internal static bool GetPlayStationExecutableInfo(string drivePath, out string serial, out Region? region, out string date)
#else
internal static bool GetPlayStationExecutableInfo(string? drivePath, out string? serial, out Region? region, out string? date)
#endif
{
serial = null; region = null; date = null;
// If there's no drive path, we can't do this part
if (string.IsNullOrWhiteSpace(drivePath))
return false;
// If the folder no longer exists, we can't do this part
if (!Directory.Exists(drivePath))
return false;
// Get the two paths that we will need to check
string psxExePath = Path.Combine(drivePath, "PSX.EXE");
string systemCnfPath = Path.Combine(drivePath, "SYSTEM.CNF");
// Try both of the common paths that contain information
#if NET48
string exeName = null;
#else
string? exeName = null;
#endif
// Read the CNF file as an INI file
var systemCnf = new IniFile(systemCnfPath);
string bootValue = string.Empty;
// PlayStation uses "BOOT" as the key
if (systemCnf.ContainsKey("BOOT"))
bootValue = systemCnf["BOOT"];
// PlayStation 2 uses "BOOT2" as the key
if (systemCnf.ContainsKey("BOOT2"))
bootValue = systemCnf["BOOT2"];
// If we had any boot value, parse it and get the executable name
if (!string.IsNullOrEmpty(bootValue))
{
var match = Regex.Match(bootValue, @"cdrom.?:\\?(.*)");
if (match.Groups.Count > 1)
{
// EXE name may have a trailing `;` after
// EXE name should always be in all caps
exeName = match.Groups[1].Value
.Split(';')[0]
.ToUpperInvariant();
// Serial is most of the EXE name normalized
serial = exeName
.Replace('_', '-')
.Replace(".", string.Empty);
// Some games may have the EXE in a subfolder
serial = Path.GetFileName(serial);
}
}
// If the SYSTEM.CNF value can't be found, try PSX.EXE
if (string.IsNullOrWhiteSpace(exeName) && File.Exists(psxExePath))
exeName = "PSX.EXE";
// If neither can be found, we return false
if (string.IsNullOrWhiteSpace(exeName))
return false;
// Get the region, if possible
region = GetPlayStationRegion(exeName);
// Now that we have the EXE name, try to get the fileinfo for it
string exePath = Path.Combine(drivePath, exeName);
if (!File.Exists(exePath))
return false;
// Fix the Y2K timestamp issue
var fi = new FileInfo(exePath);
var dt = new DateTime(fi.LastWriteTimeUtc.Year >= 1900 && fi.LastWriteTimeUtc.Year < 1920 ? 2000 + fi.LastWriteTimeUtc.Year % 100 : fi.LastWriteTimeUtc.Year,
fi.LastWriteTimeUtc.Month, fi.LastWriteTimeUtc.Day);
date = dt.ToString("yyyy-MM-dd");
return true;
}
/// <summary>
/// Get the version from a PlayStation 2 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Game version if possible, null on error</returns>
#if NET48
internal static string GetPlayStation2Version(char? driveLetter)
#else
internal static string? GetPlayStation2Version(char? driveLetter)
#endif
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
return (GetPlayStation2Version(drivePath));
}
/// <summary>
/// Get the version from a PlayStation 2 disc, if possible
/// </summary>
/// <param name="drivePath">Drive path to use to check</param>
/// <returns>Game version if possible, null on error</returns>
#if NET48
internal static string GetPlayStation2Version(string drivePath)
#else
internal static string? GetPlayStation2Version(string? drivePath)
#endif
{
// If there's no drive path, we can't do this part
if (string.IsNullOrWhiteSpace(drivePath))
return null;
// If the folder no longer exists, we can't do this part
if (!Directory.Exists(drivePath))
return null;
// Get the SYSTEM.CNF path to check
string systemCnfPath = Path.Combine(drivePath, "SYSTEM.CNF");
// Try to parse the SYSTEM.CNF file
var systemCnf = new IniFile(systemCnfPath);
if (systemCnf.ContainsKey("VER"))
return systemCnf["VER"];
// If "VER" can't be found, we can't do much
return null;
}
/// <summary>
/// Get the internal serial from a PlayStation 3 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Internal disc serial if possible, null on error</returns>
#if NET48
internal static string GetPlayStation3Serial(char? driveLetter)
#else
internal static string? GetPlayStation3Serial(char? driveLetter)
#endif
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
return GetPlayStation3Serial(drivePath);
}
/// <summary>
/// Get the internal serial from a PlayStation 3 disc, if possible
/// </summary>
/// <param name="drivePath">Drive path to use to check</param>
/// <returns>Internal disc serial if possible, null on error</returns>
#if NET48
internal static string GetPlayStation3Serial(string drivePath)
#else
internal static string? GetPlayStation3Serial(string? drivePath)
#endif
{
// If there's no drive path, we can't do this part
if (string.IsNullOrWhiteSpace(drivePath))
return null;
// If the folder no longer exists, we can't do this part
if (!Directory.Exists(drivePath))
return null;
// If we can't find PARAM.SFO, we don't have a PlayStation 3 disc
string paramSfoPath = Path.Combine(drivePath, "PS3_GAME", "PARAM.SFO");
if (!File.Exists(paramSfoPath))
return null;
// Let's try reading PARAM.SFO to find the serial at the end of the file
try
{
using (var br = new BinaryReader(File.OpenRead(paramSfoPath)))
{
br.BaseStream.Seek(-0x18, SeekOrigin.End);
return new string(br.ReadChars(9));
}
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the version from a PlayStation 3 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Game version if possible, null on error</returns>
#if NET48
internal static string GetPlayStation3Version(char? driveLetter)
#else
internal static string? GetPlayStation3Version(char? driveLetter)
#endif
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
return GetPlayStation3Version(drivePath);
}
/// <summary>
/// Get the version from a PlayStation 3 disc, if possible
/// </summary>
/// <param name="drivePath">Drive path to use to check</param>
/// <returns>Game version if possible, null on error</returns>
#if NET48
internal static string GetPlayStation3Version(string drivePath)
#else
internal static string? GetPlayStation3Version(string? drivePath)
#endif
{
// If there's no drive path, we can't do this part
if (string.IsNullOrWhiteSpace(drivePath))
return null;
// If the folder no longer exists, we can't do this part
if (!Directory.Exists(drivePath))
return null;
// If we can't find PARAM.SFO, we don't have a PlayStation 3 disc
string paramSfoPath = Path.Combine(drivePath, "PS3_GAME", "PARAM.SFO");
if (!File.Exists(paramSfoPath))
return null;
// Let's try reading PARAM.SFO to find the version at the end of the file
try
{
using (var br = new BinaryReader(File.OpenRead(paramSfoPath)))
{
br.BaseStream.Seek(-0x08, SeekOrigin.End);
return new string(br.ReadChars(5));
}
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the internal serial from a PlayStation 4 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Internal disc serial if possible, null on error</returns>
#if NET48
internal static string GetPlayStation4Serial(char? driveLetter)
#else
internal static string? GetPlayStation4Serial(char? driveLetter)
#endif
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
return GetPlayStation4Serial(drivePath);
}
/// <summary>
/// Get the internal serial from a PlayStation 4 disc, if possible
/// </summary>
/// <param name="drivePath">Drive path to use to check</param>
/// <returns>Internal disc serial if possible, null on error</returns>
#if NET48
internal static string GetPlayStation4Serial(string drivePath)
#else
internal static string? GetPlayStation4Serial(string? drivePath)
#endif
{
// If there's no drive path, we can't do this part
if (string.IsNullOrWhiteSpace(drivePath))
return null;
// If the folder no longer exists, we can't do this part
if (!Directory.Exists(drivePath))
return null;
// If we can't find param.sfo, we don't have a PlayStation 4 disc
string paramSfoPath = Path.Combine(drivePath, "bd", "param.sfo");
if (!File.Exists(paramSfoPath))
return null;
// Let's try reading param.sfo to find the serial at the end of the file
try
{
using (var br = new BinaryReader(File.OpenRead(paramSfoPath)))
{
br.BaseStream.Seek(-0x14, SeekOrigin.End);
return new string(br.ReadChars(9));
}
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the version from a PlayStation 4 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Game version if possible, null on error</returns>
#if NET48
internal static string GetPlayStation4Version(char? driveLetter)
#else
internal static string? GetPlayStation4Version(char? driveLetter)
#endif
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
return GetPlayStation4Version(drivePath);
}
/// <summary>
/// Get the version from a PlayStation 4 disc, if possible
/// </summary>
/// <param name="drivePath">Drive path to use to check</param>
/// <returns>Game version if possible, null on error</returns>
#if NET48
internal static string GetPlayStation4Version(string drivePath)
#else
internal static string? GetPlayStation4Version(string? drivePath)
#endif
{
// If there's no drive path, we can't do this part
if (string.IsNullOrWhiteSpace(drivePath))
return null;
// If the folder no longer exists, we can't do this part
if (!Directory.Exists(drivePath))
return null;
// If we can't find param.sfo, we don't have a PlayStation 4 disc
string paramSfoPath = Path.Combine(drivePath, "bd", "param.sfo");
if (!File.Exists(paramSfoPath))
return null;
// Let's try reading param.sfo to find the version at the end of the file
try
{
using (var br = new BinaryReader(File.OpenRead(paramSfoPath)))
{
br.BaseStream.Seek(-0x08, SeekOrigin.End);
return new string(br.ReadChars(5));
}
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the internal serial from a PlayStation 5 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Internal disc serial if possible, null on error</returns>
#if NET48
internal static string GetPlayStation5Serial(char? driveLetter)
#else
internal static string? GetPlayStation5Serial(char? driveLetter)
#endif
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
return GetPlayStation5Serial(drivePath);
}
/// <summary>
/// Get the internal serial from a PlayStation 5 disc, if possible
/// </summary>
/// <param name="drivePath">Drive path to use to check</param>
/// <returns>Internal disc serial if possible, null on error</returns>
#if NET48
internal static string GetPlayStation5Serial(string drivePath)
#else
internal static string? GetPlayStation5Serial(string? drivePath)
#endif
{
// If there's no drive path, we can't do this part
if (string.IsNullOrWhiteSpace(drivePath))
return null;
// If the folder no longer exists, we can't do this part
if (!Directory.Exists(drivePath))
return null;
// If we can't find param.json, we don't have a PlayStation 5 disc
string paramJsonPath = Path.Combine(drivePath, "bd", "param.json");
if (!File.Exists(paramJsonPath))
return null;
// Let's try reading param.json to find the serial in the unencrypted JSON
try
{
using (var br = new BinaryReader(File.OpenRead(paramJsonPath)))
{
br.BaseStream.Seek(0x82E, SeekOrigin.Begin);
return new string(br.ReadChars(9));
}
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the version from a PlayStation 5 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Game version if possible, null on error</returns>
#if NET48
internal static string GetPlayStation5Version(char? driveLetter)
#else
internal static string? GetPlayStation5Version(char? driveLetter)
#endif
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
return GetPlayStation5Version(drivePath);
}
/// <summary>
/// Get the version from a PlayStation 5 disc, if possible
/// </summary>
/// <param name="drivePath">Drive path to use to check</param>
/// <returns>Game version if possible, null on error</returns>
#if NET48
internal static string GetPlayStation5Version(string drivePath)
#else
internal static string? GetPlayStation5Version(string? drivePath)
#endif
{
// If there's no drive path, we can't do this part
if (string.IsNullOrWhiteSpace(drivePath))
return null;
// If the folder no longer exists, we can't do this part
if (!Directory.Exists(drivePath))
return null;
// If we can't find param.json, we don't have a PlayStation 5 disc
string paramJsonPath = Path.Combine(drivePath, "bd", "param.json");
if (!File.Exists(paramJsonPath))
return null;
// Let's try reading param.json to find the version in the unencrypted JSON
try
{
using (var br = new BinaryReader(File.OpenRead(paramJsonPath)))
{
br.BaseStream.Seek(0x89E, SeekOrigin.Begin);
return new string(br.ReadChars(5));
}
}
catch
{
// We don't care what the error was
return null;
}
}
#endregion
#region Category Extraction
/// <summary>
/// Determine the category based on the UMDImageCreator string
/// </summary>
/// <param name="region">String representing the category</param>
/// <returns>Category, if possible</returns>
#if NET48
internal static DiscCategory? GetUMDCategory(string category)
#else
internal static DiscCategory? GetUMDCategory(string? category)
#endif
{
#if NET48
switch (category)
{
case "GAME": return DiscCategory.Games;
case "VIDEO": return DiscCategory.Video;
case "AUDIO": return DiscCategory.Audio;
default: return null;
}
#else
return category switch
{
"GAME" => DiscCategory.Games,
"VIDEO" => DiscCategory.Video,
"AUDIO" => DiscCategory.Audio,
_ => null,
};
#endif
}
#endregion
#region Region Extraction
/// <summary>
/// Determine the region based on the PlayStation serial code
/// </summary>
/// <param name="serial">PlayStation serial code</param>
/// <returns>Region mapped from name, if possible</returns>
#if NET48
internal static Region? GetPlayStationRegion(string serial)
#else
internal static Region? GetPlayStationRegion(string? serial)
#endif
{
// If we have a fully invalid serial
if (string.IsNullOrWhiteSpace(serial))
return null;
// Standardized "S" serials
if (serial.StartsWith("S"))
{
// string publisher = serial[0] + serial[1];
// char secondRegion = serial[3];
switch (serial[2])
{
case 'A': return Region.Asia;
case 'C': return Region.China;
case 'E': return Region.Europe;
case 'K': return Region.SouthKorea;
case 'U': return Region.UnitedStatesOfAmerica;
case 'P':
// Region of S_P_ serials may be Japan, Asia, or SouthKorea
switch (serial[3])
{
case 'S':
// Check first two digits of S_PS serial
#if NET48
switch (serial.Substring(5, 2))
{
case "46": return Region.SouthKorea;
case "51": return Region.Asia;
case "56": return Region.SouthKorea;
case "55": return Region.Asia;
default: return Region.Japan;
}
#else
return serial.Substring(5, 2) switch
{
"46" => Region.SouthKorea,
"51" => Region.Asia,
"56" => Region.SouthKorea,
"55" => Region.Asia,
_ => Region.Japan,
};
#endif
case 'M':
// Check first three digits of S_PM serial
#if NET48
switch (serial.Substring(5, 3))
{
case "645": return Region.SouthKorea;
case "675": return Region.SouthKorea;
case "885": return Region.SouthKorea;
default: return Region.Japan; // Remaining S_PM serials may be Japan or Asia
}
#else
return serial.Substring(5, 3) switch
{
"645" => Region.SouthKorea,
"675" => Region.SouthKorea,
"885" => Region.SouthKorea,
_ => Region.Japan, // Remaining S_PM serials may be Japan or Asia
};
#endif
default: return Region.Japan;
}
}
}
// Japan-only special serial
else if (serial.StartsWith("PAPX"))
return Region.Japan;
// Region appears entirely random
else if (serial.StartsWith("PABX"))
return null;
// Region appears entirely random
else if (serial.StartsWith("PBPX"))
return null;
// Japan-only special serial
else if (serial.StartsWith("PCBX"))
return Region.Japan;
// Japan-only special serial
else if (serial.StartsWith("PCXC"))
return Region.Japan;
// Single disc known, Japan
else if (serial.StartsWith("PDBX"))
return Region.Japan;
// Single disc known, Europe
else if (serial.StartsWith("PEBX"))
return Region.Europe;
// Single disc known, USA
else if (serial.StartsWith("PUBX"))
return Region.UnitedStatesOfAmerica;
return null;
}
#endregion
#region Information Output
/// <summary>
/// Compress log files to save space
/// </summary>
/// <param name="outputDirectory">Output folder to write to</param>
/// <param name="filenameSuffix">Output filename to use as the base path</param>
/// <param name="outputFilename">Output filename to use as the base path</param>
/// <param name="parameters">Parameters object to use to derive log file paths</param>
/// <returns>True if the process succeeded, false otherwise</returns>
#if NET48
public static (bool, string) CompressLogFiles(string outputDirectory, string filenameSuffix, string outputFilename, BaseParameters parameters)
#else
public static (bool, string) CompressLogFiles(string? outputDirectory, string? filenameSuffix, string outputFilename, BaseParameters? parameters)
#endif
{
// If there are no parameters
if (parameters == null)
return (false, "No parameters provided!");
// Prepare the necessary paths
outputFilename = Path.GetFileNameWithoutExtension(outputFilename);
string combinedBase;
if (string.IsNullOrWhiteSpace(outputDirectory))
combinedBase = outputFilename;
else
combinedBase = Path.Combine(outputDirectory, outputFilename);
string archiveName = combinedBase + "_logs.zip";
// Get the list of log files from the parameters object
var files = parameters.GetLogFilePaths(combinedBase);
// Add on generated log files if they exist
var mpfFiles = GetGeneratedFilePaths(outputDirectory, filenameSuffix);
files.AddRange(mpfFiles);
if (!files.Any())
return (true, "No files to compress!");
// If the file already exists, we want to delete the old one
try
{
if (File.Exists(archiveName))
File.Delete(archiveName);
}
catch
{
return (false, "Could not delete old archive!");
}
// Add the log files to the archive and delete the uncompressed file after
#if NET48
ZipArchive zf = null;
#else
ZipArchive? zf = null;
#endif
try
{
zf = ZipFile.Open(archiveName, ZipArchiveMode.Create);
foreach (string file in files)
{
if (string.IsNullOrWhiteSpace(outputDirectory))
{
zf.CreateEntryFromFile(file, file, CompressionLevel.Optimal);
}
else
{
#if NET48
string entryName = file.Substring(outputDirectory.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
zf.CreateEntryFromFile(file, entryName, CompressionLevel.Optimal);
#else
string entryName = file[outputDirectory.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
zf.CreateEntryFromFile(file, entryName, CompressionLevel.SmallestSize);
#endif
}
// If the file is MPF-specific, don't delete
if (mpfFiles.Contains(file))
continue;
try
{
File.Delete(file);
}
catch { }
}
return (true, "Compression complete!");
}
catch (Exception ex)
{
return (false, $"Compression could not complete: {ex}");
}
finally
{
zf?.Dispose();
}
}
/// <summary>
/// Compress log files to save space
/// </summary>
/// <param name="outputDirectory">Output folder to write to</param>
/// <param name="outputFilename">Output filename to use as the base path</param>
/// <param name="parameters">Parameters object to use to derive log file paths</param>
/// <returns>True if the process succeeded, false otherwise</returns>
#if NET48
public static (bool, string) DeleteUnnecessaryFiles(string outputDirectory, string outputFilename, BaseParameters parameters)
#else
public static (bool, string) DeleteUnnecessaryFiles(string? outputDirectory, string outputFilename, BaseParameters? parameters)
#endif
{
// If there are no parameters
if (parameters == null)
return (false, "No parameters provided!");
// Prepare the necessary paths
outputFilename = Path.GetFileNameWithoutExtension(outputFilename);
string combinedBase;
if (string.IsNullOrWhiteSpace(outputDirectory))
combinedBase = outputFilename;
else
combinedBase = Path.Combine(outputDirectory, outputFilename);
// Get the list of deleteable files from the parameters object
var files = parameters.GetDeleteableFilePaths(combinedBase);
if (!files.Any())
return (true, "No files to delete!");
// Attempt to delete all of the files
try
{
foreach (string file in files)
{
try
{
File.Delete(file);
}
catch { }
}
return (true, "Deletion complete!");
}
catch (Exception ex)
{
return (false, $"Deletion could not complete: {ex}");
}
}
/// <summary>
/// Format the output data in a human readable way, separating each printed line into a new item in the list
/// </summary>
/// <param name="info">Information object that should contain normalized values</param>
/// <param name="options">Options object representing user-defined options</param>
/// <returns>List of strings representing each line of an output file, null on error</returns>
#if NET48
public static (List<string>, string) FormatOutputData(SubmissionInfo info, Data.Options options)
#else
public static (List<string>?, string?) FormatOutputData(SubmissionInfo? info, Data.Options options)
#endif
{
// Check to see if the inputs are valid
if (info == null)
return (null, "Submission information was missing");
try
{
// Sony-printed discs have layers in the opposite order
var system = info.CommonDiscInfo?.System;
bool reverseOrder = system.HasReversedRingcodes();
// Preamble for submission
#pragma warning disable IDE0028
var output = new List<string>
{
"Users who wish to submit this information to Redump must ensure that all of the fields below are accurate for the exact media they have.",
"Please double-check to ensure that there are no fields that need verification, such as the version or copy protection.",
"If there are no fields in need of verification or all fields are accurate, this preamble can be removed before submission.",
"",
};
// Common Disc Info section
output.Add("Common Disc Info:");
AddIfExists(output, Template.TitleField, info.CommonDiscInfo?.Title, 1);
AddIfExists(output, Template.ForeignTitleField, info.CommonDiscInfo?.ForeignTitleNonLatin, 1);
AddIfExists(output, Template.DiscNumberField, info.CommonDiscInfo?.DiscNumberLetter, 1);
AddIfExists(output, Template.DiscTitleField, info.CommonDiscInfo?.DiscTitle, 1);
AddIfExists(output, Template.SystemField, info.CommonDiscInfo?.System.LongName(), 1);
AddIfExists(output, Template.MediaTypeField, GetFixedMediaType(
info.CommonDiscInfo?.Media.ToMediaType(),
info.SizeAndChecksums?.PICIdentifier,
info.SizeAndChecksums?.Size,
info.SizeAndChecksums?.Layerbreak,
info.SizeAndChecksums?.Layerbreak2,
info.SizeAndChecksums?.Layerbreak3),
1);
AddIfExists(output, Template.CategoryField, info.CommonDiscInfo?.Category.LongName(), 1);
AddIfExists(output, Template.FullyMatchingIDField, info.FullyMatchedID?.ToString(), 1);
AddIfExists(output, Template.PartiallyMatchingIDsField, info.PartiallyMatchedIDs, 1);
AddIfExists(output, Template.RegionField, info.CommonDiscInfo?.Region.LongName() ?? "SPACE! (CHANGE THIS)", 1);
AddIfExists(output, Template.LanguagesField, (info.CommonDiscInfo?.Languages ?? new Language?[] { null }).Select(l => l.LongName() ?? "SILENCE! (CHANGE THIS)").ToArray(), 1);
AddIfExists(output, Template.PlaystationLanguageSelectionViaField, (info.CommonDiscInfo?.LanguageSelection ?? Array.Empty<LanguageSelection?>()).Select(l => l.LongName()).ToArray(), 1);
AddIfExists(output, Template.DiscSerialField, info.CommonDiscInfo?.Serial, 1);
// All ringcode information goes in an indented area
output.Add(""); output.Add("\tRingcode Information:"); output.Add("");
// If we have a triple-layer disc
if (info.SizeAndChecksums?.Layerbreak3 != default && info.SizeAndChecksums?.Layerbreak3 != default(long))
{
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer0MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer0MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer0ToolstampMasteringCode, 0);
AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer0MouldSID, 0);
AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer0AdditionalMould, 0);
AddIfExists(output, "Layer 1 " + Template.MasteringRingField, info.CommonDiscInfo?.Layer1MasteringRing, 0);
AddIfExists(output, "Layer 1 " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer1MasteringSID, 0);
AddIfExists(output, "Layer 1 " + Template.ToolstampField, info.CommonDiscInfo?.Layer1ToolstampMasteringCode, 0);
AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer1MouldSID, 0);
AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer1AdditionalMould, 0);
AddIfExists(output, "Layer 2 " + Template.MasteringRingField, info.CommonDiscInfo?.Layer2MasteringRing, 0);
AddIfExists(output, "Layer 2 " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer2MasteringSID, 0);
AddIfExists(output, "Layer 2 " + Template.ToolstampField, info.CommonDiscInfo?.Layer2ToolstampMasteringCode, 0);
AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer3MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer3MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer3ToolstampMasteringCode, 0);
}
// If we have a triple-layer disc
else if (info.SizeAndChecksums?.Layerbreak2 != default && info.SizeAndChecksums?.Layerbreak2 != default(long))
{
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer0MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer0MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer0ToolstampMasteringCode, 0);
AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer0MouldSID, 0);
AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer0AdditionalMould, 0);
AddIfExists(output, "Layer 1 " + Template.MasteringRingField, info.CommonDiscInfo?.Layer1MasteringRing, 0);
AddIfExists(output, "Layer 1 " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer1MasteringSID, 0);
AddIfExists(output, "Layer 1 " + Template.ToolstampField, info.CommonDiscInfo?.Layer1ToolstampMasteringCode, 0);
AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer1MouldSID, 0);
AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer1AdditionalMould, 0);
AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer2MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer2MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer2ToolstampMasteringCode, 0);
}
// If we have a dual-layer disc
else if (info.SizeAndChecksums?.Layerbreak != default && info.SizeAndChecksums?.Layerbreak != default(long))
{
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer0MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer0MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer0ToolstampMasteringCode, 0);
AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer0MouldSID, 0);
AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer0AdditionalMould, 0);
AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo?.Layer1MasteringRing, 0);
AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo?.Layer1MasteringSID, 0);
AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo?.Layer1ToolstampMasteringCode, 0);
AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer1MouldSID, 0);
AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer1AdditionalMould, 0);
}
// If we have a single-layer disc
else
{
AddIfExists(output, "Data Side " + Template.MasteringRingField, info.CommonDiscInfo?.Layer0MasteringRing, 0);
AddIfExists(output, "Data Side " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer0MasteringSID, 0);
AddIfExists(output, "Data Side " + Template.ToolstampField, info.CommonDiscInfo?.Layer0ToolstampMasteringCode, 0);
AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer0MouldSID, 0);
AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer0AdditionalMould, 0);
AddIfExists(output, "Label Side " + Template.MasteringRingField, info.CommonDiscInfo?.Layer1MasteringRing, 0);
AddIfExists(output, "Label Side " + Template.MasteringSIDField, info.CommonDiscInfo?.Layer1MasteringSID, 0);
AddIfExists(output, "Label Side " + Template.ToolstampField, info.CommonDiscInfo?.Layer1ToolstampMasteringCode, 0);
AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo?.Layer1MouldSID, 0);
AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo?.Layer1AdditionalMould, 0);
}
output.Add("");
AddIfExists(output, Template.BarcodeField, info.CommonDiscInfo?.Barcode, 1);
AddIfExists(output, Template.EXEDateBuildDate, info.CommonDiscInfo?.EXEDateBuildDate, 1);
AddIfExists(output, Template.ErrorCountField, info.CommonDiscInfo?.ErrorsCount, 1);
AddIfExists(output, Template.CommentsField, info.CommonDiscInfo?.Comments?.Trim(), 1);
AddIfExists(output, Template.ContentsField, info.CommonDiscInfo?.Contents?.Trim(), 1);
// Version and Editions section
output.Add(""); output.Add("Version and Editions:");
AddIfExists(output, Template.VersionField, info.VersionAndEditions?.Version, 1);
AddIfExists(output, Template.EditionField, info.VersionAndEditions?.OtherEditions, 1);
// EDC section
if (info.CommonDiscInfo?.System == RedumpSystem.SonyPlayStation)
{
output.Add(""); output.Add("EDC:");
AddIfExists(output, Template.PlayStationEDCField, info.EDC?.EDC.LongName(), 1);
}
// Parent/Clone Relationship section
// output.Add(""); output.Add("Parent/Clone Relationship:");
// AddIfExists(output, Template.ParentIDField, info.ParentID);
// AddIfExists(output, Template.RegionalParentField, info.RegionalParent.ToString());
// Extras section
if (info.Extras?.PVD != null || info.Extras?.PIC != null || info.Extras?.BCA != null || info.Extras?.SecuritySectorRanges != null)
{
output.Add(""); output.Add("Extras:");
AddIfExists(output, Template.PVDField, info.Extras.PVD?.Trim(), 1);
AddIfExists(output, Template.PlayStation3WiiDiscKeyField, info.Extras.DiscKey, 1);
AddIfExists(output, Template.PlayStation3DiscIDField, info.Extras.DiscID, 1);
AddIfExists(output, Template.PICField, info.Extras.PIC, 1);
AddIfExists(output, Template.HeaderField, info.Extras.Header, 1);
AddIfExists(output, Template.GameCubeWiiBCAField, info.Extras.BCA, 1);
AddIfExists(output, Template.XBOXSSRanges, info.Extras.SecuritySectorRanges, 1);
}
// Copy Protection section
if (!string.IsNullOrWhiteSpace(info.CopyProtection?.Protection)
|| (info.CopyProtection?.AntiModchip != null && info.CopyProtection.AntiModchip != YesNo.NULL)
|| (info.CopyProtection?.LibCrypt != null && info.CopyProtection.LibCrypt != YesNo.NULL)
|| !string.IsNullOrWhiteSpace(info.CopyProtection?.LibCryptData)
|| !string.IsNullOrWhiteSpace(info.CopyProtection?.SecuROMData))
{
output.Add(""); output.Add("Copy Protection:");
if (info.CommonDiscInfo?.System == RedumpSystem.SonyPlayStation)
{
AddIfExists(output, Template.PlayStationAntiModchipField, info.CopyProtection.AntiModchip.LongName(), 1);
AddIfExists(output, Template.PlayStationLibCryptField, info.CopyProtection.LibCrypt.LongName(), 1);
AddIfExists(output, Template.SubIntentionField, info.CopyProtection.LibCryptData, 1);
}
AddIfExists(output, Template.CopyProtectionField, info.CopyProtection.Protection, 1);
AddIfExists(output, Template.SubIntentionField, info.CopyProtection.SecuROMData, 1);
}
// Dumpers and Status section
// output.Add(""); output.Add("Dumpers and Status");
// AddIfExists(output, Template.StatusField, info.Status.Name());
// AddIfExists(output, Template.OtherDumpersField, info.OtherDumpers);
// Tracks and Write Offsets section
if (!string.IsNullOrWhiteSpace(info.TracksAndWriteOffsets?.ClrMameProData))
{
output.Add(""); output.Add("Tracks and Write Offsets:");
AddIfExists(output, Template.DATField, info.TracksAndWriteOffsets.ClrMameProData + "\n", 1);
AddIfExists(output, Template.CuesheetField, info.TracksAndWriteOffsets.Cuesheet, 1);
var offset = info.TracksAndWriteOffsets.OtherWriteOffsets;
if (Int32.TryParse(offset, out int i))
offset = i.ToString("+#;-#;0");
AddIfExists(output, Template.WriteOffsetField, offset, 1);
}
// Size & Checksum section
else
{
output.Add(""); output.Add("Size & Checksum:");
// Gross hack because of automatic layerbreaks in Redump
if (!options.EnableRedumpCompatibility
|| (info.CommonDiscInfo?.Media.ToMediaType() != MediaType.BluRay
&& info.CommonDiscInfo?.System.IsXGD() == false))
{
AddIfExists(output, Template.LayerbreakField, (info.SizeAndChecksums?.Layerbreak == default && info.SizeAndChecksums?.Layerbreak != default(long) ? null : info.SizeAndChecksums?.Layerbreak.ToString()), 1);
}
AddIfExists(output, Template.SizeField, info.SizeAndChecksums?.Size.ToString(), 1);
AddIfExists(output, Template.CRC32Field, info.SizeAndChecksums?.CRC32, 1);
AddIfExists(output, Template.MD5Field, info.SizeAndChecksums?.MD5, 1);
AddIfExists(output, Template.SHA1Field, info.SizeAndChecksums?.SHA1, 1);
}
// Dumping Info section
output.Add(""); output.Add("Dumping Info:");
AddIfExists(output, Template.DumpingProgramField, info.DumpingInfo?.DumpingProgram, 1);
AddIfExists(output, Template.DumpingDateField, info.DumpingInfo?.DumpingDate, 1);
AddIfExists(output, Template.DumpingDriveManufacturer, info.DumpingInfo?.Manufacturer, 1);
AddIfExists(output, Template.DumpingDriveModel, info.DumpingInfo?.Model, 1);
AddIfExists(output, Template.DumpingDriveFirmware, info.DumpingInfo?.Firmware, 1);
AddIfExists(output, Template.ReportedDiscType, info.DumpingInfo?.ReportedDiscType, 1);
// Make sure there aren't any instances of two blank lines in a row
#if NET48
string last = null;
#else
string? last = null;
#endif
for (int i = 0; i < output.Count;)
{
if (output[i] == last && string.IsNullOrWhiteSpace(last))
{
output.RemoveAt(i);
}
else
{
last = output[i];
i++;
}
}
return (output, "Formatting complete!");
}
catch (Exception ex)
{
return (null, $"Error formatting submission info: {ex}");
}
}
/// <summary>
/// Get the adjusted name of the media based on layers, if applicable
/// </summary>
/// <param name="mediaType">MediaType to get the proper name for</param>
/// <param name="picIdentifier">PIC identifier string (BD only)</param>
/// <param name="size">Size of the current media</param>
/// <param name="layerbreak">First layerbreak value, as applicable</param>
/// <param name="layerbreak2">Second layerbreak value, as applicable</param>
/// <param name="layerbreak3">Third layerbreak value, as applicable</param>
/// <returns>String representation of the media, including layer specification</returns>
/// TODO: Figure out why we have this and NormalizeDiscType as well
#if NET48
public static string GetFixedMediaType(MediaType? mediaType, string picIdentifier, long? size, long? layerbreak, long? layerbreak2, long? layerbreak3)
#else
public static string? GetFixedMediaType(MediaType? mediaType, string? picIdentifier, long? size, long? layerbreak, long? layerbreak2, long? layerbreak3)
#endif
{
switch (mediaType)
{
case MediaType.DVD:
if (layerbreak != default && layerbreak != default(long))
return $"{mediaType.LongName()}-9";
else
return $"{mediaType.LongName()}-5";
case MediaType.BluRay:
if (layerbreak3 != default && layerbreak3 != default(long))
return $"{mediaType.LongName()}-128";
else if (layerbreak2 != default && layerbreak2 != default(long))
return $"{mediaType.LongName()}-100";
else if (layerbreak != default && layerbreak != default(long) && picIdentifier == SabreTools.Models.PIC.Constants.DiscTypeIdentifierROMUltra)
return $"{mediaType.LongName()}-66";
else if (layerbreak != default && layerbreak != default(long) && size > 53_687_063_712)
return $"{mediaType.LongName()}-66";
else if (layerbreak != default && layerbreak != default(long))
return $"{mediaType.LongName()}-50";
else if (picIdentifier == SabreTools.Models.PIC.Constants.DiscTypeIdentifierROMUltra)
return $"{mediaType.LongName()}-33";
else if (size > 26_843_531_856)
return $"{mediaType.LongName()}-33";
else
return $"{mediaType.LongName()}-25";
case MediaType.UMD:
if (layerbreak != default && layerbreak != default(long))
return $"{mediaType.LongName()}-DL";
else
return $"{mediaType.LongName()}-SL";
default:
return mediaType.LongName();
}
}
/// <summary>
/// Process any fields that have to be combined
/// </summary>
/// <param name="info">Information object to normalize</param>
#if NET48
public static void ProcessSpecialFields(SubmissionInfo info)
#else
public static void ProcessSpecialFields(SubmissionInfo? info)
#endif
{
// If there is no submission info
if (info == null)
return;
// Process the comments field
if (info.CommonDiscInfo?.CommentsSpecialFields != null && info.CommonDiscInfo.CommentsSpecialFields?.Any() == true)
{
// If the field is missing, add an empty one to fill in
if (info.CommonDiscInfo.Comments == null)
info.CommonDiscInfo.Comments = string.Empty;
// Add all special fields before any comments
info.CommonDiscInfo.Comments = string.Join(
"\n", OrderCommentTags(info.CommonDiscInfo.CommentsSpecialFields)
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
.Select(FormatSiteTag)
.Where(s => !string.IsNullOrEmpty(s))
) + "\n" + info.CommonDiscInfo.Comments;
// Normalize newlines
info.CommonDiscInfo.Comments = info.CommonDiscInfo.Comments.Replace("\r\n", "\n");
// Trim the comments field
info.CommonDiscInfo.Comments = info.CommonDiscInfo.Comments.Trim();
// Wipe out the special fields dictionary
info.CommonDiscInfo.CommentsSpecialFields = null;
}
// Process the contents field
if (info.CommonDiscInfo?.ContentsSpecialFields != null && info.CommonDiscInfo.ContentsSpecialFields?.Any() == true)
{
// If the field is missing, add an empty one to fill in
if (info.CommonDiscInfo.Contents == null)
info.CommonDiscInfo.Contents = string.Empty;
// Add all special fields before any contents
info.CommonDiscInfo.Contents = string.Join(
"\n", OrderContentTags(info.CommonDiscInfo.ContentsSpecialFields)
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
.Select(FormatSiteTag)
.Where(s => !string.IsNullOrEmpty(s))
) + "\n" + info.CommonDiscInfo.Contents;
// Normalize newlines
info.CommonDiscInfo.Contents = info.CommonDiscInfo.Contents.Replace("\r\n", "\n");
// Trim the contents field
info.CommonDiscInfo.Contents = info.CommonDiscInfo.Contents.Trim();
// Wipe out the special fields dictionary
info.CommonDiscInfo.ContentsSpecialFields = null;
}
}
/// <summary>
/// Write the data to the output folder
/// </summary>
/// <param name="outputDirectory">Output folder to write to</param>
/// <param name="filenameSuffix">Optional suffix to append to the filename</param>
/// <param name="lines">Preformatted list of lines to write out to the file</param>
/// <returns>True on success, false on error</returns>
#if NET48
public static (bool, string) WriteOutputData(string outputDirectory, string filenameSuffix, List<string> lines)
#else
public static (bool, string) WriteOutputData(string? outputDirectory, string? filenameSuffix, List<string>? lines)
#endif
{
// Check to see if the inputs are valid
if (lines == null)
return (false, "No formatted data found to write!");
// Now write out to a generic file
try
{
// Get the file path
var path = string.Empty;
if (string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
path = "!submissionInfo.txt";
else if (string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
path = $"!submissionInfo_{filenameSuffix}.txt";
else if (!string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
path = Path.Combine(outputDirectory, "!submissionInfo.txt");
else if (!string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
path = Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.txt");
using (var sw = new StreamWriter(File.Open(path, FileMode.Create, FileAccess.Write)))
{
foreach (string line in lines)
{
sw.WriteLine(line);
}
}
}
catch (Exception ex)
{
return (false, $"Writing could not complete: {ex}");
}
return (true, "Writing complete!");
}
/// <summary>
/// Write the data to the output folder
/// </summary>
/// <param name="outputDirectory">Output folder to write to</param>
/// <param name="filenameSuffix">Optional suffix to append to the filename</param>
/// <param name="info">SubmissionInfo object representing the JSON to write out to the file</param>
/// <param name="includedArtifacts">True if artifacts were included, false otherwise</param>
/// <returns>True on success, false on error</returns>
#if NET48
public static bool WriteOutputData(string outputDirectory, string filenameSuffix, SubmissionInfo info, bool includedArtifacts)
#else
public static bool WriteOutputData(string? outputDirectory, string? filenameSuffix, SubmissionInfo? info, bool includedArtifacts)
#endif
{
// Check to see if the input is valid
if (info == null)
return false;
try
{
// Serialize the JSON and get it writable
string json = JsonConvert.SerializeObject(info, Formatting.Indented);
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
// If we included artifacts, write to a GZip-compressed file
if (includedArtifacts)
{
var path = string.Empty;
if (string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
path = "!submissionInfo.json.gz";
else if (string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
path = $"!submissionInfo_{filenameSuffix}.json.gz";
else if (!string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
path = Path.Combine(outputDirectory, "!submissionInfo.json.gz");
else if (!string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
path = Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json.gz");
using (var fs = File.Create(path))
using (var gs = new GZipStream(fs, CompressionMode.Compress))
{
gs.Write(jsonBytes, 0, jsonBytes.Length);
}
}
// Otherwise, write out to a normal JSON
else
{
var path = string.Empty;
if (string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
path = "!submissionInfo.json";
else if (string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
path = $"!submissionInfo_{filenameSuffix}.json";
else if (!string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
path = Path.Combine(outputDirectory, "!submissionInfo.json");
else if (!string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
path = Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json");
using (var fs = File.Create(path))
{
fs.Write(jsonBytes, 0, jsonBytes.Length);
}
}
}
catch
{
// We don't care what the error is right now
return false;
}
return true;
}
/// <summary>
/// Write the protection data to the output folder
/// </summary>
/// <param name="outputDirectory">Output folder to write to</param>
/// <param name="filenameSuffix">Optional suffix to append to the filename</param>
/// <param name="info">SubmissionInfo object containing the protection information</param>
/// <returns>True on success, false on error</returns>
#if NET48
public static bool WriteProtectionData(string outputDirectory, string filenameSuffix, SubmissionInfo info)
#else
public static bool WriteProtectionData(string? outputDirectory, string? filenameSuffix, SubmissionInfo? info)
#endif
{
// Check to see if the inputs are valid
if (info?.CopyProtection?.FullProtections == null || !info.CopyProtection.FullProtections.Any())
return true;
// Now write out to a generic file
try
{
var path = string.Empty;
if (string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
path = "!protectionInfo.txt";
else if (string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
path = $"!protectionInfo{filenameSuffix}.txt";
else if (!string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
path = Path.Combine(outputDirectory, "!protectionInfo.txt");
else if (!string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
path = Path.Combine(outputDirectory, $"!protectionInfo{filenameSuffix}.txt");
using (var sw = new StreamWriter(File.Open(path, FileMode.Create, FileAccess.Write)))
{
foreach (var kvp in info.CopyProtection.FullProtections)
{
if (kvp.Value == null)
sw.WriteLine($"{kvp.Key}: None");
else
sw.WriteLine($"{kvp.Key}: {string.Join(", ", kvp.Value)}");
}
}
}
catch
{
// We don't care what the error is right now
return false;
}
return true;
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
#if NET48
private static void AddIfExists(List<string> output, string key, string value, int indent)
#else
private static void AddIfExists(List<string> output, string key, string? value, int indent)
#endif
{
// If there's no valid value to write
if (value == null)
return;
string prefix = "";
for (int i = 0; i < indent; i++)
prefix += "\t";
// Skip fields that need to keep internal whitespace intact
if (key != "Primary Volume Descriptor (PVD)"
&& key != "Header"
&& key != "Cuesheet")
{
// Convert to tabs
value = value.Replace("<tab>", "\t");
value = value.Replace("<TAB>", "\t");
value = value.Replace(" ", "\t");
// Sanitize whitespace around tabs
value = Regex.Replace(value, @"\s*\t\s*", "\t");
}
// If the value contains a newline
value = value.Replace("\r\n", "\n");
#if NET48
if (value.Contains("\n"))
#else
if (value.Contains('\n'))
#endif
{
output.Add(prefix + key + ":"); output.Add("");
string[] values = value.Split('\n');
foreach (string val in values)
output.Add(val);
output.Add("");
}
// For all regular values
else
{
output.Add(prefix + key + ": " + value);
}
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
#if NET48
private static void AddIfExists(List<string> output, string key, string[] value, int indent)
#else
private static void AddIfExists(List<string> output, string key, string?[]? value, int indent)
#endif
{
// If there's no valid value to write
if (value == null || value.Length == 0)
return;
AddIfExists(output, key, string.Join(", ", value), indent);
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
#if NET48
private static void AddIfExists(List<string> output, string key, List<int> value, int indent)
#else
private static void AddIfExists(List<string> output, string key, List<int>? value, int indent)
#endif
{
// If there's no valid value to write
if (value == null || value.Count == 0)
return;
AddIfExists(output, key, string.Join(", ", value.Select(o => o.ToString())), indent);
}
/// <summary>
/// Generate a list of all MPF-specific log files generated
/// </summary>
/// <param name="outputDirectory">Output folder to write to</param>
/// <param name="filenameSuffix">Optional suffix to append to the filename</param>
/// <returns>List of all log file paths, empty otherwise</returns>
#if NET48
private static List<string> GetGeneratedFilePaths(string outputDirectory, string filenameSuffix)
#else
private static List<string> GetGeneratedFilePaths(string? outputDirectory, string? filenameSuffix)
#endif
{
var files = new List<string>();
if (string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
{
if (File.Exists("!submissionInfo.txt"))
files.Add("!submissionInfo.txt");
if (File.Exists("!submissionInfo.json"))
files.Add("!submissionInfo.json");
if (File.Exists("!submissionInfo.json.gz"))
files.Add("!submissionInfo.json.gz");
if (File.Exists("!protectionInfo.txt"))
files.Add("!protectionInfo.txt");
}
else if (string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
{
if (File.Exists($"!submissionInfo_{filenameSuffix}.txt"))
files.Add($"!submissionInfo_{filenameSuffix}.txt");
if (File.Exists($"!submissionInfo_{filenameSuffix}.json"))
files.Add($"!submissionInfo_{filenameSuffix}.json");
if (File.Exists($"!submissionInfo_{filenameSuffix}.json.gz"))
files.Add($"!submissionInfo_{filenameSuffix}.json.gz");
if (File.Exists($"!protectionInfo_{filenameSuffix}.txt"))
files.Add($"!protectionInfo_{filenameSuffix}.txt");
}
else if (!string.IsNullOrWhiteSpace(outputDirectory) && string.IsNullOrWhiteSpace(filenameSuffix))
{
if (File.Exists(Path.Combine(outputDirectory, "!submissionInfo.txt")))
files.Add(Path.Combine(outputDirectory, "!submissionInfo.txt"));
if (File.Exists(Path.Combine(outputDirectory, "!submissionInfo.json")))
files.Add(Path.Combine(outputDirectory, "!submissionInfo.json"));
if (File.Exists(Path.Combine(outputDirectory, "!submissionInfo.json.gz")))
files.Add(Path.Combine(outputDirectory, "!submissionInfo.json.gz"));
if (File.Exists(Path.Combine(outputDirectory, "!protectionInfo.txt")))
files.Add(Path.Combine(outputDirectory, "!protectionInfo.txt"));
}
else if (!string.IsNullOrWhiteSpace(outputDirectory) && !string.IsNullOrWhiteSpace(filenameSuffix))
{
if (File.Exists(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.txt")))
files.Add(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.txt"));
if (File.Exists(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json")))
files.Add(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json"));
if (File.Exists(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json.gz")))
files.Add(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json.gz"));
if (File.Exists(Path.Combine(outputDirectory, $"!protectionInfo_{filenameSuffix}.txt")))
files.Add(Path.Combine(outputDirectory, $"!protectionInfo_{filenameSuffix}.txt"));
}
return files;
}
#endregion
#region Normalization
/// <summary>
/// Adjust a disc title so that it will be processed correctly by Redump
/// </summary>
/// <param name="title">Existing title to potentially reformat</param>
/// <param name="languages">Array of languages to use for assuming articles</param>
/// <returns>The reformatted title</returns>
public static string NormalizeDiscTitle(string title, Language[] languages)
{
// If we have no set languages, then assume English
if (languages == null || languages.Length == 0)
languages = new Language[] { Language.English };
// Loop through all of the given languages
foreach (var language in languages)
{
// If the new title is different, assume it was normalized and return it
string newTitle = NormalizeDiscTitle(title, language);
if (newTitle == title)
return newTitle;
}
// If we didn't already try English, try it now
if (!languages.Contains(Language.English))
return NormalizeDiscTitle(title, Language.English);
// If all fails, then the title didn't need normalization
return title;
}
/// <summary>
/// Adjust a disc title so that it will be processed correctly by Redump
/// </summary>
/// <param name="title">Existing title to potentially reformat</param>
/// <param name="language">Language to use for assuming articles</param>
/// <returns>The reformatted title</returns>
/// <remarks>
/// If the language of the title is unknown or if it's multilingual,
/// pass in Language.English for standardized coverage.
/// </remarks>
public static string NormalizeDiscTitle(string title, Language language)
{
// If we have an invalid title, just return it as-is
if (string.IsNullOrWhiteSpace(title))
return title;
// Get the title split into parts
string[] splitTitle = title.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
// If we only have one part, we can't do anything
if (splitTitle.Length <= 1)
return title;
// Determine if we have a definite or indefinite article as the first item
string firstItem = splitTitle[0];
switch (firstItem.ToLowerInvariant())
{
// Latin script articles
case "'n"
when language is Language.Manx:
case "a"
when language is Language.English
|| language is Language.Hungarian
|| language is Language.Portuguese
|| language is Language.Scots:
case "a'"
when language is Language.English
|| language is Language.Hungarian
|| language is Language.Irish
|| language is Language.Gaelic: // Scottish Gaelic
case "al"
when language is Language.Breton:
case "am"
when language is Language.Gaelic: // Scottish Gaelic
case "an"
when language is Language.Breton
|| language is Language.Cornish
|| language is Language.English
|| language is Language.Irish
|| language is Language.Gaelic: // Scottish Gaelic
case "anek"
when language is Language.Nepali:
case "ar"
when language is Language.Breton:
case "az"
when language is Language.Hungarian:
case "ān"
when language is Language.Persian:
case "as"
when language is Language.Portuguese:
case "d'"
when language is Language.Luxembourgish:
case "das"
when language is Language.German:
case "dat"
when language is Language.Luxembourgish:
case "de"
when language is Language.Dutch:
case "déi"
when language is Language.Luxembourgish:
case "dem"
when language is Language.German
|| language is Language.Luxembourgish:
case "den"
when language is Language.Dutch
|| language is Language.German
|| language is Language.Luxembourgish:
case "der"
when language is Language.Dutch
|| language is Language.German
|| language is Language.Luxembourgish:
case "des"
when language is Language.Dutch
|| language is Language.French
|| language is Language.German:
case "die"
when language is Language.Afrikaans
|| language is Language.German:
case "e"
when language is Language.Papiamento:
case "een"
when language is Language.Dutch:
case "egy"
when language is Language.Hungarian:
case "ei"
when language is Language.Norwegian:
case "ein"
when language is Language.German
|| language is Language.Norwegian:
case "eine"
when language is Language.German:
case "einem"
when language is Language.German:
case "einen"
when language is Language.German:
case "einer"
when language is Language.German:
case "eines"
when language is Language.German:
case "eit"
when language is Language.Norwegian:
case "ek"
when language is Language.Nepali:
case "el"
when language is Language.Arabic
|| language is Language.Catalan
|| language is Language.Spanish:
case "els"
when language is Language.Catalan:
case "en"
when language is Language.Danish
|| language is Language.Luxembourgish
|| language is Language.Norwegian
|| language is Language.Swedish:
case "eng"
when language is Language.Luxembourgish:
case "engem"
when language is Language.Luxembourgish:
case "enger"
when language is Language.Luxembourgish:
case "es"
when language is Language.Catalan:
case "et"
when language is Language.Danish
|| language is Language.Norwegian:
case "ett"
when language is Language.Swedish:
case "euta"
when language is Language.Nepali:
case "euti"
when language is Language.Nepali:
case "gli"
when language is Language.Italian:
case "he"
when language is Language.Hawaiian
|| language is Language.Maori:
case "het"
when language is Language.Dutch:
case "i"
when language is Language.Italian
|| language is Language.Khasi:
case "il"
when language is Language.Italian:
case "in"
when language is Language.Persian:
case "ka"
when language is Language.Hawaiian
|| language is Language.Khasi:
case "ke"
when language is Language.Hawaiian:
case "ki"
when language is Language.Khasi:
case "kunai"
when language is Language.Nepali:
case "l'"
when language is Language.Catalan
|| language is Language.French
|| language is Language.Italian:
case "la"
when language is Language.Catalan
|| language is Language.Esperanto
|| language is Language.French
|| language is Language.Italian
|| language is Language.Spanish:
case "las"
when language is Language.Spanish:
case "le"
when language is Language.French
|| language is Language.Interlingua
|| language is Language.Italian:
case "les"
when language is Language.Catalan
|| language is Language.French:
case "lo"
when language is Language.Catalan
|| language is Language.Italian
|| language is Language.Spanish:
case "los"
when language is Language.Catalan
|| language is Language.Spanish:
case "na"
when language is Language.Irish
|| language is Language.Gaelic: // Scottish Gaelic
case "nam"
when language is Language.Gaelic: // Scottish Gaelic
case "nan"
when language is Language.Gaelic: // Scottish Gaelic
case "nā"
when language is Language.Hawaiian:
case "ngā"
when language is Language.Maori:
case "niște"
when language is Language.Romanian:
case "ny"
when language is Language.Manx:
case "o"
when language is Language.Portuguese
|| language is Language.Romanian:
case "os"
when language is Language.Portuguese:
case "sa"
when language is Language.Catalan:
case "sang"
when language is Language.Malay:
case "se"
when language is Language.Finnish:
case "ses"
when language is Language.Catalan:
case "si"
when language is Language.Malay:
case "te"
when language is Language.Maori:
case "the"
when language is Language.English
|| language is Language.Scots:
case "u"
when language is Language.Khasi:
case "ul"
when language is Language.Breton:
case "um"
when language is Language.Portuguese:
case "uma"
when language is Language.Portuguese:
case "umas"
when language is Language.Portuguese:
case "un"
when language is Language.Breton
|| language is Language.Catalan
|| language is Language.French
|| language is Language.Interlingua
|| language is Language.Italian
|| language is Language.Papiamento
|| language is Language.Romanian
|| language is Language.Spanish:
case "un'"
when language is Language.Italian:
case "una"
when language is Language.Catalan
|| language is Language.Italian:
case "unas"
when language is Language.Spanish:
case "une"
when language is Language.French:
case "uno"
when language is Language.Italian:
case "unos"
when language is Language.Spanish:
case "uns"
when language is Language.Catalan
|| language is Language.Portuguese:
case "unei"
when language is Language.Romanian:
case "unes"
when language is Language.Catalan:
case "unor"
when language is Language.Romanian:
case "unui"
when language is Language.Romanian:
case "ur"
when language is Language.Breton:
case "y"
when language is Language.Manx
|| language is Language.Welsh:
case "ye"
when language is Language.Persian:
case "yek"
when language is Language.Persian:
case "yn"
when language is Language.Manx:
case "yr"
when language is Language.Welsh:
// Non-latin script articles
case "ο"
when language is Language.Greek:
case "η"
when language is Language.Greek:
case "το"
when language is Language.Greek:
case "οι"
when language is Language.Greek:
case "τα"
when language is Language.Greek:
case "ένας"
when language is Language.Greek:
case "μια"
when language is Language.Greek:
case "ένα"
when language is Language.Greek:
case "еден"
when language is Language.Macedonian:
case "една"
when language is Language.Macedonian:
case "едно"
when language is Language.Macedonian:
case "едни"
when language is Language.Macedonian:
case "एउटा"
when language is Language.Nepali:
case "एउटी"
when language is Language.Nepali:
case "एक"
when language is Language.Nepali:
case "अनेक"
when language is Language.Nepali:
case "कुनै"
when language is Language.Nepali:
case "דער"
when language is Language.Yiddish:
case "די"
when language is Language.Yiddish:
case "דאָס"
when language is Language.Yiddish:
case "דעם"
when language is Language.Yiddish:
case "אַ"
when language is Language.Yiddish:
case "אַן"
when language is Language.Yiddish:
// Seen by Redump, unknown origin
case "du":
break;
// Otherwise, just return it as-is
default:
return title;
}
// Insert the first item if we have a `:` or `-`
bool itemInserted = false;
var newTitleBuilder = new StringBuilder();
for (int i = 1; i < splitTitle.Length; i++)
{
string segment = splitTitle[i];
if (segment.EndsWith(":") || segment.EndsWith("-"))
{
itemInserted = true;
newTitleBuilder.Append($"{segment}, {firstItem}");
}
else
{
newTitleBuilder.Append($"{segment} ");
}
}
// If we didn't insert the item yet, add it to the end
string newTitle = newTitleBuilder.ToString().Trim();
if (!itemInserted)
newTitle = $"{newTitle}, {firstItem}";
return newTitle;
}
/// <summary>
/// Adjust the disc type based on size and layerbreak information
/// </summary>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <returns>Corrected disc type, if possible</returns>
public static void NormalizeDiscType(SubmissionInfo info)
{
// If we have nothing valid, do nothing
if (info?.CommonDiscInfo?.Media == null || info?.SizeAndChecksums == null)
return;
switch (info.CommonDiscInfo.Media)
{
case DiscType.BD25:
case DiscType.BD33:
case DiscType.BD50:
case DiscType.BD66:
case DiscType.BD100:
case DiscType.BD128:
if (info.SizeAndChecksums.Layerbreak3 != default)
info.CommonDiscInfo.Media = DiscType.BD128;
else if (info.SizeAndChecksums.Layerbreak2 != default)
info.CommonDiscInfo.Media = DiscType.BD100;
else if (info.SizeAndChecksums.Layerbreak != default && info.SizeAndChecksums.PICIdentifier == SabreTools.Models.PIC.Constants.DiscTypeIdentifierROMUltra)
info.CommonDiscInfo.Media = DiscType.BD66;
else if (info.SizeAndChecksums.Layerbreak != default && info.SizeAndChecksums.Size > 50_050_629_632)
info.CommonDiscInfo.Media = DiscType.BD66;
else if (info.SizeAndChecksums.Layerbreak != default)
info.CommonDiscInfo.Media = DiscType.BD50;
else if (info.SizeAndChecksums.PICIdentifier == SabreTools.Models.PIC.Constants.DiscTypeIdentifierROMUltra)
info.CommonDiscInfo.Media = DiscType.BD33;
else if (info.SizeAndChecksums.Size > 25_025_314_816)
info.CommonDiscInfo.Media = DiscType.BD33;
else
info.CommonDiscInfo.Media = DiscType.BD25;
break;
case DiscType.DVD5:
case DiscType.DVD9:
if (info.SizeAndChecksums.Layerbreak != default)
info.CommonDiscInfo.Media = DiscType.DVD9;
else
info.CommonDiscInfo.Media = DiscType.DVD5;
break;
case DiscType.HDDVDSL:
case DiscType.HDDVDDL:
if (info.SizeAndChecksums.Layerbreak != default)
info.CommonDiscInfo.Media = DiscType.HDDVDDL;
else
info.CommonDiscInfo.Media = DiscType.HDDVDSL;
break;
case DiscType.UMDSL:
case DiscType.UMDDL:
if (info.SizeAndChecksums.Layerbreak != default)
info.CommonDiscInfo.Media = DiscType.UMDDL;
else
info.CommonDiscInfo.Media = DiscType.UMDSL;
break;
// All other disc types are not processed
default:
break;
}
}
/// <summary>
/// Normalize a split set of paths
/// </summary>
/// <param name="path">Path value to normalize</param>
#if NET48
public static string NormalizeOutputPaths(string path, bool getFullPath)
#else
public static string NormalizeOutputPaths(string? path, bool getFullPath)
#endif
{
// The easy way
try
{
// If we have an invalid path
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
// Remove quotes from path
path = path.Replace("\"", string.Empty);
// Try getting the combined path and returning that directly
string fullPath = getFullPath ? Path.GetFullPath(path) : path;
var fullDirectory = Path.GetDirectoryName(fullPath);
string fullFile = Path.GetFileName(fullPath);
// Remove invalid path characters
if (fullDirectory != null)
{
foreach (char c in Path.GetInvalidPathChars())
fullDirectory = fullDirectory.Replace(c, '_');
}
// Remove invalid filename characters
foreach (char c in Path.GetInvalidFileNameChars())
fullFile = fullFile.Replace(c, '_');
if (string.IsNullOrWhiteSpace(fullDirectory))
return fullFile;
else
return Path.Combine(fullDirectory, fullFile);
}
catch { }
return path ?? string.Empty;
}
#endregion
#region Helpers
/// <summary>
/// Format a single site tag to string
/// </summary>
/// <param name="kvp">KeyValuePair representing the site tag and value</param>
/// <returns>String-formatted tag and value</returns>
private static string FormatSiteTag(KeyValuePair<SiteCode?, string> kvp)
{
bool isMultiLine = SubmissionInfoTool.IsMultiLine(kvp.Key);
string line = $"{kvp.Key.ShortName()}{(isMultiLine ? "\n" : " ")}";
// Special case for boolean fields
if (IsBoolean(kvp.Key))
{
if (kvp.Value != true.ToString())
return string.Empty;
return line.Trim();
}
return $"{line}{kvp.Value}{(isMultiLine ? "\n" : string.Empty)}";
}
/// <summary>
/// Check if a site code is boolean or not
/// </summary>
/// <param name="siteCode">SiteCode to check</param>
/// <returns>True if the code field is a flag with no value, false otherwise</returns>
/// <remarks>TODO: This should move to Extensions at some point</remarks>
private static bool IsBoolean(SiteCode? siteCode)
{
#if NET48
switch (siteCode)
{
case SiteCode.PostgapType:
case SiteCode.VCD:
return true;
default:
return false;
}
#else
return siteCode switch
{
SiteCode.PostgapType => true,
SiteCode.VCD => true,
_ => false,
};
#endif
}
/// <summary>
/// Order comment code tags according to Redump requirements
/// </summary>
/// <returns>Ordered list of KeyValuePairs representing the tags and values</returns>
#if NET48
private static List<KeyValuePair<SiteCode?, string>> OrderCommentTags(Dictionary<SiteCode?, string> tags)
#else
private static List<KeyValuePair<SiteCode?, string>> OrderCommentTags(Dictionary<SiteCode, string> tags)
#endif
{
var sorted = new List<KeyValuePair<SiteCode?, string>>();
// If the input is invalid, just return an empty set
if (tags == null || tags.Count == 0)
return sorted;
// Identifying Info
if (tags.ContainsKey(SiteCode.AlternativeTitle))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.AlternativeTitle, tags[SiteCode.AlternativeTitle]));
if (tags.ContainsKey(SiteCode.AlternativeForeignTitle))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.AlternativeForeignTitle, tags[SiteCode.AlternativeForeignTitle]));
if (tags.ContainsKey(SiteCode.InternalName))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.InternalName, tags[SiteCode.InternalName]));
if (tags.ContainsKey(SiteCode.InternalSerialName))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.InternalSerialName, tags[SiteCode.InternalSerialName]));
if (tags.ContainsKey(SiteCode.VolumeLabel))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.VolumeLabel, tags[SiteCode.VolumeLabel]));
if (tags.ContainsKey(SiteCode.Multisession))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Multisession, tags[SiteCode.Multisession]));
if (tags.ContainsKey(SiteCode.UniversalHash))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.UniversalHash, tags[SiteCode.UniversalHash]));
if (tags.ContainsKey(SiteCode.RingNonZeroDataStart))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.RingNonZeroDataStart, tags[SiteCode.RingNonZeroDataStart]));
if (tags.ContainsKey(SiteCode.XMID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.XMID, tags[SiteCode.XMID]));
if (tags.ContainsKey(SiteCode.XeMID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.XeMID, tags[SiteCode.XeMID]));
if (tags.ContainsKey(SiteCode.DMIHash))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.DMIHash, tags[SiteCode.DMIHash]));
if (tags.ContainsKey(SiteCode.PFIHash))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PFIHash, tags[SiteCode.PFIHash]));
if (tags.ContainsKey(SiteCode.SSHash))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SSHash, tags[SiteCode.SSHash]));
if (tags.ContainsKey(SiteCode.SSVersion))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SSVersion, tags[SiteCode.SSVersion]));
if (tags.ContainsKey(SiteCode.Filename))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Filename, tags[SiteCode.Filename]));
if (tags.ContainsKey(SiteCode.BBFCRegistrationNumber))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.BBFCRegistrationNumber, tags[SiteCode.BBFCRegistrationNumber]));
if (tags.ContainsKey(SiteCode.CDProjektID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.CDProjektID, tags[SiteCode.CDProjektID]));
if (tags.ContainsKey(SiteCode.DiscHologramID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.DiscHologramID, tags[SiteCode.DiscHologramID]));
if (tags.ContainsKey(SiteCode.DNASDiscID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.DNASDiscID, tags[SiteCode.DNASDiscID]));
if (tags.ContainsKey(SiteCode.ISBN))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ISBN, tags[SiteCode.ISBN]));
if (tags.ContainsKey(SiteCode.ISSN))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ISSN, tags[SiteCode.ISSN]));
if (tags.ContainsKey(SiteCode.PPN))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PPN, tags[SiteCode.PPN]));
if (tags.ContainsKey(SiteCode.VFCCode))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.VFCCode, tags[SiteCode.VFCCode]));
if (tags.ContainsKey(SiteCode.Genre))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Genre, tags[SiteCode.Genre]));
if (tags.ContainsKey(SiteCode.Series))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Series, tags[SiteCode.Series]));
if (tags.ContainsKey(SiteCode.PostgapType))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PostgapType, tags[SiteCode.PostgapType]));
if (tags.ContainsKey(SiteCode.VCD))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.VCD, tags[SiteCode.VCD]));
// Publisher / Company IDs
if (tags.ContainsKey(SiteCode.AcclaimID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.AcclaimID, tags[SiteCode.AcclaimID]));
if (tags.ContainsKey(SiteCode.ActivisionID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ActivisionID, tags[SiteCode.ActivisionID]));
if (tags.ContainsKey(SiteCode.BandaiID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.BandaiID, tags[SiteCode.BandaiID]));
if (tags.ContainsKey(SiteCode.ElectronicArtsID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ElectronicArtsID, tags[SiteCode.ElectronicArtsID]));
if (tags.ContainsKey(SiteCode.FoxInteractiveID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.FoxInteractiveID, tags[SiteCode.FoxInteractiveID]));
if (tags.ContainsKey(SiteCode.GTInteractiveID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.GTInteractiveID, tags[SiteCode.GTInteractiveID]));
if (tags.ContainsKey(SiteCode.JASRACID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.JASRACID, tags[SiteCode.JASRACID]));
if (tags.ContainsKey(SiteCode.KingRecordsID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.KingRecordsID, tags[SiteCode.KingRecordsID]));
if (tags.ContainsKey(SiteCode.KoeiID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.KoeiID, tags[SiteCode.KoeiID]));
if (tags.ContainsKey(SiteCode.KonamiID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.KonamiID, tags[SiteCode.KonamiID]));
if (tags.ContainsKey(SiteCode.LucasArtsID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.LucasArtsID, tags[SiteCode.LucasArtsID]));
if (tags.ContainsKey(SiteCode.MicrosoftID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.MicrosoftID, tags[SiteCode.MicrosoftID]));
if (tags.ContainsKey(SiteCode.NaganoID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.NaganoID, tags[SiteCode.NaganoID]));
if (tags.ContainsKey(SiteCode.NamcoID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.NamcoID, tags[SiteCode.NamcoID]));
if (tags.ContainsKey(SiteCode.NipponIchiSoftwareID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.NipponIchiSoftwareID, tags[SiteCode.NipponIchiSoftwareID]));
if (tags.ContainsKey(SiteCode.OriginID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.OriginID, tags[SiteCode.OriginID]));
if (tags.ContainsKey(SiteCode.PonyCanyonID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PonyCanyonID, tags[SiteCode.PonyCanyonID]));
if (tags.ContainsKey(SiteCode.SegaID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SegaID, tags[SiteCode.SegaID]));
if (tags.ContainsKey(SiteCode.SelenID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SelenID, tags[SiteCode.SelenID]));
if (tags.ContainsKey(SiteCode.SierraID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.SierraID, tags[SiteCode.SierraID]));
if (tags.ContainsKey(SiteCode.TaitoID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.TaitoID, tags[SiteCode.TaitoID]));
if (tags.ContainsKey(SiteCode.UbisoftID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.UbisoftID, tags[SiteCode.UbisoftID]));
if (tags.ContainsKey(SiteCode.ValveID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ValveID, tags[SiteCode.ValveID]));
return sorted;
}
/// <summary>
/// Order content code tags according to Redump requirements
/// </summary>
/// <returns>Ordered list of KeyValuePairs representing the tags and values</returns>
#if NET48
private static List<KeyValuePair<SiteCode?, string>> OrderContentTags(Dictionary<SiteCode?, string> tags)
#else
private static List<KeyValuePair<SiteCode?, string>> OrderContentTags(Dictionary<SiteCode, string> tags)
#endif
{
var sorted = new List<KeyValuePair<SiteCode?, string>>();
// If the input is invalid, just return an empty set
if (tags == null || tags.Count == 0)
return sorted;
// Games
if (tags.ContainsKey(SiteCode.Games))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Games, tags[SiteCode.Games]));
if (tags.ContainsKey(SiteCode.NetYarozeGames))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.NetYarozeGames, tags[SiteCode.NetYarozeGames]));
// Demos
if (tags.ContainsKey(SiteCode.PlayableDemos))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.PlayableDemos, tags[SiteCode.PlayableDemos]));
if (tags.ContainsKey(SiteCode.RollingDemos))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.RollingDemos, tags[SiteCode.RollingDemos]));
if (tags.ContainsKey(SiteCode.TechDemos))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.TechDemos, tags[SiteCode.TechDemos]));
// Video
if (tags.ContainsKey(SiteCode.GameFootage))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.GameFootage, tags[SiteCode.GameFootage]));
if (tags.ContainsKey(SiteCode.Videos))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Videos, tags[SiteCode.Videos]));
// Miscellaneous
if (tags.ContainsKey(SiteCode.Patches))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Patches, tags[SiteCode.Patches]));
if (tags.ContainsKey(SiteCode.Savegames))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Savegames, tags[SiteCode.Savegames]));
if (tags.ContainsKey(SiteCode.Extras))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Extras, tags[SiteCode.Extras]));
return sorted;
}
#endregion
}
}