Skip to content

Commit 5f8d921

Browse files
committed
Renamed Automatic to Adaptive
Brought back my old logic for splitting up the list based on user settings for the grouping type. Fixed the FullSpellListReevaluation not actually being a queue.
1 parent ba1f139 commit 5f8d921

File tree

6 files changed

+93
-87
lines changed

6 files changed

+93
-87
lines changed

EQTool/Models/EQToolSettings.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public SpellsFilterType SpellsFilter
154154
}
155155
}
156156

157-
private SpellGroupingType _PlayerSpellGroupingType = SpellGroupingType.Automatic;
157+
private SpellGroupingType _PlayerSpellGroupingType = SpellGroupingType.ByTarget;
158158
public SpellGroupingType PlayerSpellGroupingType
159159
{
160160
get => _PlayerSpellGroupingType;
@@ -170,7 +170,7 @@ public SpellGroupingType PlayerSpellGroupingType
170170
}
171171
}
172172

173-
private SpellGroupingType _NpcSpellGroupingType = SpellGroupingType.Automatic;
173+
private SpellGroupingType _NpcSpellGroupingType = SpellGroupingType.ByTarget;
174174
public SpellGroupingType NpcSpellGroupingType
175175
{
176176
get => _NpcSpellGroupingType;

EQTool/Models/SpellGroupingType.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ namespace EQTool.Models
66
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
77
public enum SpellGroupingType
88
{
9-
[Description("Automatic")]
10-
Automatic,
9+
[Description("Adaptive")]
10+
Adaptive,
1111
[Description("By Target")]
1212
ByTarget,
1313
[Description("By Spell")]

EQTool/Services/SpellGroupingEngine.cs

Lines changed: 63 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,46 @@ public SpellGroupingEngine(PlayerInfo activePlayer, EQToolSettings settings)
2222
this.settings = settings;
2323
}
2424

25-
public void Recategorize(IEnumerable<SpellViewModel> changedSpells = null)
25+
public void Recategorize(IEnumerable<SpellViewModel> recentSpells = null)
2626
{
27-
if (changedSpells != null && changedSpells.Any())
28-
changedSpells = GetConnectedSpells(changedSpells);
27+
if (recentSpells != null && recentSpells.Any())
28+
recentSpells = GetConnectedSpells(recentSpells);
2929

30-
var visibleForEval = GetSpellsNeedingAutomaticGrouping(changedSpells ?? allSpells);
31-
if (!visibleForEval.Any())
30+
var allRelatedSpells = recentSpells ?? allSpells;
31+
if (!allRelatedSpells.Any())
3232
return;
3333

34-
PerformAutomaticGrouping(visibleForEval);
34+
var nonConciseGroupingSpells = new List<SpellViewModel>();
35+
var adaptiveGroupingSpells = new List<SpellViewModel>();
36+
if (settings.PlayerSpellGroupingType == SpellGroupingType.Adaptive && settings.NpcSpellGroupingType == SpellGroupingType.Adaptive)
37+
{
38+
adaptiveGroupingSpells = allRelatedSpells.Where(s => s.ColumnVisibility == Visibility.Visible).ToList();
39+
}
40+
else if (settings.PlayerSpellGroupingType == SpellGroupingType.Adaptive)
41+
{
42+
nonConciseGroupingSpells.AddRange(allRelatedSpells.Where(s => !s.IsPlayerTarget)); // Handle em all, even the hidden ones
43+
adaptiveGroupingSpells = allRelatedSpells.Where(s => s.ColumnVisibility == Visibility.Visible && s.IsPlayerTarget).ToList();
44+
}
45+
else if (settings.NpcSpellGroupingType == SpellGroupingType.Adaptive)
46+
{
47+
nonConciseGroupingSpells.AddRange(allRelatedSpells.Where(s => s.IsPlayerTarget)); // Handle em all, even the hidden ones
48+
adaptiveGroupingSpells = allRelatedSpells.Where(s => s.ColumnVisibility == Visibility.Visible && !s.IsPlayerTarget).ToList();
49+
}
50+
else
51+
{
52+
nonConciseGroupingSpells.AddRange(allRelatedSpells);
53+
}
54+
55+
foreach (var spell in nonConciseGroupingSpells)
56+
ApplyNonAdaptiveGroupingRules(spell);
57+
58+
if (adaptiveGroupingSpells.Any())
59+
PerformAdaptiveGrouping(adaptiveGroupingSpells);
3560
}
3661

37-
private IEnumerable<SpellViewModel> GetConnectedSpells(IEnumerable<SpellViewModel> changedSpells)
62+
private IEnumerable<SpellViewModel> GetConnectedSpells(IEnumerable<SpellViewModel> recentSpells)
3863
{
39-
var seed = changedSpells as IList<SpellViewModel> ?? changedSpells.ToList();
64+
var seed = recentSpells as IList<SpellViewModel> ?? recentSpells.ToList();
4065
if (!seed.Any())
4166
return Enumerable.Empty<SpellViewModel>();
4267

@@ -116,7 +141,7 @@ public void RemoveSpells(IEnumerable<SpellViewModel> spells)
116141
}
117142
}
118143

