Merge pull request #1006 from 13xforever/vnext

Update game updates result formatting with discord components v2
This commit is contained in:
Ilya
2025-08-18 23:44:24 +05:00
committed by GitHub
11 changed files with 145 additions and 77 deletions

View File

@@ -10,7 +10,7 @@
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="NLog" Version="6.0.2" />
<PackageReference Include="NLog" Version="6.0.3" />
</ItemGroup>
</Project>

View File

@@ -34,28 +34,32 @@ internal static partial class Psn
}
await ctx.DeferResponseAsync(ephemeral).ConfigureAwait(false);
List<DiscordEmbedBuilder> embeds;
List<DiscordMessageBuilder> 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")]

View File

@@ -44,9 +44,9 @@
<ItemGroup>
<PackageReference Include="Blurhash.ImageSharp" Version="4.0.0" />
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
<PackageReference Include="DSharpPlus" Version="5.0.0-nightly-02536" />
<PackageReference Include="DSharpPlus.Commands" Version="5.0.0-nightly-02536" />
<PackageReference Include="DSharpPlus.Interactivity" Version="5.0.0-nightly-02536" />
<PackageReference Include="DSharpPlus" Version="5.0.0-nightly-02541" />
<PackageReference Include="DSharpPlus.Commands" Version="5.0.0-nightly-02541" />
<PackageReference Include="DSharpPlus.Interactivity" Version="5.0.0-nightly-02541" />
<PackageReference Include="DSharpPlus.Natives.Zstd" Version="1.5.7.21" />
<PackageReference Include="Florence2" Version="25.7.59767" />
<PackageReference Include="Google.Apis.Drive.v3" Version="1.70.0.3856" />
@@ -69,8 +69,8 @@
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="Nerdbank.Streams" Version="2.12.90" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NLog" Version="6.0.2" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.2" />
<PackageReference Include="NLog" Version="6.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.3" />
<PackageReference Include="NReco.Text.AhoCorasickDoubleArrayTrie" Version="1.1.1" />
<PackageReference Include="SharpCompress" Version="0.40.0" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />

View File

@@ -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()

View File

@@ -208,6 +208,14 @@ internal partial class LogParser
private static partial Regex AudioTimeStretching();
[GeneratedRegex("Pad: (?<pad_handler>[^\r\n]*?)\r?$", DefaultOptions)]
private static partial Regex GamepadType();
[GeneratedRegex("Start Paused: (?<start_paused_savestate>[^\r\n]*?)\r?$", DefaultOptions)]
private static partial Regex StartPausedSavestate();
[GeneratedRegex("Suspend Emulation Savestate Mode: (?<suspend_emulation_savestate>[^\r\n]*?)\r?$", DefaultOptions)]
private static partial Regex SuspendEmulationSavestate();
[GeneratedRegex("Compatible Savestate Mode: (?<compatible_savestate>[^\r\n]*?)\r?$", DefaultOptions)]
private static partial Regex CompatibleSavestate();
[GeneratedRegex("Automatically start games after boot: (?<auto_start_on_boot>[^\r\n]*?)\r?$", DefaultOptions)]
private static partial Regex AutoStartAfterBoot();
[GeneratedRegex("Always start after boot: (?<always_start_on_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 (?<booting_savestate>.+)...\r?$", DefaultOptions)]
private static partial Regex BootingFromSavestate();
}

View File

@@ -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);

View File

@@ -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];

View File

@@ -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();

View File

@@ -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<string> {Count: > 0} fatalErrors)
multiItems["fatal_error"] = new(fatalErrors.Select(str => str.Contains("'tex00'") ? str.Split('\n', 2)[0] : str), fatalErrors.Comparer);

View File

@@ -9,65 +9,104 @@ namespace CompatBot.Utils.ResultFormatters;
internal static class TitlePatchFormatter
{
// thanks BCES00569
public static async Task<List<DiscordEmbedBuilder>> AsEmbedAsync(this TitlePatch? patch, DiscordClient client, string productCode)
public static async Task<List<DiscordMessageBuilder>> AsMessageAsync(this TitlePatch? patch, DiscordClient client, string productCode)
{
var result = new List<DiscordEmbedBuilder>();
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<DiscordMessageBuilder>();
IReadOnlyList<DiscordTextDisplayComponent> contentParts = Split(content);
foreach (var page in contentParts)
{
IReadOnlyList<DiscordComponent> 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<DiscordTextDisplayComponent> Split(StringBuilder content)
{
var lines = content.ToString().TrimEnd().Split(Environment.NewLine);
var isMultiPage = content.Length > 4001;
var title = lines[0];
var result = new List<DiscordTextDisplayComponent>();
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);

View File

@@ -9,12 +9,12 @@
<PackageReference Include="DuoVia.FuzzyStrings" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0">
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit.Analyzers" Version="4.9.2">
<PackageReference Include="NUnit.Analyzers" Version="4.10.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>