diff --git a/src/Bot/Services/LogAnalysis/AnalysisResult.cs b/src/Bot/Services/LogAnalysis/AnalysisResult.cs index adc1333f..490e3707 100644 --- a/src/Bot/Services/LogAnalysis/AnalysisResult.cs +++ b/src/Bot/Services/LogAnalysis/AnalysisResult.cs @@ -11,7 +11,11 @@ public LogAnalysis(string rawLogText, LogAnalysisService service) } public string RawLogContent { get; } - public List LogErrors; + public List Errors; + public List Notes; + // Fatal errors are errors that prevent the log from being scanned + public List FatalErrors; + public HardwareInfo Hardware; public EmulatorInfo Emulator; public GameInfo Game; @@ -23,6 +27,7 @@ public class HardwareInfo public string Cpu { get; set; } public string Gpu { get; set; } public string Ram { get; set; } + public string RamAvailable { get; set; } public string Os { get; set; } } @@ -30,13 +35,16 @@ public class EmulatorInfo { public (RyujinxVersion VersionType, string VersionString) Version { get; set; } public string Firmware { get; set; } - public List EnabledLogs { get; set; } = []; + public string EnabledLogs { get; set; } + public string Timestamp { get; set; } } public class GameInfo { public string Name { get; set; } - public string Errors { get; set; } + public string AppId { get; set; } + public string AppIdBids { get; set; } + public string BuildIDs { get; set; } public string Mods { get; set; } public string Cheats { get; set; } } @@ -54,9 +62,11 @@ public class Settings public bool Pptc { get; set; } public bool ShaderCache { get; set; } public string VSyncMode { get; set; } - public bool? Hypervisor { get; set; } + public string Hypervisor { get; set; } public string ResScale { get; set; } public string AnisotropicFiltering { get; set; } public string AspectRatio { get; set; } public bool TextureRecompression { get; set; } + public bool CustomVSyncInterval { get; set; } + public string MultiplayerMode { get; set; } } \ No newline at end of file diff --git a/src/Bot/Services/LogAnalysis/Notes.cs b/src/Bot/Services/LogAnalysis/Notes.cs new file mode 100644 index 00000000..c92dc91d --- /dev/null +++ b/src/Bot/Services/LogAnalysis/Notes.cs @@ -0,0 +1,79 @@ +namespace RyuBot.Services; + +public class Notes +{ + public readonly string AMDOpenGL = "⚠️ **AMD GPU users should consider using Vulkan graphics backend**"; + public readonly string IntelOpenGL = "⚠️ **Intel GPU users should consider using Vulkan graphics backend**"; + public readonly string IntelMac = "⚠️ **Intel Macs are not supported.**"; + public readonly string Rosetta = "🔴 **Rosetta should be disabled**"; + public readonly string DebugLogs = "⚠️ **Debug logs enabled will have a negative impact on performance**"; + + public readonly string MissingLogs = "⚠️ **Logs settings are not default. Consider enabled `Info`, " + + "`Warning`, `Error` and `Guest` logs.**"; + + public readonly string DummyAudio = "⚠️ Dummy audio backend, consider changing to SDL2."; + public readonly string Pptc = "🔴 **PPTC cache should be enabled**"; + public readonly string ShaderCache = "🔴 **Shader cache should be enabled.**"; + + public readonly string SoftwareMemory = "🔴 **`Software` setting in Memory Manager Mode will give slower " + + "performance than the default setting of `Host unchecked`.**"; + + public readonly string MissingServices = "⚠️ `Ignore Missing Services` being enabled can cause instability."; + + public readonly string FsIntegrity = + "⚠️ Disabling file integrity checks may cause corrupted dumps to not be detected."; + + public readonly string VSync = "⚠️ V-Sync disabled can cause instability like games running faster than " + + "intended or longer load times."; + + public readonly string HashError = "🔴 Dump error detected. Investigate possible bad game/firmware dump issues."; + public readonly string GameCrashed = "🔴 The game itself crashed, not Ryujinx."; + public readonly string MissingKeys = "⚠️ Keys or firmware out of date, consider updating them."; + + public readonly string PermissionError = "🔴 File permission error. Consider deleting save directory and " + + "allowing Ryujinx to make a new one."; + + public readonly string FsTargetError = "🔴 Save not found error. Consider starting game without a save file or " + + "using a new save file."; + + public readonly string ServiceError = "⚠️ Consider enabling `Ignore Missing Services` in Ryujinx settings."; + public readonly string VramError = "⚠️ Consider enabling `Texture Recompression` in Ryujinx settings."; + public readonly string DefaultProfile = "⚠️ Default user profile in use, consider creating a custom one."; + public readonly string SaveDataIndex = "🔴 **Save data index for the game may be corrupted.**"; + public readonly string DramSize = "⚠️ `DRAM size` should only be increased for 4K mods."; + public readonly string BackendThreadingAuto = "🔴 **Graphics Backend Multithreading should be set to `Auto`.**"; + + public readonly string CustomRefreshRate = "⚠️ Custom Refresh Rate is experimental, it should only be " + + "enabled in specific cases."; + + public readonly string Firmware = + "❌ **Nintendo Switch firmware not found**, consider adding your keys and firmware."; + + public readonly string Metal = "⚠️ **The Metal backend is experimental. " + + "If you're experiencing issues, switch to Vulkan or Auto.**"; + + public readonly string ShaderCacheCollision = + "⚠️ Cache collision detected. Investigate possible shader cache issues."; + + public readonly string ShaderCacheCorruption = + "⚠️ Cache corruption detected. Investigate possible shader cache issues."; +} + +public class FatalErrors +{ + public readonly string Custom = "⚠️ **Custom builds are not officially supported**"; + + public readonly string OriginalLdn = + "**The old Ryujinx LDN build no longer works. Please update to " + + "[this version](). *Yes, it has LDN functionality.***"; + + public readonly string Original = + "**⚠️ It seems you're still using the original Ryujinx. " + + "Please update to [this version]()," + + " as that's what this Discord server is for.**"; + + public readonly string Mirror = + "**It seems you're using the other Ryujinx fork, ryujinx-mirror. " + + "Please update to [this version](), " + + "as that's what this Discord server is for; or go to their Discord server for support.**"; +} \ No newline at end of file diff --git a/src/Bot/Services/LogAnalysis/Regexes.cs b/src/Bot/Services/LogAnalysis/Regexes.cs index 20c5d727..6c63fbfc 100644 --- a/src/Bot/Services/LogAnalysis/Regexes.cs +++ b/src/Bot/Services/LogAnalysis/Regexes.cs @@ -9,6 +9,7 @@ public static partial class LogAnalysisPatterns public static readonly Regex OriginalProjectVersion = OriginalProjectVersionRegex(); public static readonly Regex OriginalProjectLdnVersion = OriginalProjectLdnVersionRegex(); public static readonly Regex PrVersion = PrVersionRegex(); + public static readonly Regex OriginalPrVersion = OriginalPrVersionRegex(); public static readonly Regex MirrorVersion = MirrorVersionRegex(); [GeneratedRegex(@"^1\.2\.\d+$")] @@ -26,6 +27,9 @@ public static partial class LogAnalysisPatterns [GeneratedRegex(@"^1\.2\.\d\+([a-f]|\d){7}$")] private static partial Regex PrVersionRegex(); + [GeneratedRegex(@"^1\.1\.\d\+([a-f]|\d){7}$")] + private static partial Regex OriginalPrVersionRegex(); + [GeneratedRegex(@"^r\.(\d|\w){7}$")] private static partial Regex MirrorVersionRegex(); } \ No newline at end of file diff --git a/src/Bot/Services/LogAnalysis/RyuLogReader.cs b/src/Bot/Services/LogAnalysis/RyuLogReader.cs new file mode 100644 index 00000000..7209eba5 --- /dev/null +++ b/src/Bot/Services/LogAnalysis/RyuLogReader.cs @@ -0,0 +1,616 @@ +using System.Text.RegularExpressions; + +namespace RyuBot.Services; + +public class RyuLogReader +{ + private LogAnalysis _log; + private Notes _notes; + private FatalErrors _fatalErrors; + + double ConvertGiBtoMiB(double GiB) + { + return Math.Round(GiB * 1024); + } + + static bool IsHomebrew(string logFile) + { + Match m = Regex.Match(logFile, "Load.*Application: Loading as [Hh]omebrew"); + return m.Success; + } + + static bool IsUsingMetal(string logFile) + { + Match m = Regex.Match(logFile, "Gpu : Backend \\(Metal\\): Metal"); + return m.Success; + } + + static bool IsDefaultUserProfile(string logFile) + { + Match m = Regex.Match(logFile, "UserId: 00000000000000010000000000000000"); + return m.Success; + } + + (RyujinxVersion VersionType, string VersionString) GetEmuVersion() + { + // Ryujinx Version check + foreach (string line in _log.RawLogContent.Split("\n")) + { + // Greem's Stable build + if (LogAnalysisPatterns.StableVersion.IsMatch(line)) + { + return (RyujinxVersion.Stable, line[-1].ToString().Trim()); + } + + // Greem's Canary build + if (LogAnalysisPatterns.CanaryVersion.IsMatch(line)) + { + return (RyujinxVersion.Canary, line[-1].ToString().Trim()); + } + + // PR build + if (LogAnalysisPatterns.PrVersion.IsMatch(line) + || LogAnalysisPatterns.OriginalPrVersion.IsMatch(line)) + { + return (RyujinxVersion.Pr, line[-1].ToString().Trim()); + } + + // Original Project build + if (LogAnalysisPatterns.OriginalProjectVersion.IsMatch(line)) + { + return (RyujinxVersion.OriginalProject, line[-1].ToString().Trim()); + } + + // Original Project LDN build + if (LogAnalysisPatterns.OriginalProjectLdnVersion.IsMatch(line)) + { + return (RyujinxVersion.OriginalProjectLdn, line[-1].ToString().Trim()); + } + + if (LogAnalysisPatterns.MirrorVersion.IsMatch(line)) + { + return (RyujinxVersion.Mirror, line[-1].ToString().Trim()); + } + } + + return (RyujinxVersion.Custom, "1.0.0-dirty"); + } + + void GetAppInfo() + { + MatchCollection gameNameMatch = Regex.Matches(_log.RawLogContent, + @"Loader [A-Za-z]*: Application Loaded:\s([^;\n\r]*)", RegexOptions.Multiline); + if (!gameNameMatch.None()) + { + string gameName = gameNameMatch[-1].ToString().Trim(); + + Match appIdMatch = Regex.Match(gameName, @".* \[([a-zA-Z0-9]*)\]"); + string appId = appIdMatch.Success ? appIdMatch.Groups[1].Value.Trim().ToUpper() : "Unknown"; + + MatchCollection bidsMatchAll = Regex.Matches(_log.RawLogContent, + @"Build ids found for (?:title|application) ([a-zA-Z0-9]*):[\n\r]*((?:\s+.*[\n\r]+)+)"); + if (!bidsMatchAll.None() && bidsMatchAll.Count > 0) + { + // this whole thing might not work properly + string bidsMatch = bidsMatchAll[-1].ToString(); + + string appIdFromBids = bidsMatch[0].ToString() != "" ? bidsMatch[0].ToString().Trim().ToUpper() : "Unknown"; + + // this might not work + string buildIDs = bidsMatch[1].ToString() != "" ? bidsMatch[1].ToString().Trim().ToUpper() : "Unknown"; + + _log.Game.Name = gameName; + _log.Game.AppId = appId; + _log.Game.AppIdBids = appIdFromBids; + _log.Game.BuildIDs = buildIDs; + } + } + } + + static bool ContainsError(string[] searchTerm, List errors) + { + foreach (string term in searchTerm) + { + foreach (string errorLines in errors) + { + string line = errorLines.JoinToString("\n"); + if (term.Contains(line)) + { + return true; + } + } + } + + return false; + } + + void GetErrors() + { + List errors = new List(); + List currentErrorsLines = new List(); + bool errorLine = false; + + foreach (string line in _log.RawLogContent.Split("\n")) + { + if (line.IsNullOrWhitespace()) + { + continue; + } + + if (line.Contains("|E|")) + { + currentErrorsLines = [line]; + errors.AddRange(currentErrorsLines); + errorLine = true; + } + else if (errorLine && line[0].ToString() == "") + { + currentErrorsLines.AddRange(line); + } + } + + if (currentErrorsLines.Count > 0) + { + errors.AddRange(currentErrorsLines); + } + + _log.Errors = errors; + } + + readonly string[] _sizes = ["KB", "KiB", "MB", "MiB", "GB", "GiB"]; + + void GetHardwareInfo() + { + // CPU + Match cpuMatch = Regex.Match(_log.RawLogContent, @"CPU:\s([^;\n\r]*)", RegexOptions.Multiline); + _log.Hardware.Cpu = cpuMatch.Success ? cpuMatch.Groups[1].Value.TrimEnd() : "Unknown"; + + // RAM + Match ramMatch = Regex.Match(_log.RawLogContent, + @$"RAM: Total ([\d.]+) ({_sizes}) ; Available ([\d.]+) ({_sizes})", RegexOptions.Multiline); + if (ramMatch.Success) + { + double ramAvailable = ConvertGiBtoMiB(Convert.ToDouble(ramMatch.Groups[3].Value)); + double ramTotal = ConvertGiBtoMiB(Convert.ToDouble(ramMatch.Groups[1].Value)); + _log.Hardware.Ram = $"{ramAvailable:.0f}/{ramTotal:.0f} MiB"; + _log.Hardware.RamAvailable = ramAvailable.ToString("0.0"); + } + else + { + _log.Hardware.Ram = "Unknown"; + _log.Hardware.RamAvailable = "Unknown"; + } + + // Operating System (OS) + Match osMatch = Regex.Match(_log.RawLogContent, @"Operating System:\s([^;\n\r]*)", + RegexOptions.Multiline); + _log.Hardware.Os = osMatch.Success ? osMatch.Groups[1].Value.TrimEnd() : "Unknown"; + + // GPU + Match gpuMatch = Regex.Match(_log.RawLogContent, @"PrintGpuInformation:\s([^;\n\r]*)", + RegexOptions.Multiline); + _log.Hardware.Gpu = gpuMatch.Success ? gpuMatch.Groups[1].Value.TrimEnd() : + // If android logs starts showing up, we can detect android GPUs here + "Unknown"; + } + + void GetEmuInfo() + { + _log.Emulator.Version = GetEmuVersion(); + + // Logs Enabled ? + Match logsMatch = Regex.Match(_log.RawLogContent, @"Logs Enabled:\s([^;\n\r]*)", + RegexOptions.Multiline); + _log.Emulator.EnabledLogs = logsMatch.Success ? logsMatch.Groups[1].Value.TrimEnd() : "Unknown"; + + // Firmware + Match firmwareMatch = Regex.Match(_log.RawLogContent, @"Firmware Version:\s([^;\n\r]*)", + RegexOptions.Multiline); + _log.Emulator.Firmware = firmwareMatch.Success ? firmwareMatch.Groups[-1].Value.Trim() : "Unknown"; + + } + + void GetSettings() + { + MatchCollection settingsMatch = Regex.Matches(_log.RawLogContent, @"LogValueChange:\s([^;\n\r]*)", + RegexOptions.Multiline); + + foreach (string line in settingsMatch) + { + switch (line) + { + // Resolution Scale + case "ResScaleCustom set to:": + case "ResScale set to:": + _log.Settings.ResScale = settingsMatch[0].Groups[2].Value.Trim(); + switch (_log.Settings.ResScale) + { + case "1": + _log.Settings.ResScale = "Native (720p/1080p)"; + break; + case "2": + _log.Settings.ResScale = "2x (1440p/2060p(4K))"; + break; + case "3": + _log.Settings.ResScale = "3x (2160p(4K)/3240p)"; + break; + case "4": + _log.Settings.ResScale = "4x (3240p/4320p(8K))"; + break; + case "-1": + _log.Settings.ResScale = "Custom"; + break; + } + + break; + // Anisotropic Filtering + case "MaxAnisotropy set to:": + _log.Settings.AnisotropicFiltering = settingsMatch[0].Groups[2].Value.Trim(); + break; + // Aspect Ratio + case "AspectRatio set to:": + _log.Settings.AspectRatio = settingsMatch[0].Groups[2].Value.Trim(); + switch (_log.Settings.AspectRatio) + { + case "Fixed16x9": + _log.Settings.AspectRatio = "16:9"; + break; + // TODO: add more aspect ratios + } + + break; + // Graphics Backend + case "GraphicsBackend set to:": + _log.Settings.GraphicsBackend = settingsMatch[0].Groups[2].Value.Trim(); + break; + // Custom VSync Interval + case "CustomVSyncInterval set to:": + string a = settingsMatch[0].Groups[2].Value.Trim(); + if (a == "False") + { + _log.Settings.CustomVSyncInterval = false; + } + else if (a == "True") + { + _log.Settings.CustomVSyncInterval = true; + } + break; + // Shader cache + case "EnableShaderCache set to: True": + _log.Settings.ShaderCache = true; + break; + case "EnableShaderCache set to: False": + _log.Settings.ShaderCache = false; + break; + // Docked or Handheld + case "EnableDockedMode set to: True": + _log.Settings.Docked = true; + break; + case "EnableDockedMode set to: False": + _log.Settings.Docked = false; + break; + // PPTC Cache + case "EnablePtc set to: True": + _log.Settings.Pptc = true; + break; + case "EnablePtc set to: False": + _log.Settings.Pptc = false; + break; + // FS Integrity check + case "EnableFsIntegrityChecks set to: True": + _log.Settings.FsIntegrityChecks = true; + break; + case "EnableFsIntegrityChecks set to: False": + _log.Settings.FsIntegrityChecks = false; + break; + // Audio Backend + case "AudioBackend set to:": + _log.Settings.AudioBackend = settingsMatch[0].Groups[2].Value.Trim(); + break; + // Memory Manager Mode + case "MemoryManagerMode set to:": + _log.Settings.MemoryManager = settingsMatch[0].Groups[2].Value.Trim(); + switch (_log.Settings.MemoryManager) + { + case "HostMappedUnsafe": + _log.Settings.MemoryManager = "Unsafe"; + break; + // TODO: Add more memory manager modes + } + + break; + // Hypervisor + case "UseHypervisor set to:": + _log.Settings.Hypervisor = settingsMatch[0].Groups[2].Value.Trim(); + // If the OS is windows or linux, set hypervisor to 'N/A' because it's only on macOS + if (_log.Hardware.Os.ToLower() == "windows" || _log.Hardware.Os.ToLower() == "linux") + { + _log.Settings.Hypervisor = "N/A"; + } + + break; + // Ldn Mode + case "MultiplayerMode set to:": + _log.Settings.MultiplayerMode = settingsMatch[0].Groups[2].Value.Trim(); + break; + case "DramSize set to:": + _log.Settings.DramSize = settingsMatch[0].Groups[2].Value.Trim(); + break; + // This is just in case EVERYTHING fails + default: + _log.Settings.ResScale = "Failed"; + _log.Settings.AnisotropicFiltering = "Failed"; + _log.Settings.AspectRatio = "Failed"; + _log.Settings.GraphicsBackend = "Failed"; + _log.Settings.CustomVSyncInterval = false; + _log.Settings.ShaderCache = false; + _log.Settings.Docked = false; + _log.Settings.Pptc = false; + _log.Settings.FsIntegrityChecks = false; + _log.Settings.AudioBackend = "Failed"; + _log.Settings.MemoryManager = "Failed"; + _log.Settings.Hypervisor = "Failed"; + break; + } + } + } + + void GetMods() + { + MatchCollection modsMatch = Regex.Matches(_log.RawLogContent, + "Found\\s(enabled)?\\s?mod\\s\\'(.+?)\\'\\s(\\[.+?\\])"); + _log.Game.Mods = modsMatch.ToString(); + } + + void GetCheats() + { + MatchCollection cheatsMatch = Regex.Matches(_log.RawLogContent, + @"Installing cheat\s'(.+)'(?!\s\d{2}:\d{2}:\d{2}\.\d{3}\s\|E\|\sTamperMachine\sCompile)"); + _log.Game.Cheats = cheatsMatch.ToString(); + } + + void GetAppName() + { + Match appNameMatch = Regex.Match(_log.RawLogContent, + @"Loader [A-Za-z]*: Application Loaded:\s([^;\n\r]*)", + RegexOptions.Multiline); + _log.Game.Name = appNameMatch.ToString(); + } + + void GetNotes() + { + string GetControllerNotes() + { + MatchCollection controllerNotesMatch = Regex.Matches(_log.RawLogContent, + @"Hid Configure: ([^\r\n]+)"); + if (controllerNotesMatch.Count != 0) + { + return controllerNotesMatch.ToString(); + } + + return null; + } + + string GetOsNotes() + { + if (_log.Hardware.Os.ToLower().Contains("windows") + && !_log.Settings.GraphicsBackend.Contains("Vulkan")) + { + if (_log.Hardware.Gpu.Contains("Intel")) + { + return _notes.IntelOpenGL; + } + if (_log.Hardware.Gpu.Contains("AMD")) + { + return _notes.AMDOpenGL; + } + } + + if (_log.Hardware.Os.ToLower().Contains("macos") && !_log.Hardware.Cpu.Contains("Intel")) + { + return _notes.IntelMac; + } + + return null; + } + + string GetCpuNotes() + { + if (_log.Hardware.Cpu.ToLower().Contains("VirtualApple")) + { + return _notes.Rosetta; + } + + return null; + } + + string GetLogNotes() + { + if (_log.Emulator.EnabledLogs.Contains("Debug")) + { + return _notes.DebugLogs; + } + + if (_log.Emulator.EnabledLogs.Length < 4) + { + return _notes.MissingLogs; + } + + return null; + } + + void GetSettingsNotes() + { + if (_log.Settings.AudioBackend == "Dummy") + { + _log.Notes.Add(_notes.DummyAudio); + } + + if (_log.Settings.Pptc == false) + { + _log.Notes.Add(_notes.Pptc); + } + + if (_log.Settings.ShaderCache == false) + { + _log.Notes.Add(_notes.ShaderCache); + } + + if (_log.Settings.DramSize != "" && !_log.Game.Mods.Contains("4K")) + { + _log.Notes.Add(_notes.DramSize); + } + + if (_log.Settings.MemoryManager == "SoftwarePageTable") + { + _log.Notes.Add(_notes.SoftwareMemory); + } + + if (_log.Settings.VSyncMode == "Unbounded") + { + _log.Notes.Add(_notes.VSync); + } + + if (_log.Settings.FsIntegrityChecks == false) + { + _log.Notes.Add(_notes.FsIntegrity); + } + + if (_log.Settings.BackendThreading == false) + { + _log.Notes.Add(_notes.BackendThreadingAuto); + } + + if (_log.Settings.CustomVSyncInterval) + { + _log.Notes.Add(_notes.CustomRefreshRate); + } + + if (_log.Settings.IgnoreMissingServices) + { + _log.Notes.Add(_notes.ServiceError); + } + } + + void GetEmulatorNotes() + { + if (ContainsError(["Cache collision found"], _log.Errors)) + { + _log.Errors.Add(_notes.ShaderCacheCollision); + } + + if (ContainsError([ + "ResultFsInvalidIvfcHash", + "ResultFsNonRealDataVerificationFailed",], _log.Errors)) + { + _log.Errors.Add(_notes.HashError); + } + + if (ContainsError([ + "Ryujinx.Graphics.Gpu.Shader.ShaderCache.Initialize()", + "System.IO.InvalidDataException: End of Central Directory record could not be found", + "ICSharpCode.SharpZipLib.Zip.ZipException: Cannot find central directory", + ], _log.Errors)) + { + _log.Errors.Add(_notes.ShaderCacheCorruption); + } + + if (ContainsError(["MissingKeyException"], _log.Errors)) + { + _log.Errors.Add(_notes.MissingKeys); + } + + if (ContainsError(["ResultFsPermissionDenied"], _log.Errors)) + { + _log.Errors.Add(_notes.PermissionError); + } + + if (ContainsError(["ResultFsTargetNotFound"], _log.Errors)) + { + _log.Errors.Add(_notes.FsTargetError); + } + + if (ContainsError(["ServiceNotImplementedException"], _log.Errors)) + { + _log.Errors.Add(_notes.MissingServices); + } + + if (ContainsError(["ErrorOutOfDeviceMemory"], _log.Errors)) + { + _log.Errors.Add(_notes.VramError); + } + + if (ContainsError(["ResultKvdbInvalidKeyValue (2020-0005)"], _log.Errors)) + { + _log.Errors.Add(_notes.SaveDataIndex); + } + + Match gameCrashMatch = + Regex.Match(_log.RawLogContent, "/\\(ResultErrApplicationAborted \\(\\d{4}-\\d{4}\\)\\)/"); + + if (gameCrashMatch.Success) + { + _log.Errors.Add(_notes.GameCrashed); + } + } + + Match latestTimestamp = Regex.Matches(_log.RawLogContent, @"(\d{2}:\d{2}:\d{2}\.\d{3})\s+?\|")[-1]; + if (latestTimestamp.Success) + { + _log.Notes.Add("ℹ️ Time elapsed: " + latestTimestamp.Value); + } + + if (IsDefaultUserProfile(_log.RawLogContent)) + { + _log.Notes.Add(_notes.DefaultProfile); + } + + _log.Notes.Add(GetControllerNotes()); + _log.Notes.Add(GetOsNotes()); + _log.Notes.Add(GetCpuNotes()); + + if (_log.Emulator.Firmware == "Unknown" || _log.Emulator.Firmware == null + && _log.Game.Name != "Unknown" || _log.Game.Name == null) + { + _log.Notes.Add(_notes.Firmware); + } + + _log.Notes.Add(GetLogNotes()); + GetSettingsNotes(); + GetEmulatorNotes(); + + if (IsUsingMetal(_log.RawLogContent)) + { + _log.Notes.Add(_notes.Metal); + } + + var ryujinxVersion = GetEmuVersion(); + + if (ryujinxVersion.VersionType == RyujinxVersion.Custom) + { + _log.FatalErrors.Add(_fatalErrors.Custom); + } else if (ryujinxVersion.VersionType == RyujinxVersion.OriginalProjectLdn) + { + _log.FatalErrors.Add(_fatalErrors.OriginalLdn); + } + else if (ryujinxVersion.VersionType == RyujinxVersion.OriginalProject) + { + _log.FatalErrors.Add(_fatalErrors.Original); + } else if (ryujinxVersion.VersionType == RyujinxVersion.Mirror) + { + _log.FatalErrors.Add(_fatalErrors.Mirror); + } + + } + + string GetLastError() + { + if (_log.Errors.Count > 0) + { + return _log.Errors[-1]; + } + + return null; + } + +}