Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions spec/System/TestSearchHost_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
describe("SearchHost", function()
it("merges all overlapping ranges when word order is ignored", function()
local searchHost = new("SearchHost"):SearchHost(function()
return { "caster" }
end, nil, true)

for char in ("caster ast ste"):gmatch(".") do
searchHost:OnSearchChar(char)
end

assert.same({ { from = 1, to = 6 } }, searchHost.searchInfos[1].ranges)
end)
end)
46 changes: 30 additions & 16 deletions spec/System/TestTradeQueryGenerator_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -104,23 +104,37 @@ describe("TradeQueryGenerator", function()
end)

describe("Filter prioritization", function()
-- Pass: Limits mods to MAX_FILTERS (2 in test), preserving top priorities
-- Fail: Exceeds limit, indicating over-generation of filters, risking API query size errors or rate limits
it("respects MAX_FILTERS", function()
local orig_max = _G.MAX_FILTERS
_G.MAX_FILTERS = 2
mock_queryGen.modWeights = { { weight = 10, tradeModId = "id1" }, { weight = 5, tradeModId = "id2" } }
table.sort(mock_queryGen.modWeights, function(a, b)
return math.abs(a.weight) > math.abs(b.weight)
end)
local prioritized = {}
for i, entry in ipairs(mock_queryGen.modWeights) do
if #prioritized < _G.MAX_FILTERS then
table.insert(prioritized, entry)
end
it("counts socket constraints against MAX_FILTERS", function()
local queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = { items = {} } })
queryGen.modWeights = {}
for index = 1, 40 do
table.insert(queryGen.modWeights, {
tradeModId = "explicit.stat_" .. index,
weight = 1,
meanStatDiff = 41 - index,
})
end
assert.are.equal(#prioritized, 2)
_G.MAX_FILTERS = orig_max
queryGen.calcContext = {
testItem = new("Item"):Item("Rarity: RARE\nNew Item\nGold Ring\nImplicits: 0"),
baseOutput = {},
baseStatValue = 0,
itemCategoryQueryStr = "accessory.ring",
special = {},
options = {
statWeights = {},
includeMirrored = false,
sockets = 3,
},
}
queryGen.tradeTypeIndex = 1
local query
queryGen.requesterCallback = function(_, queryJson)
query = require("dkjson").decode(queryJson).query
end
queryGen:FinishQuery()

assert.are.equal(31, #query.stats[1].filters)
assert.is_not_nil(query.filters.equipment_filters.filters.rune_sockets)
end)
end)
end)
10 changes: 6 additions & 4 deletions src/Classes/DropDownControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ local m_floor = math.floor
---@class DropDownControl: Control, ControlHost, TooltipHost, SearchHost
local DropDownClass = newClass("DropDownControl", "Control", "ControlHost", "TooltipHost", "SearchHost")

