Skip to content

Commit a60184b

Browse files
committed
Made all our new spell view model lookups private, and added some comments for clarity.
Improved the workflow on full spell list reeevaluation to not do extra unnecessary work. added some parameter names to a few things to make them easier to follow. Did a bit of a general refactor on the method names / method ordering on all the new regrouping logic.
1 parent 62cc8e9 commit a60184b

File tree

2 files changed

+92
-80
lines changed

2 files changed

+92
-80
lines changed

EQTool/ViewModels/ActivePlayerInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public void FinishUserCastingSpell()
205205

206206
public void ClearUserCastingSpellLater(Spell spell, DateTime? castedOn)
207207
{
208-
appDispatcher.DebounceToUI(ref aoeDebounceTs, 200, ClearUserCastingSpellImmediately, () =>
208+
appDispatcher.DebounceToUI(ref aoeDebounceTs, 200, ClearUserCastingSpellImmediately, shouldCancel: () =>
209209
{
210210
var castTimeDiff = Math.Abs(((castedOn ?? DateTime.MaxValue) - (UserCastSpellDateTime ?? DateTime.MinValue)).TotalMilliseconds);
211211
return UserCastingSpell != spell || castTimeDiff > 200;

EQTool/ViewModels/SpellWindowViewModel.cs

Lines changed: 91 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@ public ObservableCollection<PersistentViewModel> SpellList
6161
}
6262
}
6363

64-
public Dictionary<string, List<PersistentViewModel>> ActiveSpells = new Dictionary<string, List<PersistentViewModel>>();
65-
public Dictionary<string, List<PersistentViewModel>> ActiveTargets = new Dictionary<string, List<PersistentViewModel>>();
66-
public Dictionary<string, List<PersistentViewModel>> NpcTargets = new Dictionary<string, List<PersistentViewModel>>();
67-
public Dictionary<string, List<PersistentViewModel>> PlayerTargets = new Dictionary<string, List<PersistentViewModel>>();
64+
// These are all updated whenever the SpellList is modified in any way, using the same exact object references, so it's not as bad as it looks.
65+
// The purpose of these lists is to assist our automatic grouping logic. Categorized lookups that are vastly more performant than using linq on lists make the whole thing much smoother.
66+
private Dictionary<string, List<PersistentViewModel>> activeSpellIds = new Dictionary<string, List<PersistentViewModel>>();
67+
private Dictionary<string, List<PersistentViewModel>> activeTargets = new Dictionary<string, List<PersistentViewModel>>();
68+
private Dictionary<string, List<PersistentViewModel>> npcTargets = new Dictionary<string, List<PersistentViewModel>>();
69+
private Dictionary<string, List<PersistentViewModel>> playerTargets = new Dictionary<string, List<PersistentViewModel>>();
70+
private HashSet<SpellViewModel> recentlyImpactedSpells = new HashSet<SpellViewModel>();
6871

6972
private void CreateTriggerList()
7073
{
@@ -790,6 +793,25 @@ public void UpdateAPITimers()
790793
}
791794
}
792795

796+
private CancellationTokenSource timersModifiedDebounceTs;
797+
private void QueueSpellGroupingReevaluation(int delay, bool findRelated = true)
798+
{
799+
// We need to queue the re-evaluation with a debounce cancellation because we don't want to be doing constant iteration over the whole list while it is actively being modified.
800+
// There is also an issue when removing whole groups that can cause the removal to remove the wrong items because the ReevaluateRelatedGroupings function actively reshapes the groupings after every pass.
801+
// This debounce is necessary to ensure we do not waste unnecessary time or cause undesirable side effects when doing bulk operations or when several timers fall off at roughly the same time.
802+
803+
var evaluationItemCount = recentlyImpactedSpells.Count;
804+
appDispatcher.DebounceToUI(ref timersModifiedDebounceTs, delay, () =>
805+
{
806+
var safeCloneOfRecentlyImpacted = recentlyImpactedSpells.ToList(); // Clone it since we're about to wipe it clean
807+
recentlyImpactedSpells.Clear();
808+
var spellsToReevaluate = findRelated ? GetAllRelatedSpells(safeCloneOfRecentlyImpacted) : safeCloneOfRecentlyImpacted;
809+
810+
ReevaluateRelatedGroupings(spellsToReevaluate);
811+
},
812+
shouldCancel: () => evaluationItemCount != recentlyImpactedSpells.Count);
813+
}
814+
793815
private void ReevaluateRelatedGroupings(IEnumerable<SpellViewModel> spellsToEvaluate)
794816
{
795817
var visibleSpells = spellsToEvaluate.Where(s => s.ColumnVisibility == Visibility.Visible).ToList();
@@ -835,7 +857,7 @@ private void ApplyNpcGrouping(IEnumerable<SpellViewModel> spellsToEvaluate)
835857
foreach (var s in spellsToEvaluate)
836858
s.IsCategorizeById = false;
837859

838-
GroupByIdIfItMakesSense(NpcTargets, spellsToEvaluate);
860+
GroupByIdIfItMakesSense(npcTargets, spellsToEvaluate);
839861
}
840862

841863
private void ApplyPlayerGrouping(IEnumerable<SpellViewModel> spellsToEvaluate)
@@ -848,15 +870,15 @@ private void ApplyPlayerGrouping(IEnumerable<SpellViewModel> spellsToEvaluate)
848870
s.IsCategorizeById = false;
849871

850872
// If we only have one or two targets, leave it alone.
851-
var totalGlobalUniqueTargetCount = PlayerTargets.Keys.Count;
873+
var totalGlobalUniqueTargetCount = playerTargets.Keys.Count;
852874
if (totalGlobalUniqueTargetCount < 3)
853875
return;
854876

855-
GroupByIdIfItMakesSense(PlayerTargets, spellsToEvaluate);
877+
GroupByIdIfItMakesSense(playerTargets, spellsToEvaluate);
856878
FixOrphanedTargets(spellsToEvaluate);
857879
}
858880

