using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Schema;
using System.Xml;
using System.Xml.Serialization;
using MPF.Core.Data;
using MPF.Core.Hashing;
using MPF.Core.Utilities;
using SabreTools.Models.PIC;
using SabreTools.RedumpLib.Data;
namespace MPF.Core.Modules
{
public abstract class BaseParameters
{
#region Event Handlers
///
/// Geneeic way of reporting a message
///
/// String value to report
#if NET48
public EventHandler ReportStatus;
#else
public EventHandler? ReportStatus;
#endif
#endregion
#region Generic Dumping Information
///
/// Base command to run
///
#if NET48
public string BaseCommand { get; set; }
#else
public string? BaseCommand { get; set; }
#endif
///
/// Set of flags to pass to the executable
///
protected Dictionary flags = new Dictionary();
protected internal IEnumerable Keys => flags.Keys;
///
/// Safe access to currently set flags
///
public bool? this[string key]
{
get
{
if (flags.ContainsKey(key))
return flags[key];
return null;
}
set
{
flags[key] = value;
}
}
///
/// Process to track external program
///
#if NET48
private Process process;
#else
private Process? process;
#endif
#endregion
#region Virtual Dumping Information
///
/// Command to flag support mappings
///
#if NET48
public Dictionary> CommandSupport => GetCommandSupport();
#else
public Dictionary>? CommandSupport => GetCommandSupport();
#endif
///
/// Input path for operations
///
#if NET48
public virtual string InputPath => null;
#else
public virtual string? InputPath => null;
#endif
///
/// Output path for operations
///
/// String representing the path, null on error
#if NET48
public virtual string OutputPath => null;
#else
public virtual string? OutputPath => null;
#endif
///
/// Get the processing speed from the implementation
///
public virtual int? Speed { get; set; } = null;
#endregion
#region Metadata
///
/// Path to the executable
///
#if NET48
public string ExecutablePath { get; set; }
#else
public string? ExecutablePath { get; set; }
#endif
///
/// Program that this set of parameters represents
///
public virtual InternalProgram InternalProgram { get; }
///
/// Currently represented system
///
public RedumpSystem? System { get; set; }
///
/// Currently represented media type
///
public MediaType? Type { get; set; }
#endregion
///
/// Populate a Parameters object from a param string
///
/// String possibly representing a set of parameters
#if NET48
public BaseParameters(string parameters)
#else
public BaseParameters(string? parameters)
#endif
{
// If any parameters are not valid, wipe out everything
if (!ValidateAndSetParameters(parameters))
ResetValues();
}
///
/// Generate parameters based on a set of known inputs
///
/// RedumpSystem value to use
/// MediaType value to use
/// Drive letter to use
/// Filename to use
/// Drive speed to use
/// Options object containing all settings that may be used for setting parameters
public BaseParameters(RedumpSystem? system, MediaType? type, char driveLetter, string filename, int? driveSpeed, Options options)
{
this.System = system;
this.Type = type;
SetDefaultParameters(driveLetter, filename, driveSpeed, options);
}
#region Abstract Methods
///
/// Validate if all required output files exist
///
/// Base filename and path to use for checking
/// True if this is a check done before a dump, false if done after
/// Tuple of true if all required files exist, false otherwise and a list representing missing files
public abstract (bool, List) CheckAllOutputFilesExist(string basePath, bool preCheck);
///
/// Generate a SubmissionInfo for the output files
///
/// Base submission info to fill in specifics for
/// Options object representing user-defined options
/// Base filename and path to use for checking
/// Drive representing the disc to get information from
/// True to include output files as encoded artifacts, false otherwise
#if NET48
public abstract void GenerateSubmissionInfo(SubmissionInfo submissionInfo, Options options, string basePath, Drive drive, bool includeArtifacts);
#else
public abstract void GenerateSubmissionInfo(SubmissionInfo submissionInfo, Options options, string basePath, Drive? drive, bool includeArtifacts);
#endif
#endregion
#region Virtual Methods
///
/// Get all commands mapped to the supported flags
///
/// Mappings from command to supported flags
#if NET48
public virtual Dictionary> GetCommandSupport() => null;
#else
public virtual Dictionary>? GetCommandSupport() => null;
#endif
///
/// Blindly generate a parameter string based on the inputs
///
/// Parameter string for invocation, null on error
#if NET48
public virtual string GenerateParameters() => null;
#else
public virtual string? GenerateParameters() => null;
#endif
///
/// Get the default extension for a given media type
///
/// MediaType value to check
/// String representing the media type, null on error
#if NET48
public virtual string GetDefaultExtension(MediaType? mediaType) => null;
#else
public virtual string? GetDefaultExtension(MediaType? mediaType) => null;
#endif
///
/// Generate a list of all log files generated
///
/// Base filename and path to use for checking
/// List of all log file paths, empty otherwise
public virtual List GetLogFilePaths(string basePath) => new List();
///
/// Get the MediaType from the current set of parameters
///
/// MediaType value if successful, null on error
public virtual MediaType? GetMediaType() => null;
///
/// Gets if the current command is considered a dumping command or not
///
/// True if it's a dumping command, false otherwise
public virtual bool IsDumpingCommand() => true;
///
/// Gets if the flag is supported by the current command
///
/// Flag value to check
/// True if the flag value is supported, false otherwise
public virtual bool IsFlagSupported(string flag)
{
if (CommandSupport == null)
return false;
if (this.BaseCommand == null)
return false;
if (!CommandSupport.ContainsKey(this.BaseCommand))
return false;
return CommandSupport[this.BaseCommand].Contains(flag);
}
///
/// Returns if the current Parameter object is valid
///
///
public bool IsValid() => GenerateParameters() != null;
///
/// Reset all special variables to have default values
///
protected virtual void ResetValues() { }
///
/// Set default parameters for a given system and media type
///
/// Drive letter to use
/// Filename to use
/// Drive speed to use
/// Options object containing all settings that may be used for setting parameters
protected virtual void SetDefaultParameters(char driveLetter, string filename, int? driveSpeed, Options options) { }
///
/// Scan a possible parameter string and populate whatever possible
///
/// String possibly representing parameters
/// True if the parameters were set correctly, false otherwise
#if NET48
protected virtual bool ValidateAndSetParameters(string parameters) => !string.IsNullOrWhiteSpace(parameters);
#else
protected virtual bool ValidateAndSetParameters(string? parameters) => !string.IsNullOrWhiteSpace(parameters);
#endif
#endregion
#region Execution
///
/// Run internal program
///
/// True to show in separate window, false otherwise
public void ExecuteInternalProgram(bool separateWindow)
{
// Create the start info
var startInfo = new ProcessStartInfo()
{
FileName = ExecutablePath,
Arguments = GenerateParameters() ?? "",
CreateNoWindow = !separateWindow,
UseShellExecute = separateWindow,
RedirectStandardOutput = !separateWindow,
RedirectStandardError = !separateWindow,
};
// Create the new process
process = new Process() { StartInfo = startInfo };
// Start the process
process.Start();
// Start processing tasks, if necessary
if (!separateWindow)
{
_ = Logging.OutputToLog(process.StandardOutput, this, ReportStatus);
_ = Logging.OutputToLog(process.StandardError, this, ReportStatus);
}
process.WaitForExit();
process.Close();
}
///
/// Cancel an in-progress dumping process
///
public void KillInternalProgram()
{
try
{
while (process != null && !process.HasExited)
{
process.Kill();
}
}
catch
{ }
}
#endregion
#region Parameter Parsing
///
/// Returns whether or not the selected item exists
///
/// List of parameters to check against
/// Current index
/// True if the next item exists, false otherwise
protected static bool DoesExist(List parameters, int index)
=> index < parameters.Count;
///
/// Get the Base64 representation of a string
///
/// String content to encode
/// Base64-encoded contents, if possible
#if NET48
protected static string GetBase64(string content)
#else
protected static string? GetBase64(string? content)
#endif
{
if (string.IsNullOrEmpty(content))
return null;
byte[] temp = Encoding.UTF8.GetBytes(content);
return Convert.ToBase64String(temp);
}
///
/// Get the full lines from the input file, if possible
///
/// file location
/// True if should read as binary, false otherwise (default)
/// Full text of the file, null on error
#if NET48
protected static string GetFullFile(string filename, bool binary = false)
#else
protected 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);
}
///
/// Returns whether a string is a valid drive letter
///
/// String value to check
/// True if it's a valid drive letter, false otherwise
protected static bool IsValidDriveLetter(string parameter)
=> Regex.IsMatch(parameter, @"^[A-Z]:?\\?$");
///
/// Returns whether a string is a valid bool
///
/// String value to check
/// True if it's a valid bool, false otherwise
protected static bool IsValidBool(string parameter)
=> bool.TryParse(parameter, out bool _);
///
/// Returns whether a string is a valid byte
///
/// String value to check
/// Lower bound (>=)
/// Upper bound (<=)
/// True if it's a valid byte, false otherwise
protected static bool IsValidInt8(string parameter, sbyte lowerBound = -1, sbyte upperBound = -1)
{
(string value, long _) = ExtractFactorFromValue(parameter);
if (!sbyte.TryParse(value, out sbyte temp))
return false;
else if (lowerBound != -1 && temp < lowerBound)
return false;
else if (upperBound != -1 && temp > upperBound)
return false;
return true;
}
///
/// Returns whether a string is a valid Int16
///
/// String value to check
/// Lower bound (>=)
/// Upper bound (<=)
/// True if it's a valid Int16, false otherwise
protected static bool IsValidInt16(string parameter, short lowerBound = -1, short upperBound = -1)
{
(string value, long _) = ExtractFactorFromValue(parameter);
if (!short.TryParse(value, out short temp))
return false;
else if (lowerBound != -1 && temp < lowerBound)
return false;
else if (upperBound != -1 && temp > upperBound)
return false;
return true;
}
///
/// Returns whether a string is a valid Int32
///
/// String value to check
/// Lower bound (>=)
/// Upper bound (<=)
/// True if it's a valid Int32, false otherwise
protected static bool IsValidInt32(string parameter, int lowerBound = -1, int upperBound = -1)
{
(string value, long _) = ExtractFactorFromValue(parameter);
if (!int.TryParse(value, out int temp))
return false;
else if (lowerBound != -1 && temp < lowerBound)
return false;
else if (upperBound != -1 && temp > upperBound)
return false;
return true;
}
///
/// Returns whether a string is a valid Int64
///
/// String value to check
/// Lower bound (>=)
/// Upper bound (<=)
/// True if it's a valid Int64, false otherwise
protected static bool IsValidInt64(string parameter, long lowerBound = -1, long upperBound = -1)
{
(string value, long _) = ExtractFactorFromValue(parameter);
if (!long.TryParse(value, out long temp))
return false;
else if (lowerBound != -1 && temp < lowerBound)
return false;
else if (upperBound != -1 && temp > upperBound)
return false;
return true;
}
///
/// Process a flag parameter
///
/// List of parts to be referenced
/// Flag string, if available
/// Reference to the position in the parts
/// True if the parameter was processed successfully or skipped, false otherwise
protected bool ProcessFlagParameter(List parts, string flagString, ref int i)
=> ProcessFlagParameter(parts, null, flagString, ref i);
///
/// Process a flag parameter
///
/// List of parts to be referenced
/// Short flag string, if available
/// Long flag string, if available
/// Reference to the position in the parts
/// True if the parameter was processed successfully or skipped, false otherwise
#if NET48
protected bool ProcessFlagParameter(List parts, string shortFlagString, string longFlagString, ref int i)
#else
protected bool ProcessFlagParameter(List parts, string? shortFlagString, string longFlagString, ref int i)
#endif
{
if (parts == null)
return false;
if (parts[i] == shortFlagString || parts[i] == longFlagString)
{
if (!IsFlagSupported(longFlagString))
return false;
this[longFlagString] = true;
}
return true;
}
///
/// Process a boolean parameter
///
/// List of parts to be referenced
/// Flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// True if the parameter was processed successfully or skipped, false otherwise
protected bool ProcessBooleanParameter(List parts, string flagString, ref int i, bool missingAllowed = false)
=> ProcessBooleanParameter(parts, null, flagString, ref i, missingAllowed);
///
/// Process a boolean parameter
///
/// List of parts to be referenced
/// Short flag string, if available
/// Long flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// True if the parameter was processed successfully or skipped, false otherwise
#if NET48
protected bool ProcessBooleanParameter(List parts, string shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#else
protected bool ProcessBooleanParameter(List parts, string? shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#endif
{
if (parts == null)
return false;
if (parts[i] == shortFlagString || parts[i] == longFlagString)
{
if (!IsFlagSupported(longFlagString))
{
return false;
}
else if (!DoesExist(parts, i + 1))
{
if (missingAllowed)
{
this[longFlagString] = true;
return true;
}
else
{
return false;
}
}
else if (IsFlagSupported(parts[i + 1]))
{
if (missingAllowed)
{
this[longFlagString] = true;
return true;
}
else
{
return false;
}
}
else if (!IsValidBool(parts[i + 1]))
{
if (missingAllowed)
{
this[longFlagString] = true;
return true;
}
else
{
return false;
}
}
this[longFlagString] = bool.Parse(parts[i + 1]);
i++;
}
return true;
}
///
/// Process a sbyte parameter
///
/// List of parts to be referenced
/// Flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// SByte value if success, SByte.MinValue if skipped, null on error/returns>
protected sbyte? ProcessInt8Parameter(List parts, string flagString, ref int i, bool missingAllowed = false)
=> ProcessInt8Parameter(parts, null, flagString, ref i, missingAllowed);
///
/// Process an sbyte parameter
///
/// List of parts to be referenced
/// Short flag string, if available
/// Long flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// SByte value if success, SByte.MinValue if skipped, null on error/returns>
#if NET48
protected sbyte? ProcessInt8Parameter(List parts, string shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#else
protected sbyte? ProcessInt8Parameter(List parts, string? shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#endif
{
if (parts == null)
return null;
if (parts[i] == shortFlagString || parts[i] == longFlagString)
{
if (!IsFlagSupported(longFlagString))
{
return null;
}
else if (!DoesExist(parts, i + 1))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (IsFlagSupported(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (!IsValidInt8(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
this[longFlagString] = true;
i++;
(string value, long factor) = ExtractFactorFromValue(parts[i]);
return (sbyte)(sbyte.Parse(value) * factor);
}
else if (parts[i].StartsWith(shortFlagString + "=") || parts[i].StartsWith(longFlagString + "="))
{
if (!IsFlagSupported(longFlagString))
return null;
string[] commandParts = parts[i].Split('=');
if (commandParts.Length != 2)
return null;
string valuePart = commandParts[1];
this[longFlagString] = true;
(string value, long factor) = ExtractFactorFromValue(valuePart);
return (sbyte)(sbyte.Parse(value) * factor);
}
return SByte.MinValue;
}
///
/// Process an Int16 parameter
///
/// List of parts to be referenced
/// Flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// Int16 value if success, Int16.MinValue if skipped, null on error/returns>
protected short? ProcessInt16Parameter(List parts, string flagString, ref int i, bool missingAllowed = false)
=> ProcessInt16Parameter(parts, null, flagString, ref i, missingAllowed);
///
/// Process an Int16 parameter
///
/// List of parts to be referenced
/// Short flag string, if available
/// Long flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// Int16 value if success, Int16.MinValue if skipped, null on error/returns>
#if NET48
protected short? ProcessInt16Parameter(List parts, string shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#else
protected short? ProcessInt16Parameter(List parts, string? shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#endif
{
if (parts == null)
return null;
if (parts[i] == shortFlagString || parts[i] == longFlagString)
{
if (!IsFlagSupported(longFlagString))
{
return null;
}
else if (!DoesExist(parts, i + 1))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (IsFlagSupported(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (!IsValidInt16(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
this[longFlagString] = true;
i++;
(string value, long factor) = ExtractFactorFromValue(parts[i]);
return (short)(short.Parse(value) * factor);
}
else if (parts[i].StartsWith(shortFlagString + "=") || parts[i].StartsWith(longFlagString + "="))
{
if (!IsFlagSupported(longFlagString))
return null;
string[] commandParts = parts[i].Split('=');
if (commandParts.Length != 2)
return null;
string valuePart = commandParts[1];
this[longFlagString] = true;
(string value, long factor) = ExtractFactorFromValue(valuePart);
return (short)(short.Parse(value) * factor);
}
return Int16.MinValue;
}
///
/// Process an Int32 parameter
///
/// List of parts to be referenced
/// Flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// Int32 value if success, Int32.MinValue if skipped, null on error/returns>
protected int? ProcessInt32Parameter(List parts, string flagString, ref int i, bool missingAllowed = false)
=> ProcessInt32Parameter(parts, null, flagString, ref i, missingAllowed);
///
/// Process an Int32 parameter
///
/// List of parts to be referenced
/// Short flag string, if available
/// Long flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// Int32 value if success, Int32.MinValue if skipped, null on error/returns>
#if NET48
protected int? ProcessInt32Parameter(List parts, string shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#else
protected int? ProcessInt32Parameter(List parts, string? shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#endif
{
if (parts == null)
return null;
if (parts[i] == shortFlagString || parts[i] == longFlagString)
{
if (!IsFlagSupported(longFlagString))
{
return null;
}
else if (!DoesExist(parts, i + 1))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (IsFlagSupported(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (!IsValidInt32(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
this[longFlagString] = true;
i++;
(string value, long factor) = ExtractFactorFromValue(parts[i]);
return (int)(int.Parse(value) * factor);
}
else if (parts[i].StartsWith(shortFlagString + "=") || parts[i].StartsWith(longFlagString + "="))
{
if (!IsFlagSupported(longFlagString))
return null;
string[] commandParts = parts[i].Split('=');
if (commandParts.Length != 2)
return null;
string valuePart = commandParts[1];
this[longFlagString] = true;
(string value, long factor) = ExtractFactorFromValue(valuePart);
return (int)(int.Parse(value) * factor);
}
return Int32.MinValue;
}
///
/// Process an Int64 parameter
///
/// List of parts to be referenced
/// Flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// Int64 value if success, Int64.MinValue if skipped, null on error/returns>
protected long? ProcessInt64Parameter(List parts, string flagString, ref int i, bool missingAllowed = false)
=> ProcessInt64Parameter(parts, null, flagString, ref i, missingAllowed);
///
/// Process an Int64 parameter
///
/// List of parts to be referenced
/// Short flag string, if available
/// Long flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// Int64 value if success, Int64.MinValue if skipped, null on error/returns>
#if NET48
protected long? ProcessInt64Parameter(List parts, string shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#else
protected long? ProcessInt64Parameter(List parts, string? shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#endif
{
if (parts == null)
return null;
if (parts[i] == shortFlagString || parts[i] == longFlagString)
{
if (!IsFlagSupported(longFlagString))
{
return null;
}
else if (!DoesExist(parts, i + 1))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (IsFlagSupported(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (!IsValidInt64(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
this[longFlagString] = true;
i++;
(string value, long factor) = ExtractFactorFromValue(parts[i]);
return long.Parse(value) * factor;
}
else if (parts[i].StartsWith(shortFlagString + "=") || parts[i].StartsWith(longFlagString + "="))
{
if (!IsFlagSupported(longFlagString))
return null;
string[] commandParts = parts[i].Split('=');
if (commandParts.Length != 2)
return null;
string valuePart = commandParts[1];
this[longFlagString] = true;
(string value, long factor) = ExtractFactorFromValue(valuePart);
return long.Parse(value) * factor;
}
return Int64.MinValue;
}
///
/// Process an string parameter
///
/// List of parts to be referenced
/// Flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// String value if possible, string.Empty on missing, null on error
#if NET48
protected string ProcessStringParameter(List parts, string flagString, ref int i, bool missingAllowed = false)
#else
protected string? ProcessStringParameter(List parts, string flagString, ref int i, bool missingAllowed = false)
#endif
=> ProcessStringParameter(parts, null, flagString, ref i, missingAllowed);
///
/// Process a string parameter
///
/// List of parts to be referenced
/// Short flag string, if available
/// Long flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// String value if possible, string.Empty on missing, null on error
#if NET48
protected string ProcessStringParameter(List parts, string shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#else
protected string? ProcessStringParameter(List parts, string? shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#endif
{
if (parts == null)
return null;
if (parts[i] == shortFlagString || parts[i] == longFlagString)
{
if (!IsFlagSupported(longFlagString))
{
return null;
}
else if (!DoesExist(parts, i + 1))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (IsFlagSupported(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (string.IsNullOrWhiteSpace(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
this[longFlagString] = true;
i++;
return parts[i].Trim('"');
}
else if (parts[i].StartsWith(shortFlagString + "=") || parts[i].StartsWith(longFlagString + "="))
{
if (!IsFlagSupported(longFlagString))
return null;
string[] commandParts = parts[i].Split('=');
if (commandParts.Length != 2)
return null;
string valuePart = commandParts[1];
this[longFlagString] = true;
return valuePart.Trim('"');
}
return string.Empty;
}
///
/// Process a byte parameter
///
/// List of parts to be referenced
/// Flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// Byte value if success, Byte.MinValue if skipped, null on error/returns>
protected byte? ProcessUInt8Parameter(List parts, string flagString, ref int i, bool missingAllowed = false)
=> ProcessUInt8Parameter(parts, null, flagString, ref i, missingAllowed);
///
/// Process a byte parameter
///
/// List of parts to be referenced
/// Short flag string, if available
/// Long flag string, if available
/// Reference to the position in the parts
/// True if missing values are allowed, false otherwise
/// Byte value if success, Byte.MinValue if skipped, null on error/returns>
#if NET48
protected byte? ProcessUInt8Parameter(List parts, string shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#else
protected byte? ProcessUInt8Parameter(List parts, string? shortFlagString, string longFlagString, ref int i, bool missingAllowed = false)
#endif
{
if (parts == null)
return null;
if (parts[i] == shortFlagString || parts[i] == longFlagString)
{
if (!IsFlagSupported(longFlagString))
{
return null;
}
else if (!DoesExist(parts, i + 1))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (IsFlagSupported(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
else if (!IsValidInt8(parts[i + 1]))
{
if (missingAllowed)
this[longFlagString] = true;
return null;
}
this[longFlagString] = true;
i++;
(string value, long factor) = ExtractFactorFromValue(parts[i]);
return (byte)(byte.Parse(value) * factor);
}
else if (parts[i].StartsWith(shortFlagString + "=") || parts[i].StartsWith(longFlagString + "="))
{
if (!IsFlagSupported(longFlagString))
return null;
string[] commandParts = parts[i].Split('=');
if (commandParts.Length != 2)
return null;
string valuePart = commandParts[1];
this[longFlagString] = true;
(string value, long factor) = ExtractFactorFromValue(valuePart);
return (byte)(byte.Parse(value) * factor);
}
return Byte.MinValue;
}
///
/// Get yhe trimmed value and multiplication factor from a value
///
/// String value to treat as suffixed number
/// Trimmed value and multiplication factor
private static (string trimmed, long factor) ExtractFactorFromValue(string value)
{
value = value.Trim('"');
long factor = 1;
// Characters
if (value.EndsWith("c", StringComparison.Ordinal))
{
factor = 1;
value = value.TrimEnd('c');
}
// Words
else if (value.EndsWith("w", StringComparison.Ordinal))
{
factor = 2;
value = value.TrimEnd('w');
}
// Double Words
else if (value.EndsWith("d", StringComparison.Ordinal))
{
factor = 4;
value = value.TrimEnd('d');
}
// Quad Words
else if (value.EndsWith("q", StringComparison.Ordinal))
{
factor = 8;
value = value.TrimEnd('q');
}
// Kilobytes
else if (value.EndsWith("k", StringComparison.Ordinal))
{
factor = 1024;
value = value.TrimEnd('k');
}
// Megabytes
else if (value.EndsWith("M", StringComparison.Ordinal))
{
factor = 1024 * 1024;
value = value.TrimEnd('M');
}
// Gigabytes
else if (value.EndsWith("G", StringComparison.Ordinal))
{
factor = 1024 * 1024 * 1024;
value = value.TrimEnd('G');
}
return (value, factor);
}
#endregion
#region Common Information Extraction
///
/// Generate the proper datfile from the input Datafile, if possible
///
/// .dat file location
/// Relevant pieces of the datfile, null on error
#if NET48
protected static string GenerateDatfile(Datafile datafile)
#else
protected 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 += $"\n";
}
return datString.TrimEnd('\n');
}
catch
{
// We don't care what the exception is right now
return null;
}
}
///
/// Get Datafile from a standard DAT
///
/// Path to the DAT file to parse
/// Filled Datafile on success, null on error
#if NET48
protected static Datafile GetDatafile(string dat)
#else
protected 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;
}
}
///
/// Gets disc information from a PIC file
///
/// Path to a PIC.bin file
/// Filled DiscInformation on success, null on error
/// This omits the emergency brake information, if it exists
#if NET48
protected static DiscInformation GetDiscInformation(string pic)
#else
protected 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;
}
}
///
/// Get hashes from an input file path
///
/// Path to the input file
/// True if hashing was successful, false otherwise
#if NET48
protected static bool GetFileHashes(string filename, out long size, out string crc32, out string md5, out string sha1)
#else
protected 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
{
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();
}
}
///
/// Get the last modified date from a file path, if possible
///
/// Path to the input file
/// Filled DateTime on success, null on failure
#if NET48
protected static DateTime? GetFileModifiedDate(string filename, bool fallback = false)
#else
protected 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;
}
///
/// Get the split values for ISO-based media
///
/// String representing the combined hash data
/// True if extraction was successful, false otherwise
#if NET48
protected static bool GetISOHashValues(string hashData, out long size, out string crc32, out string md5, out string sha1)
#else
protected 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;
// TODO: Use deserialization to Rom instead of Regex
var hashreg = new Regex(@"
/// Get the split values for ISO-based media
///
/// Datafile represenging the hash data
/// True if extraction was successful, false otherwise
#if NET48
protected static bool GetISOHashValues(Datafile datafile, out long size, out string crc32, out string md5, out string sha1)
#else
protected 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;
}
///
/// Get the layerbreak info associated from the disc information
///
/// Disc information containing unformatted data
/// True if layerbreak info was set, false otherwise
#if NET48
protected static bool GetLayerbreaks(DiscInformation di, out long? layerbreak1, out long? layerbreak2, out long? layerbreak3)
#else
protected 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(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;
}
///
/// Get the PIC identifier from the first disc information unit, if possible
///
/// Disc information containing the data
/// String representing the PIC identifier, null on error
#if NET48
protected static string GetPICIdentifier(DiscInformation di)
#else
protected 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;
}
///
/// Get the EXE date from a PlayStation disc, if possible
///
/// Drive letter to use to check
/// Internal disc serial, if possible
/// Output region, if possible
/// Output EXE date in "yyyy-mm-dd" format if possible, null on error
///
#if NET48
protected static bool GetPlayStationExecutableInfo(char? driveLetter, out string serial, out Region? region, out string date)
#else
protected 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 + ":\\";
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;
}
///
/// Get the version from a PlayStation 2 disc, if possible
///
/// Drive letter to use to check
/// Game version if possible, null on error
#if NET48
protected static string GetPlayStation2Version(char? driveLetter)
#else
protected 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 + ":\\";
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;
}
///
/// Get the internal serial from a PlayStation 3 disc, if possible
///
/// Drive letter to use to check
/// Internal disc serial if possible, null on error
#if NET48
protected static string GetPlayStation3Serial(char? driveLetter)
#else
protected 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 + ":\\";
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;
}
}
///
/// Get the version from a PlayStation 3 disc, if possible
///
/// Drive letter to use to check
/// Game version if possible, null on error
#if NET48
protected static string GetPlayStation3Version(char? driveLetter)
#else
protected 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 + ":\\";
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;
}
}
///
/// Get the internal serial from a PlayStation 4 disc, if possible
///
/// Drive letter to use to check
/// Internal disc serial if possible, null on error
#if NET48
protected static string GetPlayStation4Serial(char? driveLetter)
#else
protected 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 + ":\\";
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;
}
}
///
/// Get the version from a PlayStation 4 disc, if possible
///
/// Drive letter to use to check
/// Game version if possible, null on error
#if NET48
protected static string GetPlayStation4Version(char? driveLetter)
#else
protected 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 + ":\\";
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;
}
}
///
/// Get the internal serial from a PlayStation 5 disc, if possible
///
/// Drive letter to use to check
/// Internal disc serial if possible, null on error
#if NET48
protected static string GetPlayStation5Serial(char? driveLetter)
#else
protected 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 + ":\\";
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;
}
}
///
/// Get the version from a PlayStation 5 disc, if possible
///
/// Drive letter to use to check
/// Game version if possible, null on error
#if NET48
protected static string GetPlayStation5Version(char? driveLetter)
#else
protected 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 + ":\\";
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
///
/// Determine the category based on the UMDImageCreator string
///
/// String representing the category
/// Category, if possible
protected static DiscCategory? GetUMDCategory(string category)
{
switch (category)
{
case "GAME": return DiscCategory.Games;
case "VIDEO": return DiscCategory.Video;
case "AUDIO": return DiscCategory.Audio;
default: return null;
}
}
#endregion
#region Region Extraction
///
/// Determine the region based on the PlayStation serial code
///
/// PlayStation serial code
/// Region mapped from name, if possible
protected static Region? GetPlayStationRegion(string serial)
{
// 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
switch (serial.Substring(5, 2))
{
case "46": return Region.SouthKorea;
case "56": return Region.SouthKorea;
case "51": return Region.Asia;
case "55": return Region.Asia;
default: return Region.Japan;
}
case 'M':
// Check first three digits of S_PM serial
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
}
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
}
}