function DropDownClass:DropDownControl(anchor, rect, list, selFunc, tooltipText)
function DropDownClass:DropDownControl(anchor, rect, list, selFunc, tooltipText, ignoreSearchOrder)
self:Control(anchor, rect)
self:ControlHost()
self:TooltipHost(tooltipText)
Expand All @@ -31,7 +31,8 @@ function DropDownClass:DropDownControl(anchor, rect, list, selFunc, tooltipText)
end
end
return StripEscapes(listVal)
end
end,
ignoreSearchOrder
)
self.controls.scrollBar = new("ScrollBarControl"):ScrollBarControl({ "TOPRIGHT", self, "TOPRIGHT" }, { -1, 0, 18, 0 }, (self.height - 4) * 4)
self.controls.scrollBar.height = function()
Expand Down Expand Up @@ -115,13 +116,14 @@ function DropDownClass:DrawSearchHighlights(label, searchInfo, x, y, width, heig
local endX = 0
local last = 0
SetDrawColor(1, 1, 0, 0.2)
local strippedLabel = StripEscapes(label)
for _, range in ipairs(searchInfo.ranges) do
if range.from - last - 1 > 0 then
startX = DrawStringWidth(height, "VAR", label:sub(last + 1, range.from - 1)) + x + endX
startX = DrawStringWidth(height, "VAR", strippedLabel:sub(last + 1, range.from - 1)) + x + endX
else
startX = endX
end
endX = DrawStringWidth(height, "VAR", label:sub(range.from, range.to)) + x + startX
endX = DrawStringWidth(height, "VAR", strippedLabel:sub(range.from, range.to)) + x + startX
last = range.to

DrawImage(nil, startX, y, endX - startX, height)
Expand Down
36 changes: 29 additions & 7 deletions src/Classes/SearchHost.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
---@class SearchHost
local SearchHostClass = newClass("SearchHost")

function SearchHostClass:SearchHost(listAccessor, valueAccessor)
function SearchHostClass:SearchHost(listAccessor, valueAccessor, ignoreOrder)
self.searchListAccessor = listAccessor
self.valueAccessor = valueAccessor
self.searchTerm = ""
self.searchInfos = {}
self.ignoreOrder = ignoreOrder or false
return self
end

Expand All @@ -38,7 +39,7 @@ local function wordsToCaselessPatterns(words)
return patterns
end

local function matchWords(searchWords, entry, valueAccessor)
local function matchWords(searchWords, entry, valueAccessor, ignoreOrder)
local value = valueAccessor and valueAccessor(entry) or entry
local searchInfo = { ranges = {}, matches = true }
local lastMatchEnd = 0
Expand All @@ -47,24 +48,45 @@ local function matchWords(searchWords, entry, valueAccessor)
if (from) then
local range = { from = from, to = to }
table.insert(searchInfo.ranges, range)
lastMatchEnd = to
if not ignoreOrder then
lastMatchEnd = to
end
else
-- at least one search word did not match at least once (respecting order)
searchInfo.matches = false
end
end
if ignoreOrder then
-- sort to be in left to right order
table.sort(searchInfo.ranges, function(a, b)
return a.from < b.from
end)
-- merge overlapping ranges
local i = 1
while searchInfo.ranges[i] do
local this = searchInfo.ranges[i]
local next = searchInfo.ranges[i + 1]
if next and next.from <= this.to then
this.to = math.max(this.to, next.to)
table.remove(searchInfo.ranges, i + 1)
-- Check this range again because another range may overlap it.
else
i = i + 1
end
end
end
return searchInfo
end

local function matchTerm(searchTerm, list, valueAccessor)
local function matchTerm(searchTerm, list, valueAccessor, ignoreOrder)
if not searchTerm or searchTerm == "" or not list then
return {}
end

local searchInfos = {}
local searchPatterns = wordsToCaselessPatterns(splitWords(searchTerm))
for idx, entry in ipairs(list) do
searchInfos[idx] = matchWords(searchPatterns, entry, valueAccessor)
searchInfos[idx] = matchWords(searchPatterns, entry, valueAccessor, ignoreOrder)
end
return searchInfos
end
Expand Down Expand Up @@ -114,7 +136,7 @@ end

function SearchHostClass:UpdateSearch()
if self.searchListAccessor then
self.searchInfos = matchTerm(self.searchTerm, self.searchListAccessor(), self.valueAccessor)
self.searchInfos = matchTerm(self.searchTerm, self.searchListAccessor(), self.valueAccessor, self.ignoreOrder)
self:UpdateMatchCount()
end
end
Expand All @@ -128,4 +150,4 @@ end
function SearchHostClass:GetSearchTermPretty()
local color = self:IsSearchActive() and self.matchCount > 0 and "^xFFFFFF" or "^xFF0000"
return color .. self.searchTerm
end
end
80 changes: 70 additions & 10 deletions src/Classes/TradeQueryGenerator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ local function getStatEntries(modType)
["Rune"] = "rune",
["HeartOfTheWell"] = "explicit",
["AgainstTheDarkness"] = "explicit",
["pseudo"] = "pseudo",
["Enchant"] = "enchant",
}
if tradeStatCategoryIndices[modType] then
for i, cat in ipairs(tradeStats) do
Expand Down Expand Up @@ -1016,23 +1018,62 @@ function TradeQueryGeneratorClass:FinishQuery()

local effective_max = MAX_FILTERS - num_extra

