diff --git a/SysBot.Pokemon.Discord/Commands/Bots/RaidModule.cs b/SysBot.Pokemon.Discord/Commands/Bots/RaidModule.cs index 3de5dea..046cd9b 100644 --- a/SysBot.Pokemon.Discord/Commands/Bots/RaidModule.cs +++ b/SysBot.Pokemon.Discord/Commands/Bots/RaidModule.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using static SysBot.Pokemon.RotatingRaidSettingsSV; @@ -20,7 +21,7 @@ namespace SysBot.Pokemon.Discord.Commands.Bots { [Summary("Generates and queues various silly trade additions")] - public class RaidModule : ModuleBase where T : PKM, new() + public partial class RaidModule : ModuleBase where T : PKM, new() { private readonly PokeRaidHub Hub = SysCord.Runner.Hub; private static DiscordSocketClient _client => SysCord.Instance.GetClient(); @@ -428,7 +429,9 @@ public async Task AddNewRaidParamNext( [Summary("Seed")] string seed, [Summary("Difficulty Level (1-7)")] int level, [Summary("Story Progress Level")] int storyProgressLevel = 6, - [Summary("Species Name (Optional)")] string? speciesName = null) + [Summary("Species Name or User Mention (Optional)")] string? speciesNameOrUserMention = null, + [Summary("User Mention 2 (Optional)")] SocketGuildUser? user2 = null, + [Summary("User Mention 3 (Optional)")] SocketGuildUser? user3 = null) { var botPrefix = SysCord.Runner.Config.Discord.CommandPrefix; if (Hub.Config.RotatingRaidSV.RaidSettings.DisableRequests) @@ -443,6 +446,35 @@ public async Task AddNewRaidParamNext( return; } + // Check if the first parameter after story progress level is a user mention + bool isUserMention = speciesNameOrUserMention != null && MyRegex1().IsMatch(speciesNameOrUserMention); + SocketGuildUser? user1 = null; + string? speciesName = null; + + if (isUserMention) + { + // Extract the user ID from the mention and retrieve the user + var userId2 = ulong.Parse(Regex.Match(speciesNameOrUserMention, @"\d+").Value); + user1 = Context.Guild.GetUser(userId2); + } + else + { + speciesName = speciesNameOrUserMention; + } + + // Check if private raids are enabled + if (!Hub.Config.RotatingRaidSV.RaidSettings.PrivateRaidsEnabled && (user1 != null || user2 != null || user3 != null)) + { + await ReplyAsync("Private raids are currently disabled by the host.").ConfigureAwait(false); + return; + } + // Check if the number of user mentions exceeds the limit + int mentionCount = (user1 != null ? 1 : 0) + (user2 != null ? 1 : 0) + (user3 != null ? 1 : 0); + if (mentionCount > 3) + { + await ReplyAsync("You can only mention up to 3 users for a private raid.").ConfigureAwait(false); + return; + } var userId = Context.User.Id; if (Hub.Config.RotatingRaidSV.ActiveRaids.Any(r => r.RequestedByUserID == userId)) { @@ -453,7 +485,7 @@ public async Task AddNewRaidParamNext( var userRoles = (Context.User as SocketGuildUser)?.Roles.Select(r => r.Id) ?? new List(); if (!Hub.Config.RotatingRaidSV.RaidSettings.BypassLimitRequests.ContainsKey(userId) && - !userRoles.Any(roleId => Hub.Config.RotatingRaidSV.RaidSettings.BypassLimitRequests.ContainsKey(roleId))) + !userRoles.Any(Hub.Config.RotatingRaidSV.RaidSettings.BypassLimitRequests.ContainsKey)) { if (!userRequestManager.CanRequest(userId, Hub.Config.RotatingRaidSV.RaidSettings.LimitRequests, Hub.Config.RotatingRaidSV.RaidSettings.LimitRequestsTime, out var remainingCooldown)) { @@ -505,12 +537,12 @@ public async Task AddNewRaidParamNext( int raidDeliveryGroupID = -1; - if (!string.IsNullOrEmpty(speciesName) && SpeciesToGroupIDMap.TryGetValue(speciesName, out var groupIDAndIndices)) + if (isEvent && SpeciesToGroupIDMap.TryGetValue(speciesName, out var groupIDAndIndices)) { var firstRaidGroupID = groupIDAndIndices.First().GroupID; raidDeliveryGroupID = firstRaidGroupID; } - else if (!string.IsNullOrEmpty(speciesName)) + else if (isEvent) { await ReplyAsync("Species name not recognized or not associated with an active event. Please check the name and try again."); return; @@ -566,6 +598,7 @@ public async Task AddNewRaidParamNext( Title = $"{Context.User.Username}'s Requested Raid{(isEvent ? $" ({speciesName} Event Raid)" : "")}", RaidUpNext = false, User = Context.User, + MentionedUsers = new List { user1, user2, user3 }.Where(u => u != null).ToList(), }; // Check if Species is Ditto and set PartyPK to Showdown template @@ -608,10 +641,26 @@ public async Task AddNewRaidParamNext( var replyMsg = $"{Context.User.Mention}, added your raid to the queue! I'll DM you when it's about to start."; await ReplyAsync(replyMsg, embed: raidEmbed).ConfigureAwait(false); + // Notify the mentioned users + var mentionedUsers = new List(); + if (user1 != null) mentionedUsers.Add(user1); + if (user2 != null) mentionedUsers.Add(user2); + if (user3 != null) mentionedUsers.Add(user3); + + foreach (var user in mentionedUsers) + { + try + { + await user.SendMessageAsync($"{Context.User.Username} invited you to a private raid! I'll DM you the code when it's about to start.", false, raidEmbed).ConfigureAwait(false); + } + catch + { + await ReplyAsync($"Failed to send DM to {user.Mention}. Please make sure their DMs are open.").ConfigureAwait(false); + } + } try { - var user = Context.User as SocketGuildUser; - if (user != null) + if (Context.User is SocketGuildUser user) { await user.SendMessageAsync($"Here's your raid information:\n{queuePositionMessage}\nYour request command: `{newparam.RequestCommand}`", false, raidEmbed).ConfigureAwait(false); } @@ -1080,5 +1129,8 @@ public async Task GetRaidHelpListAsync() _ => EntityConverter.ConvertToType(dl.Data, typeof(T), out _) as T, }; } + + [GeneratedRegex(@"^<@!?\d+>$")] + private static partial Regex MyRegex1(); } } \ No newline at end of file diff --git a/SysBot.Pokemon/SV/BotRaid/RotatingRaidBotSV.cs b/SysBot.Pokemon/SV/BotRaid/RotatingRaidBotSV.cs index cd7c9c8..486235e 100644 --- a/SysBot.Pokemon/SV/BotRaid/RotatingRaidBotSV.cs +++ b/SysBot.Pokemon/SV/BotRaid/RotatingRaidBotSV.cs @@ -25,7 +25,7 @@ public class RotatingRaidBotSV : PokeRoutineExecutor9SV private readonly PokeRaidHub Hub; private readonly RotatingRaidSettingsSV Settings; private RemoteControlAccessList RaiderBanList => Settings.RaiderBanList; - public static Dictionary> SpeciesToGroupIDMap = new(); + public static Dictionary> SpeciesToGroupIDMap = []; public RotatingRaidBotSV(PokeBotState cfg, PokeRaidHub hub) : base(cfg) { @@ -62,7 +62,7 @@ public class PlayerInfo private readonly ulong[] TeraNIDOffsets = new ulong[3]; private string TeraRaidCode { get; set; } = string.Empty; private string BaseDescription = string.Empty; - private readonly Dictionary RaidTracker = new(); + private readonly Dictionary RaidTracker = []; private SAV9SV HostSAV = new(); private DateTime StartTime = DateTime.Now; public static RaidContainer? container; @@ -371,22 +371,30 @@ private async Task InnerLoop(CancellationToken token) if (Settings.ActiveRaids[RotationCount].AddedByRACommand) { var user = Settings.ActiveRaids[RotationCount].User; + var mentionedUsers = Settings.ActiveRaids[RotationCount].MentionedUsers; // Determine if the raid is a "Free For All" bool isFreeForAll = !Settings.ActiveRaids[RotationCount].IsCoded || EmptyRaid >= Settings.LobbyOptions.EmptyRaidLimit; - if (user != null && !isFreeForAll) + if (!isFreeForAll) { try { // Only get and send the raid code if it's not a "Free For All" var code = await GetRaidCode(token).ConfigureAwait(false); - await user.SendMessageAsync($"Your Raid Code is **{code}**").ConfigureAwait(false); + if (user != null) + { + await user.SendMessageAsync($"Your Raid Code is **{code}**").ConfigureAwait(false); + } + foreach (var mentionedUser in mentionedUsers) + { + await mentionedUser.SendMessageAsync($"The Raid Code for the private raid you were invited to by {user?.Username ?? "the host"} is **{code}**").ConfigureAwait(false); + } } catch (Discord.Net.HttpException ex) { // Handle exception (e.g., log the error or send a message to a logging channel) - Log($"Failed to send DM to {user.Username}. They might have DMs turned off. Exception: {ex.Message}"); + Log($"Failed to send DM to the user or mentioned users. They might have DMs turned off. Exception: {ex.Message}"); } } } @@ -1509,21 +1517,30 @@ private async Task PrepareForRaid(CancellationToken token) if (Settings.ActiveRaids[RotationCount].AddedByRACommand) { var user = Settings.ActiveRaids[RotationCount].User; + var mentionedUsers = Settings.ActiveRaids[RotationCount].MentionedUsers; // Determine if the raid is a "Free For All" bool isFreeForAll = !Settings.ActiveRaids[RotationCount].IsCoded || EmptyRaid >= Settings.LobbyOptions.EmptyRaidLimit; - if (user != null && !isFreeForAll) + if (!isFreeForAll) { try { // Only send the message if it's not a "Free For All" - await user.SendMessageAsync("Get Ready! Your raid is about to start!").ConfigureAwait(false); + if (user != null) + { + await user.SendMessageAsync("Get Ready! Your raid is being prepared now!").ConfigureAwait(false); + } + + foreach (var mentionedUser in mentionedUsers) + { + await mentionedUser.SendMessageAsync($"Get Ready! The raid you were invited to by {user?.Username ?? "the host"} is about to start!").ConfigureAwait(false); + } } catch (Discord.Net.HttpException ex) { // Handle exception (e.g., log the error or send a message to a logging channel) - Log($"Failed to send DM to {user.Username}. They might have DMs turned off. Exception: {ex.Message}"); + Log($"Failed to send DM to the user or mentioned users. They might have DMs turned off. Exception: {ex.Message}"); } } } @@ -1748,7 +1765,7 @@ private async Task CheckIfTrainerBanned(RaidMyStatus trainer, ulong nid, i await EnqueueEmbed(null, "", false, false, false, false, token).ConfigureAwait(false); - List<(ulong, RaidMyStatus)> lobbyTrainers = new(); + List<(ulong, RaidMyStatus)> lobbyTrainers = []; var wait = TimeSpan.FromSeconds(Settings.RaidSettings.TimeToWait); var endTime = DateTime.Now + wait; bool full = false; @@ -2074,7 +2091,7 @@ private async Task EnqueueEmbed(List? names, string message, bool hatTri Settings.ActiveRaids[RotationCount].Title != "Mystery Shiny Raid" && code != "Free For All") { - await Task.Delay(Settings.EmbedToggles.RequestEmbedTime * 1000).ConfigureAwait(false); + await Task.Delay(Settings.EmbedToggles.RequestEmbedTime * 1000, token).ConfigureAwait(false); } // Description can only be up to 4096 characters. @@ -2088,11 +2105,11 @@ private async Task EnqueueEmbed(List? names, string message, bool hatTri if (disband) // Wait for trainer to load before disband await Task.Delay(5_000, token).ConfigureAwait(false); - byte[]? bytes = Array.Empty(); + byte[]? bytes = []; if (Settings.EmbedToggles.TakeScreenshot && !upnext) try { - bytes = await SwitchConnection.PixelPeek(token).ConfigureAwait(false) ?? Array.Empty(); + bytes = await SwitchConnection.PixelPeek(token).ConfigureAwait(false) ?? []; } catch (Exception ex) { @@ -2395,42 +2412,6 @@ private async Task ConnectToOnline(PokeRaidHubConfig config, CancellationT return true; } - private async Task RecoverToOverworld(CancellationToken token) - { - if (await IsOnOverworld(OverworldOffset, token).ConfigureAwait(false)) - return true; - - Log("Attempting to recover to overworld."); - var attempts = 0; - while (!await IsOnOverworld(OverworldOffset, token).ConfigureAwait(false)) - { - attempts++; - if (attempts >= 30) - break; - - await Click(B, 1_300, token).ConfigureAwait(false); - if (await IsOnOverworld(OverworldOffset, token).ConfigureAwait(false)) - break; - - await Click(B, 2_000, token).ConfigureAwait(false); - if (await IsOnOverworld(OverworldOffset, token).ConfigureAwait(false)) - break; - - await Click(A, 1_300, token).ConfigureAwait(false); - if (await IsOnOverworld(OverworldOffset, token).ConfigureAwait(false)) - break; - } - - // We didn't make it for some reason. - if (!await IsOnOverworld(OverworldOffset, token).ConfigureAwait(false)) - { - Log("Failed to recover to overworld, rebooting the game."); - await ReOpenGame(Hub.Config, token).ConfigureAwait(false); - } - await Task.Delay(1_000, token).ConfigureAwait(false); - return true; - } - public async Task StartGameRaid(PokeRaidHubConfig config, CancellationToken token) { // First, check if the time rollback feature is enabled @@ -3005,8 +2986,8 @@ private async Task ReadBlueberryRaids(CancellationToken token) private static (List distGroupIDs, List mightGroupIDs) GetPossibleGroups(RaidContainer container) { - List distGroupIDs = new(); - List mightGroupIDs = new(); + List distGroupIDs = []; + List mightGroupIDs = []; if (container.DistTeraRaids != null) { @@ -3127,7 +3108,7 @@ private async Task ProcessAllRaids(CancellationToken token) { if (!SpeciesToGroupIDMap.ContainsKey(speciesKey)) { - SpeciesToGroupIDMap[speciesKey] = new List<(int GroupID, int Index, string DenIdentifier)> { (groupID, i, denIdentifier) }; + SpeciesToGroupIDMap[speciesKey] = [(groupID, i, denIdentifier)]; } else { @@ -3493,9 +3474,5 @@ private async Task SaveGame(PokeRaidHubConfig config, CancellationToken to await Click(B, 1_000, token).ConfigureAwait(false); return true; } - - public class RaidEmbedInfo - { - } } } \ No newline at end of file diff --git a/SysBot.Pokemon/SV/BotRaid/RotatingRaidSettingsSV.cs b/SysBot.Pokemon/SV/BotRaid/RotatingRaidSettingsSV.cs index 32b4919..37d05b1 100644 --- a/SysBot.Pokemon/SV/BotRaid/RotatingRaidSettingsSV.cs +++ b/SysBot.Pokemon/SV/BotRaid/RotatingRaidSettingsSV.cs @@ -82,6 +82,10 @@ public class RotatingRaidParameters [Browsable(false)] [System.Text.Json.Serialization.JsonIgnore] public SocketUser? User { get; set; } + + [Browsable(false)] + [System.Text.Json.Serialization.JsonIgnore] + public List MentionedUsers { get; set; } = []; } [Category(Hosting), TypeConverter(typeof(CategoryConverter))] @@ -119,6 +123,9 @@ public class RotatingRaidSettingsCategory [Category(Hosting), Description("When true, the bot will not allow user requested raids and will inform them that this setting is on.")] public bool DisableRequests { get; set; } = false; + [Category(Hosting), Description("When true, the bot will allow private raids.")] + public bool PrivateRaidsEnabled { get; set; } = true; + [Category(Hosting), Description("Limit the number of requests a user can issue. Set to 0 to disable.\nCommands: $lr ")] public int LimitRequests { get; set; } = 0;