Skip to content

Commit 5f3bb5d

Browse files
mcagnionclaude
andcommitted
fix(trade): support Ignore/None/Any influence query states
Port of bugfix/trade-query-influence-none onto current origin/dev. Adapted to upstream PathOfBuildingCommunity#9691 changes: combined normalizeInfluenceSelections with copyEldritch interaction, Watcher's Eye guard, SetSel pattern, ^7 label prefixes. Updated isSpecificInfluenceSelection check for eldritch weight skip in ExecuteQuery. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e2bb84f commit 5f3bb5d

2 files changed

Lines changed: 221 additions & 24 deletions

File tree

spec/System/TestTradeQueryGenerator_spec.lua

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
describe("TradeQueryGenerator", function()
2-
local mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} })
2+
local mock_queryGen = new("TradeQueryGenerator", { itemsTab = {}, GetTradeStatusOption = function() return "online" end })
33

44
describe("ProcessMod", function()
55
-- Pass: Mod line maps correctly to trade stat entry without error
@@ -37,6 +37,70 @@ describe("TradeQueryGenerator", function()
3737
end)
3838
end)
3939

40+
describe("Influence query state", function()
41+
local IGNORE = mock_queryGen._INFLUENCE_IGNORE_INDEX -- 1
42+
local NONE = mock_queryGen._INFLUENCE_NONE_INDEX -- 2
43+
local ANY = mock_queryGen._INFLUENCE_ANY_INDEX -- 3
44+
local SHAPER = ANY + 1 -- 4
45+
local ELDER = ANY + 2 -- 5
46+
local resolve = mock_queryGen._resolveInfluenceQueryState
47+
local cost = mock_queryGen._getInfluenceFilterCost
48+
local needs = mock_queryGen._needsHasInfluenceFilter
49+
50+
-- None: uses pseudo_has_influence=0 (1 slot instead of 6-slot NOT filter)
51+
it("None uses 1-slot pseudo_has_influence=0", function()
52+
local state = resolve(NONE, IGNORE)
53+
assert.are.equal(state.exactCount, 0)
54+
assert.is_true(state.hasNoneConstraint)
55+
assert.are.equal(cost(state), 1)
56+
assert.is_true(needs(state))
57+
end)
58+
59+
-- Shaper+None: needs pseudo_has_influence=1 to cap at 1 influence (avoids Shaper+Elder matches)
60+
it("Shaper+None uses 2-slot filter (specific + pseudo_has_influence=1)", function()
61+
local state = resolve(SHAPER, NONE)
62+
assert.are.equal(state.exactCount, 1)
63+
assert.is_true(state.hasNoneConstraint)
64+
assert.are.equal(#state.specificInfluenceModIds, 1)
65+
assert.are.equal(cost(state), 2)
66+
assert.is_true(needs(state))
67+
end)
68+
69+
-- Shaper+Elder: 2 named influences, no None → no pseudo_has_influence needed (saves 1 slot)
70+
it("Shaper+Elder uses 2-slot filter (specific mods only, no pseudo_has_influence)", function()
71+
local state = resolve(SHAPER, ELDER)
72+
assert.are.equal(state.exactCount, 2)
73+
assert.is_false(state.hasNoneConstraint)
74+
assert.are.equal(#state.specificInfluenceModIds, 2)
75+
assert.are.equal(cost(state), 2)
76+
assert.is_false(needs(state))
77+
end)
78+
79+
-- Any+Ignore: minCount=1 → pseudo_has_influence min=1 (1 slot)
80+
it("Any uses 1-slot pseudo_has_influence min=1", function()
81+
local state = resolve(ANY, IGNORE)
82+
assert.are.equal(state.minCount, 1)
83+
assert.are.equal(state.exactCount, nil)
84+
assert.are.equal(cost(state), 1)
85+
assert.is_true(needs(state))
86+
end)
87+
88+
-- Any+Shaper: exactCount=2 with one unnamed slot → needs pseudo_has_influence=2
89+
it("Any+Shaper uses 2-slot filter (specific + pseudo_has_influence=2)", function()
90+
local state = resolve(ANY, SHAPER)
91+
assert.are.equal(state.exactCount, 2)
92+
assert.is_false(state.hasNoneConstraint)
93+
assert.are.equal(#state.specificInfluenceModIds, 1)
94+
assert.are.equal(cost(state), 2)
95+
assert.is_true(needs(state))
96+
end)
97+
98+
-- pseudo_has_influence mod ID is correct
99+
it("hasAnyInfluenceModId is pseudo.pseudo_has_influence_count", function()
100+
assert.are.equal(mock_queryGen._hasAnyInfluenceModId, "pseudo.pseudo_has_influence_count")
101+
end)
102+
end)
103+
40104
describe("Filter prioritization", function()
41105
-- Pass: Limits mods to MAX_FILTERS (2 in test), preserving top priorities
42106
-- Fail: Exceeds limit, indicating over-generation of filters, risking API query size errors or rate limits

src/Classes/TradeQueryGenerator.lua

Lines changed: 156 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,16 @@ local tradeStatCategoryIndices = {
8585
}
8686

8787
local influenceSuffixes = { "_shaper", "_elder", "_adjudicator", "_basilisk", "_crusader", "_eyrie"}
88-
local influenceDropdownNames = { "None" }
88+
local INFLUENCE_IGNORE_INDEX = 1
89+
local INFLUENCE_NONE_INDEX = 2
90+
local INFLUENCE_ANY_INDEX = 3
91+
local influenceDropdownNames = { "Ignore", "None", "Any" }
8992
local hasInfluenceModIds = { }
9093
for i, curInfluenceInfo in ipairs(itemLib.influenceInfo.default) do
91-
influenceDropdownNames[i + 1] = curInfluenceInfo.display
94+
influenceDropdownNames[i + INFLUENCE_ANY_INDEX] = curInfluenceInfo.display
9295
hasInfluenceModIds[i] = "pseudo.pseudo_has_" .. string.lower(curInfluenceInfo.display) .. "_influence"
9396
end
97+
local hasAnyInfluenceModId = "pseudo.pseudo_has_influence_count"
9498

9599
-- slots that allow eldritch mods (non-unique only)
96100
local eldritchModSlots = {
@@ -106,6 +110,91 @@ local function logToFile(...)
106110
ConPrintf(...)
107111
end
108112

113+
local function isIgnoredSelection(selectionIndex)
114+
return selectionIndex == nil or selectionIndex == INFLUENCE_IGNORE_INDEX
115+
end
116+
117+
local function isSpecificInfluenceSelection(selectionIndex)
118+
return selectionIndex and selectionIndex > INFLUENCE_ANY_INDEX
119+
end
120+
121+
local function isNoInfluenceSelection(selectionIndex)
122+
return selectionIndex == INFLUENCE_NONE_INDEX
123+
end
124+
125+
local function getInfluenceInfoForSelection(selectionIndex)
126+
if not isSpecificInfluenceSelection(selectionIndex) then
127+
return nil
128+
end
129+
return itemLib.influenceInfo.default[selectionIndex - INFLUENCE_ANY_INDEX]
130+
end
131+
132+
-- Influence dropdown semantics:
133+
-- Ignore = no constraint for that slot, None = missing influence slot,
134+
-- Any = present but unspecified influence slot, Specific = named influence slot.
135+
local function resolveInfluenceQueryState(selection1, selection2)
136+
local state = {
137+
exactCount = nil,
138+
minCount = nil,
139+
specificInfluenceModIds = { },
140+
hasNoneConstraint = false,
141+
}
142+
local positiveSelectionCount = 0
143+
local ignoreSelectionCount = 0
144+
local noneSelectionCount = 0
145+
local seenSpecificInfluenceModIds = { }
146+
147+
for _, selectionIndex in ipairs({ selection1 or INFLUENCE_IGNORE_INDEX, selection2 or INFLUENCE_IGNORE_INDEX }) do
148+
if isIgnoredSelection(selectionIndex) then
149+
ignoreSelectionCount = ignoreSelectionCount + 1
150+
elseif isNoInfluenceSelection(selectionIndex) then
151+
noneSelectionCount = noneSelectionCount + 1
152+
else
153+
positiveSelectionCount = positiveSelectionCount + 1
154+
if isSpecificInfluenceSelection(selectionIndex) then
155+
local influenceModId = hasInfluenceModIds[selectionIndex - INFLUENCE_ANY_INDEX]
156+
if not seenSpecificInfluenceModIds[influenceModId] then
157+
seenSpecificInfluenceModIds[influenceModId] = true
158+
t_insert(state.specificInfluenceModIds, influenceModId)
159+
end
160+
end
161+
end
162+
end
163+
164+
if noneSelectionCount > 0 then
165+
state.hasNoneConstraint = true
166+
state.exactCount = positiveSelectionCount
167+
elseif ignoreSelectionCount == 2 then
168+
return state
169+
elseif ignoreSelectionCount > 0 then
170+
if positiveSelectionCount > #state.specificInfluenceModIds then
171+
state.minCount = positiveSelectionCount
172+
end
173+
else
174+
state.exactCount = positiveSelectionCount
175+
end
176+
177+
return state
178+
end
179+
180+
-- Returns true when pseudo_has_influence must be added to enforce a count constraint.
181+
local function needsHasInfluenceFilter(influenceState)
182+
if influenceState.exactCount ~= nil then
183+
return influenceState.exactCount == 0
184+
or influenceState.hasNoneConstraint
185+
or #influenceState.specificInfluenceModIds < influenceState.exactCount
186+
end
187+
return influenceState.minCount ~= nil
188+
end
189+
190+
local function getInfluenceFilterCost(influenceState)
191+
local cost = #influenceState.specificInfluenceModIds
192+
if needsHasInfluenceFilter(influenceState) then
193+
cost = cost + 1
194+
end
195+
return cost
196+
end
197+
109198
local TradeQueryGeneratorClass = newClass("TradeQueryGenerator", function(self, queryTab)
110199
self:InitMods()
111200
self.queryTab = queryTab
@@ -789,11 +878,13 @@ function TradeQueryGeneratorClass:StartQuery(slot, options)
789878
local testItem = new("Item", itemRawStr)
790879

791880
-- Apply any requests influences
792-
if options.influence1 > 1 then
793-
testItem[itemLib.influenceInfo.default[options.influence1 - 1].key] = true
881+
local influence1 = getInfluenceInfoForSelection(options.influence1)
882+
if influence1 then
883+
testItem[influence1.key] = true
794884
end
795-
if options.influence2 > 1 then
796-
testItem[itemLib.influenceInfo.default[options.influence2 - 1].key] = true
885+
local influence2 = getInfluenceInfoForSelection(options.influence2)
886+
if influence2 then
887+
testItem[influence2.key] = true
797888
end
798889

799890
-- Calculate base output with a blank item
@@ -850,8 +941,8 @@ function TradeQueryGeneratorClass:ExecuteQuery()
850941
if self.calcContext.options.includeEldritch ~= "None" and
851942
-- skip weights if we need an influenced item as they can produce really
852943
-- bad results due to the filter limit
853-
self.calcContext.options.influence1 == 1 and
854-
self.calcContext.options.influence2 == 1 then
944+
not isSpecificInfluenceSelection(self.calcContext.options.influence1) and
945+
not isSpecificInfluenceSelection(self.calcContext.options.influence2) then
855946
local omitConditional = self.calcContext.options.includeEldritch == "Omit While"
856947
local eaterMods = self.modData["Eater"]
857948
local exarchMods = self.modData["Exarch"]
@@ -985,8 +1076,10 @@ function TradeQueryGeneratorClass:FinishQuery()
9851076
}
9861077

9871078
local options = self.calcContext.options
1079+
local influenceState = resolveInfluenceQueryState(options.influence1, options.influence2)
1080+
local influenceFilterCost = getInfluenceFilterCost(influenceState)
9881081

989-
local num_extra = 2
1082+
local num_extra = influenceFilterCost
9901083
if not options.includeMirrored then
9911084
num_extra = num_extra + 1
9921085
end
@@ -1019,12 +1112,20 @@ function TradeQueryGeneratorClass:FinishQuery()
10191112

10201113
local andFilters = { type = "and", filters = { } }
10211114
local options = self.calcContext.options
1022-
if options.influence1 > 1 then
1023-
t_insert(andFilters.filters, { id = hasInfluenceModIds[options.influence1 - 1] })
1115+
if needsHasInfluenceFilter(influenceState) then
1116+
if influenceState.exactCount == 0 then
1117+
-- "has 0 influences" cannot be queried with a value range; use NOT instead
1118+
t_insert(queryTable.query.stats, { type = "not", filters = { { id = hasAnyInfluenceModId } } })
1119+
elseif influenceState.exactCount ~= nil then
1120+
t_insert(andFilters.filters, { id = hasAnyInfluenceModId, value = { min = influenceState.exactCount, max = influenceState.exactCount } })
1121+
else
1122+
t_insert(andFilters.filters, { id = hasAnyInfluenceModId, value = { min = influenceState.minCount } })
1123+
end
10241124
filters = filters + 1
10251125
end
1026-
if options.influence2 > 1 then
1027-
t_insert(andFilters.filters, { id = hasInfluenceModIds[options.influence2 - 1] })
1126+
1127+
for _, modId in ipairs(influenceState.specificInfluenceModIds) do
1128+
t_insert(andFilters.filters, { id = modId })
10281129
filters = filters + 1
10291130
end
10301131

@@ -1118,6 +1219,15 @@ function TradeQueryGeneratorClass:FinishQuery()
11181219
main:ClosePopup()
11191220
end
11201221

1222+
-- Test accessors for influence query state logic (not used in production paths)
1223+
TradeQueryGeneratorClass._resolveInfluenceQueryState = resolveInfluenceQueryState
1224+
TradeQueryGeneratorClass._getInfluenceFilterCost = getInfluenceFilterCost
1225+
TradeQueryGeneratorClass._needsHasInfluenceFilter = needsHasInfluenceFilter
1226+
TradeQueryGeneratorClass._hasAnyInfluenceModId = hasAnyInfluenceModId
1227+
TradeQueryGeneratorClass._INFLUENCE_IGNORE_INDEX = INFLUENCE_IGNORE_INDEX
1228+
TradeQueryGeneratorClass._INFLUENCE_NONE_INDEX = INFLUENCE_NONE_INDEX
1229+
TradeQueryGeneratorClass._INFLUENCE_ANY_INDEX = INFLUENCE_ANY_INDEX
1230+
11211231
function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callback)
11221232
self.requesterCallback = callback
11231233
self.requesterContext = context
@@ -1218,23 +1328,46 @@ Remove: %s will be removed from the search results.]], term, term, term)
12181328
controls.jewelTypeLabel = new("LabelControl", {"RIGHT",controls.jewelType,"LEFT"}, {-5, 0, 0, 16}, "Jewel Type:")
12191329
updateLastAnchor(controls.jewelType)
12201330
elseif slot and not isAbyssalJewelSlot and context.slotTbl.slotName ~= "Watcher's Eye" then
1221-
local selFunc = function()
1331+
local function normalizeInfluenceSelections(changedControl)
1332+
local changedDropdown = changedControl == 1 and controls.influence1 or changedControl == 2 and controls.influence2 or nil
1333+
local otherDropdown = changedControl == 1 and controls.influence2 or changedControl == 2 and controls.influence1 or nil
1334+
1335+
if changedDropdown and otherDropdown then
1336+
if isIgnoredSelection(changedDropdown.selIndex) and isNoInfluenceSelection(otherDropdown.selIndex) then
1337+
changedDropdown:SetSel(INFLUENCE_NONE_INDEX)
1338+
return
1339+
elseif isNoInfluenceSelection(changedDropdown.selIndex) and isIgnoredSelection(otherDropdown.selIndex) then
1340+
otherDropdown:SetSel(INFLUENCE_NONE_INDEX)
1341+
return
1342+
elseif isSpecificInfluenceSelection(changedDropdown.selIndex) and changedDropdown.selIndex == otherDropdown.selIndex then
1343+
changedDropdown:SetSel(INFLUENCE_ANY_INDEX)
1344+
return
1345+
end
1346+
end
1347+
1348+
if isIgnoredSelection(controls.influence1.selIndex) and isNoInfluenceSelection(controls.influence2.selIndex) then
1349+
controls.influence1:SetSel(INFLUENCE_NONE_INDEX)
1350+
elseif isNoInfluenceSelection(controls.influence1.selIndex) and isIgnoredSelection(controls.influence2.selIndex) then
1351+
controls.influence2:SetSel(INFLUENCE_NONE_INDEX)
1352+
end
1353+
12221354
-- influenced items can't have eldritch implicits
12231355
if controls.copyEldritch and isEldritchModSlot then
1224-
local hasInfluence1 = controls.influence1 and controls.influence1:GetSelValue() ~= "None"
1225-
local hasInfluence2 = controls.influence2 and controls.influence2:GetSelValue() ~= "None"
1356+
local hasInfluence1 = controls.influence1 and not isIgnoredSelection(controls.influence1.selIndex) and not isNoInfluenceSelection(controls.influence1.selIndex)
1357+
local hasInfluence2 = controls.influence2 and not isIgnoredSelection(controls.influence2.selIndex) and not isNoInfluenceSelection(controls.influence2.selIndex)
12261358
controls.copyEldritch.enabled = not hasInfluence1 and not hasInfluence2
12271359
end
12281360
end
1361+
12291362
controls.influence1 = new("DropDownControl", { "TOPLEFT", lastItemAnchor, "BOTTOMLEFT" }, { 0, 5, 100, 18 },
1230-
influenceDropdownNames, selFunc)
1231-
controls.influence1:SetSel(self.lastInfluence1 or 1)
1363+
influenceDropdownNames, function() normalizeInfluenceSelections(1) end)
1364+
controls.influence1:SetSel(self.lastInfluence1 or INFLUENCE_IGNORE_INDEX)
12321365
controls.influence1Label = new("LabelControl", {"RIGHT",controls.influence1,"LEFT"}, {-5, 0, 0, 16}, "^7Influence 1:")
12331366

