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