Adjusted sorting order for preferred and weathered items

master v0.6
Liza 2024-06-19 15:20:55 +02:00
parent c60c929502
commit 6c172a84f4
Signed by: liza
GPG Key ID: 7199F8D727D55F67
7 changed files with 213 additions and 80 deletions

3
Gearsetter.Test/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/dist
/obj
/bin

View File

@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
</ItemGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gearsetter\Gearsetter.csproj"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,59 @@
using Gearsetter.GameData;
using Gearsetter.Model;
using Lumina.Excel.GeneratedSheets;
namespace Gearsetter.Test;
public sealed class ItemSortingTest
{
Lumina.GameData _lumina = new( "C:/Program Files (x86)/steam/steamapps/common/FINAL FANTASY XIV Online/game/sqpack" );
[Fact]
public void Test1()
{
var items = _lumina.GetExcelSheet<Item>()!;
List<uint> initialItemIds =
[
11851,
11853,
14447,
15131,
16039,
16240,
17436,
25928,
32558,
];
var itemList = new ItemList
{
ClassJob = EClassJob.Marauder,
EquipSlotCategory = EEquipSlotCategory.Ears,
ItemUiCategory = 41,
Items = initialItemIds.Select(rowId => new EquipmentItem(items.GetRow(rowId)!, false)).ToList(),
};
var primaryStats = _lumina.GetExcelSheet<ClassJob>()!
.Where(x => x.RowId > 0 && Enum.IsDefined(typeof(EClassJob), x.RowId))
.Where(x => x.PrimaryStat > 0)
.ToDictionary(x => (EClassJob)x.RowId, x => (EBaseParam)x.PrimaryStat);
itemList.UpdateStats(primaryStats, new Configuration());
itemList.Sort();
List<uint> expectedItems =
[
32558,
25928,
11851,
17436,
16240,
14447,
11853,
16039,
15131, // weathered earrings benefit from having primary stats
];
Assert.Equal(expectedItems, itemList.Items.Select(x => x.ItemId).ToList());
}
}

View File

