diff --git a/Clients/CompatApiClient/CompatApiClient.csproj b/Clients/CompatApiClient/CompatApiClient.csproj index a925407b..c00ec205 100644 --- a/Clients/CompatApiClient/CompatApiClient.csproj +++ b/Clients/CompatApiClient/CompatApiClient.csproj @@ -10,7 +10,7 @@ - + diff --git a/CompatBot/Commands/Psn.Check.cs b/CompatBot/Commands/Psn.Check.cs index 46e89596..c05ed35c 100644 --- a/CompatBot/Commands/Psn.Check.cs +++ b/CompatBot/Commands/Psn.Check.cs @@ -34,28 +34,32 @@ internal static partial class Psn } await ctx.DeferResponseAsync(ephemeral).ConfigureAwait(false); - List embeds; + List msgList; try { var updateInfo = await TitleUpdateInfoProvider.GetAsync(id, Config.Cts.Token).ConfigureAwait(false); - embeds = await updateInfo.AsEmbedAsync(ctx.Client, id).ConfigureAwait(false); + msgList = await updateInfo.AsMessageAsync(ctx.Client, id).ConfigureAwait(false); } catch (Exception e) { Config.Log.Warn(e, "Failed to get title update info"); - embeds = + msgList = [ - new() - { - Color = Config.Colors.Maintenance, - Title = "Service is unavailable", - Description = "There was an error communicating with the service. Try again in a few minutes.", - } + new DiscordMessageBuilder() + .EnableV2Components() + .AddContainerComponent( + new([new DiscordTextDisplayComponent( + $""" + ### Service is unavailable + There was an error communicating with the service. Try again in a few minutes. + """ + )], color: Config.Colors.Maintenance) + ) ]; } - await ctx.RespondAsync(embeds[0], ephemeral: ephemeral).ConfigureAwait(false); - foreach (var embed in embeds.Skip(1).Take(EmbedPager.MaxFollowupMessages)) - await ctx.FollowupAsync(embed, ephemeral: ephemeral).ConfigureAwait(false); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder(msgList[0]).AsEphemeral(ephemeral: ephemeral)).ConfigureAwait(false); + foreach (var msg in msgList.Skip(1).Take(EmbedPager.MaxFollowupMessages)) + await ctx.FollowupAsync(new DiscordInteractionResponseBuilder(msg).AsEphemeral(ephemeral: ephemeral)).ConfigureAwait(false); } [Command("firmware")] diff --git a/CompatBot/CompatBot.csproj b/CompatBot/CompatBot.csproj index 41d0629d..33432dda 100644 --- a/CompatBot/CompatBot.csproj +++ b/CompatBot/CompatBot.csproj @@ -44,9 +44,9 @@ - - - + + + @@ -69,8 +69,8 @@ - - + + diff --git a/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs b/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs index 70b89589..b8d4cdc3 100644 --- a/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs +++ b/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs @@ -43,6 +43,7 @@ internal partial class LogParser ["SYS: Path"] = BootPathSys(), ["LDR: Path:"] = BootPathDigitalLdr(), ["SYS: Path:"] = BootPathDigitalSys(), + ["SYS: Booting savestate"] = BootingFromSavestate(), ["custom config:"] = CustomConfigPath(), ["patch_log: Failed to load patch file"] = FailedPatchPath(), ["Undisputed"] = UfcModFlag(), @@ -50,7 +51,6 @@ internal partial class LogParser }, EndTrigger = ["Used configuration:"], }, - new() { Extractors = new() @@ -90,7 +90,6 @@ internal partial class LogParser }, EndTrigger = ["VFS:"], }, - new() { Extractors = new() @@ -99,7 +98,6 @@ internal partial class LogParser }, EndTrigger = ["Video:"], }, - new() { Extractors = new() @@ -140,7 +138,6 @@ internal partial class LogParser }, EndTrigger = ["Audio:"], }, - new() // Audio, Input/Output, System, Net, Miscellaneous { Extractors = new() @@ -154,6 +151,10 @@ internal partial class LogParser ["Pad:"] = GamepadType(), + ["Start Paused:"] = StartPausedSavestate(), + ["Suspend Emulation Savestate Mode:"] = SuspendEmulationSavestate(), + ["Compatible Savestate Mode:"] = CompatibleSavestate(), + ["Automatically start games after boot:"] = AutoStartAfterBoot(), ["Always start after boot:"] = AlwaysStartAfterBoot(), ["Use native user interface:"] = NativeUIMode(), @@ -161,7 +162,6 @@ internal partial class LogParser }, EndTrigger = ["Log:"], }, - new() { Extractors = new() @@ -171,7 +171,6 @@ internal partial class LogParser EndTrigger = ["·"], OnSectionEnd = MarkAsComplete, }, - new() { Extractors = new() diff --git a/CompatBot/EventHandlers/LogParsing/LogParser.RegexPatterns.cs b/CompatBot/EventHandlers/LogParsing/LogParser.RegexPatterns.cs index fd791eb9..260390ab 100644 --- a/CompatBot/EventHandlers/LogParsing/LogParser.RegexPatterns.cs +++ b/CompatBot/EventHandlers/LogParsing/LogParser.RegexPatterns.cs @@ -208,6 +208,14 @@ internal partial class LogParser private static partial Regex AudioTimeStretching(); [GeneratedRegex("Pad: (?[^\r\n]*?)\r?$", DefaultOptions)] private static partial Regex GamepadType(); + + [GeneratedRegex("Start Paused: (?[^\r\n]*?)\r?$", DefaultOptions)] + private static partial Regex StartPausedSavestate(); + [GeneratedRegex("Suspend Emulation Savestate Mode: (?[^\r\n]*?)\r?$", DefaultOptions)] + private static partial Regex SuspendEmulationSavestate(); + [GeneratedRegex("Compatible Savestate Mode: (?[^\r\n]*?)\r?$", DefaultOptions)] + private static partial Regex CompatibleSavestate(); + [GeneratedRegex("Automatically start games after boot: (?[^\r\n]*?)\r?$", DefaultOptions)] private static partial Regex AutoStartAfterBoot(); [GeneratedRegex("Always start after boot: (?[^\r\n]*?)\r?$", DefaultOptions)] @@ -370,4 +378,6 @@ internal partial class LogParser DefaultOptions )] private static partial Regex SaveDataBeforeSegfault(); + [GeneratedRegex(@"Booting savestate from gamelist per (?.+)...\r?$", DefaultOptions)] + private static partial Regex BootingFromSavestate(); } \ No newline at end of file diff --git a/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.CurrentSettingsSections.cs b/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.CurrentSettingsSections.cs index 6176c3a9..fcfe54dd 100644 --- a/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.CurrentSettingsSections.cs +++ b/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.CurrentSettingsSections.cs @@ -227,7 +227,7 @@ internal static partial class LogParserResult { if (colA.lines?.Count > 0 && colB.lines?.Count > 0) { - var isCustomSettings = items["custom_config"] != null; + var isCustomSettings = items["custom_config"] is EnabledMark; var colAToRemove = colA.lines.Count(l => l.EndsWith("N/A")); var colBToRemove = colB.lines.Count(l => l.EndsWith("N/A")); var linesToRemove = Math.Min(colAToRemove, colBToRemove); diff --git a/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.GeneralNotesSection.cs b/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.GeneralNotesSection.cs index 0581a7af..d6afc789 100644 --- a/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.GeneralNotesSection.cs +++ b/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.GeneralNotesSection.cs @@ -420,11 +420,11 @@ internal static partial class LogParserResult discAsPkg |= items["ldr_game_serial"] is string ldrGameSerial && ldrGameSerial.StartsWith("NP", StringComparison.InvariantCultureIgnoreCase); } - discAsPkg |= category == "HG" && !(items["serial"]?.StartsWith("NP", StringComparison.InvariantCultureIgnoreCase) ?? false); + discAsPkg |= category is "HG" && !(items["serial"]?.StartsWith("NP", StringComparison.InvariantCultureIgnoreCase) ?? false); if (discInsideGame) notes.Add($"❌ Disc game inside `{items["ldr_disc"]}`"); if (discAsPkg) - notes.Add($"ℹ️ Disc game installed as a PKG "); + notes.Add("ℹ️ Disc game installed as a PKG "); if (!string.IsNullOrEmpty(items["native_ui_input"])) notes.Add("⚠️ Pad initialization problem detected; try disabling `Native UI`"); @@ -433,10 +433,13 @@ internal static partial class LogParserResult else if (items["audio_backend_init_error"] is string audioBackend) notes.Add($"⚠️ {audioBackend} initialization failed; make sure you have a working audio output device"); - if (!string.IsNullOrEmpty(items["fw_missing_msg"]) - || !string.IsNullOrEmpty(items["fw_missing_something"])) + if (items["fw_missing_msg"] is not {Length: >0} + || items["fw_missing_something"] is not {Length: >0}) notes.Add("❌ PS3 firmware is missing or corrupted"); + if (items["booting_savestate"] is EnabledMark) + notes.Add("ℹ️ Game was booted from a save state"); + if (multiItems["game_mod"] is { Length: >0 } mods) { var mod = mods[0]; diff --git a/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.WeirdSettingsSection.cs b/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.WeirdSettingsSection.cs index db0bc498..71dad30c 100644 --- a/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.WeirdSettingsSection.cs +++ b/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.WeirdSettingsSection.cs @@ -552,12 +552,22 @@ internal static partial class LogParserResult notes.Add($"⚠️ Please change `SPU Block Size` to `Safe/Mega`, currently `{spuBlockSize}` is unstable."); } - if (items["auto_start_on_boot"] == DisabledMark) - notes.Add("❓ `Automatically start games after boot` is disabled"); - else if (items["always_start_on_boot"] == DisabledMark) - notes.Add("❓ `Always start after boot` is disabled"); + if (items["booting_savestate"] is DisabledMark) + { + if (items["auto_start_on_boot"] is DisabledMark) + notes.Add("❓ `Automatically start games after boot` is disabled"); + else if (items["always_start_on_boot"] is DisabledMark) + notes.Add("❓ `Always start after boot` is disabled"); + } + else + { + if (items["start_paused_savestate"] is EnabledMark) + notes.Add("❓ `Pause emulation after loading savestates` is disabled"); + if (items["compatible_savestate"] is not EnabledMark) + notes.Add("ℹ️ If you have weird compatibility issues after boot, try enabling `SPU-Compatible Savestate Mode`"); + } - if (items["custom_config"] != null && notes.Any()) + if (items["custom_config"] is EnabledMark && notes.Count > 0) generalNotes.Add("⚠️ To change custom configuration, **Right-click on the game**, then `Configure`"); var notesContent = new StringBuilder(); diff --git a/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.cs b/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.cs index e8f81871..250004e3 100644 --- a/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.cs +++ b/CompatBot/Utils/ResultFormatters/LogParserResultFormatter.cs @@ -745,6 +745,9 @@ internal static partial class LogParserResult if (items["game_update_version"] is string gameUpVer && gameUpVer.StartsWith("0")) items["game_update_version"] = gameUpVer[1..]; + items["custom_config"] = items["custom_config"] is { Length: > 0 } ? EnabledMark : DisabledMark; + items["booting_savestate"] = items["booting_savestate"] is {Length: >0} ? EnabledMark : DisabledMark; + if (multiItems["fatal_error"] is UniqueList {Count: > 0} fatalErrors) multiItems["fatal_error"] = new(fatalErrors.Select(str => str.Contains("'tex00'") ? str.Split('\n', 2)[0] : str), fatalErrors.Comparer); diff --git a/CompatBot/Utils/ResultFormatters/TitlePatchFormatter.cs b/CompatBot/Utils/ResultFormatters/TitlePatchFormatter.cs index ba160b33..4e27934c 100644 --- a/CompatBot/Utils/ResultFormatters/TitlePatchFormatter.cs +++ b/CompatBot/Utils/ResultFormatters/TitlePatchFormatter.cs @@ -9,65 +9,104 @@ namespace CompatBot.Utils.ResultFormatters; internal static class TitlePatchFormatter { // thanks BCES00569 - public static async Task> AsEmbedAsync(this TitlePatch? patch, DiscordClient client, string productCode) + public static async Task> AsMessageAsync(this TitlePatch? patch, DiscordClient client, string productCode) { - var result = new List(); var pkgs = patch?.Tag?.Packages; var title = pkgs?.Select(p => p.ParamSfo?.Title).LastOrDefault(t => !string.IsNullOrEmpty(t)) ?? await ThumbnailProvider.GetTitleNameAsync(productCode, Config.Cts.Token).ConfigureAwait(false) ?? productCode; + var content = new StringBuilder(); var thumbnailUrl = await client.GetThumbnailUrlAsync(productCode).ConfigureAwait(false); - var embedBuilder = new DiscordEmbedBuilder + if (pkgs is {Length: >0}) { - Title = title, - Color = Config.Colors.DownloadLinks, - }.WithThumbnail(thumbnailUrl); - if (pkgs?.Length > 1) - { - var pages = pkgs.Length / EmbedPager.MaxFields + (pkgs.Length % EmbedPager.MaxFields == 0 ? 0 : 1); - if (pages > 1) - embedBuilder.Title = $"{title} [Part 1 of {pages}]".Trim(EmbedPager.MaxFieldTitleLength); - embedBuilder.Description = $""" - ℹ️ Total download size of all {pkgs.Length} packages is {pkgs.Sum(p => p.Size).AsStorageUnit()}. - ⏩ You can use tools such as [rusty-psn](https://github.com/RainbowCookie32/rusty-psn/releases/latest) or [PySN](https://github.com/AphelionWasTaken/PySN/releases/latest) for mass download of all updates. + content.AppendLine($"### {title}"); + if (pkgs.Length > 1) + content.AppendLine( + $""" + ℹ️ Total download size of all {pkgs.Length} packages is {pkgs.Sum(p => p.Size).AsStorageUnit()}. + ⏩ You can use tools such as [rusty-psn](https://github.com/RainbowCookie32/rusty-psn/releases/latest) or [PySN](https://github.com/AphelionWasTaken/PySN/releases/latest) for mass download of all updates. - ⚠️ You **must** install listed updates in order, starting with the first one. You **can not** skip intermediate versions. - """; - var i = 0; - do - { - var pkg = pkgs[i++]; - embedBuilder.AddField($"Update v{pkg.Version} ({pkg.Size.AsStorageUnit()})", $"[⏬ {GetLinkName(pkg.Url)}]({pkg.Url})"); - if (i % EmbedPager.MaxFields == 0) - { - result.Add(embedBuilder); - embedBuilder = new DiscordEmbedBuilder - { - Title = $"{title} [Part {i / EmbedPager.MaxFields + 1} of {pages}]".Trim(EmbedPager.MaxFieldTitleLength), - Color = Config.Colors.DownloadLinks, - }.WithThumbnail(thumbnailUrl); - } - } while (i < pkgs.Length); + ⚠️ You **must** install listed updates in order, starting with the first one. You **can not** skip intermediate versions. + """ + ).AppendLine(); + foreach (var pkg in pkgs) + content.AppendLine($"""[⏬ Update v`{pkg.Version}` ({pkg.Size.AsStorageUnit()})]({pkg.Url})"""); } - else if (pkgs?.Length == 1) + else if (pkgs is [var pkg]) { - embedBuilder.Title = $"{title} update v{pkgs[0].Version} ({pkgs[0].Size.AsStorageUnit()})"; - embedBuilder.Description = $"[⏬ {Path.GetFileName(GetLinkName(pkgs[0].Url))}]({pkgs[0].Url})"; + content.AppendLine( + $""" + ### {title} update v{pkg.Version} ({pkg.Size.AsStorageUnit()}) + [⏬ {Path.GetFileName(GetLinkName(pkg.Url))}]({pkg.Url}) + """ + ); } - else if (patch != null) - embedBuilder.Description = "No updates available"; else - embedBuilder.Description = "No update information available"; - if (!result.Any() || embedBuilder.Fields.Any()) - result.Add(embedBuilder); + content.AppendLine($"### {title}") + .AppendLine("No updates available"); if (patch?.OfflineCacheTimestamp is DateTime cacheTimestamp) - result[^1].WithFooter($"Offline cache, last updated {(DateTime.UtcNow - cacheTimestamp).AsTimeDeltaDescription()} ago"); + content.AppendLine() + .AppendLine($"-# Offline cache, last updated {(DateTime.UtcNow - cacheTimestamp).AsTimeDeltaDescription()} ago"); + + var result = new List(); + IReadOnlyList contentParts = Split(content); + foreach (var page in contentParts) + { + IReadOnlyList msgBody; + if (thumbnailUrl is { Length: > 0 }) + msgBody = + [ + new DiscordSectionComponent([page], new DiscordThumbnailComponent(thumbnailUrl)) + ]; + else + msgBody = [page]; + var msgBuilder = new DiscordMessageBuilder() + .EnableV2Components() + .AddContainerComponent( + new( + msgBody, + color: Config.Colors.DownloadLinks + ) + ); + result.Add(msgBuilder); + } + return result; + } + + private static List Split(StringBuilder content) + { + var lines = content.ToString().TrimEnd().Split(Environment.NewLine); + var isMultiPage = content.Length > 4001; + var title = lines[0]; + var result = new List(); + content.Clear(); + content.Append(title); + if (isMultiPage) + content.Append(" [Page 1 of 2]"); + foreach (var l in lines.Skip(1)) + { + check: + if (content.Length + l.Length + 1 <= 4000) + content.Append('\n').Append(l); + else if (content.Length is 0) + content.Append(l.Trim(4000)); + else + { + result.Add(new(content.ToString())); + content.Clear(); + content.Append(title).Append(" [Page 2 of 2]"); + goto check; + } + } + result.Add(new(content.ToString())); return result; } private static string GetLinkName(string link) { var fname = Path.GetFileName(link); + if (fname.EndsWith("-PE.pkg")) + fname = fname[..^7] + fname[^4..]; try { var match = PsnScraper.ContentIdMatcher().Match(fname); diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 0b6599a5..ea9d64d6 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -9,12 +9,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive