From 36fa36b707c388958ffff2727d3f86f5e91e2919 Mon Sep 17 00:00:00 2001 From: mateuscechetto Date: Wed, 20 Nov 2024 10:50:13 -0300 Subject: [PATCH] feat: upload arena draft when arena game ends --- HearthWatcher/ArenaWatcher.cs | 16 +- .../EventArgs/CardPickedEventArgs.cs | 8 +- .../EventArgs/ChoicesChangedEventArgs.cs | 5 +- .../Hearthstone/Watchers.cs | 37 ++++ .../HsReplay/UploadMetaDataGenerator.cs | 33 ++++ .../Utility/Arena/ArenaLastDrafts.cs | 179 ++++++++++++++++++ 6 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 Hearthstone Deck Tracker/Utility/Arena/ArenaLastDrafts.cs diff --git a/HearthWatcher/ArenaWatcher.cs b/HearthWatcher/ArenaWatcher.cs index ed89f25255..15024f8871 100644 --- a/HearthWatcher/ArenaWatcher.cs +++ b/HearthWatcher/ArenaWatcher.cs @@ -19,8 +19,8 @@ public class ArenaWatcher private bool _watch; private int _prevSlot = -1; private bool _sameChoices; - private Card[] _prevChoices; - private ArenaInfo _prevInfo; + private Card[]? _prevChoices; + private ArenaInfo? _prevInfo; private const int MaxDeckSize = 30; private readonly IArenaProvider _arenaProvider; @@ -86,7 +86,7 @@ public bool Update() if(ChoicesChanged(choices) || _sameChoices) { _sameChoices = false; - OnChoicesChanged?.Invoke(this, new ChoicesChangedEventArgs(choices, arenaInfo.Deck)); + OnChoicesChanged?.Invoke(this, new ChoicesChangedEventArgs(choices, arenaInfo.Deck, arenaInfo.CurrentSlot)); } else { @@ -105,23 +105,23 @@ public bool Update() return false; } - private bool ChoicesChanged(Card[] choices) => _prevChoices == null || choices[0] != _prevChoices[0] || choices[1] != _prevChoices[1] || choices[2] != _prevChoices[2]; + private bool ChoicesChanged(Card[] choices) => _prevChoices == null || choices[0].Id != _prevChoices[0].Id || choices[1].Id != _prevChoices[1].Id || choices[2].Id != _prevChoices[2].Id; - private bool HasChanged(ArenaInfo arenaInfo, int slot) + private bool HasChanged(ArenaInfo arenaInfo, int slot) => _prevInfo == null || _prevInfo.Deck.Hero != arenaInfo.Deck.Hero || slot > _prevSlot; private void HeroPicked(ArenaInfo arenaInfo) { - var hero = _prevChoices.FirstOrDefault(x => x.Id == arenaInfo.Deck.Hero); + var hero = _prevChoices?.FirstOrDefault(x => x.Id == arenaInfo.Deck.Hero); if(hero != null) - OnCardPicked?.Invoke(this, new CardPickedEventArgs(hero, _prevChoices)); + OnCardPicked?.Invoke(this, new CardPickedEventArgs(hero, _prevChoices!, arenaInfo.Deck, _prevSlot)); } private void CardPicked(ArenaInfo arenaInfo) { var pick = arenaInfo.Deck.Cards.FirstOrDefault(x => !_prevInfo?.Deck.Cards.Any(c => x.Id == c.Id && x.Count == c.Count) ?? false); if(pick != null) - OnCardPicked?.Invoke(this, new CardPickedEventArgs(new Card(pick.Id, 1, pick.PremiumType), _prevChoices)); + OnCardPicked?.Invoke(this, new CardPickedEventArgs(new Card(pick.Id, 1, pick.PremiumType), _prevChoices!, arenaInfo.Deck, _prevSlot)); } } } diff --git a/HearthWatcher/EventArgs/CardPickedEventArgs.cs b/HearthWatcher/EventArgs/CardPickedEventArgs.cs index ea1ce57526..27c22328e7 100644 --- a/HearthWatcher/EventArgs/CardPickedEventArgs.cs +++ b/HearthWatcher/EventArgs/CardPickedEventArgs.cs @@ -7,10 +7,16 @@ public class CardPickedEventArgs : System.EventArgs public Card Picked { get; } public Card[] Choices { get; } - public CardPickedEventArgs(Card picked, Card[] choices) + public Deck Deck { get; } + + public int Slot { get; } + + public CardPickedEventArgs(Card picked, Card[] choices, Deck deck, int slot) { Picked = picked; Choices = choices; + Deck = deck; + Slot = slot; } } } diff --git a/HearthWatcher/EventArgs/ChoicesChangedEventArgs.cs b/HearthWatcher/EventArgs/ChoicesChangedEventArgs.cs index aa86d361ea..11cbc42a45 100644 --- a/HearthWatcher/EventArgs/ChoicesChangedEventArgs.cs +++ b/HearthWatcher/EventArgs/ChoicesChangedEventArgs.cs @@ -7,10 +7,13 @@ public class ChoicesChangedEventArgs : System.EventArgs public Card[] Choices { get; } public Deck Deck { get; } - public ChoicesChangedEventArgs(Card[] choices, Deck deck) + public int Slot { get; } + + public ChoicesChangedEventArgs(Card[] choices, Deck deck, int slot) { Choices = choices; Deck = deck; + Slot = slot; } } } diff --git a/Hearthstone Deck Tracker/Hearthstone/Watchers.cs b/Hearthstone Deck Tracker/Hearthstone/Watchers.cs index 02078b9bde..98042f5e07 100644 --- a/Hearthstone Deck Tracker/Hearthstone/Watchers.cs +++ b/Hearthstone Deck Tracker/Hearthstone/Watchers.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Linq; using HearthDb.Enums; using HearthMirror; using HearthMirror.Enums; @@ -6,6 +8,7 @@ using Hearthstone_Deck_Tracker.Enums; using Hearthstone_Deck_Tracker.Enums.Hearthstone; using Hearthstone_Deck_Tracker.Importing; +using Hearthstone_Deck_Tracker.Utility.Arena; using Hearthstone_Deck_Tracker.Utility.Extensions; using HearthWatcher; using HearthWatcher.Providers; @@ -16,6 +19,8 @@ public static class Watchers { static Watchers() { + ArenaWatcher.OnChoicesChanged += OnChoiceChanged; + ArenaWatcher.OnCardPicked += OnCardPicked; ArenaWatcher.OnCompleteDeck += (sender, args) => DeckManager.AutoImportArena(Config.Instance.SelectedArenaImportingBehaviour ?? ArenaImportingBehaviour.AutoImportSave, args.Info); DungeonRunWatcher.DungeonRunMatchStarted += (newRun, set) => DeckManager.DungeonRunMatchStarted(newRun, set, false); DungeonRunWatcher.DungeonInfoChanged += dungeonInfo => DeckManager.UpdateDungeonRunDeck(dungeonInfo, false); @@ -51,6 +56,38 @@ internal static void Stop() BattlegroundsLeaderboardWatcher.Stop(); } + private static readonly Dictionary _currentArenaDraftInfo = new(); + + internal static void OnChoiceChanged(object sender, HearthWatcher.EventArgs.ChoicesChangedEventArgs args) + { + + var newChoices = args.Choices.Select(c => c.Id).ToArray(); + var pickStartTime = DateTime.Now.ToString("o"); + + _currentArenaDraftInfo[args.Slot] = (newChoices, pickStartTime); + } + + internal static void OnCardPicked(object sender, HearthWatcher.EventArgs.CardPickedEventArgs args) + { + if(!_currentArenaDraftInfo.TryGetValue(args.Slot, out var draftInfo) || + draftInfo.choices == null || draftInfo.choices.Length == 0 || + string.IsNullOrEmpty(draftInfo.pickStartTime)) + { + return; + } + var pickTime = DateTime.Now.ToString("o"); + ArenaLastDrafts.Instance.AddPick( + draftInfo.pickStartTime, + pickTime, + args.Picked.Id, + draftInfo.choices, + args.Slot, + overlayVisible: false, + args.Deck.Id + ); + + } + internal static void OnBaconChange(object sender, HearthWatcher.EventArgs.BaconEventArgs args) { Core.Overlay.SetBaconState(args.SelectedBattlegroundsGameMode, args.IsShopOpen || args.IsJournalOpen || args.IsPopupShowing || args.IsBlurActive); diff --git a/Hearthstone Deck Tracker/HsReplay/UploadMetaDataGenerator.cs b/Hearthstone Deck Tracker/HsReplay/UploadMetaDataGenerator.cs index 4bf7290850..8f10354021 100644 --- a/Hearthstone Deck Tracker/HsReplay/UploadMetaDataGenerator.cs +++ b/Hearthstone Deck Tracker/HsReplay/UploadMetaDataGenerator.cs @@ -7,6 +7,7 @@ using HSReplay; using System.Collections.Generic; using HearthDb.Enums; +using Hearthstone_Deck_Tracker.Utility.Arena; namespace Hearthstone_Deck_Tracker.HsReplay { @@ -158,6 +159,29 @@ public static UploadMetaData Generate(GameMetaData? gameMetaData, GameStats? gam friendly.Wins = game.ArenaWins; if(game.ArenaLosses > 0) friendly.Losses = game.ArenaLosses; + + var draft = ArenaLastDrafts.Instance.Drafts.FirstOrDefault(d => d.DeckId == game.HsDeckId); + if(draft != null && ValidateArenaDraft(friendly.DeckList, draft)) + { + friendly.ArenaDraft = + new UploadMetaData.ArenaDraft + { + StartTime = draft.StartTime, + // we use groupBy to be safer against picks being duplicated on xml file + // and avoid a duplicate key error + Picks = draft.Picks + .GroupBy(pick => pick.Slot) + .Select(group => new UploadMetaData.ArenaPick + { + Pick = group.Last().Slot, + Chosen = group.Last().Picked, + Offered = group.Last().Choices, + TimeOnChoice = group.Last().TimeOnChoice, + OverlayVisible = false, + } + ).ToArray() + }; + } } else if(game.GameMode == GameMode.Brawl) { @@ -174,6 +198,15 @@ public static UploadMetaData Generate(GameMetaData? gameMetaData, GameStats? gam opposing }; } + + // we already check the deckId, this is an extra confirmation that the draft is for that deck + private static bool ValidateArenaDraft(string[] deckList, ArenaLastDrafts.DraftItem draft) + { + var pickedCards = draft.Picks + .Where(pick => pick.Picked != null && !pick.Picked.StartsWith("HERO")) + .Select(pick => pick.Picked).ToList(); + return pickedCards.All(deckList.Contains); + } } public class PlayerInfo diff --git a/Hearthstone Deck Tracker/Utility/Arena/ArenaLastDrafts.cs b/Hearthstone Deck Tracker/Utility/Arena/ArenaLastDrafts.cs new file mode 100644 index 0000000000..0e81dbd7bd --- /dev/null +++ b/Hearthstone Deck Tracker/Utility/Arena/ArenaLastDrafts.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Serialization; +using HearthMirror; +using Hearthstone_Deck_Tracker.Utility.Logging; + +namespace Hearthstone_Deck_Tracker.Utility.Arena +{ + [XmlRoot("ArenaLastDrafts")] + public sealed class ArenaLastDrafts + { + private static readonly Lazy LazyInstance = new Lazy(Load); + + public static ArenaLastDrafts Instance = LazyInstance.Value; + + [XmlElement("Draft")] + public List Drafts { get; set; } = new(); + + private static string DataPath => Path.Combine(Config.AppDataPath, "ArenaLastDrafts.xml"); + + private static ArenaLastDrafts Load() + { + if(!File.Exists(DataPath)) + return new ArenaLastDrafts(); + try + { + return XmlManager.Load(DataPath); + } + catch(Exception ex) + { + Log.Error(ex); + } + return new ArenaLastDrafts(); + } + + private async Task GetPlayerId() + { + var accountId = await Helper.RetryWhileNull(Reflection.Client.GetAccountId, 2, 3000); + return accountId != null ? $"{accountId.Hi}_{accountId.Lo}" : null; + } + + public async Task> PlayerDrafts() + { + var playerId = await GetPlayerId(); + if (playerId == null) + return new List(); + + return Drafts.Where(draft => draft.Player == null || draft.Player == playerId).ToList(); + } + + public async void AddPick( + string startTime, string pickedTime, string picked, string[] choices, int slot, bool overlayVisible, long deckId, bool save = true + ) + { + var playerId = await GetPlayerId(); + if(playerId == null) + { + Log.Info("Unable to save the game. User account can not found..."); + return; + } + + var currentDraft = GetOrCreateDraft(startTime, playerId, deckId); + + var start = DateTime.Parse(startTime); + var end = DateTime.Parse(pickedTime); + var timeSpent = end - start; + + currentDraft.Picks.Add(new PickItem(picked, choices, slot, (int)timeSpent.TotalMilliseconds, overlayVisible)); + + if(save) + Save(); + } + + public void RemoveDraft(string player, bool save = true) + { + // the same player can't have 2 drafts open at same time + var existingEntry = Drafts.FirstOrDefault(x => x.Player != null && x.Player.Equals(player)); + if (existingEntry != null) + Drafts.Remove(existingEntry); + if(save) + Save(); + } + + public static void Save() + { + try + { + XmlManager.Save(DataPath, Instance); + } + catch(Exception ex) + { + Log.Error(ex); + } + } + + public void Reset() + { + Drafts.Clear(); + Save(); + } + + private DraftItem GetOrCreateDraft(string startTime, string player, long deckId) + { + var draft = Drafts.FirstOrDefault(d => d.DeckId == deckId); + if(draft != null) + { + return draft; + } + + draft = new DraftItem(startTime, player, deckId); + RemoveDraft(player, false); + Drafts.Add(draft); + return draft; + } + + public class DraftItem + { + public DraftItem(string startTime, string player, long deckId) + { + Player = player; + StartTime = startTime; + DeckId = deckId; + } + + public DraftItem() + { + } + + [XmlAttribute("Player")] + public string? Player { get; set; } + + [XmlAttribute("StartTime")] + public string? StartTime { get; set; } + + [XmlAttribute("DeckId")] + public long DeckId { get; set; } + + [XmlElement("Pick")] + public List Picks { get; set; } = new(); + + } + + public class PickItem + { + + public PickItem(string picked, string[] choices, int slot, int timeOnChoice, bool overlayVisible) + { + Picked = picked; + Choices = choices; + Slot = slot; + TimeOnChoice = timeOnChoice; + OverlayVisible = overlayVisible; + } + + public PickItem() { } + + [XmlElement("Slot")] + public int Slot { get; set; } + + [XmlElement("Picked")] + public string? Picked { get; set; } + + [XmlElement("Choice")] + public string[] Choices { get; set; } = { }; + + [XmlElement("TimeOnChoice")] + public int TimeOnChoice { get; set; } + + [XmlElement("OverlayVisible")] + public bool OverlayVisible { get; set; } + } + + } +} + +