mirror of
https://github.com/RPCS3/discord-bot.git
synced 2025-03-09 04:12:05 +00:00
commit
f5468a41ca
1
.gitignore
vendored
1
.gitignore
vendored
@ -264,3 +264,4 @@ launchSettings.json
|
||||
*.db
|
||||
*.db-journal
|
||||
logs/
|
||||
*.ird
|
||||
|
@ -33,7 +33,7 @@ namespace CompatBot.Commands
|
||||
}
|
||||
|
||||
[Command("compat"), Aliases("c")]
|
||||
[Description("Searches the compatibility database, USE: !compat searchterm")]
|
||||
[Description("Searches the compatibility database, USE: !compat search term")]
|
||||
public async Task Compat(CommandContext ctx, [RemainingText, Description("Game title to look up")] string title)
|
||||
{
|
||||
try
|
||||
@ -205,7 +205,7 @@ Example usage:
|
||||
result.AppendFormat(returnCode.info, compatResult.SearchTerm);
|
||||
yield return result.ToString();
|
||||
result.Clear();
|
||||
var footer = $"Retrieved from: *<{request.Build(false).ToString().Replace(' ', '+')}>* in {compatResult.RequestDuration.TotalMilliseconds:0} milliseconds!";
|
||||
//var footer = $"Retrieved from: *<{request.Build(false).ToString().Replace(' ', '+')}>* in {compatResult.RequestDuration.TotalMilliseconds:0} milliseconds!";
|
||||
|
||||
if (returnCode.displayResults)
|
||||
{
|
||||
@ -217,10 +217,10 @@ Example usage:
|
||||
}
|
||||
result.Append("```");
|
||||
yield return result.ToString();
|
||||
yield return footer;
|
||||
//yield return footer;
|
||||
}
|
||||
else if (returnCode.displayFooter)
|
||||
yield return footer;
|
||||
//else if (returnCode.displayFooter)
|
||||
// yield return footer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,9 +26,11 @@ namespace CompatBot
|
||||
public static readonly int AttachmentSizeLimit = 8 * 1024 * 1024;
|
||||
public static readonly int LogSizeLimit = 64 * 1024 * 1024;
|
||||
public static readonly int MinimumBufferSize = 512;
|
||||
public static readonly int MaxBuildNumberDifferenceForOutdatedBuilds = 10;
|
||||
|
||||
public static readonly string Token;
|
||||
public static readonly string LogPath = "../../../logs/bot.log"; // paths are relative to the assembly, so this will put it in the project's root
|
||||
public static readonly string IrdCachePath = "./ird/";
|
||||
|
||||
internal static readonly ILogger Log;
|
||||
|
||||
|
@ -116,7 +116,7 @@ namespace CompatBot.EventHandlers.LogParsing
|
||||
@"RSX: (?<driver_manuf>.*?)\r?\n[^\n]*?" +
|
||||
@"RSX: Supported texel buffer size", DefaultOptions),
|
||||
["GL RENDERER:"] = new Regex(@"GL RENDERER: (?<driver_manuf_new>.*?)\r?$", DefaultOptions),
|
||||
["GL VERSION:"] = new Regex(@"GL VERSION: (?<opengl_version>(\d|\.)+)(\d|\.|\s|\w|-)*(?<driver_version_new>(\d+\.)*\d+)?\r?$", DefaultOptions),
|
||||
["GL VERSION:"] = new Regex(@"GL VERSION: (?<opengl_version>(\d|\.)+)(\d|\.|\s|\w|-)*( (?<driver_version_new>(\d+\.)*\d+))?\r?$", DefaultOptions),
|
||||
["GLSL VERSION:"] = new Regex(@"GLSL VERSION: (?<glsl_version>(\d|\.)+).*?\r?$", DefaultOptions),
|
||||
["texel buffer size reported:"] = new Regex(@"RSX: Supported texel buffer size reported: (?<texel_buffer_size_new>\d*?) bytes", DefaultOptions),
|
||||
["Physical device intialized"] = new Regex(@"Physical device intialized\. GPU=(?<vulkan_gpu>.+), driver=(?<vulkan_driver_version_raw>-?\d+)\r?$", DefaultOptions),
|
||||
@ -129,6 +129,8 @@ namespace CompatBot.EventHandlers.LogParsing
|
||||
["XAudio2Thread"] = new Regex(@"XAudio2Thread\s*: (?<xaudio_init_error>.+failed\s*\((?<xaudio_error_code>0x.+)\).*)\r?$", DefaultOptions),
|
||||
["PPU executable hash:"] = new Regex(@"PPU executable hash: PPU-(?<ppu_hash>\w+) \(<-\s*(?<ppu_hash_patch>(?!0)\d+)\).*?\r?$", DefaultOptions),
|
||||
["Loaded SPU image:"] = new Regex(@"Loaded SPU image: SPU-(?<spu_hash>\w+) \(<-\s*(?<spu_hash_patch>(?!0)\d+)\).*?\r?$", DefaultOptions),
|
||||
["'sys_fs_open' failed"] = new Regex(@"'sys_fs_open' failed .+\xE2\x80\x9C/dev_bdvd/(?<broken_filename>.+)\xE2\x80\x9D.*?\r?$", DefaultOptions),
|
||||
["'sys_fs_opendir' failed"] = new Regex(@"'sys_fs_opendir' failed .+\xE2\x80\x9C/dev_bdvd/(?<broken_directory>.+)\xE2\x80\x9D.*?\r?$", DefaultOptions),
|
||||
},
|
||||
OnSectionEnd = MarkAsCompleteAndReset,
|
||||
EndTrigger = "All threads stopped...",
|
||||
@ -143,6 +145,8 @@ namespace CompatBot.EventHandlers.LogParsing
|
||||
"ppu_hash_patch",
|
||||
"spu_hash",
|
||||
"spu_hash_patch",
|
||||
"broken_filename",
|
||||
"broken_directory",
|
||||
};
|
||||
|
||||
private static async Task PiracyCheckAsync(string line, LogParseState state)
|
||||
|
@ -2,10 +2,8 @@
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Utils;
|
||||
using DSharpPlus.Entities;
|
||||
using SharpCompress.Archives.SevenZip;
|
||||
|
||||
@ -15,40 +13,12 @@ namespace CompatBot.EventHandlers.LogParsing.SourceHandlers
|
||||
{
|
||||
private static readonly ArrayPool<byte> bufferPool = ArrayPool<byte>.Create(1024, 16);
|
||||
|
||||
public async Task<bool> CanHandleAsync(DiscordAttachment attachment)
|
||||
public Task<bool> CanHandleAsync(DiscordAttachment attachment)
|
||||
{
|
||||
if (!attachment.FileName.EndsWith(".7z", StringComparison.InvariantCultureIgnoreCase))
|
||||
return false;
|
||||
return Task.FromResult(false);
|
||||
|
||||
return true;
|
||||
/*
|
||||
try
|
||||
{
|
||||
using (var client = HttpClientFactory.Create())
|
||||
using (var stream = await client.GetStreamAsync(attachment.Url).ConfigureAwait(false))
|
||||
{
|
||||
var buf = bufferPool.Rent(4096);
|
||||
bool result;
|
||||
try
|
||||
{
|
||||
var read = await stream.ReadBytesAsync(buf).ConfigureAwait(false);
|
||||
using (var memStream = new MemoryStream(read))
|
||||
using (var zipArchive = SevenZipArchive.Open(memStream))
|
||||
result = zipArchive.Entries.Any(e => !e.IsDirectory && e.Key.EndsWith(".log", StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
bufferPool.Return(buf);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Config.Log.Error(e, "Error sniffing the 7z content");
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public async Task FillPipeAsync(DiscordAttachment attachment, PipeWriter writer)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CompatBot.Commands;
|
||||
@ -81,6 +82,16 @@ namespace CompatBot
|
||||
var gameTdbScrapingTask = GameTdbScraper.RunAsync(Config.Cts.Token);
|
||||
await amdDriverRefreshTask.ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(Config.IrdCachePath))
|
||||
Directory.CreateDirectory(Config.IrdCachePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Config.Log.Warn(e, $"Failed to create new folder {Config.IrdCachePath}: {e.Message}");
|
||||
}
|
||||
|
||||
var config = new DiscordConfiguration
|
||||
{
|
||||
Token = Config.Token,
|
||||
@ -88,7 +99,6 @@ namespace CompatBot
|
||||
//UseInternalLogHandler = true,
|
||||
//LogLevel = LogLevel.Debug,
|
||||
};
|
||||
|
||||
using (var client = new DiscordClient(config))
|
||||
{
|
||||
var commands = client.UseCommandsNext(new CommandsNextConfiguration
|
||||
|
@ -29,7 +29,7 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
else if (parts.Length == 1)
|
||||
parts = new[] {null, item.Filename};
|
||||
result.AddField(
|
||||
$"[{parts?[0]}] {item.Title?.Sanitize().Trim(EmbedPager.MaxFieldTitleLength)}",
|
||||
$"[{parts?[0]} v{item.GameVersion}] {item.Title?.Sanitize().Trim(EmbedPager.MaxFieldTitleLength)}",
|
||||
$"⏬ [`{parts[1]?.Sanitize().Trim(200)}`]({IrdClient.GetDownloadLink(item.Filename)}) ℹ [Info]({IrdClient.GetInfoLink(item.Filename)})"
|
||||
);
|
||||
}
|
||||
|
@ -9,17 +9,21 @@ using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.POCOs;
|
||||
using CompatApiClient.Utils;
|
||||
using CompatBot.Commands;
|
||||
using CompatBot.Database.Providers;
|
||||
using CompatBot.EventHandlers;
|
||||
using CompatBot.EventHandlers.LogParsing.POCOs;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.Entities;
|
||||
using IrdLibraryClient;
|
||||
using IrdLibraryClient.IrdFormat;
|
||||
|
||||
namespace CompatBot.Utils.ResultFormatters
|
||||
{
|
||||
internal static class LogParserResult
|
||||
{
|
||||
private static readonly Client compatClient = new Client();
|
||||
private static readonly IrdClient irdClient = new IrdClient();
|
||||
|
||||
// RPCS3 v0.0.3-3-3499d08 Alpha | HEAD
|
||||
// RPCS3 v0.0.4-6422-95c6ac699 Alpha | HEAD
|
||||
@ -289,10 +293,8 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
PageSection(builder, notesContent, "Important Settings to Review");
|
||||
}
|
||||
|
||||
private static async Task BuildNotesSectionAsync(DiscordEmbedBuilder builder, LogParseState state, NameValueCollection items)
|
||||
private static void BuildMissingLicensesSection(DiscordEmbedBuilder builder, NameValueCollection items)
|
||||
{
|
||||
BuildWeirdSettingsSection(builder, items);
|
||||
var notes = new StringBuilder();
|
||||
if (items["rap_file"] is string rap)
|
||||
{
|
||||
var limitTo = 5;
|
||||
@ -308,12 +310,65 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
content = string.Join(Environment.NewLine, licenseNames);
|
||||
builder.AddField("Missing Licenses", content);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> BuildMissingFilesSection(DiscordEmbedBuilder builder, NameValueCollection items)
|
||||
{
|
||||
if (!(items["serial"] is string productCode))
|
||||
return false;
|
||||
|
||||
if (!productCode.StartsWith("B") && !productCode.StartsWith("M"))
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(items["broken_directory"]) && string.IsNullOrEmpty(items["broken_filename"]))
|
||||
return false;
|
||||
|
||||
var getIrdTask = irdClient.DownloadAsync(productCode, Config.IrdCachePath, Config.Cts.Token);
|
||||
var missingDirs = items["broken_directory"]?.Split(Environment.NewLine).Distinct().ToList() ?? new List<string>(0);
|
||||
var missingFiles = items["broken_filename"]?.Split(Environment.NewLine).Distinct().ToList() ?? new List<string>(0);
|
||||
HashSet<string> knownFiles;
|
||||
try
|
||||
{
|
||||
var irdFiles = await getIrdTask.ConfigureAwait(false);
|
||||
knownFiles = new HashSet<string>(
|
||||
from ird in irdFiles
|
||||
from name in ird.GetFilenames()
|
||||
select name,
|
||||
StringComparer.InvariantCultureIgnoreCase
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Config.Log.Warn(e, "Failed to get IRD files for " + productCode);
|
||||
return false;
|
||||
}
|
||||
var broken = missingFiles.Any(knownFiles.Contains);
|
||||
if (broken)
|
||||
return true;
|
||||
|
||||
var knownDirs = new HashSet<string>(knownFiles.Select(f => Path.GetDirectoryName(f).Replace('\\', '/')), StringComparer.InvariantCultureIgnoreCase);
|
||||
return missingDirs.Any(knownDirs.Contains);
|
||||
}
|
||||
|
||||
private static async Task BuildNotesSectionAsync(DiscordEmbedBuilder builder, LogParseState state, NameValueCollection items)
|
||||
{
|
||||
BuildWeirdSettingsSection(builder, items);
|
||||
BuildMissingLicensesSection(builder, items);
|
||||
var brokenDump = await BuildMissingFilesSection(builder, items).ConfigureAwait(false);
|
||||
var notes = new StringBuilder();
|
||||
if (items["fatal_error"] is string fatalError)
|
||||
{
|
||||
builder.AddField("Fatal Error", $"```{fatalError.Trim(1022)}```");
|
||||
if (fatalError.Contains("psf.cpp"))
|
||||
notes.AppendLine("Game save data might be corrupted");
|
||||
}
|
||||
if (items["failed_to_decrypt"] is string _)
|
||||
notes.AppendLine("Failed to decrypt game content, license file might be corrupted");
|
||||
if (items["failed_to_boot"] is string _)
|
||||
notes.AppendLine("Failed to boot the game, the dump might be encrypted or corrupted");
|
||||
if (brokenDump)
|
||||
notes.AppendLine("Some game files are missing or corrupted, please check and redump if needed.");
|
||||
|
||||
Version oglVersion = null;
|
||||
if (items["opengl_version"] is string oglVersionString)
|
||||
Version.TryParse(oglVersionString, out oglVersion);
|
||||
@ -327,35 +382,9 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
{
|
||||
if (oglVersion < MinimumOpenGLVersion)
|
||||
notes.AppendLine($"GPU only supports OpenGL {oglVersion.Major}.{oglVersion.Minor}, which is below the minimum requirement of {MinimumOpenGLVersion}");
|
||||
/*
|
||||
else if (oglVersion < RecommendedOpenGLVersion)
|
||||
notes.AppendLine($"GPU only supports OpenGL {oglVersion.Major}.{oglVersion.Minor}, which is below the recommended requirement of {RecommendedOpenGLVersion}");
|
||||
*/
|
||||
}
|
||||
/*
|
||||
var patchCount = 0;
|
||||
if (items["ppu_hash_patch"] is string ppuPatch)
|
||||
patchCount += ppuPatch.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Select(s =>
|
||||
{
|
||||
int.TryParse(s, out var result);
|
||||
return result;
|
||||
}).Sum();
|
||||
if (items["spu_hash_patch"] is string spuPatch)
|
||||
patchCount += spuPatch.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Select(s =>
|
||||
{
|
||||
int.TryParse(s, out var result);
|
||||
return result;
|
||||
}).Sum();
|
||||
if (patchCount > 0)
|
||||
notes.AppendLine($"{patchCount} game patch{(patchCount == 1 ? " was" : "es were")} applied");
|
||||
*/
|
||||
if (!string.IsNullOrEmpty(items["ppu_hash_patch"]) || !string.IsNullOrEmpty(items["spu_hash_patch"]))
|
||||
notes.AppendLine("Game-specific patches were applied");
|
||||
|
||||
if (items["failed_to_decrypt"] is string _)
|
||||
notes.AppendLine("Failed to decrypt game content, license file might be corrupted");
|
||||
if (items["failed_to_boot"] is string _)
|
||||
notes.AppendLine("Failed to boot the game, the dump might be encrypted or corrupted");
|
||||
if (string.IsNullOrEmpty(items["ppu_decoder"]) || string.IsNullOrEmpty(items["renderer"]))
|
||||
notes.AppendLine("The log is empty, you need to run the game before uploading the log");
|
||||
if (!string.IsNullOrEmpty(items["hdd_game_path"]) && !(items["serial"]?.StartsWith("NP", StringComparison.InvariantCultureIgnoreCase) ?? false))
|
||||
@ -408,10 +437,27 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
return null;
|
||||
|
||||
var latestBuildInfo = BuildInfoInUpdate.Match(link.ToLowerInvariant());
|
||||
if (!latestBuildInfo.Success || SameCommits(buildInfo.Groups["commit"].Value, latestBuildInfo.Groups["commit"].Value))
|
||||
return null;
|
||||
if (latestBuildInfo.Success && VersionIsTooOld(buildInfo, latestBuildInfo))
|
||||
return updateInfo;
|
||||
|
||||
return updateInfo;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool VersionIsTooOld(Match log, Match update)
|
||||
{
|
||||
if (Version.TryParse(log.Groups["version"].Value, out var logVersion) && Version.TryParse(update.Groups["version"].Value, out var updateVersion))
|
||||
{
|
||||
if (logVersion < updateVersion)
|
||||
return true;
|
||||
|
||||
if (int.TryParse(log.Groups["build"].Value, out var logBuild) && int.TryParse(update.Groups["build"].Value, out var updateBuild))
|
||||
{
|
||||
if (logBuild + Config.MaxBuildNumberDifferenceForOutdatedBuilds < updateBuild)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return !SameCommits(log.Groups["commit"].Value, update.Groups["commit"].Value);
|
||||
}
|
||||
|
||||
private static bool SameCommits(string commitA, string commitB)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@ -11,6 +12,7 @@ using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.Compression;
|
||||
using CompatApiClient.Utils;
|
||||
using IrdLibraryClient.IrdFormat;
|
||||
using IrdLibraryClient.POCOs;
|
||||
using Newtonsoft.Json;
|
||||
using JsonContractResolver = CompatApiClient.JsonContractResolver;
|
||||
@ -70,7 +72,7 @@ namespace IrdLibraryClient
|
||||
["start"] = "0",
|
||||
["length"] = "10",
|
||||
|
||||
["search[value]"] = query,
|
||||
["search[value]"] = query.Trim(100),
|
||||
|
||||
["_"] = DateTime.UtcNow.Ticks.ToString(),
|
||||
});
|
||||
@ -101,6 +103,105 @@ namespace IrdLibraryClient
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Ird>> DownloadAsync(string productCode, string localCachePath, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<Ird>();
|
||||
try
|
||||
{
|
||||
// first we search local cache and try to load whatever data we can
|
||||
var localCacheItems = new List<string>();
|
||||
try
|
||||
{
|
||||
var tmpCacheItemList = Directory.GetFiles(localCachePath, productCode + "*.ird", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).ToList();
|
||||
foreach (var item in tmpCacheItemList)
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Add(IrdParser.Parse(File.ReadAllBytes(Path.Combine(localCachePath, item))));
|
||||
localCacheItems.Add(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ApiConfig.Log.Warn(ex, "Error reading local IRD file: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ApiConfig.Log.Warn(e, "Error accessing local IRD cache: " + e.Message);
|
||||
}
|
||||
ApiConfig.Log.Debug($"Found {localCacheItems.Count} cached items for {productCode}");
|
||||
SearchResult searchResult = null;
|
||||
|
||||
// then try to do IRD Library search
|
||||
try
|
||||
{
|
||||
searchResult = await SearchAsync(productCode, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ApiConfig.Log.Error(e);
|
||||
}
|
||||
var tmpFilesToGet = searchResult?.Data.Select(i => i.Filename).Except(localCacheItems, StringComparer.InvariantCultureIgnoreCase).ToList();
|
||||
if ((tmpFilesToGet?.Count ?? 0) == 0)
|
||||
return result;
|
||||
|
||||
// as IRD Library could return more data than we found, try to check for all the items locally
|
||||
var filesToDownload = new List<string>();
|
||||
foreach (var item in tmpFilesToGet)
|
||||
{
|
||||
try
|
||||
{
|
||||
var localItemPath = Path.Combine(localCachePath, item);
|
||||
if (File.Exists(localItemPath))
|
||||
{
|
||||
result.Add(IrdParser.Parse(File.ReadAllBytes(localItemPath)));
|
||||
localCacheItems.Add(item);
|
||||
}
|
||||
else
|
||||
filesToDownload.Add(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ApiConfig.Log.Warn(ex, "Error reading local IRD file: " + ex.Message);
|
||||
filesToDownload.Add(item);
|
||||
}
|
||||
}
|
||||
ApiConfig.Log.Debug($"Found {tmpFilesToGet.Count} total matches for {productCode}, {result.Count} already cached");
|
||||
if (filesToDownload.Count == 0)
|
||||
return result;
|
||||
|
||||
// download the remaining .ird files
|
||||
foreach (var item in filesToDownload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resultBytes = await client.GetByteArrayAsync(GetDownloadLink(item)).ConfigureAwait(false);
|
||||
result.Add(IrdParser.Parse(resultBytes));
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(Path.Combine(localCachePath, item), resultBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ApiConfig.Log.Warn(ex, $"Failed to write {item} to local cache: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ApiConfig.Log.Warn(e, $"Failed to download {item}: {e.Message}");
|
||||
}
|
||||
}
|
||||
ApiConfig.Log.Debug($"Returning {result.Count} .ird files for {productCode}");
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ApiConfig.Log.Error(e);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetIrdFilename(string html)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
|
40
IrdLibraryClient/IrdFormat/Ird.cs
Normal file
40
IrdLibraryClient/IrdFormat/Ird.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace IrdLibraryClient.IrdFormat
|
||||
{
|
||||
public class Ird
|
||||
{
|
||||
public static int Magic = BitConverter.ToInt32(Encoding.ASCII.GetBytes("3IRD"), 0);
|
||||
public byte Version;
|
||||
public string ProductCode; // 9
|
||||
public byte TitleLength;
|
||||
public string Title;
|
||||
public string UpdateVersion; // 4
|
||||
public string GameVersion; // 5
|
||||
public string AppVersion; // 5
|
||||
public int Id; // v7 only?
|
||||
public int HeaderLength;
|
||||
public byte[] Header; // gz
|
||||
public int FooterLength;
|
||||
public byte[] Footer; // gz
|
||||
public byte RegionCount;
|
||||
public List<byte[]> RegionMd5Checksums; // 16 each
|
||||
public int FileCount;
|
||||
public List<IrdFile> Files;
|
||||
public int Unknown; // always 0?
|
||||
public byte[] Pic; // 115, v9 only?
|
||||
public byte[] Data1; // 16
|
||||
public byte[] Data2; // 16
|
||||
// Pic for <v9
|
||||
public int Uid;
|
||||
public uint Crc32;
|
||||
}
|
||||
|
||||
public class IrdFile
|
||||
{
|
||||
public long Offset;
|
||||
public byte[] Md5Checksum;
|
||||
}
|
||||
}
|
99
IrdLibraryClient/IrdFormat/IrdParser.cs
Normal file
99
IrdLibraryClient/IrdFormat/IrdParser.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscUtils.Iso9660;
|
||||
using Force.Crc32;
|
||||
|
||||
namespace IrdLibraryClient.IrdFormat
|
||||
{
|
||||
public static class IrdParser
|
||||
{
|
||||
public static Ird Parse(byte[] content)
|
||||
{
|
||||
if (content == null)
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
|
||||
if (content.Length < 200)
|
||||
throw new ArgumentException("Data is too small to be a valid IRD structure", nameof(content));
|
||||
|
||||
if (BitConverter.ToInt32(content, 0) != Ird.Magic)
|
||||
using (var compressedStream = new MemoryStream(content, false))
|
||||
using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
|
||||
using (var decompressedStream = new MemoryStream())
|
||||
{
|
||||
gzip.CopyTo(decompressedStream);
|
||||
content = decompressedStream.ToArray();
|
||||
}
|
||||
if (BitConverter.ToInt32(content, 0) != Ird.Magic)
|
||||
throw new FormatException("Not a valid IRD file");
|
||||
|
||||
var result = new Ird();
|
||||
using (var stream = new MemoryStream(content, false))
|
||||
using (var reader = new BinaryReader(stream, Encoding.UTF8))
|
||||
{
|
||||
reader.ReadInt32(); // magic
|
||||
result.Version = reader.ReadByte();
|
||||
result.ProductCode = Encoding.ASCII.GetString(reader.ReadBytes(9));
|
||||
result.TitleLength = reader.ReadByte();
|
||||
result.Title = Encoding.UTF8.GetString(reader.ReadBytes(result.TitleLength));
|
||||
result.UpdateVersion = Encoding.ASCII.GetString(reader.ReadBytes(4)).Trim();
|
||||
result.GameVersion = Encoding.ASCII.GetString(reader.ReadBytes(5)).Trim();
|
||||
result.AppVersion = Encoding.ASCII.GetString(reader.ReadBytes(5)).Trim();
|
||||
if (result.Version == 7)
|
||||
result.Id = reader.ReadInt32();
|
||||
result.HeaderLength = reader.ReadInt32();
|
||||
result.Header = reader.ReadBytes(result.HeaderLength);
|
||||
result.FooterLength = reader.ReadInt32();
|
||||
result.Footer = reader.ReadBytes(result.FooterLength);
|
||||
result.RegionCount = reader.ReadByte();
|
||||
result.RegionMd5Checksums = new List<byte[]>(result.RegionCount);
|
||||
for (var i = 0; i < result.RegionCount; i++)
|
||||
result.RegionMd5Checksums.Add(reader.ReadBytes(16));
|
||||
result.FileCount = reader.ReadInt32();
|
||||
result.Files = new List<IrdFile>(result.FileCount);
|
||||
for (var i = 0; i < result.FileCount; i++)
|
||||
{
|
||||
var file = new IrdFile();
|
||||
file.Offset = reader.ReadInt64();
|
||||
file.Md5Checksum = reader.ReadBytes(16);
|
||||
result.Files.Add(file);
|
||||
}
|
||||
result.Unknown = reader.ReadInt32();
|
||||
if (result.Version == 9)
|
||||
result.Pic = reader.ReadBytes(115);
|
||||
result.Data1 = reader.ReadBytes(16);
|
||||
result.Data2 = reader.ReadBytes(16);
|
||||
if (result.Version < 9)
|
||||
result.Pic = reader.ReadBytes(115);
|
||||
result.Uid = reader.ReadInt32();
|
||||
var dataLength = reader.BaseStream.Position;
|
||||
result.Crc32 = reader.ReadUInt32();
|
||||
|
||||
var crc32 = Crc32Algorithm.Compute(content, 0, (int)dataLength);
|
||||
if (result.Crc32 != crc32)
|
||||
throw new InvalidDataException($"Corrupted IRD data, expected {result.Crc32:x8}, but was {crc32:x8}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class IsoHeaderParser
|
||||
{
|
||||
public static List<string> GetFilenames(this Ird ird)
|
||||
{
|
||||
using (var decompressedStream = new MemoryStream())
|
||||
{
|
||||
using (var compressedStream = new MemoryStream(ird.Header, false))
|
||||
using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
|
||||
gzip.CopyTo(decompressedStream);
|
||||
|
||||
decompressedStream.Seek(0, SeekOrigin.Begin);
|
||||
var reader = new CDReader(decompressedStream, true, true);
|
||||
return reader.GetFiles(reader.Root.FullName, "*.*", SearchOption.AllDirectories).Distinct().Select(n => n.TrimStart('\\').Replace('\\', '/')).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,14 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crc32.NET" Version="1.2.0" />
|
||||
<PackageReference Include="DiscUtils.OpticalDisk" Version="0.13.0-alpha" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CompatApiClient\CompatApiClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
42
Tests/IrdTests.cs
Normal file
42
Tests/IrdTests.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System.IO;
|
||||
using IrdLibraryClient.IrdFormat;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class IrdTests
|
||||
{
|
||||
[Test]
|
||||
public void ParsingTest()
|
||||
{
|
||||
var baseDir = TestContext.CurrentContext.TestDirectory;
|
||||
var testFiles = Directory.GetFiles(baseDir, "*.ird", SearchOption.AllDirectories);
|
||||
Assert.That(testFiles.Length, Is.GreaterThan(0));
|
||||
|
||||
foreach (var file in testFiles)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(file);
|
||||
Assert.That(() => IrdParser.Parse(bytes), Throws.Nothing, "Failed to parse " + Path.GetFileName(file));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HeaderParsingTest()
|
||||
{
|
||||
var baseDir = TestContext.CurrentContext.TestDirectory;
|
||||
var testFiles = Directory.GetFiles(baseDir, "*.ird", SearchOption.AllDirectories);
|
||||
Assert.That(testFiles.Length, Is.GreaterThan(0));
|
||||
|
||||
foreach (var file in testFiles)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(file);
|
||||
var ird = IrdParser.Parse(bytes);
|
||||
Assert.That(ird.FileCount, Is.GreaterThan(0));
|
||||
|
||||
var fileList = ird.GetFilenames();
|
||||
Assert.That(fileList.Count, Is.EqualTo(ird.FileCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
Tests/Test Files/Put IRD files here.txt
Normal file
1
Tests/Test Files/Put IRD files here.txt
Normal file
@ -0,0 +1 @@
|
||||
|
25
Tests/Tests.csproj
Normal file
25
Tests/Tests.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="NUnit" Version="3.11.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\IrdLibraryClient\IrdLibraryClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Test Files\*.ird">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsnClient", "PsnClient\PsnC
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HomoglyphConverter", "HomoglyphConverter\HomoglyphConverter.csproj", "{A1E6566C-F506-43C8-B06E-9A472AEBF447}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IrdLibraryClient", "IrdLibraryClient\IrdLibraryClient.csproj", "{AA2A333B-CD30-41A5-A680-CC9BCB2D726B}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IrdLibraryClient", "IrdLibraryClient\IrdLibraryClient.csproj", "{AA2A333B-CD30-41A5-A680-CC9BCB2D726B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{815D60C3-F84B-4AE2-B4E4-48004515FCFD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -39,6 +41,9 @@ Global
|
||||
{AA2A333B-CD30-41A5-A680-CC9BCB2D726B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AA2A333B-CD30-41A5-A680-CC9BCB2D726B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AA2A333B-CD30-41A5-A680-CC9BCB2D726B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{815D60C3-F84B-4AE2-B4E4-48004515FCFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{815D60C3-F84B-4AE2-B4E4-48004515FCFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{815D60C3-F84B-4AE2-B4E4-48004515FCFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
Loading…
x
Reference in New Issue
Block a user