119-
public void ApplyNonAutomaticGroupingRule(SpellViewModel spell)
144+
private void ApplyNonAdaptiveGroupingRules(SpellViewModel spell)
120145
{
121146
var mode = GetConfiguredGroupingMode(spell);
122147

@@ -129,17 +154,6 @@ public void ApplyNonAutomaticGroupingRule(SpellViewModel spell)
129154

130155
// Automatic grouping handled elsewhere due to it needed to evaluate the whole list at once.
131156
}
132-
133-
private IEnumerable<SpellViewModel> GetSpellsNeedingAutomaticGrouping(IEnumerable<SpellViewModel> spells)
134-
{
135-
var playerMode = settings.PlayerSpellGroupingType == SpellGroupingType.Automatic;
136-
var npcMode = settings.NpcSpellGroupingType == SpellGroupingType.Automatic;
137-
138-
if (!playerMode && !npcMode)
139-
return Enumerable.Empty<SpellViewModel>();
140-
141-
return spells.Where(s => s.ColumnVisibility == Visibility.Visible && ((s.IsPlayerTarget && playerMode) || (!s.IsPlayerTarget && npcMode)));
142-
}
143157

144158
private SpellGroupingType GetConfiguredGroupingMode(SpellViewModel spell)
145159
=> spell.IsPlayerTarget
@@ -149,7 +163,7 @@ private SpellGroupingType GetConfiguredGroupingMode(SpellViewModel spell)
149163
// -----------------------
150164
// Branch-and-bound algorithm. Determine what should be grouped by target and what should be grouped by Id.
151165
// -----------------------
152-
private void PerformAutomaticGrouping(IEnumerable<SpellViewModel> visibleSpells)
166+
private void PerformAdaptiveGrouping(IEnumerable<SpellViewModel> visibleSpells)
153167
{
154168
var context = new BranchAndBoundContext(activePlayer, visibleSpells);
155169

@@ -161,14 +175,9 @@ private void PerformAutomaticGrouping(IEnumerable<SpellViewModel> visibleSpells)
161175
spell.IsCategorizeById = context.SelectedIdGroups.Contains(spell.Id);
162176
}
163177

164-
private static void ComputeOptimalGroups(
165-
BranchAndBoundContext context,
166-
int groupIndex,
167-
int selectedGroupCount,
168-
int totalSpellsSelectedById,
169-
int remainingTargets)
178+
private static void ComputeOptimalGroups(BranchAndBoundContext context, int groupIndex, int selectedGroupCount, int totalSpellsSelectedById, int remainingTargets)
170179
{
171-
var stateKey = groupIndex + "|" + new string(context.SelectionMask);
180+
var stateKey = groupIndex + "|" + new string(context.DecisionCache);
172181
if (!context.Visited.Add(stateKey))
173182
return;
174183

@@ -187,16 +196,18 @@ private static void ComputeOptimalGroups(
187196
context.SelectedIdGroups.Clear();
188197

189198
for (var i = 0; i < context.Groups.Length; i++)
190-
if (context.SelectionMask[i] == '1')
199+
{
200+
if (context.DecisionCache[i] == '1')
191201
context.SelectedIdGroups.Add(context.Groups[i].GroupId);
202+
}
192203
}
193204
return;
194205
}
195206

