@@ -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