859-
private void GroupByIdIfItMakesSense(Dictionary<string, List<PersistentViewModel>> targetsToReference, IEnumerable<SpellViewModel> spellsToEvaluate)
881+
private static void GroupByIdIfItMakesSense(Dictionary<string, List<PersistentViewModel>> targetsToReference, IEnumerable<SpellViewModel> spellsToEvaluate)
860882
{
861883
// Try to group by spell name when it would reduce the total number of visible groups
862884
var spellsById = spellsToEvaluate.GroupBy(s => s.Id).ToDictionary(g => g.Key, g => g.ToList());
@@ -913,41 +935,50 @@ private void HandleNonConciseGroupingForSpell(SpellViewModel spell)
913935
{
914936
var groupingType = GetGroupingType(spell);
915937
if (groupingType == SpellGroupingType.ByTarget)
916-
{
917938
spell.IsCategorizeById = false;
918-
}
919939
else if (groupingType == SpellGroupingType.BySpell)
920-
{
921940
spell.IsCategorizeById = true;
922-
}
923941
else if (groupingType == SpellGroupingType.BySpellExceptYou)
924-
{
925942
spell.IsCategorizeById = !spell.CastOnYou(activePlayer.Player);
926-
}
943+
927944
// Automatic grouping handled elsewhere due to it needed to evaluate the whole list at once.
928945
}
929946

930-
private SpellGroupingType GetGroupingType(SpellViewModel spell)
931-
=> spell.IsPlayerTarget
932-
? settings.PlayerSpellGroupingType
933-
: settings.NpcSpellGroupingType;
934-
935-
private List<SpellViewModel> GetAllRelatedSpells(SpellViewModel spell)
936-
=> GetAllRelatedSpells(new List<string> { spell.Id }, new List<string> { spell.Target });
947+
private void AddToActiveSpellLookups(SpellViewModel newItem)
948+
{
949+
activeTargets.SafelyAdd(newItem.Target, newItem);
950+
activeSpellIds.SafelyAdd(newItem.Id, newItem);
951+
952+
if (newItem.IsPlayerTarget)
953+
playerTargets.SafelyAdd(newItem.Target, newItem);
954+
else
955+
npcTargets.SafelyAdd(newItem.Target, newItem);
956+
}
957+
958+
private void RemoveFromActiveSpellLookups(SpellViewModel oldItem)
959+
{
960+
activeTargets.SafelyRemove(oldItem.Target, oldItem);
961+
activeSpellIds.SafelyAdd(oldItem.Id, oldItem);
962+
963+
if (oldItem.IsPlayerTarget)
964+
playerTargets.SafelyRemove(oldItem.Target, oldItem);
965+
else
966+
npcTargets.SafelyRemove(oldItem.Target, oldItem);
967+
}
937968

938969
private List<SpellViewModel> GetAllRelatedSpells(List<SpellViewModel> spells)
939970
{
940971
var relevantTargets = spells.Select(x => x.Target).Distinct();
941972
var relevantSpellNames = spells.Select(x => x.Id).Distinct();
942-
return GetAllRelatedSpells(relevantSpellNames, relevantTargets);
973+
return GetAllSpellsByNameAndTarget(relevantSpellNames, relevantTargets);
943974
}
944975

945-
private List<SpellViewModel> GetAllRelatedSpells(IEnumerable<string> relevantSpellNames, IEnumerable<string> relevantTargets)
976+
private List<SpellViewModel> GetAllSpellsByNameAndTarget(IEnumerable<string> relevantSpellNames, IEnumerable<string> relevantTargets)
946977
{
947978
var relevantSpells = new HashSet<SpellViewModel>();
948979
foreach (var spellName in relevantSpellNames)
949980
{
950-
if (!ActiveSpells.TryGetValue(spellName, out var list))
981+
if (!activeSpellIds.TryGetValue(spellName, out var list))
951982
continue;
952983

953984
foreach (var item in list)
@@ -959,7 +990,7 @@ private List<SpellViewModel> GetAllRelatedSpells(IEnumerable<string> relevantSpe
959990

960991
foreach (var target in relevantTargets)
961992
{
962-
if (!ActiveTargets.TryGetValue(target, out var list))
993+
if (!activeTargets.TryGetValue(target, out var list))
963994
continue;
964995

965996
foreach (var item in list)
@@ -971,76 +1002,48 @@ private List<SpellViewModel> GetAllRelatedSpells(IEnumerable<string> relevantSpe
9711002

9721003
return relevantSpells.ToList();
9731004
}
1005+
1006+
private bool IsAutomaticMode() => settings.PlayerSpellGroupingType == SpellGroupingType.Automatic || settings.NpcSpellGroupingType == SpellGroupingType.Automatic;
1007+
1008+
private SpellGroupingType GetGroupingType(SpellViewModel spell)
1009+
=> spell.IsPlayerTarget
1010+
? settings.PlayerSpellGroupingType
1011+
: settings.NpcSpellGroupingType;
9741012

9751013
private void SpellList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
9761014
{
9771015
if (e.NewItems == null && e.OldItems == null)
9781016
return;
9791017

980-
var isAutomaticMode = settings.PlayerSpellGroupingType == SpellGroupingType.Automatic || settings.NpcSpellGroupingType == SpellGroupingType.Automatic;
1018+
var isAutomaticMode = IsAutomaticMode();
9811019

982-
if (e.NewItems != null)
1020+
var newItems = (e.NewItems ?? new List<PersistentViewModel>()).OfType<SpellViewModel>().Where(x => x.ColumnVisibility == Visibility.Visible);
1021+
var oldItems = (e.OldItems ?? new List<PersistentViewModel>()).OfType<SpellViewModel>().Where(x => x.ColumnVisibility == Visibility.Visible);
1022+
1023+
foreach (var newItem in newItems)
9831024
{
984-
foreach (var newItem in e.NewItems.OfType<SpellViewModel>())
985-
{
986-
ActiveTargets.SafelyAdd(newItem.Target, newItem);
987-
ActiveSpells.SafelyAdd(newItem.Id, newItem);
988-
989-
if (newItem.IsPlayerTarget)
990-
PlayerTargets.SafelyAdd(newItem.Target, newItem);
991-
else
992-
NpcTargets.SafelyAdd(newItem.Target, newItem);
993-
994-
if (isAutomaticMode)
995-
recentlyImpactedSpells.Add(newItem);
996-
else
997-
HandleNonConciseGroupingForSpell(newItem);
998-
}
1025+
AddToActiveSpellLookups(newItem);
1026+
1027+
if (isAutomaticMode)
1028+
recentlyImpactedSpells.Add(newItem);
1029+
else
1030+
HandleNonConciseGroupingForSpell(newItem); // If not in automatic mode, we can just update each item individually as they come in.
9991031
}
10001032

1001-
if (e.OldItems != null)
1033+
foreach (var oldItem in oldItems)
10021034
{
1003-
foreach (var oldItem in e.OldItems.OfType<SpellViewModel>())
1004-
{
1005-
ActiveTargets.SafelyRemove(oldItem.Target, oldItem);
1006-
ActiveSpells.SafelyAdd(oldItem.Id, oldItem);
1007-
1008-
if (oldItem.IsPlayerTarget)
1009-
PlayerTargets.SafelyRemove(oldItem.Target, oldItem);
1010-
else
1011-
NpcTargets.SafelyRemove(oldItem.Target, oldItem);
1012-
1013-
if (isAutomaticMode)
1014-
recentlyImpactedSpells.Add(oldItem);
1015-
}
1035+
RemoveFromActiveSpellLookups(oldItem);
1036+
1037+
if (isAutomaticMode)
1038+
recentlyImpactedSpells.Add(oldItem);
10161039
}
10171040

1018-
if (!isAutomaticMode)
1041+
if (!recentlyImpactedSpells.Any() && !isAutomaticMode)
10191042
return;
10201043

10211044
QueueSpellGroupingReevaluation(1000);
10221045
}
10231046

1024-
private HashSet<SpellViewModel> recentlyImpactedSpells = new HashSet<SpellViewModel>();
1025-
private CancellationTokenSource listModifiedDebounceTs;
1026-
private void QueueSpellGroupingReevaluation(int delay)
1027-
{
1028-
// We need to queue the re-evaluation with a debounce cancellation because we don't want to be doing constant iteration over the whole list while it is actively being modified.
1029-
// There is also an issue when removing whole groups that can cause the removal to remove the wrong items because the ReevaluateRelatedGroupings function actively reshapes the groupings after every pass.
1030-
// This debounce is necessary to ensure we do not waste unnecessary time or cause undesirable side effects when doing bulk operations or when several timers fall off at roughly the same time.
1031-
1032-
var evaluationItemCount = recentlyImpactedSpells.Count;
1033-
appDispatcher.DebounceToUI(ref listModifiedDebounceTs, delay,
1034-
() =>
1035-
{
1036-
var safeCloneOfRecentlyImpacted = recentlyImpactedSpells.ToList();
1037-
recentlyImpactedSpells.Clear();
1038-
1039-
ReevaluateRelatedGroupings(GetAllRelatedSpells(safeCloneOfRecentlyImpacted));
1040-
},
1041-
() => evaluationItemCount != recentlyImpactedSpells.Count);
1042-
}
1043-
10441047
private void Base_PropertyChanged(object sender, PropertyChangedEventArgs e)
10451048
{
10461049
if (e.PropertyName == nameof(IsCurrentlyClickThrough))
@@ -1054,8 +1057,17 @@ private void Base_PropertyChanged(object sender, PropertyChangedEventArgs e)
10541057
if (_SpellList == null)
10551058
return;
10561059

1057-
recentlyImpactedSpells = _SpellList.OfType<SpellViewModel>().ToHashSet();
1058-
QueueSpellGroupingReevaluation(1250);
1060+
var isAutomaticMode = IsAutomaticMode();
1061+
if (isAutomaticMode)
1062+
{
1063+
recentlyImpactedSpells = _SpellList.OfType<SpellViewModel>().ToHashSet();
1064+
QueueSpellGroupingReevaluation(1250, findRelated: false); // Don't need to find related since we're already doing the whole list.
1065+
}
1066+
else
1067+
{
1068+
recentlyImpactedSpells.Clear();
1069+
ReevaluateRelatedGroupings(_SpellList.OfType<SpellViewModel>());
1070+
}
10591071
}
10601072
}
10611073
}

0 commit comments

Comments
 (0)