196207
var group = context.Groups[groupIndex];
197208
if (group.SpellCount > 1)
198209
{
199-
context.SelectionMask[groupIndex] = '1';
210+
context.DecisionCache[groupIndex] = '1';
200211
var touchedTargets = new List<int>();
201212

202213
foreach (var t in context.TargetIndicesPerGroup[groupIndex])
@@ -208,21 +219,17 @@ private static void ComputeOptimalGroups(
208219
touchedTargets.Add(t);
209220
}
210221

211-
ComputeOptimalGroups(
212-
context,
213-
groupIndex + 1,
214-
selectedGroupCount + 1,
215-
totalSpellsSelectedById + group.SpellCount,
216-
remainingTargets);
222+
ComputeOptimalGroups(context, groupIndex + 1, selectedGroupCount + 1, totalSpellsSelectedById + group.SpellCount, remainingTargets);
217223

218224
foreach (var t in touchedTargets)
219225
{
220226
if (context.RemainingSpellsPerTarget[t] == 0)
221227
remainingTargets++;
228+
222229
context.RemainingSpellsPerTarget[t]++;
223230
}
224231

225-
context.SelectionMask[groupIndex] = '0';
232+
context.DecisionCache[groupIndex] = '0';
226233
}
227234

228235
ComputeOptimalGroups(context, groupIndex + 1, selectedGroupCount, totalSpellsSelectedById, remainingTargets);
@@ -233,8 +240,7 @@ private class BranchAndBoundContext
233240
public SpellIdGroupInfo[] Groups { get; }
234241
public List<int>[] TargetIndicesPerGroup { get; }
235242
public int[] RemainingSpellsPerTarget { get; }
236-
public string[] TargetList { get; }
237-
public char[] SelectionMask { get; }
243+
public char[] DecisionCache { get; }
238244
public HashSet<string> Visited { get; } = new HashSet<string>(StringComparer.Ordinal);
239245
public HashSet<string> SelectedIdGroups { get; } = new HashSet<string>();
240246

@@ -247,32 +253,38 @@ public BranchAndBoundContext(PlayerInfo activePlayer, IEnumerable<SpellViewModel
247253
.GroupBy(s => s.Id)
248254
.ToDictionary(g => g.Key, g => g.ToList());
249255

256+
var groupCount = groupsById.Count;
257+
var classSpellWeight = (int) Math.Log(groupCount, 2); // Log weight since it's weak on small datasets and strong on larger ones.
258+
250259
Groups = groupsById.Values
251-
.Select(list =>
260+
.Select(instances =>
252261
{
253262
var info = new SpellIdGroupInfo
254263
{
255-
GroupId = list[0].Id,
256-
Spells = list,
257-
SpellCount = list.Count,
258-
DistinctTargetCount = list.Select(s => s.Target).Distinct().Count(),
259-
CastableByPlayerClass = list.Any(s => s.CastByYourClass(activePlayer))
264+
GroupId = instances[0].Id,
265+
Spells = instances,
266+
SpellCount = instances.Count,
267+
DistinctTargetCount = instances.Select(s => s.Target).Distinct().Count(),
268+
CastableByPlayerClass = instances.Any(s => s.CastByYourClass(activePlayer))
260269
};
270+
261271
info.PriorityScore = info.SpellCount - info.DistinctTargetCount;
272+
// Attribute a bit more weight towards grouping by spell Id when the spell is cast by your class.
273+
if (info.CastableByPlayerClass)
274+
info.PriorityScore += classSpellWeight;
275+
262276
return info;
263277
})
264278
.OrderByDescending(g => g.CastableByPlayerClass)
265279
.ThenByDescending(g => g.PriorityScore)
266280
.ThenByDescending(g => g.SpellCount)
267281
.ToArray();
268282

269-
TargetList = visibleSpells.Select(s => s.Target).Distinct().ToArray();
270-
var targetIndex = TargetList
271-
.Select((t, i) => (t, i))
272-
.ToDictionary(x => x.t, x => x.i, StringComparer.Ordinal);
283+
var distinctTargets = visibleSpells.Select(s => s.Target).Distinct().ToArray();
284+
var targetIndex = distinctTargets.Select((t, i) => (t, i)).ToDictionary(x => x.t, x => x.i, StringComparer.Ordinal);
273285

274286
TargetIndicesPerGroup = new List<int>[Groups.Length];
275-
RemainingSpellsPerTarget = new int[TargetList.Length];
287+
RemainingSpellsPerTarget = new int[distinctTargets.Length];
276288

277289
for (var i = 0; i < Groups.Length; i++)
278290
{
@@ -287,7 +299,7 @@ public BranchAndBoundContext(PlayerInfo activePlayer, IEnumerable<SpellViewModel
287299
RemainingSpellsPerTarget[idx]++;
288300
}
289301

290-
SelectionMask = Enumerable.Repeat('0', Groups.Length).ToArray();
302+
DecisionCache = Enumerable.Repeat('0', Groups.Length).ToArray();
291303
}
292304
}
293305

EQTool/ViewModels/SpellWindowViewModel.cs

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ public bool RaidModeEnabled
222222
}
223223
};
224224
}
225-
QueueFullSpellListReevaluationIfNecessary();
225+
QueueFullSpellListReevaluation();
226226
OnPropertyChanged(nameof(RaidModeButtonToolTip));
227227
OnPropertyChanged();
228228
}
@@ -799,39 +799,33 @@ public void UpdateAPITimers()
799799
}
800800
}
801801

802-
private void QueueFullSpellListReevaluationIfNecessary()
803-
{
804-
if (IsAutomaticMode())
805-
{
806-
engine.Recategorize();
807-
}
808-
else
809-
{
810-
// Non-automatic mode: handle individually
811-
foreach (var spell in SpellList.OfType<SpellViewModel>())
812-
engine.ApplyNonAutomaticGroupingRule(spell);
813-
}
814-
}
815-
816802
private CancellationTokenSource timersModifiedDebounceTs;
817-
private void QueueSpellGroupingReevaluation(int delay)
803+
private void QueueFullSpellListReevaluation() => QueueSpellGroupingReevaluation(groupRevaluationDebounceTime, useRecentlyModified: false);
804+
private void QueueSpellGroupingReevaluation(int delay, bool useRecentlyModified = true)
818805
{
819806
// 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.
820-
// 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.
807+
// There is also an issue when the user removes whole groups at once that can cause it to remove items the user did not mean to remove due to the grouping changing in the middle of that process.
821808
// 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.
822809

823810
var evaluationItemCount = recentlyImpactedSpells.Count;
824811
appDispatcher.DebounceToUI(ref timersModifiedDebounceTs, delay, () =>
825812
{
826-
var safeCloneOfRecentlyImpacted = recentlyImpactedSpells.ToList(); // Clone it since we're about to wipe it clean
827-
recentlyImpactedSpells.Clear();
828-
829-
engine.Recategorize(safeCloneOfRecentlyImpacted);
813+
if (useRecentlyModified)
814+
{
815+
var safeCloneOfRecentlyImpacted = recentlyImpactedSpells.ToList(); // Clone it since we're about to wipe it clean but want to use it.
816+
recentlyImpactedSpells.Clear();
817+
engine.Recategorize(safeCloneOfRecentlyImpacted);
818+
}
819+
else
820+
{
821+
recentlyImpactedSpells.Clear();
822+
engine.Recategorize(recentSpells: null);
823+
}
830824
},
831-
shouldCancel: () => evaluationItemCount != recentlyImpactedSpells.Count);
825+
shouldCancel: () => useRecentlyModified && evaluationItemCount != recentlyImpactedSpells.Count);
832826
}
833827

834-
private bool IsAutomaticMode() => settings.PlayerSpellGroupingType == SpellGroupingType.Automatic || settings.NpcSpellGroupingType == SpellGroupingType.Automatic;
828+
private bool IsAdaptiveMode() => settings.PlayerSpellGroupingType == SpellGroupingType.Adaptive || settings.NpcSpellGroupingType == SpellGroupingType.Adaptive;
835829

836830
private void SpellList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
837831
{
@@ -860,7 +854,7 @@ private void Base_PropertyChanged(object sender, PropertyChangedEventArgs e)
860854
if (_SpellList == null)
861855
return;
862856

863-
QueueFullSpellListReevaluationIfNecessary();
857+
QueueFullSpellListReevaluation();
864858
}
865859
}
866860
}