12341367
controls.influence2 = new("DropDownControl", { "TOPLEFT", controls.influence1, "BOTTOMLEFT" }, { 0, 5, 100, 18 },
1235-
influenceDropdownNames, selFunc)
1236-
controls.influence2:SetSel(self.lastInfluence2 or 1)
1237-
selFunc()
1368+
influenceDropdownNames, function() normalizeInfluenceSelections(2) end)
1369+
controls.influence2:SetSel(self.lastInfluence2 or INFLUENCE_IGNORE_INDEX)
1370+
normalizeInfluenceSelections()
12381371
controls.influence2Label = new("LabelControl", { "RIGHT", controls.influence2, "LEFT" }, { -5, 0, 0, 16 },
12391372
"^7Influence 2:")
12401373
updateLastAnchor(controls.influence2, 46)
@@ -1335,12 +1468,12 @@ Remove: %s will be removed from the search results.]], term, term, term)
13351468
if controls.influence1 then
13361469
self.lastInfluence1, options.influence1 = controls.influence1.selIndex, controls.influence1.selIndex
13371470
else
1338-
options.influence1 = 1
1471+
options.influence1 = INFLUENCE_IGNORE_INDEX
13391472
end
13401473
if controls.influence2 then
13411474
self.lastInfluence2, options.influence2 = controls.influence2.selIndex, controls.influence2.selIndex
13421475
else
1343-
options.influence2 = 1
1476+
options.influence2 = INFLUENCE_IGNORE_INDEX
13441477
end
13451478
if controls.jewelType then
13461479
self.lastJewelType = controls.jewelType.selIndex

0 commit comments

Comments
 (0)