Files
archived-discord-bot/CompatBot/EventHandlers/IsTheGamePlayableHandler.cs
2025-05-11 01:17:55 +05:00

136 lines
6.0 KiB
C#

using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using CompatApiClient;
using CompatApiClient.POCOs;
using CompatApiClient.Utils;
using CompatBot.Commands;
using CompatBot.Database.Providers;
using CompatBot.Utils.ResultFormatters;
namespace CompatBot.EventHandlers;
internal static partial class IsTheGamePlayableHandler
{
[GeneratedRegex(
@"(\b((is|does|can I play|any(one|1) tr(y|ied)|how's|(wonder(ing)?|me|knows?) if)\s+)(?<game_title_1>.+?)\s+((now|currently|at all|possibly|fully|(on (this|the) )emu(lator))\s+)?((it?s )?playable|work(s|ing)?|runs?|doing))\b"+
@"|(\b(((can I|possible to) (play|run)|any(one|1) tr(y|ied)|compat[ai]bility (with|of))\s+)(?<game_title_2>.+?)(\s+((now|currently|at all|possibly|fully)\s+)?((it?s )?playable|work(s|ing)?|on (it|this))\b|\?|$))"+
@"|(^(?<game_title_3>.+?)\s+((is )?(playable|work(s|ing)?))\?)",
RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.ExplicitCapture
)]
private static partial Regex GameNameStatusMention();
private static readonly ConcurrentDictionary<ulong, DateTime> CooldownBuckets = new();
private static readonly TimeSpan CooldownThreshold = TimeSpan.FromSeconds(5);
private static readonly Client Client = new();
public static async Task OnMessageCreated(DiscordClient c, MessageCreatedEventArgs args)
{
if (DefaultHandlerFilter.IsFluff(args.Message) || args.Channel is null)
return;
#if !DEBUG
if (!(args.Channel.Id == Config.BotGeneralChannelId
|| args.Channel.IsHelpChannel()))
return;
if (CooldownBuckets.TryGetValue(args.Channel.Id, out var lastCheck)
&& DateTime.UtcNow - lastCheck < CooldownThreshold)
return;
if (await args.Author.IsSmartlistedAsync(c, args.Guild).ConfigureAwait(false))
return;
#endif
var matches = GameNameStatusMention().Matches(args.Message.Content);
if (!matches.Any())
return;
try
{
var gameTitle = matches.Select(m => m.Groups["game_title_1"].Value)
.Concat(matches.Select(m => m.Groups["game_title_2"].Value))
.Concat(matches.Select(m => m.Groups["game_title_3"].Value))
.FirstOrDefault(t => !string.IsNullOrEmpty(t));
if (gameTitle is null or { Length: < 2 })
return;
gameTitle = CompatList.FixGameTitleSearch(gameTitle);
if (gameTitle.Length < 4)
return;
if (ProductCodeLookup.Pattern().IsMatch(args.Message.Content))
return;
var (_, info) = await LookupGameAsync(args.Channel, args.Message, gameTitle).ConfigureAwait(false);
if (string.IsNullOrEmpty(info?.Status))
return;
gameTitle = info.Title?.StripMarks();
if (string.IsNullOrEmpty(gameTitle))
return;
var botSpamChannel = await c.GetChannelAsync(Config.BotSpamId).ConfigureAwait(false);
var status = info.Status.ToLowerInvariant();
string msg;
if (status == "unknown")
msg = $"{args.Message.Author.Mention} {gameTitle} status is {status}";
else
{
if (status != "playable")
status += " (not playable)";
msg = $"{args.Message.Author.Mention} {gameTitle} is {status}";
if (!string.IsNullOrEmpty(info.Date))
msg += $" since {info.ToUpdated()}";
}
msg += $"\nfor more results please use [compatibility list](<https://rpcs3.net/compatibility>) or `/compatibility title:{gameTitle.Sanitize()}` command in {botSpamChannel.Mention}";
await args.Channel.SendMessageAsync(msg).ConfigureAwait(false);
CooldownBuckets[args.Channel.Id] = DateTime.UtcNow;
}
catch (Exception e)
{
Config.Log.Error(e);
}
}
public static async Task<(string? productCode, TitleInfo? info)> LookupGameAsync(DiscordChannel channel, DiscordMessage message, string gameTitle)
{
var lastBotMessages = await channel.GetMessagesBeforeAsync(message.Id, 20, DateTime.UtcNow.AddSeconds(-30)).ConfigureAwait(false);
foreach (var msg in lastBotMessages)
if (BotReactionsHandler.NeedToSilence(msg).needToChill)
return (null, null);
try
{
var requestBuilder = RequestBuilder.Start().SetSearch(gameTitle);
var searchCompatListTask = Client.GetCompatResultAsync(requestBuilder, Config.Cts.Token);
var localList = await CompatList.GetLocalCompatResultAsync(requestBuilder).ConfigureAwait(false);
var status = await searchCompatListTask.ConfigureAwait(false);
status = status?.Append(localList);
if (status is null
|| status.ReturnCode is not CompatApiStatus.Success and not CompatApiStatus.NoExactMatch
|| status.Results.Count is 0)
return (null, null);
var sortedList = status.GetSortedList();
var bestMatch = sortedList.First();
var listWithStatus = sortedList
.TakeWhile(i => Math.Abs(i.score - bestMatch.score) < double.Epsilon)
.Where(i => !string.IsNullOrEmpty(i.info.Status) && i.info.Status != "Unknown")
.ToList();
if (listWithStatus.Count > 0)
bestMatch = listWithStatus.First();
var (code, info, score) = bestMatch;
Config.Log.Debug($"Looked up \"{gameTitle}\", got \"{info.Title}\" with score {score}");
if (score < Config.GameTitleMatchThreshold)
return (null, null);
if (!string.IsNullOrEmpty(info.Title))
StatsStorage.IncGameStat(info.Title);
return (code, info);
}
catch (Exception e)
{
Config.Log.Warn(e);
return (null, null);
}
}
}