@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gearsetter", "Gearsetter\Gearsetter.csproj", "{3E87693D-1FEE-486D-80E9-C6D95E68160F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gearsetter.Test", "Gearsetter.Test\Gearsetter.Test.csproj", "{19044F87-4C6D-4926-B5C8-5BB7760DC44C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -12,5 +14,9 @@ Global
{3E87693D-1FEE-486D-80E9-C6D95E68160F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E87693D-1FEE-486D-80E9-C6D95E68160F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E87693D-1FEE-486D-80E9-C6D95E68160F}.Release|Any CPU.Build.0 = Release|Any CPU
{19044F87-4C6D-4926-B5C8-5BB7760DC44C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19044F87-4C6D-4926-B5C8-5BB7760DC44C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19044F87-4C6D-4926-B5C8-5BB7760DC44C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19044F87-4C6D-4926-B5C8-5BB7760DC44C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Gearsetter.Test")]

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Version>0.5</Version>
<Version>0.6</Version>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Dalamud.Logging;
using Gearsetter.GameData;
using Lumina.Excel.GeneratedSheets;
namespace Gearsetter.Model;
@ -25,88 +25,26 @@ internal sealed class ItemList
public void Sort()
{
Items.Sort((a, b) => -Sort(a, b));
}
var preferredItems = Items
.Where(x => PreferredItems.ContainsKey(x.ItemId))
.ToList();
var defaultItems = Items
.Except(preferredItems)
.OrderDescending(new EquipmentItemComparer(SubstatPriorities))
.ToList();
private int Sort(EquipmentItem a, EquipmentItem b)
{
// special items
if (PreferredItems.ContainsKey(a.ItemId) || PreferredItems.ContainsKey(b.ItemId))
// insert the preferred items
foreach (EquipmentItem preferredItem in preferredItems)
{
byte? levelA = null;
byte? levelB = null;
if (PreferredItems.TryGetValue(a.ItemId, out byte overrideA))
levelA = overrideA;
if (PreferredItems.TryGetValue(b.ItemId, out byte overrideB))
levelB = overrideB;
if (levelA != null && levelB != null)
return levelA.Value.CompareTo(levelB.Value);
else if (levelA != null)
{
if (levelA == b.Level)
return (a.ItemLevel - 1).CompareTo(b.ItemLevel);
return levelA.Value.CompareTo(b.Level);
}
else if (levelB != null)
{
if (a.Level == levelB)
return a.ItemLevel.CompareTo(b.ItemLevel - 1);
return a.Level.CompareTo(levelB.Value);
}
int level = PreferredItems[preferredItem.ItemId];
int index = defaultItems.FindIndex(x => x.Level < level);
if (index >= 0)
defaultItems.Insert(index, preferredItem);
else
defaultItems.Add(preferredItem);
}
// weapons: most damage wins
int damageA = a.Damage;
int damageB = b.Damage;
if (damageA != damageB)
return damageA.CompareTo(damageB);
// gear: primary stat wins
int primaryStatA = a.PrimaryStat;
int primaryStatB = b.PrimaryStat;
if (primaryStatA != primaryStatB)
return primaryStatA.CompareTo(primaryStatB);
// gear: vitality wins
int vitalityA = a.Stats.Get(EBaseParam.Vitality);
int vitalityB = b.Stats.Get(EBaseParam.Vitality);
if (vitalityA != vitalityB)
return vitalityA.CompareTo(vitalityB);
// sum of relevant substats
int sumOfSubstatsA = SubstatPriorities.Sum(x => a.Stats.Get(x));
int sumOfSubstatsB = SubstatPriorities.Sum(x => b.Stats.Get(x));
// some relics have no substats in the sheets, since they can be allocated dynamically
// they are -generally- better/equal to any other weapon on that ilvl
if (sumOfSubstatsA == 0 && a.IsCombatRelicWithoutSubstats())
sumOfSubstatsA = int.MaxValue;
if (sumOfSubstatsB == 0 && b.IsCombatRelicWithoutSubstats())
sumOfSubstatsB = int.MaxValue;
if (sumOfSubstatsA != sumOfSubstatsB)
return sumOfSubstatsA.CompareTo(sumOfSubstatsB);
// level-based sorting
if (a.Level != b.Level)
return a.Level.CompareTo(b.Level);
if (a.ItemLevel != b.ItemLevel)
return a.ItemLevel.CompareTo(b.ItemLevel);
if (a.Rarity != b.Rarity)
return a.Rarity.CompareTo(b.Rarity);
// individual substats
foreach (EBaseParam substat in SubstatPriorities)
{
int substatA = a.Stats.Get(substat);
int substatB = b.Stats.Get(substat);
if (substatA != substatB)
return substatA.CompareTo(substatB);
}
// fallback
return string.CompareOrdinal(a.Name, b.Name);
Items = defaultItems;
}
public void UpdateStats(Dictionary<EClassJob, EBaseParam> primaryStats, Configuration configuration)
@ -135,4 +73,81 @@ internal sealed class ItemList
.ToList();
}
}
private sealed class EquipmentItemComparer(IReadOnlyList<EBaseParam> substatPriorities) : IComparer<EquipmentItem>
{
public int Compare(EquipmentItem? a, EquipmentItem? b)
{
ArgumentNullException.ThrowIfNull(a);
ArgumentNullException.ThrowIfNull(b);
// weapons: most damage wins
int damageA = a.Damage;
int damageB = b.Damage;
if (damageA != damageB)
return damageA.CompareTo(damageB);
// gear: primary stat wins
//
// we pretend every gear item has at least 1 primary stat to ensure weathered items are sorted last(ish),
// where they would otherwise get sorted as better-than-shire items (while that may be correct, it's also
// stupid)
int primaryStatA = Math.Max(1, a.PrimaryStat);
int primaryStatB = Math.Max(1, b.PrimaryStat);
if (primaryStatA != primaryStatB)
return primaryStatA.CompareTo(primaryStatB);
// gear: vitality wins
int vitalityA = a.Stats.Get(EBaseParam.Vitality);
int vitalityB = b.Stats.Get(EBaseParam.Vitality);
if (vitalityA != vitalityB)
return vitalityA.CompareTo(vitalityB);
// sum of relevant substats
int sumOfSubstatsA = substatPriorities.Sum(x => a.Stats.Get(x));
int sumOfSubstatsB = substatPriorities.Sum(x => b.Stats.Get(x));
// some relics have no substats in the sheets, since they can be allocated dynamically
// they are -generally- better/equal to any other weapon on that ilvl
if (sumOfSubstatsA == 0 && a.IsCombatRelicWithoutSubstats())
sumOfSubstatsA = int.MaxValue;
if (sumOfSubstatsB == 0 && b.IsCombatRelicWithoutSubstats())
sumOfSubstatsB = int.MaxValue;
if (sumOfSubstatsA != sumOfSubstatsB)
return sumOfSubstatsA.CompareTo(sumOfSubstatsB);
// level-based sorting
if (a.Level != b.Level)
return a.Level.CompareTo(b.Level);
if (a.ItemLevel != b.ItemLevel)
return a.ItemLevel.CompareTo(b.ItemLevel);
if (a.Rarity != b.Rarity)
{
// aetherial items aren't "special" enough to be sorted higher than normal gear
int rarityA = a.Rarity;
int rarityB = b.Rarity;
if (rarityA == 7)
rarityA = 1;
if (rarityB == 7)
rarityB = 1;
return rarityA.CompareTo(rarityB);
}
// individual substats
foreach (EBaseParam substat in substatPriorities)
{
int substatA = a.Stats.Get(substat);
int substatB = b.Stats.Get(substat);
if (substatA != substatB)
return substatA.CompareTo(substatB);
}
// fallback
return string.CompareOrdinal(a.Name, b.Name);
}
}
}