EQtoolsTests/EQtoolsTests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@
206206
<Reference Include="System.Xml" />
207207
</ItemGroup>
208208
<ItemGroup>
209-
<Compile Include="SpellAutomaticGroupingTests.cs" />
209+
<Compile Include="SpellAdaptiveGroupingTests.cs" />
210210
<Compile Include="BaseTestClass.cs" />
211211
<Compile Include="BoatScheduleTests.cs" />
212212
<Compile Include="GroupLeaderTests.cs" />

EQtoolsTests/SpellAutomaticGroupingTests.cs renamed to EQtoolsTests/SpellAdaptiveGroupingTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
namespace EQtoolsTests
1414
{
1515
[TestClass]
16-
public class SpellAutomaticGroupingTests : BaseTestClass
16+
public class SpellAdaptiveGroupingTests : BaseTestClass
1717
{
1818
private readonly LogParser logParser;
1919
private readonly SpellWindowViewModel spellWindowViewModel;
2020
private readonly EQSpells eqSpells;
2121
private readonly EQToolSettings settings;
2222

23-
public SpellAutomaticGroupingTests()
23+
public SpellAdaptiveGroupingTests()
2424
{
2525
spellWindowViewModel = container.Resolve<SpellWindowViewModel>();
2626
logParser = container.Resolve<LogParser>();
@@ -43,7 +43,7 @@ public void RoutineDebuffScenario1_AllAreGroupedByTarget()
4343
* Everything should be grouped by Target */
4444

4545
// Arrange
46-
settings.NpcSpellGroupingType = SpellGroupingType.Automatic;
46+
settings.NpcSpellGroupingType = SpellGroupingType.Adaptive;
4747
settings.PlayerSpellGroupingType = SpellGroupingType.ByTarget;
4848
var logTime = DateTime.Now.Subtract(TimeSpan.FromMinutes(1));
4949

@@ -71,7 +71,7 @@ public void BasicGroupBuffScenario1_AllById()
7171

7272
// Arrange
7373
settings.NpcSpellGroupingType = SpellGroupingType.ByTarget;
74-
settings.PlayerSpellGroupingType = SpellGroupingType.Automatic;
74+
settings.PlayerSpellGroupingType = SpellGroupingType.Adaptive;
7575

7676
var groupBuffTargets = new List<string> { EQSpells.You, "Joe", "Huntor", "Sanare", "Pigy" };
7777
var logTime = DateTime.Now.Subtract(TimeSpan.FromMinutes(1));
@@ -97,7 +97,7 @@ public void DuoBuffScenario1_AllByTarget()
9797

9898
// Arrange
9999
settings.NpcSpellGroupingType = SpellGroupingType.ByTarget;
100-
settings.PlayerSpellGroupingType = SpellGroupingType.Automatic;
100+
settings.PlayerSpellGroupingType = SpellGroupingType.Adaptive;
101101

102102
var groupBuffTargets = new List<string> { EQSpells.You, "Joe" };
103103
var logTime = DateTime.Now.Subtract(TimeSpan.FromMinutes(1));
@@ -130,7 +130,7 @@ public void ComplexBuffScenario1_PerformsAsExpected()
130130

131131
// Arrange
132132
settings.NpcSpellGroupingType = SpellGroupingType.ByTarget;
133-
settings.PlayerSpellGroupingType = SpellGroupingType.Automatic;
133+
settings.PlayerSpellGroupingType = SpellGroupingType.Adaptive;
134134

135135
var groupBuffTargets = new List<string> { EQSpells.You, "Joe", "Huntor", "Sanare", "Pigy" };
136136
var logTime = DateTime.Now.Subtract(TimeSpan.FromMinutes(1));
@@ -163,7 +163,7 @@ public void ComplexBuffScenario2_PerformsAsExpected()
163163

164164
// Arrange
165165
settings.NpcSpellGroupingType = SpellGroupingType.ByTarget;
166-
settings.PlayerSpellGroupingType = SpellGroupingType.Automatic;
166+
settings.PlayerSpellGroupingType = SpellGroupingType.Adaptive;
167167

168168
var groupBuffTargets = new List<string> { EQSpells.You, "Joe", "Huntor", "Sanare", "Pigy" };
169169
var oneOffTargets = new List<string> {"Aasgard", "Pigy"};

0 commit comments

Comments
 (0)