From 1a6ebfdbf073854b4565fadf447ea145bbc8649f Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Thu, 16 Nov 2023 00:59:40 -0500 Subject: [PATCH] Add more logic from MPF, update syntax --- Builder.cs | 765 ++++++++++++++++++++++++++++++++++++ Data/Constants.cs | 61 ++- Data/Extensions.cs | 425 +++++++++++--------- Data/SubmissionInfo.cs | 2 +- Data/Template.cs | 72 ++++ SabreTools.RedumpLib.csproj | 3 +- Validator.cs | 270 +++++++++++++ Web/RedumpHttpClient.cs | 13 +- Web/RedumpWebClient.cs | 24 +- Writer.cs | 683 ++++++++++++++++++++++++++++++++ 10 files changed, 2078 insertions(+), 240 deletions(-) create mode 100644 Builder.cs create mode 100644 Data/Template.cs create mode 100644 Validator.cs create mode 100644 Writer.cs diff --git a/Builder.cs b/Builder.cs new file mode 100644 index 0000000..216963f --- /dev/null +++ b/Builder.cs @@ -0,0 +1,765 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; +using Newtonsoft.Json; +using SabreTools.RedumpLib.Data; +using SabreTools.RedumpLib.Web; + +namespace SabreTools.RedumpLib +{ + public static class Builder + { + #region Creation + + /// + /// Create a SubmissionInfo from a JSON file path + /// + /// Path to the SubmissionInfo JSON + /// Filled SubmissionInfo on success, null on error + public static SubmissionInfo? CreateFromFile(string? path) + { + // If the path is invalid + if (string.IsNullOrWhiteSpace(path)) + return null; + + // If the file doesn't exist + if (!File.Exists(path)) + return null; + + // Try to open and deserialize the file + try + { + byte[] data = File.ReadAllBytes(path); + string dataString = Encoding.UTF8.GetString(data); + return JsonConvert.DeserializeObject(dataString); + } + catch + { + // We don't care what the exception was + return null; + } + } + + /// + /// Create a new SubmissionInfo object from a disc page + /// + /// String containing the HTML disc data + /// Filled SubmissionInfo object on success, null on error + /// Not currently working + private static SubmissionInfo? CreateFromID(string discData) + { + var info = new SubmissionInfo() + { + CommonDiscInfo = new CommonDiscInfoSection(), + VersionAndEditions = new VersionAndEditionsSection(), + }; + + // No disc data means we can't parse it + if (string.IsNullOrWhiteSpace(discData)) + return null; + + try + { + // Load the current disc page into an XML document + var redumpPage = new XmlDocument() { PreserveWhitespace = true }; + redumpPage.LoadXml(discData); + + // If the current page isn't valid, we can't parse it + if (!redumpPage.HasChildNodes) + return null; + + // Get the body node, if possible + var bodyNode = redumpPage["html"]?["body"]; + if (bodyNode == null || !bodyNode.HasChildNodes) + return null; + + // Loop through and get the main node, if possible + XmlNode? mainNode = null; + foreach (XmlNode? tempNode in bodyNode.ChildNodes) + { + // Invalid nodes are skipped + if (tempNode == null) + continue; + + // We only care about div elements + if (!string.Equals(tempNode.Name, "div", StringComparison.OrdinalIgnoreCase)) + continue; + + // We only care if it has attributes + if (tempNode.Attributes == null) + continue; + + // The main node has a class of "main" + if (string.Equals(tempNode.Attributes["class"]?.Value, "main", StringComparison.OrdinalIgnoreCase)) + { + mainNode = tempNode; + break; + } + } + + // If the main node is invalid, we can't do anything + if (mainNode == null || !mainNode.HasChildNodes) + return null; + + // Try to find elements as we're going + foreach (XmlNode? childNode in mainNode.ChildNodes) + { + // Invalid nodes are skipped + if (childNode == null) + continue; + + // The title is the only thing in h1 tags + if (string.Equals(childNode.Name, "h1", StringComparison.OrdinalIgnoreCase)) + info.CommonDiscInfo.Title = childNode.InnerText; + + // Most things are div elements but can be hard to parse out + else if (!string.Equals(childNode.Name, "div", StringComparison.OrdinalIgnoreCase)) + continue; + + // Only 2 of the internal divs have classes attached and one is not used here + if (childNode.Attributes != null && string.Equals(childNode.Attributes["class"]?.Value, "game", + StringComparison.OrdinalIgnoreCase)) + { + // If we don't have children nodes, skip this one over + if (!childNode.HasChildNodes) + continue; + + // The game node contains multiple other elements + foreach (XmlNode? gameNode in childNode.ChildNodes) + { + // Invalid nodes are skipped + if (gameNode == null) + continue; + + // Table elements contain multiple other parts of information + if (string.Equals(gameNode.Name, "table", StringComparison.OrdinalIgnoreCase)) + { + // All tables have some attribute we can use + if (gameNode.Attributes == null) + continue; + + // The gameinfo node contains most of the major information + if (string.Equals(gameNode.Attributes["class"]?.Value, "gameinfo", + StringComparison.OrdinalIgnoreCase)) + { + // If we don't have children nodes, skip this one over + if (!gameNode.HasChildNodes) + continue; + + // Loop through each of the rows + foreach (XmlNode? gameInfoNode in gameNode.ChildNodes) + { + // Invalid nodes are skipped + if (gameInfoNode == null) + continue; + + // If we run into anything not a row, ignore it + if (!string.Equals(gameInfoNode.Name, "tr", StringComparison.OrdinalIgnoreCase)) + continue; + + // If we don't have the required nodes, ignore it + if (gameInfoNode["th"] == null || gameInfoNode["td"] == null) + continue; + + var gameInfoNodeHeader = gameInfoNode["th"]; + var gameInfoNodeData = gameInfoNode["td"]; + + if (gameInfoNodeHeader == null || gameInfoNodeData == null) + { + // No-op for invalid data + } + else if (string.Equals(gameInfoNodeHeader.InnerText, "System", StringComparison.OrdinalIgnoreCase)) + { + info.CommonDiscInfo.System = Extensions.ToRedumpSystem(gameInfoNodeData["a"]?.InnerText ?? string.Empty); + } + else if (string.Equals(gameInfoNodeHeader.InnerText, "Media", StringComparison.OrdinalIgnoreCase)) + { + info.CommonDiscInfo.Media = Extensions.ToDiscType(gameInfoNodeData.InnerText); + } + else if (string.Equals(gameInfoNodeHeader.InnerText, "Category", StringComparison.OrdinalIgnoreCase)) + { + info.CommonDiscInfo.Category = Extensions.ToDiscCategory(gameInfoNodeData.InnerText); + } + else if (string.Equals(gameInfoNodeHeader.InnerText, "Region", StringComparison.OrdinalIgnoreCase)) + { + // TODO: COMPLETE + } + else if (string.Equals(gameInfoNodeHeader.InnerText, "Languages", StringComparison.OrdinalIgnoreCase)) + { + // TODO: COMPLETE + } + else if (string.Equals(gameInfoNodeHeader.InnerText, "Edition", StringComparison.OrdinalIgnoreCase)) + { + info.VersionAndEditions.OtherEditions = gameInfoNodeData.InnerText; + } + else if (string.Equals(gameInfoNodeHeader.InnerText, "Added", StringComparison.OrdinalIgnoreCase)) + { + if (DateTime.TryParse(gameInfoNodeData.InnerText, out DateTime added)) + info.Added = added; + } + else if (string.Equals(gameInfoNodeHeader.InnerText, "Last modified", StringComparison.OrdinalIgnoreCase)) + { + if (DateTime.TryParse(gameInfoNodeData.InnerText, out DateTime lastModified)) + info.LastModified = lastModified; + } + } + } + + // The gamecomments node contains way more than it implies + if (string.Equals(gameNode.Attributes["class"]?.Value, "gamecomments", StringComparison.OrdinalIgnoreCase)) + { + // TODO: COMPLETE + } + + // TODO: COMPLETE + } + + // The only other supported elements are divs + else if (!string.Equals(gameNode.Name, "div", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Check the div for dumper info + // TODO: COMPLETE + } + } + + // Figure out what the div contains, if possible + // TODO: COMPLETE + } + } + catch + { + return null; + } + + return info; + } + + /// + /// Fill out an existing SubmissionInfo object based on a disc page + /// + /// RedumpWebClient for making the connection + /// Existing SubmissionInfo object to fill + /// Redump disc ID to retrieve + /// True to include all pullable information, false to do bare minimum +#if NET40 + public static bool FillFromId(RedumpWebClient wc, SubmissionInfo info, int id, bool includeAllData) +#elif NETFRAMEWORK + public async static Task FillFromId(RedumpWebClient wc, SubmissionInfo info, int id, bool includeAllData) +#else + public async static Task FillFromId(RedumpHttpClient wc, SubmissionInfo info, int id, bool includeAllData) +#endif + { + // Ensure that required sections exist + info = EnsureAllSections(info); +#if NET40 + var discData = wc.DownloadSingleSiteID(id); +#elif NETFRAMEWORK + var discData = await Task.Run(() => wc.DownloadSingleSiteID(id)); +#else + var discData = await wc.DownloadSingleSiteID(id); +#endif + if (string.IsNullOrEmpty(discData)) + return false; + + // Title, Disc Number/Letter, Disc Title + var match = Constants.TitleRegex.Match(discData); + if (match.Success) + { + string title = WebUtility.HtmlDecode(match.Groups[1].Value); + + // If we have parenthesis, title is everything before the first one + int firstParenLocation = title.IndexOf(" ("); + if (firstParenLocation >= 0) + { + info.CommonDiscInfo!.Title = title.Substring(0, firstParenLocation); + var subMatches = Constants.DiscNumberLetterRegex.Matches(title); + foreach (Match subMatch in subMatches.Cast()) + { + var subMatchValue = subMatch.Groups[1].Value; + + // Disc number or letter + if (subMatchValue.StartsWith("Disc")) + info.CommonDiscInfo.DiscNumberLetter = subMatchValue.Remove(0, "Disc ".Length); + + // Issue number + else if (subMatchValue.All(c => char.IsNumber(c))) + info.CommonDiscInfo.Title += $" ({subMatchValue})"; + + // Disc title + else + info.CommonDiscInfo.DiscTitle = subMatchValue; + } + } + // Otherwise, leave the title as-is + else + { + info.CommonDiscInfo!.Title = title; + } + } + + // Foreign Title + match = Constants.ForeignTitleRegex.Match(discData); + if (match.Success) + info.CommonDiscInfo!.ForeignTitleNonLatin = WebUtility.HtmlDecode(match.Groups[1].Value); + else + info.CommonDiscInfo!.ForeignTitleNonLatin = null; + + // Category + match = Constants.CategoryRegex.Match(discData); + if (match.Success) + info.CommonDiscInfo.Category = Extensions.ToDiscCategory(match.Groups[1].Value); + else + info.CommonDiscInfo.Category = DiscCategory.Games; + + // Region + if (info.CommonDiscInfo.Region == null) + { + match = Constants.RegionRegex.Match(discData); + if (match.Success) + info.CommonDiscInfo.Region = Extensions.ToRegion(match.Groups[1].Value); + } + + // Languages + var matches = Constants.LanguagesRegex.Matches(discData); + if (matches.Count > 0) + { + var tempLanguages = new List(); + foreach (Match submatch in matches.Cast()) + { + tempLanguages.Add(Extensions.ToLanguage(submatch.Groups[1].Value)); + } + + info.CommonDiscInfo.Languages = tempLanguages.Where(l => l != null).ToArray(); + } + + // Serial + if (includeAllData) + { + // TODO: Re-enable if there's a way of verifying against a disc + //match = Constants.SerialRegex.Match(discData); + //if (match.Success) + // info.CommonDiscInfo.Serial = $"(VERIFY THIS) {WebUtility.HtmlDecode(match.Groups[1].Value)}"; + } + + // Error count + if (string.IsNullOrEmpty(info.CommonDiscInfo.ErrorsCount)) + { + match = Constants.ErrorCountRegex.Match(discData); + if (match.Success) + info.CommonDiscInfo.ErrorsCount = match.Groups[1].Value; + } + + // Version + if (info.VersionAndEditions!.Version == null) + { + match = Constants.VersionRegex.Match(discData); + if (match.Success) + info.VersionAndEditions.Version = $"(VERIFY THIS) {WebUtility.HtmlDecode(match.Groups[1].Value)}"; + } + + // Dumpers + matches = Constants.DumpersRegex.Matches(discData); + if (matches.Count > 0) + { + // Start with any currently listed dumpers + var tempDumpers = new List(); + if (info.DumpersAndStatus!.Dumpers != null && info.DumpersAndStatus.Dumpers.Length > 0) + { + foreach (string dumper in info.DumpersAndStatus.Dumpers) + tempDumpers.Add(dumper); + } + + foreach (Match submatch in matches.Cast()) + { + tempDumpers.Add(WebUtility.HtmlDecode(submatch.Groups[1].Value)); + } + + info.DumpersAndStatus.Dumpers = [.. tempDumpers]; + } + + // TODO: Unify handling of fields that can include site codes (Comments/Contents) + + // Comments + if (includeAllData) + { + match = Constants.CommentsRegex.Match(discData); + if (match.Success) + { + // Process the old comments block + string oldComments = info.CommonDiscInfo.Comments + + (string.IsNullOrEmpty(info.CommonDiscInfo.Comments) ? string.Empty : "\n") + + WebUtility.HtmlDecode(match.Groups[1].Value) + .Replace("\r\n", "\n") + .Replace("
\n", "\n") + .Replace("
", string.Empty) + .Replace("", string.Empty) + .Replace("[+]", string.Empty) + .ReplaceHtmlWithSiteCodes(); + oldComments = Regex.Replace(oldComments, @"
", string.Empty, RegexOptions.Compiled); + + // Create state variables + bool addToLast = false; + SiteCode? lastSiteCode = null; + string newComments = string.Empty; + + // Process the comments block line-by-line + string[] commentsSeparated = oldComments.Split('\n'); + for (int i = 0; i < commentsSeparated.Length; i++) + { + string commentLine = commentsSeparated[i].Trim(); + + // If we have an empty line, we want to treat this as intentional + if (string.IsNullOrWhiteSpace(commentLine)) + { + addToLast = false; + lastSiteCode = null; + newComments += $"{commentLine}\n"; + continue; + } + + // Otherwise, we need to find what tag is in use + bool foundTag = false; + foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode))) + { + // If we have a null site code, just skip + if (siteCode == null) + continue; + + // If the line doesn't contain this tag, just skip + var shortName = siteCode.ShortName(); + if (shortName == null || !commentLine.Contains(shortName)) + continue; + + // Mark as having found a tag + foundTag = true; + + // Cache the current site code + lastSiteCode = siteCode; + + // A subset of tags can be multiline + addToLast = siteCode.IsMultiLine(); + + // Skip certain site codes because of data issues + switch (siteCode) + { + // Multiple + case SiteCode.InternalSerialName: + case SiteCode.Multisession: + case SiteCode.VolumeLabel: + continue; + + // Audio CD + case SiteCode.RingNonZeroDataStart: + case SiteCode.UniversalHash: + continue; + + // Microsoft Xbox and Xbox 360 + case SiteCode.DMIHash: + case SiteCode.PFIHash: + case SiteCode.SSHash: + case SiteCode.SSVersion: + case SiteCode.XMID: + case SiteCode.XeMID: + continue; + + // Microsoft Xbox One and Series X/S + case SiteCode.Filename: + continue; + + // Nintendo Gamecube + case SiteCode.InternalName: + continue; + } + + // If we don't already have this site code, add it to the dictionary + if (!info.CommonDiscInfo.CommentsSpecialFields!.ContainsKey(siteCode.Value)) + info.CommonDiscInfo.CommentsSpecialFields[siteCode.Value] = $"(VERIFY THIS) {commentLine.Replace(shortName, string.Empty).Trim()}"; + + // Otherwise, append the value to the existing key + else + info.CommonDiscInfo.CommentsSpecialFields[siteCode.Value] += $", {commentLine.Replace(shortName, string.Empty).Trim()}"; + + break; + } + + // If we didn't find a known tag, just add the line, just in case + if (!foundTag) + { + if (addToLast && lastSiteCode != null) + { + if (!string.IsNullOrWhiteSpace(info.CommonDiscInfo.CommentsSpecialFields![lastSiteCode.Value])) + info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode.Value] += "\n"; + + info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode.Value] += commentLine; + } + else + { + newComments += $"{commentLine}\n"; + } + } + } + + // Set the new comments field + info.CommonDiscInfo.Comments = newComments; + } + } + + // Contents + if (includeAllData) + { + match = Constants.ContentsRegex.Match(discData); + if (match.Success) + { + // Process the old contents block + string oldContents = info.CommonDiscInfo.Contents + + (string.IsNullOrEmpty(info.CommonDiscInfo.Contents) ? string.Empty : "\n") + + WebUtility.HtmlDecode(match.Groups[1].Value) + .Replace("\r\n", "\n") + .Replace("
\n", "\n") + .Replace("
", string.Empty) + .Replace("
", string.Empty) + .Replace("[+]", string.Empty) + .ReplaceHtmlWithSiteCodes(); + oldContents = Regex.Replace(oldContents, @"
", string.Empty, RegexOptions.Compiled); + + // Create state variables + bool addToLast = false; + SiteCode? lastSiteCode = null; + string newContents = string.Empty; + + // Process the contents block line-by-line + string[] contentsSeparated = oldContents.Split('\n'); + for (int i = 0; i < contentsSeparated.Length; i++) + { + string contentLine = contentsSeparated[i].Trim(); + + // If we have an empty line, we want to treat this as intentional + if (string.IsNullOrWhiteSpace(contentLine)) + { + addToLast = false; + lastSiteCode = null; + newContents += $"{contentLine}\n"; + continue; + } + + // Otherwise, we need to find what tag is in use + bool foundTag = false; + foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode))) + { + // If we have a null site code, just skip + if (siteCode == null) + continue; + + // If the line doesn't contain this tag, just skip + var shortName = siteCode.ShortName(); + if (shortName == null || !contentLine.Contains(shortName)) + continue; + + // Cache the current site code + lastSiteCode = siteCode; + + // If we don't already have this site code, add it to the dictionary + if (!info.CommonDiscInfo.ContentsSpecialFields!.ContainsKey(siteCode.Value)) + info.CommonDiscInfo.ContentsSpecialFields[siteCode.Value] = $"(VERIFY THIS) {contentLine.Replace(shortName, string.Empty).Trim()}"; + + // A subset of tags can be multiline + addToLast = siteCode.IsMultiLine(); + + // Mark as having found a tag + foundTag = true; + break; + } + + // If we didn't find a known tag, just add the line, just in case + if (!foundTag) + { + if (addToLast && lastSiteCode != null) + { + if (!string.IsNullOrWhiteSpace(info.CommonDiscInfo.ContentsSpecialFields![lastSiteCode.Value])) + info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode.Value] += "\n"; + + info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode.Value] += contentLine; + } + else + { + newContents += $"{contentLine}\n"; + } + } + } + + // Set the new contents field + info.CommonDiscInfo.Contents = newContents; + } + } + + // Added + match = Constants.AddedRegex.Match(discData); + if (match.Success) + { + if (DateTime.TryParse(match.Groups[1].Value, out DateTime added)) + info.Added = added; + else + info.Added = null; + } + + // Last Modified + match = Constants.LastModifiedRegex.Match(discData); + if (match.Success) + { + if (DateTime.TryParse(match.Groups[1].Value, out DateTime lastModified)) + info.LastModified = lastModified; + else + info.LastModified = null; + } + + return true; + } + + /// + /// Ensure all required sections in a submission info exist + /// + /// SubmissionInfo object to verify + public static SubmissionInfo EnsureAllSections(SubmissionInfo? info) + { + // If there's no info, create one + info ??= new SubmissionInfo(); + + // Ensure all sections + info.CommonDiscInfo ??= new CommonDiscInfoSection(); + info.VersionAndEditions ??= new VersionAndEditionsSection(); + info.EDC ??= new EDCSection(); + info.ParentCloneRelationship ??= new ParentCloneRelationshipSection(); + info.Extras ??= new ExtrasSection(); + info.CopyProtection ??= new CopyProtectionSection(); + info.DumpersAndStatus ??= new DumpersAndStatusSection(); + info.TracksAndWriteOffsets ??= new TracksAndWriteOffsetsSection(); + info.SizeAndChecksums ??= new SizeAndChecksumsSection(); + info.DumpingInfo ??= new DumpingInfoSection(); + + // Ensure special dictionaries + info.CommonDiscInfo.CommentsSpecialFields ??= []; + info.CommonDiscInfo.ContentsSpecialFields ??= []; + + return info; + } + + /// + /// Inject information from a seed SubmissionInfo into the existing one + /// + /// Existing submission information + /// User-supplied submission information + public static void InjectSubmissionInformation(SubmissionInfo? info, SubmissionInfo? seed) + { + // If we have any invalid info + if (seed == null) + return; + + // Ensure that required sections exist + info = EnsureAllSections(info); + + // Otherwise, inject information as necessary + if (info.CommonDiscInfo != null && seed.CommonDiscInfo != null) + { + // Info that only overwrites if supplied + if (!string.IsNullOrWhiteSpace(seed.CommonDiscInfo.Title)) info.CommonDiscInfo.Title = seed.CommonDiscInfo.Title; + if (!string.IsNullOrWhiteSpace(seed.CommonDiscInfo.ForeignTitleNonLatin)) info.CommonDiscInfo.ForeignTitleNonLatin = seed.CommonDiscInfo.ForeignTitleNonLatin; + if (!string.IsNullOrWhiteSpace(seed.CommonDiscInfo.DiscNumberLetter)) info.CommonDiscInfo.DiscNumberLetter = seed.CommonDiscInfo.DiscNumberLetter; + if (!string.IsNullOrWhiteSpace(seed.CommonDiscInfo.DiscTitle)) info.CommonDiscInfo.DiscTitle = seed.CommonDiscInfo.DiscTitle; + if (seed.CommonDiscInfo.Category != null) info.CommonDiscInfo.Category = seed.CommonDiscInfo.Category; + if (seed.CommonDiscInfo.Region != null) info.CommonDiscInfo.Region = seed.CommonDiscInfo.Region; + if (seed.CommonDiscInfo.Languages != null) info.CommonDiscInfo.Languages = seed.CommonDiscInfo.Languages; + if (seed.CommonDiscInfo.LanguageSelection != null) info.CommonDiscInfo.LanguageSelection = seed.CommonDiscInfo.LanguageSelection; + if (!string.IsNullOrWhiteSpace(seed.CommonDiscInfo.Serial)) info.CommonDiscInfo.Serial = seed.CommonDiscInfo.Serial; + if (!string.IsNullOrWhiteSpace(seed.CommonDiscInfo.Barcode)) info.CommonDiscInfo.Barcode = seed.CommonDiscInfo.Barcode; + if (!string.IsNullOrWhiteSpace(seed.CommonDiscInfo.Comments)) info.CommonDiscInfo.Comments = seed.CommonDiscInfo.Comments; + if (seed.CommonDiscInfo.CommentsSpecialFields != null) info.CommonDiscInfo.CommentsSpecialFields = seed.CommonDiscInfo.CommentsSpecialFields; + if (!string.IsNullOrWhiteSpace(seed.CommonDiscInfo.Contents)) info.CommonDiscInfo.Contents = seed.CommonDiscInfo.Contents; + if (seed.CommonDiscInfo.ContentsSpecialFields != null) info.CommonDiscInfo.ContentsSpecialFields = seed.CommonDiscInfo.ContentsSpecialFields; + + // Info that always overwrites + info.CommonDiscInfo.Layer0MasteringRing = seed.CommonDiscInfo.Layer0MasteringRing; + info.CommonDiscInfo.Layer0MasteringSID = seed.CommonDiscInfo.Layer0MasteringSID; + info.CommonDiscInfo.Layer0ToolstampMasteringCode = seed.CommonDiscInfo.Layer0ToolstampMasteringCode; + info.CommonDiscInfo.Layer0MouldSID = seed.CommonDiscInfo.Layer0MouldSID; + info.CommonDiscInfo.Layer0AdditionalMould = seed.CommonDiscInfo.Layer0AdditionalMould; + + info.CommonDiscInfo.Layer1MasteringRing = seed.CommonDiscInfo.Layer1MasteringRing; + info.CommonDiscInfo.Layer1MasteringSID = seed.CommonDiscInfo.Layer1MasteringSID; + info.CommonDiscInfo.Layer1ToolstampMasteringCode = seed.CommonDiscInfo.Layer1ToolstampMasteringCode; + info.CommonDiscInfo.Layer1MouldSID = seed.CommonDiscInfo.Layer1MouldSID; + info.CommonDiscInfo.Layer1AdditionalMould = seed.CommonDiscInfo.Layer1AdditionalMould; + + info.CommonDiscInfo.Layer2MasteringRing = seed.CommonDiscInfo.Layer2MasteringRing; + info.CommonDiscInfo.Layer2MasteringSID = seed.CommonDiscInfo.Layer2MasteringSID; + info.CommonDiscInfo.Layer2ToolstampMasteringCode = seed.CommonDiscInfo.Layer2ToolstampMasteringCode; + + info.CommonDiscInfo.Layer3MasteringRing = seed.CommonDiscInfo.Layer3MasteringRing; + info.CommonDiscInfo.Layer3MasteringSID = seed.CommonDiscInfo.Layer3MasteringSID; + info.CommonDiscInfo.Layer3ToolstampMasteringCode = seed.CommonDiscInfo.Layer3ToolstampMasteringCode; + } + + if (info.VersionAndEditions != null && seed.VersionAndEditions != null) + { + // Info that only overwrites if supplied + if (!string.IsNullOrWhiteSpace(seed.VersionAndEditions.Version)) info.VersionAndEditions.Version = seed.VersionAndEditions.Version; + if (!string.IsNullOrWhiteSpace(seed.VersionAndEditions.OtherEditions)) info.VersionAndEditions.OtherEditions = seed.VersionAndEditions.OtherEditions; + } + + if (info.CopyProtection != null && seed.CopyProtection != null) + { + // Info that only overwrites if supplied + if (!string.IsNullOrWhiteSpace(seed.CopyProtection.Protection)) info.CopyProtection.Protection = seed.CopyProtection.Protection; + } + } + + #endregion + + #region Helpers + + /// + /// Process a text block and replace with internal identifiers + /// + /// Text block to process + /// Processed text block, if possible + private static string ReplaceHtmlWithSiteCodes(this string text) + { + if (string.IsNullOrWhiteSpace(text)) + return text; + + foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode))) + { + var longname = siteCode.LongName(); + if (!string.IsNullOrEmpty(longname)) + text = text.Replace(longname, siteCode.ShortName()); + } + + // For some outdated tags, we need to use alternate names + text = text.Replace("Demos:", ((SiteCode?)SiteCode.PlayableDemos).ShortName()); + text = text.Replace("DMI:", ((SiteCode?)SiteCode.DMIHash).ShortName()); + text = text.Replace("LucasArts ID:", ((SiteCode?)SiteCode.LucasArtsID).ShortName()); + text = text.Replace("PFI:", ((SiteCode?)SiteCode.PFIHash).ShortName()); + text = text.Replace("SS:", ((SiteCode?)SiteCode.SSHash).ShortName()); + text = text.Replace("SSv1:", ((SiteCode?)SiteCode.SSHash).ShortName()); + text = text.Replace("SSv1:", ((SiteCode?)SiteCode.SSHash).ShortName()); + text = text.Replace("SSv2:", ((SiteCode?)SiteCode.SSHash).ShortName()); + text = text.Replace("SSv2:", ((SiteCode?)SiteCode.SSHash).ShortName()); + text = text.Replace("SS version:", ((SiteCode?)SiteCode.SSVersion).ShortName()); + text = text.Replace("Universal Hash (SHA-1):", ((SiteCode?)SiteCode.UniversalHash).ShortName()); + text = text.Replace("XeMID:", ((SiteCode?)SiteCode.XeMID).ShortName()); + text = text.Replace("XMID:", ((SiteCode?)SiteCode.XMID).ShortName()); + + return text; + } + + #endregion + } +} diff --git a/Data/Constants.cs b/Data/Constants.cs index bad33b2..38487fb 100644 --- a/Data/Constants.cs +++ b/Data/Constants.cs @@ -10,17 +10,17 @@ namespace SabreTools.RedumpLib.Data /// /// Regex matching the added field on a disc page /// - public static Regex AddedRegex = new Regex(@"Added(.*?)"); + public static Regex AddedRegex = new(@"Added(.*?)"); /// /// Regex matching the barcode field on a disc page /// - public static Regex BarcodeRegex = new Regex(@"Barcode(.*?)"); + public static Regex BarcodeRegex = new(@"Barcode(.*?)"); /// /// Regex matching the BCA field on a disc page /// - public static Regex BcaRegex = new Regex(@"

BCA .*?/>

" + public static Regex BcaRegex = new(@"

BCA .*?/>

" + "RowContentsASCII" + "(?.*?)(?.*?)(?.*?)" + "(?.*?)(?.*?)(?.*?)" @@ -30,82 +30,82 @@ namespace SabreTools.RedumpLib.Data /// /// Regex matching the category field on a disc page /// - public static Regex CategoryRegex = new Regex(@"Category(.*?)"); + public static Regex CategoryRegex = new(@"Category(.*?)"); /// /// Regex matching the comments field on a disc page /// - public static Regex CommentsRegex = new Regex(@"Comments(.*?)", RegexOptions.Singleline); + public static Regex CommentsRegex = new(@"Comments(.*?)", RegexOptions.Singleline); /// /// Regex matching the contents field on a disc page /// - public static Regex ContentsRegex = new Regex(@"Contents(.*?)", RegexOptions.Singleline); + public static Regex ContentsRegex = new(@"Contents(.*?)", RegexOptions.Singleline); /// /// Regex matching individual disc links on a results page /// - public static Regex DiscRegex = new Regex(@""); + public static Regex DiscRegex = new(@""); /// /// Regex matching the disc number or letter field on a disc page /// - public static Regex DiscNumberLetterRegex = new Regex(@"\((.*?)\)"); + public static Regex DiscNumberLetterRegex = new(@"\((.*?)\)"); /// /// Regex matching the dumpers on a disc page /// - public static Regex DumpersRegex = new Regex(@""); + public static Regex DumpersRegex = new(@""); /// /// Regex matching the edition field on a disc page /// - public static Regex EditionRegex = new Regex(@"Edition(.*?)"); + public static Regex EditionRegex = new(@"Edition(.*?)"); /// /// Regex matching the error count field on a disc page /// - public static Regex ErrorCountRegex = new Regex(@"Errors count(.*?)"); + public static Regex ErrorCountRegex = new(@"Errors count(.*?)"); /// /// Regex matching the foreign title field on a disc page /// - public static Regex ForeignTitleRegex = new Regex(@"

(.*?)

"); + public static Regex ForeignTitleRegex = new(@"

(.*?)

"); /// /// Regex matching the "full match" ID list from a WIP disc page /// - public static Regex FullMatchRegex = new Regex(@"full match ids: (.*?)"); + public static Regex FullMatchRegex = new(@"full match ids: (.*?)"); /// /// Regex matching the languages field on a disc page /// - public static Regex LanguagesRegex = new Regex(@"\s*"); + public static Regex LanguagesRegex = new(@"\s*"); /// /// Regex matching the last modified field on a disc page /// - public static Regex LastModifiedRegex = new Regex(@"Last modified(.*?)"); + public static Regex LastModifiedRegex = new(@"Last modified(.*?)"); /// /// Regex matching the media field on a disc page /// - public static Regex MediaRegex = new Regex(@"Media(.*?)"); + public static Regex MediaRegex = new(@"Media(.*?)"); /// /// Regex matching individual WIP disc links on a results page /// - public static Regex NewDiscRegex = new Regex(@"
"); + public static Regex NewDiscRegex = new(@""); /// /// Regex matching the "partial match" ID list from a WIP disc page /// - public static Regex PartialMatchRegex = new Regex(@"partial match ids: (.*?)"); + public static Regex PartialMatchRegex = new(@"partial match ids: (.*?)"); /// /// Regex matching the PVD field on a disc page /// - public static Regex PvdRegex = new Regex(@"

Primary Volume Descriptor (PVD)

" + public static Regex PvdRegex = new(@"

Primary Volume Descriptor (PVD)

" + @"Record / EntryContentsDateTimeGMT" + @"Creation(?.*?)(?.*?)(?.*?)(?.*?)" + @"Modification(?.*?)(?.*?)(?.*?)(?.*?)" @@ -115,57 +115,57 @@ namespace SabreTools.RedumpLib.Data /// /// Regex matching the region field on a disc page /// - public static Regex RegionRegex = new Regex(@"Region
"); + public static Regex RegionRegex = new(@"Region"); /// /// Regex matching a double-layer disc ringcode information /// - public static Regex RingCodeDoubleRegex = new Regex(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould + public static Regex RingCodeDoubleRegex = new(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould /// /// Regex matching a single-layer disc ringcode information /// - public static Regex RingCodeSingleRegex = new Regex(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould + public static Regex RingCodeSingleRegex = new(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould /// /// Regex matching the serial field on a disc page /// - public static Regex SerialRegex = new Regex(@"Serial(.*?)"); + public static Regex SerialRegex = new(@"Serial(.*?)"); /// /// Regex matching the system field on a disc page /// - public static Regex SystemRegex = new Regex(@"System"); + public static Regex SystemRegex = new(@"System"); /// /// Regex matching the title field on a disc page /// - public static Regex TitleRegex = new Regex(@"

(.*?)

"); + public static Regex TitleRegex = new(@"

(.*?)

"); /// /// Regex matching the current nonce token for login /// - public static Regex TokenRegex = new Regex(@""); + public static Regex TokenRegex = new(@""); /// /// Regex matching a single track on a disc page /// - public static Regex TrackRegex = new Regex(@"(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)", RegexOptions.Singleline); + public static Regex TrackRegex = new(@"(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)", RegexOptions.Singleline); /// /// Regex matching the track count on a disc page /// - public static Regex TrackCountRegex = new Regex(@"Number of tracks(.*?)"); + public static Regex TrackCountRegex = new(@"Number of tracks(.*?)"); /// /// Regex matching the version field on a disc page /// - public static Regex VersionRegex = new Regex(@"Version(.*?)"); + public static Regex VersionRegex = new(@"Version(.*?)"); /// /// Regex matching the write offset field on a disc page /// - public static Regex WriteOffsetRegex = new Regex(@"Write offset(.*?)"); + public static Regex WriteOffsetRegex = new(@"Write offset(.*?)"); #endregion @@ -301,6 +301,5 @@ namespace SabreTools.RedumpLib.Data public const string Sha1Ext = "sha1/"; #endregion - } } \ No newline at end of file diff --git a/Data/Extensions.cs b/Data/Extensions.cs index 2b6fe1d..8461672 100644 --- a/Data/Extensions.cs +++ b/Data/Extensions.cs @@ -733,31 +733,20 @@ namespace SabreTools.RedumpLib.Data /// DiscType if possible, null on error public static DiscType? ToDiscType(this MediaType? mediaType) { - switch (mediaType) + return mediaType switch { - case MediaType.BluRay: - return DiscType.BD50; - case MediaType.CDROM: - return DiscType.CD; - case MediaType.DVD: - return DiscType.DVD9; - case MediaType.GDROM: - return DiscType.GDROM; - case MediaType.HDDVD: - return DiscType.HDDVDSL; - // case MediaType.MILCD: // TODO: Support this? - // return DiscType.MILCD; - case MediaType.NintendoGameCubeGameDisc: - return DiscType.NintendoGameCubeGameDisc; - case MediaType.NintendoWiiOpticalDisc: - return DiscType.NintendoWiiOpticalDiscDL; - case MediaType.NintendoWiiUOpticalDisc: - return DiscType.NintendoWiiUOpticalDiscSL; - case MediaType.UMD: - return DiscType.UMDDL; - default: - return null; - } + MediaType.BluRay => DiscType.BD50, + MediaType.CDROM => DiscType.CD, + MediaType.DVD => DiscType.DVD9, + MediaType.GDROM => DiscType.GDROM, + MediaType.HDDVD => DiscType.HDDVDSL, + // MediaType.MILCD => DiscType.MILCD, // TODO: Support this? + MediaType.NintendoGameCubeGameDisc => DiscType.NintendoGameCubeGameDisc, + MediaType.NintendoWiiOpticalDisc => DiscType.NintendoWiiOpticalDiscDL, + MediaType.NintendoWiiUOpticalDisc => DiscType.NintendoWiiUOpticalDiscSL, + MediaType.UMD => DiscType.UMDDL, + _ => null, + }; } /// @@ -767,40 +756,29 @@ namespace SabreTools.RedumpLib.Data /// MediaType if possible, null on error public static MediaType? ToMediaType(this DiscType? discType) { - switch (discType) + return discType switch { - case DiscType.BD25: - case DiscType.BD33: - case DiscType.BD50: - case DiscType.BD66: - case DiscType.BD100: - case DiscType.BD128: - return MediaType.BluRay; - case DiscType.CD: - return MediaType.CDROM; - case DiscType.DVD5: - case DiscType.DVD9: - return MediaType.DVD; - case DiscType.GDROM: - return MediaType.GDROM; - case DiscType.HDDVDSL: - case DiscType.HDDVDDL: - return MediaType.HDDVD; - // case DiscType.MILCD: // TODO: Support this? - // return MediaType.MILCD; - case DiscType.NintendoGameCubeGameDisc: - return MediaType.NintendoGameCubeGameDisc; - case DiscType.NintendoWiiOpticalDiscSL: - case DiscType.NintendoWiiOpticalDiscDL: - return MediaType.NintendoWiiOpticalDisc; - case DiscType.NintendoWiiUOpticalDiscSL: - return MediaType.NintendoWiiUOpticalDisc; - case DiscType.UMDSL: - case DiscType.UMDDL: - return MediaType.UMD; - default: - return null; - } + DiscType.BD25 + or DiscType.BD33 + or DiscType.BD50 + or DiscType.BD66 + or DiscType.BD100 + or DiscType.BD128 => MediaType.BluRay, + DiscType.CD => MediaType.CDROM, + DiscType.DVD5 + or DiscType.DVD9 => MediaType.DVD, + DiscType.GDROM => MediaType.GDROM, + DiscType.HDDVDSL + or DiscType.HDDVDDL => MediaType.HDDVD, + // DiscType.MILCD => MediaType.MILCD, // TODO: Support this? + DiscType.NintendoGameCubeGameDisc => MediaType.NintendoGameCubeGameDisc, + DiscType.NintendoWiiOpticalDiscSL + or DiscType.NintendoWiiOpticalDiscDL => MediaType.NintendoWiiOpticalDisc, + DiscType.NintendoWiiUOpticalDiscSL => MediaType.NintendoWiiUOpticalDisc, + DiscType.UMDSL + or DiscType.UMDDL => MediaType.UMD, + _ => null, + }; } #endregion @@ -821,35 +799,23 @@ namespace SabreTools.RedumpLib.Data /// Category represented by the string, if possible public static DiscCategory? ToDiscCategory(string category) { - switch (category?.ToLowerInvariant()) + return (category?.ToLowerInvariant()) switch { - case "games": - return DiscCategory.Games; - case "demos": - return DiscCategory.Demos; - case "video": - return DiscCategory.Video; - case "audio": - return DiscCategory.Audio; - case "multimedia": - return DiscCategory.Multimedia; - case "applications": - return DiscCategory.Applications; - case "coverdiscs": - return DiscCategory.Coverdiscs; - case "educational": - return DiscCategory.Educational; - case "bonusdiscs": - case "bonus discs": - return DiscCategory.BonusDiscs; - case "preproduction": - return DiscCategory.Preproduction; - case "addons": - case "add-ons": - return DiscCategory.AddOns; - default: - return DiscCategory.Games; - } + "games" => (DiscCategory?)DiscCategory.Games, + "demos" => (DiscCategory?)DiscCategory.Demos, + "video" => (DiscCategory?)DiscCategory.Video, + "audio" => (DiscCategory?)DiscCategory.Audio, + "multimedia" => (DiscCategory?)DiscCategory.Multimedia, + "applications" => (DiscCategory?)DiscCategory.Applications, + "coverdiscs" => (DiscCategory?)DiscCategory.Coverdiscs, + "educational" => (DiscCategory?)DiscCategory.Educational, + "bonusdiscs" + or "bonus discs" => (DiscCategory?)DiscCategory.BonusDiscs, + "preproduction" => (DiscCategory?)DiscCategory.Preproduction, + "addons" + or "add-ons" => (DiscCategory?)DiscCategory.AddOns, + _ => (DiscCategory?)DiscCategory.Games, + }; } #endregion @@ -870,72 +836,52 @@ namespace SabreTools.RedumpLib.Data /// DiscType represented by the string, if possible public static DiscType? ToDiscType(string discType) { - switch (discType?.ToLowerInvariant()) + return (discType?.ToLowerInvariant()) switch { - case "bd25": - case "bd-25": - return DiscType.BD25; - case "bd33": - case "bd-33": - return DiscType.BD33; - case "bd50": - case "bd-50": - return DiscType.BD50; - case "bd66": - case "bd-66": - return DiscType.BD66; - case "bd100": - case "bd-100": - return DiscType.BD100; - case "bd128": - case "bd-128": - return DiscType.BD128; - case "cd": - case "cdrom": - case "cd-rom": - return DiscType.CD; - case "dvd5": - case "dvd-5": - return DiscType.DVD5; - case "dvd9": - case "dvd-9": - return DiscType.DVD9; - case "gd": - case "gdrom": - case "gd-rom": - return DiscType.GDROM; - case "hddvd": - case "hddvdsl": - case "hd-dvd sl": - return DiscType.HDDVDSL; - case "hddvddl": - case "hd-dvd dl": - return DiscType.HDDVDDL; - case "milcd": - case "mil-cd": - return DiscType.MILCD; - case "nintendogamecubegamedisc": - case "nintendo game cube game disc": - return DiscType.NintendoGameCubeGameDisc; - case "nintendowiiopticaldiscsl": - case "nintendo wii optical disc sl": - return DiscType.NintendoWiiOpticalDiscSL; - case "nintendowiiopticaldiscdl": - case "nintendo wii optical disc dl": - return DiscType.NintendoWiiOpticalDiscDL; - case "nintendowiiuopticaldiscsl": - case "nintendo wii u optical disc sl": - return DiscType.NintendoWiiUOpticalDiscSL; - case "umd": - case "umdsl": - case "umd sl": - return DiscType.UMDSL; - case "umddl": - case "umd dl": - return DiscType.UMDDL; - default: - return null; - } + "bd25" + or "bd-25" => (DiscType?)DiscType.BD25, + "bd33" + or "bd-33" => (DiscType?)DiscType.BD33, + "bd50" + or "bd-50" => (DiscType?)DiscType.BD50, + "bd66" + or "bd-66" => (DiscType?)DiscType.BD66, + "bd100" + or "bd-100" => (DiscType?)DiscType.BD100, + "bd128" + or "bd-128" => (DiscType?)DiscType.BD128, + "cd" + or "cdrom" + or "cd-rom" => (DiscType?)DiscType.CD, + "dvd5" + or "dvd-5" => (DiscType?)DiscType.DVD5, + "dvd9" + or "dvd-9" => (DiscType?)DiscType.DVD9, + "gd" + or "gdrom" + or "gd-rom" => (DiscType?)DiscType.GDROM, + "hddvd" + or "hddvdsl" + or "hd-dvd sl" => (DiscType?)DiscType.HDDVDSL, + "hddvddl" + or "hd-dvd dl" => (DiscType?)DiscType.HDDVDDL, + "milcd" + or "mil-cd" => (DiscType?)DiscType.MILCD, + "nintendogamecubegamedisc" + or "nintendo game cube game disc" => (DiscType?)DiscType.NintendoGameCubeGameDisc, + "nintendowiiopticaldiscsl" + or "nintendo wii optical disc sl" => (DiscType?)DiscType.NintendoWiiOpticalDiscSL, + "nintendowiiopticaldiscdl" + or "nintendo wii optical disc dl" => (DiscType?)DiscType.NintendoWiiOpticalDiscDL, + "nintendowiiuopticaldiscsl" + or "nintendo wii u optical disc sl" => (DiscType?)DiscType.NintendoWiiUOpticalDiscSL, + "umd" + or "umdsl" + or "umd sl" => (DiscType?)DiscType.UMDSL, + "umddl" + or "umd dl" => (DiscType?)DiscType.UMDDL, + _ => null, + }; } #endregion @@ -957,19 +903,16 @@ namespace SabreTools.RedumpLib.Data public static string? ShortName(this Language? language) { // Some languages need to use the alternate code instead - switch (language) + return language switch { - case Language.Albanian: - case Language.Armenian: - case Language.Icelandic: - case Language.Macedonian: - case Language.Romanian: - case Language.Slovak: - return language.ThreeLetterCodeAlt(); - - default: - return language.ThreeLetterCode(); - } + Language.Albanian + or Language.Armenian + or Language.Icelandic + or Language.Macedonian + or Language.Romanian + or Language.Slovak => language.ThreeLetterCodeAlt(), + _ => language.ThreeLetterCode(), + }; } /// @@ -1119,6 +1062,31 @@ namespace SabreTools.RedumpLib.Data #region Site Code + /// + /// Check if a site code is multi-line or not + /// + /// SiteCode to check + /// True if the code field is multiline by default, false otherwise + public static bool IsMultiLine(this SiteCode? siteCode) + { + return siteCode switch + { + SiteCode.Extras => true, + SiteCode.Filename => true, + SiteCode.Games => true, + SiteCode.GameFootage => true, + SiteCode.Multisession => true, + SiteCode.NetYarozeGames => true, + SiteCode.Patches => true, + SiteCode.PlayableDemos => true, + SiteCode.RollingDemos => true, + SiteCode.Savegames => true, + SiteCode.TechDemos => true, + SiteCode.Videos => true, + _ => false, + }; + } + /// /// Get the HTML version for each known site code /// @@ -1137,6 +1105,79 @@ namespace SabreTools.RedumpLib.Data #region System + /// + /// Determine if a system is okay if it's not detected by Windows + /// + /// RedumpSystem value to check + /// True if Windows show see a disc when dumping, false otherwise + public static bool DetectedByWindows(this RedumpSystem? system) + { + return system switch + { + RedumpSystem.AmericanLaserGames3DO + or RedumpSystem.AppleMacintosh + or RedumpSystem.Atari3DO + or RedumpSystem.AtariJaguarCDInteractiveMultimediaSystem + or RedumpSystem.NewJatreCDi + or RedumpSystem.NintendoGameCube + or RedumpSystem.NintendoWii + or RedumpSystem.NintendoWiiU + or RedumpSystem.PhilipsCDi + or RedumpSystem.PhilipsCDiDigitalVideo + or RedumpSystem.Panasonic3DOInteractiveMultiplayer + or RedumpSystem.PanasonicM2 + or RedumpSystem.PioneerLaserActive + or RedumpSystem.SuperAudioCD => false, + _ => true, + }; + } + + /// + /// Determine if a system has reversed ringcodes + /// + /// RedumpSystem value to check + /// True if the system has reversed ringcodes, false otherwise + public static bool HasReversedRingcodes(this RedumpSystem? system) + { + return system switch + { + RedumpSystem.SonyPlayStation2 + or RedumpSystem.SonyPlayStation3 + or RedumpSystem.SonyPlayStation4 + or RedumpSystem.SonyPlayStation5 + or RedumpSystem.SonyPlayStationPortable => true, + _ => false, + }; + } + + /// + /// Determine if a system is considered audio-only + /// + /// RedumpSystem value to check + /// True if the system is audio-only, false otherwise + /// + /// Philips CD-i should NOT be in this list. It's being included until there's a + /// reasonable distinction between CD-i and CD-i ready on the database side. + /// + public static bool IsAudio(this RedumpSystem? system) + { + return system switch + { + RedumpSystem.AtariJaguarCDInteractiveMultimediaSystem + or RedumpSystem.AudioCD + or RedumpSystem.DVDAudio + or RedumpSystem.HasbroiONEducationalGamingSystem + or RedumpSystem.HasbroVideoNow + or RedumpSystem.HasbroVideoNowColor + or RedumpSystem.HasbroVideoNowJr + or RedumpSystem.HasbroVideoNowXP + or RedumpSystem.PhilipsCDi + or RedumpSystem.PlayStationGameSharkUpdates + or RedumpSystem.SuperAudioCD => true, + _ => false, + }; + } + /// /// Determine if a system is a marker value /// @@ -1144,17 +1185,31 @@ namespace SabreTools.RedumpLib.Data /// True if the system is a marker value, false otherwise public static bool IsMarker(this RedumpSystem? system) { - switch (system) + return system switch { - case RedumpSystem.MarkerArcadeEnd: - case RedumpSystem.MarkerComputerEnd: - case RedumpSystem.MarkerDiscBasedConsoleEnd: - // case RedumpSystem.MarkerOtherConsoleEnd: - case RedumpSystem.MarkerOtherEnd: - return true; - default: - return false; - } + RedumpSystem.MarkerArcadeEnd + or RedumpSystem.MarkerComputerEnd + or RedumpSystem.MarkerDiscBasedConsoleEnd + or RedumpSystem.MarkerOtherEnd => true, + _ => false, + }; + } + + /// + /// Determine if a system is considered XGD + /// + /// RedumpSystem value to check + /// True if the system is XGD, false otherwise + public static bool IsXGD(this RedumpSystem? system) + { + return system switch + { + RedumpSystem.MicrosoftXbox + or RedumpSystem.MicrosoftXbox360 + or RedumpSystem.MicrosoftXboxOne + or RedumpSystem.MicrosoftXboxSeriesXS => true, + _ => false, + }; } /// @@ -2157,15 +2212,12 @@ namespace SabreTools.RedumpLib.Data /// YesNo represented by the nullable boolean, if possible public static YesNo? ToYesNo(this bool? yesno) { - switch (yesno) + return yesno switch { - case false: - return YesNo.No; - case true: - return YesNo.Yes; - default: - return YesNo.NULL; - } + false => YesNo.No, + true => YesNo.Yes, + _ => YesNo.NULL, + }; } /// @@ -2175,15 +2227,12 @@ namespace SabreTools.RedumpLib.Data /// YesNo represented by the string, if possible public static YesNo? ToYesNo(string yesno) { - switch (yesno?.ToLowerInvariant()) + return (yesno?.ToLowerInvariant()) switch { - case "no": - return YesNo.No; - case "yes": - return YesNo.Yes; - default: - return YesNo.NULL; - } + "no" => YesNo.No, + "yes" => YesNo.Yes, + _ => YesNo.NULL, + }; } #endregion diff --git a/Data/SubmissionInfo.cs b/Data/SubmissionInfo.cs index df777b2..0796210 100644 --- a/Data/SubmissionInfo.cs +++ b/Data/SubmissionInfo.cs @@ -69,7 +69,7 @@ namespace SabreTools.RedumpLib.Data public DumpingInfoSection? DumpingInfo { get; set; } = new DumpingInfoSection(); [JsonProperty(PropertyName = "artifacts", DefaultValueHandling = DefaultValueHandling.Ignore)] - public Dictionary? Artifacts { get; set; } = new Dictionary(); + public Dictionary? Artifacts { get; set; } = []; public object Clone() { diff --git a/Data/Template.cs b/Data/Template.cs new file mode 100644 index 0000000..9ef0715 --- /dev/null +++ b/Data/Template.cs @@ -0,0 +1,72 @@ +namespace SabreTools.RedumpLib.Data +{ + /// + /// Template field values for submission info + /// + internal static class Template + { + // Manual information + + public const string TitleField = "Title"; + public const string ForeignTitleField = "Foreign Title (Non-latin)"; + public const string DiscNumberField = "Disc Number / Letter"; + public const string DiscTitleField = "Disc Title"; + public const string SystemField = "System"; + public const string MediaTypeField = "Media Type"; + public const string CategoryField = "Category"; + public const string RegionField = "Region"; + public const string LanguagesField = "Languages"; + public const string PlaystationLanguageSelectionViaField = "Language Selection Via"; + public const string DiscSerialField = "Disc Serial"; + public const string BarcodeField = "Barcode"; + public const string CommentsField = "Comments"; + public const string ContentsField = "Contents"; + public const string VersionField = "Version"; + public const string EditionField = "Edition/Release"; + public const string PlayStation3WiiDiscKeyField = "Disc Key"; + public const string PlayStation3DiscIDField = "Disc ID"; + public const string GameCubeWiiBCAField = "BCA"; + public const string CopyProtectionField = "Copy Protection"; + public const string MasteringRingField = "Mastering Code (laser branded/etched)"; + public const string MasteringSIDField = "Mastering SID Code"; + public const string MouldSIDField = "Mould SID Code"; + public const string AdditionalMouldField = "Additional Mould"; + public const string ToolstampField = "Toolstamp or Mastering Code (engraved/stamped)"; + + // Automatic Information + + public const string DumpingProgramField = "Dumping Program"; + public const string DumpingDateField = "Date"; + public const string DumpingDriveManufacturer = "Manufacturer"; + public const string DumpingDriveModel = "Model"; + public const string DumpingDriveFirmware = "Firmware"; + public const string ReportedDiscType = "Reported Disc Type"; + public const string PVDField = "Primary Volume Descriptor (PVD)"; + public const string DATField = "DAT"; + public const string SizeField = "Size"; + public const string CRC32Field = "CRC32"; + public const string MD5Field = "MD5"; + public const string SHA1Field = "SHA1"; + public const string FullyMatchingIDField = "Fully Matching ID"; + public const string PartiallyMatchingIDsField = "Partially Matching IDs"; + public const string ErrorCountField = "Error Count"; + public const string CuesheetField = "Cuesheet"; + public const string SubIntentionField = "SubIntention Data (SecuROM/LibCrypt)"; + public const string WriteOffsetField = "Write Offset"; + public const string LayerbreakField = "Layerbreak"; + public const string EXEDateBuildDate = "EXE/Build Date"; + public const string HeaderField = "Header"; + public const string PICField = "Permanent Information & Control (PIC)"; + public const string PlayStationEDCField = "EDC"; + public const string PlayStationAntiModchipField = "Anti-modchip"; + public const string PlayStationLibCryptField = "LibCrypt"; + public const string XBOXSSRanges = "Security Sector Ranges"; + + // Default values + + public const string RequiredValue = "(REQUIRED)"; + public const string RequiredIfExistsValue = "(REQUIRED, IF EXISTS)"; + public const string OptionalValue = "(OPTIONAL)"; + public const string DiscNotDetected = "Disc Not Detected"; + } +} diff --git a/SabreTools.RedumpLib.csproj b/SabreTools.RedumpLib.csproj index 5a4d21c..2167be7 100644 --- a/SabreTools.RedumpLib.csproj +++ b/SabreTools.RedumpLib.csproj @@ -22,7 +22,7 @@ - + @@ -32,6 +32,7 @@ + diff --git a/Validator.cs b/Validator.cs new file mode 100644 index 0000000..1533180 --- /dev/null +++ b/Validator.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using SabreTools.RedumpLib.Data; +using SabreTools.RedumpLib.Web; + +namespace SabreTools.RedumpLib +{ + public static class Validator + { + /// + /// Adjust the disc type based on size and layerbreak information + /// + /// Existing SubmissionInfo object to fill + /// Corrected disc type, if possible + 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; + } + } + + /// + /// List the disc IDs associated with a given quicksearch query + /// + /// RedumpWebClient for making the connection + /// Query string to attempt to search for + /// True to filter forward slashes, false otherwise + /// All disc IDs for the given query, null on error +#if NET40 + public static List? ListSearchResults(RedumpWebClient wc, string? query, bool filterForwardSlashes = true) +#elif NETFRAMEWORK + public async static Task?> ListSearchResults(RedumpWebClient wc, string? query, bool filterForwardSlashes = true) +#else + public async static Task?> ListSearchResults(RedumpHttpClient wc, string? query, bool filterForwardSlashes = true) +#endif + { + // If there is an invalid query + if (string.IsNullOrWhiteSpace(query)) + return null; + + var ids = new List(); + + // Strip quotes + query = query!.Trim('"', '\''); + + // Special characters become dashes + query = query.Replace(' ', '-'); + if (filterForwardSlashes) + query = query.Replace('/', '-'); + query = query.Replace('\\', '/'); + + // Lowercase is defined per language + query = query.ToLowerInvariant(); + + // Keep getting quicksearch pages until there are none left + try + { + int pageNumber = 1; + while (true) + { +#if NET40 + List pageIds = wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++)); +#elif NETFRAMEWORK + List pageIds = await Task.Run(() => wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++))); +#else + List pageIds = await wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++)); +#endif + ids.AddRange(pageIds); + if (pageIds.Count <= 1) + break; + } + } + catch (Exception ex) + { + Console.WriteLine($"An exception occurred while trying to log in: {ex}"); + return null; + } + + return ids; + } + + /// + /// Validate a single track against Redump, if possible + /// + /// RedumpWebClient for making the connection + /// Existing SubmissionInfo object to fill + /// SHA-1 hash to check against + /// True if the track was found, false otherwise; List of found values, if possible +#if NET40 + public static (bool, List?, string?) ValidateSingleTrack(RedumpWebClient wc, SubmissionInfo info, string sha1) +#elif NETFRAMEWORK + public async static Task<(bool, List?, string?)> ValidateSingleTrack(RedumpWebClient wc, SubmissionInfo info, string sha1) +#else + public async static Task<(bool, List?, string?)> ValidateSingleTrack(RedumpHttpClient wc, SubmissionInfo info, string sha1) +#endif + { + // Get all matching IDs for the track +#if NET40 + var newIds = ListSearchResults(wc, sha1); +#else + var newIds = await ListSearchResults(wc, sha1); +#endif + + // If we got null back, there was an error + if (newIds == null) + return (false, null, "There was an unknown error retrieving information from Redump"); + + // If no IDs match, just return + if (!newIds.Any()) + return (false, null, $"There were no matching IDs for track with SHA-1 of '{sha1}'"); + + // Join the list of found IDs to the existing list, if possible + if (info.PartiallyMatchedIDs != null && info.PartiallyMatchedIDs.Any()) + info.PartiallyMatchedIDs.AddRange(newIds); + else + info.PartiallyMatchedIDs = newIds; + + return (true, newIds, $"There were matching ID(s) found for track with SHA-1 of '{sha1}'"); + } + + /// + /// Validate a universal hash against Redump, if possible + /// + /// RedumpWebClient for making the connection + /// Existing SubmissionInfo object to fill + /// Optional result progress callback + /// True if the track was found, false otherwise; List of found values, if possible +#if NET40 + public static (bool, List?, string?) ValidateUniversalHash(RedumpWebClient wc, SubmissionInfo info) +#elif NETFRAMEWORK + public async static Task<(bool, List?, string?)> ValidateUniversalHash(RedumpWebClient wc, SubmissionInfo info) +#else + public async static Task<(bool, List?, string?)> ValidateUniversalHash(RedumpHttpClient wc, SubmissionInfo info) +#endif + { + // If we don't have special fields + if (info.CommonDiscInfo?.CommentsSpecialFields == null) + return (false, null, "Universal hash was missing"); + + // If we don't have a universal hash + string? universalHash = info.CommonDiscInfo.CommentsSpecialFields[SiteCode.UniversalHash]; + if (string.IsNullOrEmpty(universalHash)) + return (false, null, "Universal hash was missing"); + + // Format the universal hash for finding within the comments + universalHash = $"{universalHash.Substring(0, universalHash.Length - 1)}/comments/only"; + + // Get all matching IDs for the hash +#if NET40 + var newIds = ListSearchResults(wc, universalHash, filterForwardSlashes: false); +#else + var newIds = await ListSearchResults(wc, universalHash, filterForwardSlashes: false); +#endif + + // If we got null back, there was an error + if (newIds == null) + return (false, null, "There was an unknown error retrieving information from Redump"); + + // If no IDs match, just return + if (!newIds.Any()) + return (false, null, $"There were no matching IDs for universal hash of '{universalHash}'"); + + // Join the list of found IDs to the existing list, if possible + if (info.PartiallyMatchedIDs != null && info.PartiallyMatchedIDs.Any()) + info.PartiallyMatchedIDs.AddRange(newIds); + else + info.PartiallyMatchedIDs = newIds; + + return (true, newIds, $"There were matching ID(s) found for universal hash of '{universalHash}'"); + } + + /// + /// Validate that the current track count and remote track count match + /// + /// RedumpWebClient for making the connection + /// Redump disc ID to retrieve + /// Local count of tracks for the current disc + /// True if the track count matches, false otherwise +#if NET40 + public static bool ValidateTrackCount(RedumpWebClient wc, int id, int localCount) +#elif NETFRAMEWORK + public async static Task ValidateTrackCount(RedumpWebClient wc, int id, int localCount) +#else + public async static Task ValidateTrackCount(RedumpHttpClient wc, int id, int localCount) +#endif + { + // If we can't pull the remote data, we can't match +#if NET40 + string? discData = wc.DownloadSingleSiteID(id); +#elif NETFRAMEWORK + string? discData = await Task.Run(() => wc.DownloadSingleSiteID(id)); +#else + string? discData = await wc.DownloadSingleSiteID(id); +#endif + if (string.IsNullOrEmpty(discData)) + return false; + + // Discs with only 1 track don't have a track count listed + var match = Constants.TrackCountRegex.Match(discData); + if (!match.Success && localCount == 1) + return true; + else if (!match.Success) + return false; + + // If the count isn't parseable, we're not taking chances + if (!int.TryParse(match.Groups[1].Value, out int remoteCount)) + return false; + + // Finally check to see if the counts match + return localCount == remoteCount; + } + } +} diff --git a/Web/RedumpHttpClient.cs b/Web/RedumpHttpClient.cs index 7968624..1ac3aac 100644 --- a/Web/RedumpHttpClient.cs +++ b/Web/RedumpHttpClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -149,7 +150,7 @@ namespace SabreTools.RedumpLib.Web /// List of IDs from the page, empty on error public async Task> CheckSingleSitePage(string url) { - List ids = new(); + List ids = []; // Try up to 3 times to retrieve the data string? dumpsPage = await DownloadString(url, retries: 3); @@ -170,7 +171,7 @@ namespace SabreTools.RedumpLib.Web // Otherwise, traverse each dump on the page var matches = Constants.DiscRegex.Matches(dumpsPage); - foreach (Match? match in matches) + foreach (Match? match in matches.Cast()) { if (match == null) continue; @@ -222,7 +223,7 @@ namespace SabreTools.RedumpLib.Web // Otherwise, traverse each dump on the page var matches = Constants.DiscRegex.Matches(dumpsPage); - foreach (Match? match in matches) + foreach (Match? match in matches.Cast()) { if (match == null) continue; @@ -253,7 +254,7 @@ namespace SabreTools.RedumpLib.Web /// List of IDs from the page, empty on error public async Task> CheckSingleWIPPage(string url) { - List ids = new(); + List ids = []; // Try up to 3 times to retrieve the data string? dumpsPage = await DownloadString(url, retries: 3); @@ -264,7 +265,7 @@ namespace SabreTools.RedumpLib.Web // Otherwise, traverse each dump on the page var matches = Constants.NewDiscRegex.Matches(dumpsPage); - foreach (Match? match in matches) + foreach (Match? match in matches.Cast()) { if (match == null) continue; @@ -302,7 +303,7 @@ namespace SabreTools.RedumpLib.Web // Otherwise, traverse each dump on the page var matches = Constants.NewDiscRegex.Matches(dumpsPage); - foreach (Match? match in matches) + foreach (Match? match in matches.Cast()) { if (match == null) continue; diff --git a/Web/RedumpWebClient.cs b/Web/RedumpWebClient.cs index 45b51fd..baa722e 100644 --- a/Web/RedumpWebClient.cs +++ b/Web/RedumpWebClient.cs @@ -13,7 +13,7 @@ namespace SabreTools.RedumpLib.Web // https://stackoverflow.com/questions/1777221/using-cookiecontainer-with-webclient-class public class RedumpWebClient : WebClient { - private readonly CookieContainer m_container = new CookieContainer(); + private readonly CookieContainer m_container = new(); /// /// Determines if user is logged into Redump @@ -64,16 +64,14 @@ namespace SabreTools.RedumpLib.Web return (false, null); // Try logging in with the supplied credentials otherwise - using (RedumpWebClient wc = new RedumpWebClient()) - { - bool? loggedIn = wc.Login(username, password); - if (loggedIn == true) - return (true, "Redump username and password accepted!"); - else if (loggedIn == false) - return (false, "Redump username and password denied!"); - else - return (null, "An error occurred validating your credentials!"); - } + using RedumpWebClient wc = new(); + bool? loggedIn = wc.Login(username, password); + if (loggedIn == true) + return (true, "Redump username and password accepted!"); + else if (loggedIn == false) + return (false, "Redump username and password denied!"); + else + return (null, "An error occurred validating your credentials!"); } /// @@ -156,7 +154,7 @@ namespace SabreTools.RedumpLib.Web /// List of IDs from the page, empty on error public List CheckSingleSitePage(string url) { - List ids = new List(); + List ids = []; string dumpsPage = string.Empty; // Try up to 3 times to retrieve the data @@ -273,7 +271,7 @@ namespace SabreTools.RedumpLib.Web /// List of IDs from the page, empty on error public List CheckSingleWIPPage(string url) { - List ids = new List(); + List ids = []; string dumpsPage = string.Empty; // Try up to 3 times to retrieve the data diff --git a/Writer.cs b/Writer.cs new file mode 100644 index 0000000..b0ab17d --- /dev/null +++ b/Writer.cs @@ -0,0 +1,683 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using SabreTools.RedumpLib.Data; + +namespace SabreTools.RedumpLib +{ + public static class Writer + { + /// + /// Format the output data in a human readable way, separating each printed line into a new item in the list + /// + /// Information object that should contain normalized values + /// True to enable Redump compatiblity, false otherwise + /// List of strings representing each line of an output file, null on error + public static (List?, string?) FormatOutputData(SubmissionInfo? info, bool enableRedumpCompatibility) + { + // 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 + { + "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 ?? [null]).Select(l => l.LongName() ?? "SILENCE! (CHANGE THIS)").ToArray(), 1); + AddIfExists(output, Template.PlaystationLanguageSelectionViaField, (info.CommonDiscInfo?.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 (!enableRedumpCompatibility + || (info.CommonDiscInfo?.Media.ToMediaType() != MediaType.BluRay + && info.CommonDiscInfo?.System.IsXGD() == false)) + { + AddIfExists(output, Template.LayerbreakField, info.SizeAndChecksums?.Layerbreak, 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 + string? last = null; + 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}"); + } + } + + /// + /// Process any fields that have to be combined + /// + /// Information object to normalize + public static void ProcessSpecialFields(SubmissionInfo? info) + { + // 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; + } + } + + #region Helpers + + /// + /// Add the properly formatted key and value, if possible + /// + /// Output list + /// Name of the output key to write + /// Name of the output value to write + /// Number of tabs to indent the line + private static void AddIfExists(List output, string key, string? value, int indent) + { + // If there's no valid value to write + if (value == null) + return; + + string prefix = string.Empty; + 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("", "\t"); + value = value.Replace("", "\t"); + value = value.Replace(" ", "\t"); + + // Sanitize whitespace around tabs + value = Regex.Replace(value, @"\s*\t\s*", "\t", RegexOptions.Compiled); + } + + // If the value contains a newline + value = value.Replace("\r\n", "\n"); + if (value.Contains('\n')) + { + 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); + } + } + + /// + /// Add the properly formatted key and value, if possible + /// + /// Output list + /// Name of the output key to write + /// Name of the output value to write + /// Number of tabs to indent the line + private static void AddIfExists(List output, string key, string?[]? value, int indent) + { + // If there's no valid value to write + if (value == null || value.Length == 0) + return; + + AddIfExists(output, key, string.Join(", ", value), indent); + } + + /// + /// Add the properly formatted key and value, if possible + /// + /// Output list + /// Name of the output key to write + /// Name of the output value to write + /// Number of tabs to indent the line + private static void AddIfExists(List output, string key, long? value, int indent) + { + // If there's no valid value to write + if (value == null || value == default(long)) + return; + + string prefix = string.Empty; + for (int i = 0; i < indent; i++) + prefix += "\t"; + + output.Add(prefix + key + ": " + value); + } + + /// + /// Add the properly formatted key and value, if possible + /// + /// Output list + /// Name of the output key to write + /// Name of the output value to write + /// Number of tabs to indent the line + private static void AddIfExists(List output, string key, List? value, int indent) + { + // 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); + } + + /// + /// Format a single site tag to string + /// + /// KeyValuePair representing the site tag and value + /// String-formatted tag and value + private static string FormatSiteTag(KeyValuePair kvp) + { + bool isMultiLine = kvp.Key.IsMultiLine(); + 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)}"; + } + + /// + /// Get the adjusted name of the media based on layers, if applicable + /// + /// MediaType to get the proper name for + /// PIC identifier string (BD only) + /// Size of the current media + /// First layerbreak value, as applicable + /// Second layerbreak value, as applicable + /// Third layerbreak value, as applicable + /// String representation of the media, including layer specification + /// TODO: Figure out why we have this and NormalizeDiscType as well + private static string? GetFixedMediaType(MediaType? mediaType, string? picIdentifier, long? size, long? layerbreak, long? layerbreak2, long? layerbreak3) + { + 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(); + } + } + + /// + /// Check if a site code is boolean or not + /// + /// SiteCode to check + /// True if the code field is a flag with no value, false otherwise + /// TODO: This should move to Extensions at some point + private static bool IsBoolean(SiteCode? siteCode) + { + return siteCode switch + { + SiteCode.PostgapType => true, + SiteCode.VCD => true, + _ => false, + }; + } + + /// + /// Order comment code tags according to Redump requirements + /// + /// Ordered list of KeyValuePairs representing the tags and values + private static List> OrderCommentTags(Dictionary tags) + { + var sorted = new List>(); + + // 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.AlternativeTitle, tags[SiteCode.AlternativeTitle])); + if (tags.ContainsKey(SiteCode.AlternativeForeignTitle)) + sorted.Add(new KeyValuePair(SiteCode.AlternativeForeignTitle, tags[SiteCode.AlternativeForeignTitle])); + if (tags.ContainsKey(SiteCode.InternalName)) + sorted.Add(new KeyValuePair(SiteCode.InternalName, tags[SiteCode.InternalName])); + if (tags.ContainsKey(SiteCode.InternalSerialName)) + sorted.Add(new KeyValuePair(SiteCode.InternalSerialName, tags[SiteCode.InternalSerialName])); + if (tags.ContainsKey(SiteCode.VolumeLabel)) + sorted.Add(new KeyValuePair(SiteCode.VolumeLabel, tags[SiteCode.VolumeLabel])); + if (tags.ContainsKey(SiteCode.Multisession)) + sorted.Add(new KeyValuePair(SiteCode.Multisession, tags[SiteCode.Multisession])); + if (tags.ContainsKey(SiteCode.UniversalHash)) + sorted.Add(new KeyValuePair(SiteCode.UniversalHash, tags[SiteCode.UniversalHash])); + if (tags.ContainsKey(SiteCode.RingNonZeroDataStart)) + sorted.Add(new KeyValuePair(SiteCode.RingNonZeroDataStart, tags[SiteCode.RingNonZeroDataStart])); + + if (tags.ContainsKey(SiteCode.XMID)) + sorted.Add(new KeyValuePair(SiteCode.XMID, tags[SiteCode.XMID])); + if (tags.ContainsKey(SiteCode.XeMID)) + sorted.Add(new KeyValuePair(SiteCode.XeMID, tags[SiteCode.XeMID])); + if (tags.ContainsKey(SiteCode.DMIHash)) + sorted.Add(new KeyValuePair(SiteCode.DMIHash, tags[SiteCode.DMIHash])); + if (tags.ContainsKey(SiteCode.PFIHash)) + sorted.Add(new KeyValuePair(SiteCode.PFIHash, tags[SiteCode.PFIHash])); + if (tags.ContainsKey(SiteCode.SSHash)) + sorted.Add(new KeyValuePair(SiteCode.SSHash, tags[SiteCode.SSHash])); + if (tags.ContainsKey(SiteCode.SSVersion)) + sorted.Add(new KeyValuePair(SiteCode.SSVersion, tags[SiteCode.SSVersion])); + + if (tags.ContainsKey(SiteCode.Filename)) + sorted.Add(new KeyValuePair(SiteCode.Filename, tags[SiteCode.Filename])); + + if (tags.ContainsKey(SiteCode.BBFCRegistrationNumber)) + sorted.Add(new KeyValuePair(SiteCode.BBFCRegistrationNumber, tags[SiteCode.BBFCRegistrationNumber])); + if (tags.ContainsKey(SiteCode.CDProjektID)) + sorted.Add(new KeyValuePair(SiteCode.CDProjektID, tags[SiteCode.CDProjektID])); + if (tags.ContainsKey(SiteCode.DiscHologramID)) + sorted.Add(new KeyValuePair(SiteCode.DiscHologramID, tags[SiteCode.DiscHologramID])); + if (tags.ContainsKey(SiteCode.DNASDiscID)) + sorted.Add(new KeyValuePair(SiteCode.DNASDiscID, tags[SiteCode.DNASDiscID])); + if (tags.ContainsKey(SiteCode.ISBN)) + sorted.Add(new KeyValuePair(SiteCode.ISBN, tags[SiteCode.ISBN])); + if (tags.ContainsKey(SiteCode.ISSN)) + sorted.Add(new KeyValuePair(SiteCode.ISSN, tags[SiteCode.ISSN])); + if (tags.ContainsKey(SiteCode.PPN)) + sorted.Add(new KeyValuePair(SiteCode.PPN, tags[SiteCode.PPN])); + if (tags.ContainsKey(SiteCode.VFCCode)) + sorted.Add(new KeyValuePair(SiteCode.VFCCode, tags[SiteCode.VFCCode])); + + if (tags.ContainsKey(SiteCode.Genre)) + sorted.Add(new KeyValuePair(SiteCode.Genre, tags[SiteCode.Genre])); + if (tags.ContainsKey(SiteCode.Series)) + sorted.Add(new KeyValuePair(SiteCode.Series, tags[SiteCode.Series])); + if (tags.ContainsKey(SiteCode.PostgapType)) + sorted.Add(new KeyValuePair(SiteCode.PostgapType, tags[SiteCode.PostgapType])); + if (tags.ContainsKey(SiteCode.VCD)) + sorted.Add(new KeyValuePair(SiteCode.VCD, tags[SiteCode.VCD])); + + // Publisher / Company IDs + if (tags.ContainsKey(SiteCode.AcclaimID)) + sorted.Add(new KeyValuePair(SiteCode.AcclaimID, tags[SiteCode.AcclaimID])); + if (tags.ContainsKey(SiteCode.ActivisionID)) + sorted.Add(new KeyValuePair(SiteCode.ActivisionID, tags[SiteCode.ActivisionID])); + if (tags.ContainsKey(SiteCode.BandaiID)) + sorted.Add(new KeyValuePair(SiteCode.BandaiID, tags[SiteCode.BandaiID])); + if (tags.ContainsKey(SiteCode.ElectronicArtsID)) + sorted.Add(new KeyValuePair(SiteCode.ElectronicArtsID, tags[SiteCode.ElectronicArtsID])); + if (tags.ContainsKey(SiteCode.FoxInteractiveID)) + sorted.Add(new KeyValuePair(SiteCode.FoxInteractiveID, tags[SiteCode.FoxInteractiveID])); + if (tags.ContainsKey(SiteCode.GTInteractiveID)) + sorted.Add(new KeyValuePair(SiteCode.GTInteractiveID, tags[SiteCode.GTInteractiveID])); + if (tags.ContainsKey(SiteCode.JASRACID)) + sorted.Add(new KeyValuePair(SiteCode.JASRACID, tags[SiteCode.JASRACID])); + if (tags.ContainsKey(SiteCode.KingRecordsID)) + sorted.Add(new KeyValuePair(SiteCode.KingRecordsID, tags[SiteCode.KingRecordsID])); + if (tags.ContainsKey(SiteCode.KoeiID)) + sorted.Add(new KeyValuePair(SiteCode.KoeiID, tags[SiteCode.KoeiID])); + if (tags.ContainsKey(SiteCode.KonamiID)) + sorted.Add(new KeyValuePair(SiteCode.KonamiID, tags[SiteCode.KonamiID])); + if (tags.ContainsKey(SiteCode.LucasArtsID)) + sorted.Add(new KeyValuePair(SiteCode.LucasArtsID, tags[SiteCode.LucasArtsID])); + if (tags.ContainsKey(SiteCode.MicrosoftID)) + sorted.Add(new KeyValuePair(SiteCode.MicrosoftID, tags[SiteCode.MicrosoftID])); + if (tags.ContainsKey(SiteCode.NaganoID)) + sorted.Add(new KeyValuePair(SiteCode.NaganoID, tags[SiteCode.NaganoID])); + if (tags.ContainsKey(SiteCode.NamcoID)) + sorted.Add(new KeyValuePair(SiteCode.NamcoID, tags[SiteCode.NamcoID])); + if (tags.ContainsKey(SiteCode.NipponIchiSoftwareID)) + sorted.Add(new KeyValuePair(SiteCode.NipponIchiSoftwareID, tags[SiteCode.NipponIchiSoftwareID])); + if (tags.ContainsKey(SiteCode.OriginID)) + sorted.Add(new KeyValuePair(SiteCode.OriginID, tags[SiteCode.OriginID])); + if (tags.ContainsKey(SiteCode.PonyCanyonID)) + sorted.Add(new KeyValuePair(SiteCode.PonyCanyonID, tags[SiteCode.PonyCanyonID])); + if (tags.ContainsKey(SiteCode.SegaID)) + sorted.Add(new KeyValuePair(SiteCode.SegaID, tags[SiteCode.SegaID])); + if (tags.ContainsKey(SiteCode.SelenID)) + sorted.Add(new KeyValuePair(SiteCode.SelenID, tags[SiteCode.SelenID])); + if (tags.ContainsKey(SiteCode.SierraID)) + sorted.Add(new KeyValuePair(SiteCode.SierraID, tags[SiteCode.SierraID])); + if (tags.ContainsKey(SiteCode.TaitoID)) + sorted.Add(new KeyValuePair(SiteCode.TaitoID, tags[SiteCode.TaitoID])); + if (tags.ContainsKey(SiteCode.UbisoftID)) + sorted.Add(new KeyValuePair(SiteCode.UbisoftID, tags[SiteCode.UbisoftID])); + if (tags.ContainsKey(SiteCode.ValveID)) + sorted.Add(new KeyValuePair(SiteCode.ValveID, tags[SiteCode.ValveID])); + + return sorted; + } + + /// + /// Order content code tags according to Redump requirements + /// + /// Ordered list of KeyValuePairs representing the tags and values + private static List> OrderContentTags(Dictionary tags) + { + var sorted = new List>(); + + // 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.Games, tags[SiteCode.Games])); + if (tags.ContainsKey(SiteCode.NetYarozeGames)) + sorted.Add(new KeyValuePair(SiteCode.NetYarozeGames, tags[SiteCode.NetYarozeGames])); + + // Demos + if (tags.ContainsKey(SiteCode.PlayableDemos)) + sorted.Add(new KeyValuePair(SiteCode.PlayableDemos, tags[SiteCode.PlayableDemos])); + if (tags.ContainsKey(SiteCode.RollingDemos)) + sorted.Add(new KeyValuePair(SiteCode.RollingDemos, tags[SiteCode.RollingDemos])); + if (tags.ContainsKey(SiteCode.TechDemos)) + sorted.Add(new KeyValuePair(SiteCode.TechDemos, tags[SiteCode.TechDemos])); + + // Video + if (tags.ContainsKey(SiteCode.GameFootage)) + sorted.Add(new KeyValuePair(SiteCode.GameFootage, tags[SiteCode.GameFootage])); + if (tags.ContainsKey(SiteCode.Videos)) + sorted.Add(new KeyValuePair(SiteCode.Videos, tags[SiteCode.Videos])); + + // Miscellaneous + if (tags.ContainsKey(SiteCode.Patches)) + sorted.Add(new KeyValuePair(SiteCode.Patches, tags[SiteCode.Patches])); + if (tags.ContainsKey(SiteCode.Savegames)) + sorted.Add(new KeyValuePair(SiteCode.Savegames, tags[SiteCode.Savegames])); + if (tags.ContainsKey(SiteCode.Extras)) + sorted.Add(new KeyValuePair(SiteCode.Extras, tags[SiteCode.Extras])); + + return sorted; + } + + #endregion + } +}