Skip to content

Commit

Permalink
feat: upload arena draft when arena game ends
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuscechetto committed Nov 29, 2024
1 parent eaee170 commit 36fa36b
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 10 deletions.
16 changes: 8 additions & 8 deletions HearthWatcher/ArenaWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
{
Expand All @@ -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));
}
}
}
8 changes: 7 additions & 1 deletion HearthWatcher/EventArgs/CardPickedEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
5 changes: 4 additions & 1 deletion HearthWatcher/EventArgs/ChoicesChangedEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
37 changes: 37 additions & 0 deletions Hearthstone Deck Tracker/Hearthstone/Watchers.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using HearthDb.Enums;
using HearthMirror;
using HearthMirror.Enums;
using HearthMirror.Objects;
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;
Expand All @@ -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);
Expand Down Expand Up @@ -51,6 +56,38 @@ internal static void Stop()
BattlegroundsLeaderboardWatcher.Stop();
}

private static readonly Dictionary<int, (string[] choices, string pickStartTime)> _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);
Expand Down
33 changes: 33 additions & 0 deletions Hearthstone Deck Tracker/HsReplay/UploadMetaDataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using HSReplay;
using System.Collections.Generic;
using HearthDb.Enums;
using Hearthstone_Deck_Tracker.Utility.Arena;

namespace Hearthstone_Deck_Tracker.HsReplay
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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
Expand Down
179 changes: 179 additions & 0 deletions Hearthstone Deck Tracker/Utility/Arena/ArenaLastDrafts.cs
Original file line number Diff line number Diff line change
@@ -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<ArenaLastDrafts> LazyInstance = new Lazy<ArenaLastDrafts>(Load);

public static ArenaLastDrafts Instance = LazyInstance.Value;

[XmlElement("Draft")]
public List<DraftItem> 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<ArenaLastDrafts>.Load(DataPath);
}
catch(Exception ex)
{
Log.Error(ex);
}
return new ArenaLastDrafts();
}

private async Task<string?> GetPlayerId()
{
var accountId = await Helper.RetryWhileNull(Reflection.Client.GetAccountId, 2, 3000);
return accountId != null ? $"{accountId.Hi}_{accountId.Lo}" : null;
}

public async Task<List<DraftItem>> PlayerDrafts()
{
var playerId = await GetPlayerId();
if (playerId == null)
return new List<DraftItem>();

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<ArenaLastDrafts>.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<PickItem> 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; }
}

}
}


0 comments on commit 36fa36b

Please sign in to comment.