diff --git a/NadekoBot/Commands/ClashOfClans.cs b/NadekoBot/Commands/ClashOfClans.cs new file mode 100644 index 00000000..c0d65122 --- /dev/null +++ b/NadekoBot/Commands/ClashOfClans.cs @@ -0,0 +1,338 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord.Commands; +using System.Collections.Concurrent; +using Discord; +using System.Threading; + +namespace NadekoBot.Commands { + class ClashOfClans : DiscordCommand { + + private static string prefix = ","; + + public static ConcurrentDictionary> ClashWars { get; } = new ConcurrentDictionary>(); + + private object writeLock { get; } = new object(); + + public ClashOfClans() : base() { + + } + + public override Func DoFunc() => async e => { + if (!e.User.ServerPermissions.ManageChannels) + return; + List wars; + if (!ClashWars.TryGetValue(e.Server.Id, out wars)) { + wars = new List(); + if (!ClashWars.TryAdd(e.Server.Id, wars)) + return; + } + string enemyClan = e.GetArg("enemy_clan"); + if (string.IsNullOrWhiteSpace(enemyClan)) { + return; + } + int size; + if (!int.TryParse(e.GetArg("size"), out size) || size < 10 || size > 50 || size % 5 != 0) { + await e.Channel.SendMessage("πŸ’’πŸ”° Not a Valid war size"); + return; + } + var cw = new ClashWar(enemyClan, size, e); + cw.Start(); + wars.Add(cw); + cw.OnUserTimeExpired += async (u) => { + await e.Channel.SendMessage($"β—πŸ”°**Claim from {u.Mention} for a war against {cw.ShortPrint()} has expired.**"); + }; + cw.OnWarEnded += async () => { + await e.Channel.SendMessage($"β—πŸ”°**War against {cw.ShortPrint()} ended.**"); + }; + await e.Channel.SendMessage($"β—πŸ”°**STARTED** `{size} v {size}` **CLAN WAR AGAINST** `{enemyClan}`"); + //war with the index X started. + }; + + public override void Init(CommandGroupBuilder cgb) { + cgb.CreateCommand(prefix + "startwar") + .Alias(prefix + "sw") + .Description($"Starts a new war by specifying a size (>10 and multiple of 5) and enemy clan name. War ends in 23 hours. You need manage channels permission to use this.\n**Usage**:{prefix}sw 15 The Enemy Clan") + .Parameter("size") + .Parameter("enemy_clan", ParameterType.Unparsed) + .Do(DoFunc()); + + cgb.CreateCommand(prefix + "listwar") + .Alias(prefix + "lw") + .Description($"Shows the active war claims by a number. Shows all wars in a short way if no number is specified.\n**Usage**: {prefix}lw [war_number] or {prefix}lw") + .Parameter("number", ParameterType.Optional) + .Do(async e => { + // if number is null, print all wars in a short way + if (string.IsNullOrWhiteSpace(e.GetArg("number"))) { + //check if there are any wars + List wars = null; + ClashWars.TryGetValue(e.Server.Id, out wars); + if (wars == null || wars.Count == 0) { + await e.Channel.SendMessage("πŸ”° **No active wars.**"); + return; + } + + var sb = new StringBuilder(); + sb.AppendLine("πŸ”° **LIST OF ACTIVE WARS**"); + sb.AppendLine("**-------------------------**"); + for (int i = 0; i < wars.Count; i++) { + sb.AppendLine($"**#{i + 1}.** `Enemy:` **{wars[i].EnemyClan}**"); + sb.AppendLine($"\t\t`Size:` **{wars[i].Size} v {wars[i].Size}**"); + sb.AppendLine("**-------------------------**"); + } + await e.Channel.SendMessage(sb.ToString()); + return; + } + //if number is not null, print the war needed + var warsInfo = GetInfo(e); + if (warsInfo == null) { + await e.Channel.SendMessage("πŸ’’πŸ”° **That war does not exist.**"); + return; + } + await e.Channel.SendMessage(warsInfo.Item1[warsInfo.Item2].ToString()); + }); + + cgb.CreateCommand(prefix + "claim") + .Alias(prefix + "call") + .Alias(prefix + "c") + .Description($"Claims a certain base from a certain war.\n**Usage**: {prefix}call [war_number] [base_number]") + .Parameter("number") + .Parameter("baseNumber") + .Do(async e => { + var warsInfo = GetInfo(e); + if (warsInfo == null || warsInfo.Item1.Count == 0) { + await e.Channel.SendMessage("πŸ’’πŸ”° **That war does not exist.**"); + return; + } + int baseNum; + if (!int.TryParse(e.GetArg("baseNumber"), out baseNum)) { + await e.Channel.SendMessage("πŸ’’πŸ”° **Invalid base number.**"); + return; + } + try { + var war = warsInfo.Item1[warsInfo.Item2]; + await war.Call(e.User, baseNum - 1); + await e.Channel.SendMessage($"πŸ”°{e.User.Mention} claimed a base #{baseNum} for a war against {war.ShortPrint()}"); + } + catch (Exception ex) { + await e.Channel.SendMessage($"πŸ’’πŸ”° {ex.Message}"); + } + }); + + cgb.CreateCommand(prefix + "cf") + .Alias(prefix + "claimfinish") + .Description($"Finish your claim if you destroyed a base.\n**Usage**: {prefix}cf [war_number]") + .Parameter("number", ParameterType.Required) + .Do(async e => { + var warInfo = GetInfo(e); + if (warInfo == null || warInfo.Item1.Count == 0) { + await e.Channel.SendMessage("πŸ’’πŸ”° **That war does not exist.**"); + return; + } + var war = warInfo.Item1[warInfo.Item2]; + try { + var baseNum = war.FinishClaim(e.User); + await e.Channel.SendMessage($"β—πŸ”°{e.User.Mention} **DESTROYED** a base #{baseNum} in a war against {war.ShortPrint()}"); + } + catch (Exception ex) { + await e.Channel.SendMessage($"πŸ’’πŸ”° {ex.Message}"); + } + }); + + cgb.CreateCommand(prefix + "unclaim") + .Alias(prefix + "uncall") + .Alias(prefix + "uc") + .Description($"Removes your claim from a certain war.\n**Usage**: {prefix}uc [war_number] [base_number]") + .Parameter("number", ParameterType.Required) + .Do(async e => { + var warsInfo = GetInfo(e); + if (warsInfo == null || warsInfo.Item1.Count == 0) { + await e.Channel.SendMessage("πŸ’’πŸ”° **That war does not exist.**"); + return; + } + try { + var war = warsInfo.Item1[warsInfo.Item2]; + int baseNumber = war.Uncall(e.User); + await e.Channel.SendMessage($"πŸ”° {e.User.Mention} has **UNCLAIMED** a base #{baseNumber + 1} from a war against {war.ShortPrint()}"); + } + catch (Exception ex) { + await e.Channel.SendMessage($"πŸ’’πŸ”° {ex.Message}"); + } + }); + + cgb.CreateCommand(prefix + "endwar") + .Alias(prefix + "ew") + .Description($"Ends the war with a given index.\n**Usage**:{prefix}ew [war_number]") + .Parameter("number") + .Do(async e => { + if (!e.User.ServerPermissions.ManageChannels) + return; + var warsInfo = GetInfo(e); + if (warsInfo == null) { + await e.Channel.SendMessage("πŸ’’πŸ”° That war does not exist."); + return; + } + warsInfo.Item1[warsInfo.Item2].End(); + + int size = warsInfo.Item1[warsInfo.Item2].Size; + warsInfo.Item1.RemoveAt(warsInfo.Item2); + }); + } + + private Tuple, int> GetInfo(CommandEventArgs e) { + //check if there are any wars + List wars = null; + ClashWars.TryGetValue(e.Server.Id, out wars); + if (wars == null || wars.Count == 0) { + return null; + } + // get the number of the war + int num; + if (string.IsNullOrWhiteSpace(e.GetArg("number"))) + num = 0; + else if (!int.TryParse(e.GetArg("number"), out num) || num > wars.Count) { + return null; + } + num -= 1; + //get the actual war + return new Tuple, int>(wars, num); + } + } + + internal class Caller { + private User _user; + + public User CallUser + { + get { return _user; } + set { _user = value; } + } + + private DateTime timeAdded; + + public DateTime TimeAdded + { + get { return timeAdded; } + set { timeAdded = value; } + } + + public bool BaseDestroyed { get; internal set; } + } + + internal class ClashWar { + + public static TimeSpan callExpire => new TimeSpan(2, 0, 0); + + private CommandEventArgs e; + private string enemyClan; + public string EnemyClan => enemyClan; + private int size; + public int Size => size; + private Caller[] bases; + private CancellationTokenSource[] baseCancelTokens; + private CancellationTokenSource endTokenSource = new CancellationTokenSource(); + public Action OnUserTimeExpired { get; set; } = null; + public Action OnWarEnded { get; set; } = null; + + public ClashWar(string enemyClan, int size, CommandEventArgs e) { + this.enemyClan = enemyClan; + this.size = size; + this.bases = new Caller[size]; + this.baseCancelTokens = new CancellationTokenSource[size]; + } + + internal void End() { + if (!endTokenSource.Token.IsCancellationRequested) { + endTokenSource.Cancel(); + if (OnWarEnded != null) + OnWarEnded(); + } + } + + internal async Task Call(User u, int baseNumber) { + if (baseNumber < 0 || baseNumber >= bases.Length) + throw new ArgumentException("Invalid base number"); + if (bases[baseNumber] != null) + throw new ArgumentException("That base is already claimed."); + for (int i = 0; i < bases.Length; i++) { + if (bases[i]?.BaseDestroyed == false && bases[i]?.CallUser == u) + throw new ArgumentException($"πŸ’’ {u.Mention} You already claimed a base #{i + 1}. You can't claim a new one."); + } + + bases[baseNumber] = new Caller { CallUser = u, TimeAdded = DateTime.Now, BaseDestroyed = false }; + } + + internal async void Start() { + try { + Task.Run(async () => await ClearArray()); + await Task.Delay(new TimeSpan(23, 0, 0), endTokenSource.Token); + } + catch (Exception) { } + finally { + End(); + } + } + internal int Uncall(User user) { + for (int i = 0; i < bases.Length; i++) { + if (bases[i]?.CallUser == user) { + bases[i] = null; + return i; + } + } + throw new InvalidOperationException("You are not participating in that war."); + } + + private async Task ClearArray() { + while (!endTokenSource.IsCancellationRequested) { + await Task.Delay(5000); + for (int i = 0; i < bases.Length; i++) { + if (bases[i] == null) continue; + if (!bases[i].BaseDestroyed && DateTime.Now - bases[i].TimeAdded >= callExpire) { + Console.WriteLine($"Removing user {bases[i].CallUser.Name}"); + if (OnUserTimeExpired != null) + OnUserTimeExpired(bases[i].CallUser); + bases[i] = null; + } + } + } + Console.WriteLine("Out of clear array"); + } + + public string ShortPrint() => + $"`{enemyClan}` ({size} v {size})"; + + public override string ToString() { + var sb = new StringBuilder(); + sb.AppendLine($"πŸ”°**WAR AGAINST `{enemyClan}` ({size} v {size}) INFO:**"); + for (int i = 0; i < bases.Length; i++) { + if (bases[i] == null) { + sb.AppendLine($"`{i + 1}.` ❌*unclaimed*"); + } + else { + if (bases[i].BaseDestroyed) { + sb.AppendLine($"`{i + 1}.` βœ… `{bases[i].CallUser.Name}` ⭐ ⭐ ⭐"); + } + else { + var left = callExpire - (DateTime.Now - bases[i].TimeAdded); + sb.AppendLine($"`{i + 1}.` βœ… `{bases[i].CallUser.Name}` {left.Hours}h {left.Minutes}m {left.Seconds}s left"); + } + } + + } + return sb.ToString(); + } + + internal int FinishClaim(User user) { + for (int i = 0; i < bases.Length; i++) { + if (bases[i]?.BaseDestroyed == false && bases[i]?.CallUser == user) { + bases[i].BaseDestroyed = true; + return i; + } + } + throw new InvalidOperationException($"{user.Mention} You are either not participating in that war, or you already destroyed a base."); + } + } +} diff --git a/NadekoBot/Commands/PollCommand.cs b/NadekoBot/Commands/PollCommand.cs index adcdbc3e..3bbf062f 100644 --- a/NadekoBot/Commands/PollCommand.cs +++ b/NadekoBot/Commands/PollCommand.cs @@ -74,7 +74,7 @@ namespace NadekoBot.Modules { foreach (var answ in answers) { msgToSend += $"`{num++}.` **{answ}**\n"; } - msgToSend += "\n**Private Message me with the corresponding number of the answer.\n @everyone**"; + msgToSend += "\n**Private Message me with the corresponding number of the answer.**"; await e.Channel.SendMessage(msgToSend); } diff --git a/NadekoBot/Modules/Administration.cs b/NadekoBot/Modules/Administration.cs index 873f11a4..9328cf6b 100644 --- a/NadekoBot/Modules/Administration.cs +++ b/NadekoBot/Modules/Administration.cs @@ -600,9 +600,9 @@ namespace NadekoBot.Modules { await Task.Run(async () => { var rows = Classes.DBHandler.Instance.GetAllRows(); var donatorsOrdered = rows.OrderByDescending(d => d.Amount); - string str = $"`Total number of people who donated is {donatorsOrdered.Count()}`\n"; + string str = $"**Thanks to the people listed below for making this project happen!**\n"; - await e.Channel.SendMessage(str + string.Join(", ", donatorsOrdered.Select(d => d.UserName))); + await e.Channel.SendMessage(str + string.Join("⭐", donatorsOrdered.Select(d => d.UserName))); }); }); diff --git a/NadekoBot/Modules/Games.cs b/NadekoBot/Modules/Games.cs index 59758838..e355e60d 100644 --- a/NadekoBot/Modules/Games.cs +++ b/NadekoBot/Modules/Games.cs @@ -18,6 +18,7 @@ namespace NadekoBot.Modules commands.Add(new Trivia()); commands.Add(new SpeedTyping()); commands.Add(new PollCommand()); + commands.Add(new ClashOfClans()); _8BallAnswers = JArray.Parse(File.ReadAllText("data/8ball.json")).Select(t => t.ToString()).ToArray(); } diff --git a/NadekoBot/Modules/Music.cs b/NadekoBot/Modules/Music.cs index e73ccdc4..04dd2c2f 100644 --- a/NadekoBot/Modules/Music.cs +++ b/NadekoBot/Modules/Music.cs @@ -195,7 +195,7 @@ namespace NadekoBot.Modules { } var ids = await SearchHelper.GetVideoIDs(await SearchHelper.GetPlaylistIdByKeyword(e.GetArg("playlist"))); //todo TEMPORARY SOLUTION, USE RESOLVE QUEUE IN THE FUTURE - var msg = await e.Send($"🎡 Attempting to queue **{ids.Count}** songs".SnPl(ids.Count)); + var msg = await e.Send($"🎡 `Attempting to queue **{ids.Count}** songs".SnPl(ids.Count)+"...`"); foreach (var id in ids) { Task.Run(async () => await QueueSong(e, id, true)).ConfigureAwait(false); await Task.Delay(150); @@ -321,7 +321,7 @@ namespace NadekoBot.Modules { sr.OnBuffering += async () => { msg = await e.Send($"🎡`Buffering...`{sr.FullPrettyName}"); }; - sr.Resolve(); + await sr.Resolve(); } catch (Exception ex) { Console.WriteLine(); await e.Send($"πŸ’’ {ex.Message}"); @@ -365,7 +365,7 @@ namespace NadekoBot.Modules { C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 */ try { - var m = Regex.Match(file, "(?^[^#].*)"); + var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); var res = m.Groups["url"]?.ToString(); return res?.Trim(); } diff --git a/NadekoBot/NadekoBot.csproj b/NadekoBot/NadekoBot.csproj index 289fc6d2..b23cc454 100644 --- a/NadekoBot/NadekoBot.csproj +++ b/NadekoBot/NadekoBot.csproj @@ -145,6 +145,7 @@ +