diff --git a/Hearthstone Deck Tracker/BobsBuddy/BobsBuddyInvoker.cs b/Hearthstone Deck Tracker/BobsBuddy/BobsBuddyInvoker.cs index f014d89f6..022e40e9e 100644 --- a/Hearthstone Deck Tracker/BobsBuddy/BobsBuddyInvoker.cs +++ b/Hearthstone Deck Tracker/BobsBuddy/BobsBuddyInvoker.cs @@ -16,6 +16,7 @@ using Hearthstone_Deck_Tracker.Utility.Extensions; using Entity = Hearthstone_Deck_Tracker.Hearthstone.Entities.Entity; using BobsBuddy; +using HearthDb; namespace Hearthstone_Deck_Tracker.BobsBuddy { @@ -48,6 +49,9 @@ internal class BobsBuddyInvoker private static List _recentHDTLog = new List(); private static List _currentOpponentSecrets = new List(); + private List _opponentHand = new(); + private readonly Dictionary _opponentHandMap = new(); + private static Guid _currentGameId; private static readonly Dictionary _instances = new Dictionary(); private static readonly Regex _debuglineToIgnore = new Regex(@"\|(Player|Opponent|TagChangeActions)\."); @@ -180,7 +184,7 @@ public async void StartCombat() await Task.Delay(LichKingDelay); if(!RunSimulationAfterCombat) - RunAndDisplaySimulationAsync().Forget(); + await RunAndDisplaySimulationAsync(); } catch(Exception e) { @@ -320,7 +324,6 @@ private void SnapshotBoardState(int turn) return; } - //We set OpponentCardId and PlayerCardId here so that later we can do lookups for these entites without using _game.Opponent/Player, which might be innacurate or null depending on when they're accessed. OpponentCardId = opponentHero.CardId ?? ""; PlayerCardId = playerHero.CardId ?? ""; @@ -428,7 +431,13 @@ private void SnapshotBoardState(int turn) foreach(var e in _game.Player.Hand) { if(e.IsMinion) - input.PlayerHand.Add(new MinionCardEntity(GetMinionFromEntity(simulator.MinionFactory, true, e, GetAttachedEntities(e.Id)), null, simulator)); + { + var minionEntity = new MinionCardEntity(GetMinionFromEntity(simulator.MinionFactory, true, e, GetAttachedEntities(e.Id)), null, simulator) + { + CanSummon = !e.HasTag(GameTag.LITERALLY_UNPLAYABLE), + }; + input.PlayerHand.Add(minionEntity); + } else if(e.CardId == NonCollectible.Neutral.BloodGem1) input.PlayerHand.Add(new BloodGem(null, simulator)); else if(e.IsSpell) @@ -443,19 +452,9 @@ private void SnapshotBoardState(int turn) foreach(var m in opponentSide) input.opponentSide.Add(m); - foreach(var e in _game.Opponent.Hand) - { - if(e.IsMinion) - input.OpponentHand.Add(new MinionCardEntity(GetMinionFromEntity(simulator.MinionFactory, false, e, GetAttachedEntities(e.Id)), null, simulator)); - else if(e.CardId == NonCollectible.Neutral.BloodGem1) - input.OpponentHand.Add(new BloodGem(null, simulator)); - else if(e.IsSpell) - input.OpponentHand.Add(new SpellCardEntity(null, simulator)); - else if(!string.IsNullOrEmpty(e.CardId)) - input.OpponentHand.Add(new CardEntity(e.CardId ?? "", null, simulator)); // Not Unknown - else - input.OpponentHand.Add(new UnknownCardEntity(null, simulator)); - } + _opponentHand = _game.Opponent.Hand.ToList(); + input.OpponentHand.Clear(); + input.OpponentHand.AddRange(GetOpponentHandEntities(simulator)); var playerAttached = GetAttachedEntities(_game.PlayerEntity.Id); var pEternalLegion = playerAttached.FirstOrDefault(x => x.CardId == NonCollectible.Invalid.EternalKnight_EternalKnightPlayerEnchant); @@ -488,6 +487,66 @@ private void SnapshotBoardState(int turn) DebugLog("Successfully snapshotted board state"); } + private int _reRunCount; + internal async void UpdateOpponentHand(Entity entity, Entity copy) + { + if(_input == null || State != BobsBuddyState.Combat) + return; + + // Only allow feathermane for now. + if(copy.CardId != NonCollectible.Neutral.FreeFlyingFeathermane && copy.CardId != NonCollectible.Neutral.FreeFlyingFeathermane_FreeFlyingFeathermane) + return; + + _opponentHandMap[entity] = copy; + + // Wait for attached entities to be logged. This should happen at the exact same timestamp. + //await _game.GameTime.WaitForDuration(1); + + var entities = GetOpponentHandEntities(new Simulator()).ToList(); + if(entities.Count(x => x is MinionCardEntity) <= _input.OpponentHand.Count(x => x is MinionCardEntity)) + return; + + _input.OpponentHand.Clear(); + _input.OpponentHand.AddRange(entities); + + if(_reRunCount++ <= 2) + { + DebugLog($"Opponent hand changed, re-running simulation! (#{_reRunCount})"); + if(ShouldRun() && !RunSimulationAfterCombat) + { + ErrorState = BobsBuddyErrorState.UpdateRequired; + BobsBuddyDisplay.SetErrorState(BobsBuddyErrorState.UpdateRequired, null, true); + await RunAndDisplaySimulationAsync(); + } + } + else + DebugLog("Opponent hand changed, but the simulation already re-ran twice"); + } + + private IEnumerable GetOpponentHandEntities(Simulator simulator) + { + foreach(var _e in _opponentHand) + { + var e = _opponentHandMap.TryGetValue(_e, out var copy) ? copy : _e; + if(e.IsMinion) + { + var attached = GetAttachedEntities(e.Id); + yield return new MinionCardEntity(GetMinionFromEntity(simulator.MinionFactory, false, e, attached), null, simulator) + { + CanSummon = !e.HasTag(GameTag.LITERALLY_UNPLAYABLE) + }; + } + else if(e.CardId == NonCollectible.Neutral.BloodGem1) + yield return new BloodGem(null, simulator); + else if(e.IsSpell) + yield return new SpellCardEntity(null, simulator); + else if(!string.IsNullOrEmpty(e.CardId)) + yield return new CardEntity(e.CardId ?? "", null, simulator); // Not Unknown + else + yield return new UnknownCardEntity(null, simulator); + } + } + private IEnumerable GetAttachedEntities(int entityId) => _game.Entities.Values .Where(x => x.IsAttachedTo(entityId) && (x.IsInPlay || x.IsInSetAside || x.IsInGraveyard)) diff --git a/Hearthstone Deck Tracker/Controls/Overlay/BobsBuddyPanel.xaml.cs b/Hearthstone Deck Tracker/Controls/Overlay/BobsBuddyPanel.xaml.cs index 5a6f4439b..97f718700 100644 --- a/Hearthstone Deck Tracker/Controls/Overlay/BobsBuddyPanel.xaml.cs +++ b/Hearthstone Deck Tracker/Controls/Overlay/BobsBuddyPanel.xaml.cs @@ -470,11 +470,13 @@ internal void SetState(BobsBuddyState state) /// until the error is cleared. /// /// The new error state - internal void SetErrorState(BobsBuddyErrorState error, string? message = null) + internal void SetErrorState(BobsBuddyErrorState error, string? message = null, bool skipShow = false) { ErrorState = error; ErrorMessage = message; - ShowResults(false); + + if(!skipShow) + ShowResults(false); } private void ClearErrorState() diff --git a/Hearthstone Deck Tracker/LogReader/Handlers/TagChangeActions.cs b/Hearthstone Deck Tracker/LogReader/Handlers/TagChangeActions.cs index d011b8e2c..73496466a 100644 --- a/Hearthstone Deck Tracker/LogReader/Handlers/TagChangeActions.cs +++ b/Hearthstone Deck Tracker/LogReader/Handlers/TagChangeActions.cs @@ -512,8 +512,19 @@ private void ZoneChange(IHsGameState gameState, int id, IGame game, int value, i else ZoneChangeFromOther(gameState, id, game, value, prevValue, controller, entity.Info.LatestCardId); break; - case GRAVEYARD: case SETASIDE: + if((Zone)value == PLAY && controller == game.Opponent.Id && game.CurrentGameMode == GameMode.Battlegrounds) + { + var copiedFrom = entity.GetTag(COPIED_FROM_ENTITY_ID); + if(copiedFrom > 0 && game.Entities.TryGetValue(copiedFrom, out var source) && source.IsInHand && !source.HasCardId) + { + if(game.CurrentGameStats != null) + BobsBuddyInvoker.GetInstance(game.CurrentGameStats.GameId, game.GetTurnNumber())?.UpdateOpponentHand(source, entity); + } + } + ZoneChangeFromOther(gameState, id, game, value, prevValue, controller, entity.Info.LatestCardId); + break; + case GRAVEYARD: case REMOVEDFROMGAME: ZoneChangeFromOther(gameState, id, game, value, prevValue, controller, entity.Info.LatestCardId); break;