From cbf7e5aa18aca7c03614488f57b485284cbfd453 Mon Sep 17 00:00:00 2001 From: Pythonic-Rainbow Date: Mon, 7 Oct 2024 18:06:48 +0800 Subject: [PATCH] New branch for coc redesign --- Bot/Clash/ClanUtil.cs | 59 ----------- Bot/Clash/Coc.cs | 180 +++++++++++++++------------------ Bot/Discord/Dc.cs | 2 + Bot/Discord/MemberConverter.cs | 8 +- Bot/Program.cs | 19 +++- Bot/Sql/Account.cs | 18 ++++ Bot/Sql/Db.cs | 2 + 7 files changed, 122 insertions(+), 166 deletions(-) diff --git a/Bot/Clash/ClanUtil.cs b/Bot/Clash/ClanUtil.cs index 0463bde..d0ede9c 100644 --- a/Bot/Clash/ClanUtil.cs +++ b/Bot/Clash/ClanUtil.cs @@ -10,64 +10,5 @@ internal sealed class ClanUtil internal readonly Dictionary _existingMembers = []; internal readonly Dictionary _joiningMembers = []; internal readonly Dictionary _leavingMembers; - - private ClanUtil(Clan clan, Dictionary leavingMembers) - { - _clan = clan; - _leavingMembers = leavingMembers; - } - - internal ClanUtil() - { - _clan = new(); - _leavingMembers = []; - } - - internal static ClanUtil FromInit(Clan clan) - { - ClanUtil c = new(clan, []); - IEnumerable existingMembers = Account.FetchAll().Select(m => m.Id); - foreach (string dbMember in existingMembers) - { - ClanMember? clanMember = clan.MemberList!.FirstOrDefault(m => m.Tag == dbMember); - if (clanMember == null) - { - ClanMember m = new() - { - Tag = dbMember - }; - c._members[dbMember] = m; // Fake a member - } - else - { - c._members[dbMember] = clanMember; - clan.MemberList!.Remove(clanMember); - } - } - return c; - } - - internal static ClanUtil FromPoll(Clan clan) - { - ClanUtil c = new(clan, new(Coc.Clan._members)); - foreach (ClanMember member in clan.MemberList!) - { - c._members[member.Tag] = member; - if (Coc.Clan.HasMember(member)) - { - c._existingMembers[member.Tag] = member; - c._leavingMembers.Remove(member.Tag); - } - else - { - c._joiningMembers[member.Tag] = member; - } - } - return c; - } - - // ReSharper disable All - private bool HasMember(ClanMember member) => _members.ContainsKey(member.Tag); - // ReSharper enable All } diff --git a/Bot/Clash/Coc.cs b/Bot/Clash/Coc.cs index 65cc9cb..c29131b 100644 --- a/Bot/Clash/Coc.cs +++ b/Bot/Clash/Coc.cs @@ -20,17 +20,19 @@ private sealed class RaidAttackerComparer : IEqualityComparer EventMemberJoined; - internal static event Action EventMemberLeft; + internal static event Action EventMemberLeft; internal static event Action EventInitRaid; internal static event Action EventRaidCompleted; internal static event Action>> EventDonatedMaxFlow; internal static event Func>, IEnumerable>, Task> EventDonated; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - static Coc() => Dc.EventBotReady += BotReadyAsync; + static Coc() + { + EventMemberLeft += MembersLeft; + } #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. private static void CheckMembersJoined(ClanUtil clan) @@ -54,110 +56,57 @@ private static void CheckMembersJoined(ClanUtil clan) } } - private static void CheckMembersLeft(ClanUtil clan) + private static void MembersLeft(Account[] leftMembers) => Console.WriteLine($"{string.Join(",", leftMembers.Select(a => a.Id))} left"); + + private static async Task GetClanAsync() => await s_client.Clans.GetClanAsync(ClanId); + + // Poll clan members + private static async Task PollClanAsync() { - if (clan._leavingMembers.Count == 0) + // Fetch data for comparison + s_clan = await GetClanAsync(); + if (s_clan.MemberList == null) { - return; + throw new InvalidDataException("Fetched clan member list is null bruh"); } + IEnumerable accounts = Account.FetchAll(); - foreach ((string id, ClanMember member) in clan._leavingMembers) - { - Account fakeMem = new(id); - IEnumerable alts = fakeMem.GetAltsByMain(); - string? altId = null; - if (alts.Any()) - { - Alt alt = alts.First(); - altId = alt.AltId; - for (int i = 1; i < alts.Count(); i++) - { - alts.ElementAt(i).UpdateMain(alt.AltId); - } - - alt.Delete(); - // Maybe adapt this in the future if need to modify attributes when replacing main - Main main = fakeMem.ToMain(); - main.Delete(); - main.MainId = altId; - main.Insert(); - } + // Some extra info + long nowTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + string[] memberIds = [.. s_clan.MemberList.Select(m => m.Tag)]; - // This is before Db.DelMem below so that we can remap Donation to new mainId - // ^ No longer true because the remap is done ABOVE now but I'll still leave this comment - EventMemberLeft(member, altId); - } + /* NOTE: We must compare new clan info with db because the db now stores left members as well. */ - string[] members = [.. clan._leavingMembers.Keys]; - foreach (string member in members) + /* Extract members and accounts changed */ + Account[] leftMembers = [.. Account.FetchMembers().ExceptBy(memberIds, a => a.Id)]; + foreach (Account account in leftMembers) { - new Account(member).Delete(); + account.LeftTime = nowTime; } - string membersMsg = string.Join(", ", members); - Console.WriteLine($"{membersMsg} left"); - } + IEnumerable newMembers = s_clan.MemberList.ExceptBy(accounts.Select(a => a.Id), m => m.Tag); + IEnumerable newJoinAccounts = newMembers.Select(m => new Account(m.Tag)); - private static async Task BotReadyAsync() - { - await s_initTask; - Poll clanPoll = new(10000, 60000, PollAsync); - Poll raidPoll = new(0, 10000, PollRaidAsync); - await Task.WhenAll(clanPoll.RunAsync(), raidPoll.RunAsync()); - } - - private static async Task GetClanAsync() => await s_client.Clans.GetClanAsync(ClanId); - - private static async Task InitAsync() - { - /* Try all tokens and init clan */ - int counter = 1; - foreach (string token in Secrets.s_coc) + Account[] prevJoinAccounts = [.. Account.FetchLeft().IntersectBy(memberIds, a => a.Id)]; + foreach (Account account in prevJoinAccounts) { - s_client = new(token); - try - { - Clan = ClanUtil.FromInit(await GetClanAsync()); - Console.WriteLine($"Logged into CoC with token {counter}"); - - // Login successful, now try init raid - ClanCapitalRaidSeason season = await GetRaidSeasonAsync(); - // If last raid happened within a week, we count it as valid - EventInitRaid(season); - s_raidSeason = season; - - return; - } - catch (ClashOfClansException ex) - { - if (ex.Error.Reason.StartsWith("accessDenied")) - { - counter++; - continue; - } - throw; - } + account.LeftTime = null; } - throw new InvalidDataException("All CoC tokens are invalid!"); - } - private static async Task PollAsync() - { - Clan clan = await GetClanAsync(); + // Update db + Db.UpdateAll(leftMembers.Concat(prevJoinAccounts)); + Db.InsertAll(newJoinAccounts); - if (clan.MemberList == null) + // Dispatch events + if (leftMembers.Length > 0) { - return; + EventMemberLeft(leftMembers); } - - ClanUtil clanUtil = ClanUtil.FromPoll(clan); - CheckMembersJoined(clanUtil); - CheckMembersLeft(clanUtil); await Task.WhenAll([ CheckDonationsAsync(clanUtil) ]); - Clan = clanUtil; + } private static async Task PollRaidAsync() @@ -225,7 +174,7 @@ private static async Task CheckDonationsAsync(ClanUtil clan) foreach (string tag in clan._existingMembers.Keys) { ClanMember current = clan._members[tag]; - ClanMember previous = Clan._members[tag]; + ClanMember previous = current; //Clan._members[tag]; if (current.Donations > previous.Donations) { @@ -249,7 +198,7 @@ private static async Task CheckDonationsAsync(ClanUtil clan) foreach (string tag in clan._existingMembers.Keys) { ClanMember current = clan._members[tag]; - ClanMember previous = Clan._members[tag]; + ClanMember previous = current; //Clan._members[tag]; if (current.DonationsReceived > previous.DonationsReceived) { @@ -323,19 +272,11 @@ private static async Task CheckDonationsAsync(ClanUtil clan) } } - internal static string? GetMemberId(string name) - { - ClanMember? result = Clan._clan.MemberList!.FirstOrDefault(m => m.Name == name); - return result?.Tag; - } + internal static ClanMember? TryGetMember(string id) => s_clan.MemberList!.FirstOrDefault(m => m.Tag == id); - internal static ClanMember? TryGetMember(string id) - { - Clan._members.TryGetValue(id, out ClanMember? result); - return result; - } + internal static ClanMember? TryGetMemberById(string name) => s_clan.MemberList!.FirstOrDefault(m => m.Name == name); - internal static ClanMember GetMember(string id) => Clan._members[id]; + internal static ClanMember GetMember(string id) => s_clan.MemberList!.First(m => m.Tag == id); internal static HashSet GetRaidAttackers(ClanCapitalRaidSeason season) { @@ -356,4 +297,41 @@ internal static HashSet GetRaidAttackers(ClanCapi return set; } + + internal static async Task InitAsync() + { + /* Try all tokens and init clan */ + int counter = 1; + foreach (string token in Secrets.s_coc) + { + s_client = new(token); + try + { + await GetClanAsync(); + Console.WriteLine($"Logged into CoC with token {counter}"); + + // Login successful, now try init raid + ClanCapitalRaidSeason season = await GetRaidSeasonAsync(); + // If last raid happened within a week, we count it as valid + EventInitRaid(season); + s_raidSeason = season; + + // Wait for Discord ready and start polling tasks + await Dc.s_readyTcs.Task; + Poll clanPoll = new(10000, 60000, PollClanAsync); + Poll raidPoll = new(0, 10000, PollRaidAsync); + await Task.WhenAll(clanPoll.RunAsync(), raidPoll.RunAsync()); + } + catch (ClashOfClansException ex) + { + if (ex.Error.Reason.StartsWith("accessDenied")) + { + counter++; + continue; + } + throw; + } + } + throw new InvalidDataException("All CoC tokens are invalid!"); + } } diff --git a/Bot/Discord/Dc.cs b/Bot/Discord/Dc.cs index ea9d430..4c06673 100644 --- a/Bot/Discord/Dc.cs +++ b/Bot/Discord/Dc.cs @@ -14,6 +14,7 @@ internal static class Dc private static readonly InteractionService s_interactionSvc; private static IApplication s_botApp; private static readonly DiscordSocketClient s_bot = new(); + internal static readonly TaskCompletionSource s_readyTcs = new(); internal static event Func EventBotReady; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. @@ -39,6 +40,7 @@ private static Task Log(LogMessage msg) private static async Task Ready() { + s_readyTcs.SetResult(); s_botLog = (SocketTextChannel)s_bot.GetChannel(Secrets.s_botLogId); _ = Task.Run(EventBotReady); await s_interactionSvc.RegisterCommandsGloballyAsync(); diff --git a/Bot/Discord/MemberConverter.cs b/Bot/Discord/MemberConverter.cs index 934c0a8..706c868 100644 --- a/Bot/Discord/MemberConverter.cs +++ b/Bot/Discord/MemberConverter.cs @@ -61,15 +61,15 @@ public override Task ReadAsync(IInteractionContext context, } // Check whether input is the name of a clan member - string? cocId = Coc.GetMemberId(input); - if (cocId != null) + ClanMember? member = Coc.TryGetMemberById(input); + if (member != null) { - Account sqlMember = Account.TryFetch(cocId)!; + Account sqlMember = Account.TryFetch(member.Tag)!; return TypeConverters.Success(sqlMember); } // Check whether input is the tag of a clan member - ClanMember? member = Coc.TryGetMember(input); + member = Coc.TryGetMember(input); if (member != null) { Account sqlMember = Account.TryFetch(input)!; diff --git a/Bot/Program.cs b/Bot/Program.cs index 16d5b00..2fa934b 100644 --- a/Bot/Program.cs +++ b/Bot/Program.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Reflection; +using System.Runtime.ExceptionServices; namespace Hyperstellar; @@ -100,6 +101,20 @@ internal static async Task TryUntilAsync(Func tryFunc, Func - await Task.WhenAll(Discord.Dc.InitAsync(), Clash.Coc.s_initTask, Sql.Db.InitAsync()); + public static async Task Main() + { + List tasks = [Discord.Dc.InitAsync(), Clash.Coc.InitAsync(), Sql.Db.InitAsync()]; + while (tasks.Count != 0) + { + Task completedTask = await Task.WhenAny(tasks); + if (completedTask.IsCompletedSuccessfully) + { + tasks.Remove(completedTask); + } + else + { + ExceptionDispatchInfo.Capture(completedTask.Exception!.InnerException!).Throw(); + } + } + } } diff --git a/Bot/Sql/Account.cs b/Bot/Sql/Account.cs index 2d90563..638518a 100644 --- a/Bot/Sql/Account.cs +++ b/Bot/Sql/Account.cs @@ -15,6 +15,10 @@ public Account() : this("") { } internal static Account? TryFetch(string cocId) => s_db.Table().FirstOrDefault(m => m.Id == cocId); + internal static TableQuery FetchMembers() => s_db.Table().Where(a => a.LeftTime == null); + + internal static TableQuery FetchLeft() => s_db.Table().Where(a => a.LeftTime != null); + public void AddAlt(Account altMember) { Alt alt = new(altMember.Id, Id); @@ -58,4 +62,18 @@ public Main GetEffectiveMain() string mainId = alt == null ? Id : alt.MainId; return s_db.Table
().First(m => m.MainId == mainId); } + + public override string ToString() + { + string s = $"{Id} "; + if (LeftTime == null) + { + s += "Member"; + } + else + { + s += $"Left at {LeftTime}"; + } + return s; + } } diff --git a/Bot/Sql/Db.cs b/Bot/Sql/Db.cs index deb40d9..b38261d 100644 --- a/Bot/Sql/Db.cs +++ b/Bot/Sql/Db.cs @@ -15,6 +15,8 @@ internal static void Commit() internal static int InsertAll(IEnumerable objects) => s_db.InsertAll(objects); + internal static int UpdateAll(IEnumerable objects) => s_db.UpdateAll(objects); + internal static async Task InitAsync() { s_db.BeginTransaction();