local prioritizedMods = {}
local pseudoMap = {
-- pseudo stats are disabled for PoE2 due to the trade site counting augment mods in them,
-- which would skew results significantly. however, the feature is kept here for PoB1 parity reasons
}
local ignoredStats = {
}
-- block all hybrid resistance stats
local resElements = {}
for _, elem1 in ipairs(resElements) do
for _, elem2 in ipairs(resElements) do
local stats = { string.format("%s_and_%s_damage_resistance_%%", elem1, elem2) }
ignoredStats[tostring(HashStats(stats))] = true
end
end
-- block all hybrid attribute stats
local attributeElements = {}
for _, elem1 in ipairs(attributeElements) do
for _, elem2 in ipairs(attributeElements) do
local stats = { string.format("base_%s_and_%s", elem1, elem2) }
ignoredStats[tostring(HashStats(stats))] = true
stats = { string.format("additional_%s_and_%s", elem1, elem2) }
ignoredStats[tostring(HashStats(stats))] = true
end
end
local statFilters = {}
local pseudoMods = {}
for _, entry in ipairs(self.modWeights) do
if #prioritizedMods < effective_max then
table.insert(prioritizedMods, entry)
local hash = entry.tradeModId:match("stat_(%d+)")
local filterEntry = { id = entry.tradeModId, value = { weight = (entry.invert == true and entry.weight * -1 or entry.weight) } }
-- avoid adding hybrid stats since we get the weight for them from
-- individual stats
if ignoredStats[hash] then
goto weightContinue
elseif pseudoMap[hash] then
local tradeId = pseudoMap[hash]
filterEntry.id = tradeId
-- avoid adding duplicate pseudo filters: update existing
if pseudoMods[tradeId] then
pseudoMods[tradeId].value.weight = math.max(filterEntry.value.weight, pseudoMods[tradeId].value.weight)
else
pseudoMods[tradeId] = filterEntry
table.insert(statFilters, filterEntry)
end
else
break
table.insert(statFilters, filterEntry)
end
end

self.modWeights = prioritizedMods
::weightContinue::
end

for k, v in pairs(self.calcContext.special.queryExtra or {}) do
queryTable.query[k] = v
end

for _, entry in ipairs(self.modWeights) do
t_insert(queryTable.query.stats[1].filters, { id = entry.tradeModId, value = { weight = (entry.invert == true and entry.weight * -1 or entry.weight) } })
for _, entry in ipairs(statFilters) do
t_insert(queryTable.query.stats[1].filters, entry)
filters = filters + 1
if filters == effective_max then
break
Expand Down Expand Up @@ -1362,6 +1403,25 @@ Remove: anoints are completely ignored, and removed from items.]]
end
end
end
local pseudoStats = getStatEntries("pseudo")
-- map stats and such which are clearly not relevant here
local ignoredStats = {
"^pseudo.lake",
"^pseudo.pseudo_lake",
"^pseudo.pseudo_logbook",
"^pseudo.pseudo_temple",
"^pseudo.pseudo_map",
"^pseudo.pseudo_ritual",
}
for _, entry in ipairs(pseudoStats or {}) do
for _, ignored in ipairs(ignoredStats) do
if entry.id:find(ignored) then
goto pseudoContinue
end
end
t_insert(mods, { label = s_format("^7%s (Pseudo)", entry.text), tradeId = entry.id })
::pseudoContinue::
end
return mods
end
-- amount of mod selectors: technically we could have 40, but the more we have the fewer
Expand Down Expand Up @@ -1403,7 +1463,7 @@ Remove: anoints are completely ignored, and removed from items.]]
selectedMods[i] = copyTable(val)
end
setModSelectors(controls)
end)
end, nil, true)
dropdown.shown = function()
return not not selectedMods[i - 1] or i == 1
end
Expand Down Expand Up @@ -1435,4 +1495,4 @@ Remove: anoints are completely ignored, and removed from items.]]
setModSelectors(controls, getModList())

main:OpenPopup(popupWidth, popupHeight, "Query Options", controls)
end
end
2 changes: 1 addition & 1 deletion src/Modules/Common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,7 @@ local GGG_STAT_HASH32_SEED = 0xC58F1A7B
-- used for calculating the trade hash from stat hash fields
local GGG_TRADE_SEED = 0x02312233
---@param stats string[]
---@param extraStat string extra stat for time-lost jewels
---@param extraStat string? extra stat for time-lost jewels
---@return integer
function HashStats(stats, extraStat)
if extraStat then
Expand Down
Loading