From 3bbb52e83830e8a96c4cc3132336b319ef60e59e Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Thu, 16 Apr 2026 20:25:51 +0200 Subject: [PATCH 1/5] fix(trade): support Ignore/None/Any influence query states Port of bugfix/trade-query-influence-none onto current origin/dev. Adapted to upstream PR 9691 changes: combined normalizeInfluenceSelections with copyEldritch interaction, Watcher's Eye guard, SetSel pattern, and ^7 label prefixes. Updated the eldritch weight skip in ExecuteQuery for every influence-positive selection. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/System/TestTradeQueryGenerator_spec.lua | 112 +++++++++++- src/Classes/TradeQueryGenerator.lua | 179 ++++++++++++++++--- 2 files changed, 266 insertions(+), 25 deletions(-) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index e11ea701b9..6d9c829da7 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -1,5 +1,5 @@ describe("TradeQueryGenerator", function() - local mock_queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = {} }) + local mock_queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = {}, GetTradeStatusOption = function() return "online" end }) describe("ProcessMod", function() -- Pass: Mod line maps correctly to trade stat entry without error @@ -153,6 +153,113 @@ describe("TradeQueryGenerator", function() end) end) + describe("Influence query state", function() + local IGNORE = mock_queryGen._INFLUENCE_IGNORE_INDEX -- 1 + local NONE = mock_queryGen._INFLUENCE_NONE_INDEX -- 2 + local ANY = mock_queryGen._INFLUENCE_ANY_INDEX -- 3 + local SHAPER = ANY + 1 -- 4 + local ELDER = ANY + 2 -- 5 + local resolve = mock_queryGen._resolveInfluenceQueryState + local cost = mock_queryGen._getInfluenceFilterCost + local needs = mock_queryGen._needsHasInfluenceFilter + + -- None: uses pseudo_has_influence=0 (1 slot instead of 6-slot NOT filter) + it("None uses 1-slot pseudo_has_influence=0", function() + local state = resolve(NONE, IGNORE) + assert.are.equal(state.exactCount, 0) + assert.is_true(state.hasNoneConstraint) + assert.are.equal(cost(state), 1) + assert.is_true(needs(state)) + end) + + -- Shaper+None: needs pseudo_has_influence=1 to cap at 1 influence (avoids Shaper+Elder matches) + it("Shaper+None uses 2-slot filter (specific + pseudo_has_influence=1)", function() + local state = resolve(SHAPER, NONE) + assert.are.equal(state.exactCount, 1) + assert.is_true(state.hasNoneConstraint) + assert.are.equal(#state.specificInfluenceModIds, 1) + assert.are.equal(cost(state), 2) + assert.is_true(needs(state)) + end) + + -- Shaper+Elder: 2 named influences, no None → no pseudo_has_influence needed (saves 1 slot) + it("Shaper+Elder uses 2-slot filter (specific mods only, no pseudo_has_influence)", function() + local state = resolve(SHAPER, ELDER) + assert.are.equal(state.exactCount, 2) + assert.is_false(state.hasNoneConstraint) + assert.are.equal(#state.specificInfluenceModIds, 2) + assert.are.equal(cost(state), 2) + assert.is_false(needs(state)) + end) + + -- Any+Ignore: minCount=1 → pseudo_has_influence min=1 (1 slot) + it("Any uses 1-slot pseudo_has_influence min=1", function() + local state = resolve(ANY, IGNORE) + assert.are.equal(state.minCount, 1) + assert.are.equal(state.exactCount, nil) + assert.are.equal(cost(state), 1) + assert.is_true(needs(state)) + end) + + -- Any+Shaper: exactCount=2 with one unnamed slot → needs pseudo_has_influence=2 + it("Any+Shaper uses 2-slot filter (specific + pseudo_has_influence=2)", function() + local state = resolve(ANY, SHAPER) + assert.are.equal(state.exactCount, 2) + assert.is_false(state.hasNoneConstraint) + assert.are.equal(#state.specificInfluenceModIds, 1) + assert.are.equal(cost(state), 2) + assert.is_true(needs(state)) + end) + + -- pseudo_has_influence mod ID is correct + it("hasAnyInfluenceModId is pseudo.pseudo_has_influence_count", function() + assert.are.equal(mock_queryGen._hasAnyInfluenceModId, "pseudo.pseudo_has_influence_count") + end) + end) + + describe("Eldritch mod weights", function() + local IGNORE = mock_queryGen._INFLUENCE_IGNORE_INDEX + local NONE = mock_queryGen._INFLUENCE_NONE_INDEX + local ANY = mock_queryGen._INFLUENCE_ANY_INDEX + + local function generatesEldritchWeights(influence1, influence2) + local queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = {} }) + local eaterMod = { } + local exarchMod = { } + queryGen.modData = { + Explicit = { }, + Implicit = { }, + Eater = { EaterMod = eaterMod }, + Exarch = { ExarchMod = exarchMod }, + } + queryGen.calcContext = { + special = { }, + options = { + includeEldritch = "Keep regular", + influence1 = influence1, + influence2 = influence2, + }, + } + local generated = { } + queryGen.GenerateModWeights = function(_, mods) + for _, mod in pairs(mods) do + generated[mod] = true + end + end + + queryGen:ExecuteQuery() + return generated[eaterMod] == true and generated[exarchMod] == true + end + + it("skips weights when Any requires an influenced item", function() + assert.is_false(generatesEldritchWeights(ANY, IGNORE)) + end) + + it("keeps weights when both influence slots are unconstrained or empty", function() + assert.is_true(generatesEldritchWeights(IGNORE, NONE)) + end) + end) + describe("Filter prioritization", function() it("counts socket and link constraints against MAX_FILTERS", function() local queryGen = new("TradeQueryGenerator"):TradeQueryGenerator({ itemsTab = { items = {} } }) @@ -187,7 +294,8 @@ describe("TradeQueryGenerator", function() queryGen:FinishQuery() - assert.are.equal(31, #query.stats[1].filters) + -- Ignore / Ignore uses no influence slots: 36 - mirrored - sockets - links. + assert.are.equal(33, #query.stats[1].filters) assert.is_not_nil(query.filters.socket_filters.filters.sockets) assert.is_not_nil(query.filters.socket_filters.filters.links) end) diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index 9b7e3f5b40..39b63904bd 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -132,12 +132,17 @@ local function getStatEntries(modType) end end -local influenceDropdownNames = { "None" } +local influenceSuffixes = { "_shaper", "_elder", "_adjudicator", "_basilisk", "_crusader", "_eyrie"} +local INFLUENCE_IGNORE_INDEX = 1 +local INFLUENCE_NONE_INDEX = 2 +local INFLUENCE_ANY_INDEX = 3 +local influenceDropdownNames = { "Ignore", "None", "Any" } local hasInfluenceModIds = { } for i, curInfluenceInfo in ipairs(itemLib.influenceInfo.default) do - influenceDropdownNames[i + 1] = curInfluenceInfo.display + influenceDropdownNames[i + INFLUENCE_ANY_INDEX] = curInfluenceInfo.display hasInfluenceModIds[i] = "pseudo.pseudo_has_" .. string.lower(curInfluenceInfo.display) .. "_influence" end +local hasAnyInfluenceModId = "pseudo.pseudo_has_influence_count" -- slots that allow eldritch mods (non-unique only) local eldritchModSlots = { @@ -153,6 +158,91 @@ local function logToFile(...) ConPrintf(...) end +local function isIgnoredSelection(selectionIndex) + return selectionIndex == nil or selectionIndex == INFLUENCE_IGNORE_INDEX +end + +local function isSpecificInfluenceSelection(selectionIndex) + return selectionIndex and selectionIndex > INFLUENCE_ANY_INDEX +end + +local function isNoInfluenceSelection(selectionIndex) + return selectionIndex == INFLUENCE_NONE_INDEX +end + +local function getInfluenceInfoForSelection(selectionIndex) + if not isSpecificInfluenceSelection(selectionIndex) then + return nil + end + return itemLib.influenceInfo.default[selectionIndex - INFLUENCE_ANY_INDEX] +end + +-- Influence dropdown semantics: +-- Ignore = no constraint for that slot, None = missing influence slot, +-- Any = present but unspecified influence slot, Specific = named influence slot. +local function resolveInfluenceQueryState(selection1, selection2) + local state = { + exactCount = nil, + minCount = nil, + specificInfluenceModIds = { }, + hasNoneConstraint = false, + } + local positiveSelectionCount = 0 + local ignoreSelectionCount = 0 + local noneSelectionCount = 0 + local seenSpecificInfluenceModIds = { } + + for _, selectionIndex in ipairs({ selection1 or INFLUENCE_IGNORE_INDEX, selection2 or INFLUENCE_IGNORE_INDEX }) do + if isIgnoredSelection(selectionIndex) then + ignoreSelectionCount = ignoreSelectionCount + 1 + elseif isNoInfluenceSelection(selectionIndex) then + noneSelectionCount = noneSelectionCount + 1 + else + positiveSelectionCount = positiveSelectionCount + 1 + if isSpecificInfluenceSelection(selectionIndex) then + local influenceModId = hasInfluenceModIds[selectionIndex - INFLUENCE_ANY_INDEX] + if not seenSpecificInfluenceModIds[influenceModId] then + seenSpecificInfluenceModIds[influenceModId] = true + t_insert(state.specificInfluenceModIds, influenceModId) + end + end + end + end + + if noneSelectionCount > 0 then + state.hasNoneConstraint = true + state.exactCount = positiveSelectionCount + elseif ignoreSelectionCount == 2 then + return state + elseif ignoreSelectionCount > 0 then + if positiveSelectionCount > #state.specificInfluenceModIds then + state.minCount = positiveSelectionCount + end + else + state.exactCount = positiveSelectionCount + end + + return state +end + +-- Returns true when pseudo_has_influence must be added to enforce a count constraint. +local function needsHasInfluenceFilter(influenceState) + if influenceState.exactCount ~= nil then + return influenceState.exactCount == 0 + or influenceState.hasNoneConstraint + or #influenceState.specificInfluenceModIds < influenceState.exactCount + end + return influenceState.minCount ~= nil +end + +local function getInfluenceFilterCost(influenceState) + local cost = #influenceState.specificInfluenceModIds + if needsHasInfluenceFilter(influenceState) then + cost = cost + 1 + end + return cost +end + ---@class TradeQueryGenerator local TradeQueryGeneratorClass = newClass("TradeQueryGenerator") @@ -730,11 +820,13 @@ function TradeQueryGeneratorClass:StartQuery(slot, options) local testItem = new("Item"):Item(itemRawStr) -- Apply any requests influences - if options.influence1 > 1 then - testItem[itemLib.influenceInfo.default[options.influence1 - 1].key] = true + local influence1 = getInfluenceInfoForSelection(options.influence1) + if influence1 then + testItem[influence1.key] = true end - if options.influence2 > 1 then - testItem[itemLib.influenceInfo.default[options.influence2 - 1].key] = true + local influence2 = getInfluenceInfoForSelection(options.influence2) + if influence2 then + testItem[influence2.key] = true end -- Calculate base output with a blank item @@ -796,8 +888,8 @@ function TradeQueryGeneratorClass:ExecuteQuery() if eldritchOption and eldritchOption:find("^Keep") and -- skip weights if we need an influenced item as they can produce really -- bad results due to the filter limit - self.calcContext.options.influence1 == 1 and - self.calcContext.options.influence2 == 1 then + (isIgnoredSelection(self.calcContext.options.influence1) or isNoInfluenceSelection(self.calcContext.options.influence1)) and + (isIgnoredSelection(self.calcContext.options.influence2) or isNoInfluenceSelection(self.calcContext.options.influence2)) then local omitConditional = eldritchOption == "Keep regular" local eaterMods = self.modData["Eater"] local exarchMods = self.modData["Exarch"] @@ -937,8 +1029,10 @@ function TradeQueryGeneratorClass:FinishQuery() end local options = self.calcContext.options + local influenceState = resolveInfluenceQueryState(options.influence1, options.influence2) + local influenceFilterCost = getInfluenceFilterCost(influenceState) - local num_extra = 2 + local num_extra = influenceFilterCost if not options.includeMirrored then num_extra = num_extra + 1 end @@ -1023,12 +1117,20 @@ function TradeQueryGeneratorClass:FinishQuery() local andFilters = { type = "and", filters = { } } local options = self.calcContext.options - if options.influence1 > 1 then - t_insert(andFilters.filters, { id = hasInfluenceModIds[options.influence1 - 1] }) + if needsHasInfluenceFilter(influenceState) then + if influenceState.exactCount == 0 then + -- "has 0 influences" cannot be queried with a value range; use NOT instead + t_insert(queryTable.query.stats, { type = "not", filters = { { id = hasAnyInfluenceModId } } }) + elseif influenceState.exactCount ~= nil then + t_insert(andFilters.filters, { id = hasAnyInfluenceModId, value = { min = influenceState.exactCount, max = influenceState.exactCount } }) + else + t_insert(andFilters.filters, { id = hasAnyInfluenceModId, value = { min = influenceState.minCount } }) + end filters = filters + 1 end - if options.influence2 > 1 then - t_insert(andFilters.filters, { id = hasInfluenceModIds[options.influence2 - 1] }) + + for _, modId in ipairs(influenceState.specificInfluenceModIds) do + t_insert(andFilters.filters, { id = modId }) filters = filters + 1 end @@ -1125,6 +1227,15 @@ function TradeQueryGeneratorClass:FinishQuery() main:ClosePopup() end +-- Test accessors for influence query state logic (not used in production paths) +TradeQueryGeneratorClass._resolveInfluenceQueryState = resolveInfluenceQueryState +TradeQueryGeneratorClass._getInfluenceFilterCost = getInfluenceFilterCost +TradeQueryGeneratorClass._needsHasInfluenceFilter = needsHasInfluenceFilter +TradeQueryGeneratorClass._hasAnyInfluenceModId = hasAnyInfluenceModId +TradeQueryGeneratorClass._INFLUENCE_IGNORE_INDEX = INFLUENCE_IGNORE_INDEX +TradeQueryGeneratorClass._INFLUENCE_NONE_INDEX = INFLUENCE_NONE_INDEX +TradeQueryGeneratorClass._INFLUENCE_ANY_INDEX = INFLUENCE_ANY_INDEX + function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callback) self.requesterCallback = callback self.requesterContext = context @@ -1250,23 +1361,45 @@ Remove: %s will be removed from the search results.]], term, term, term) controls.jewelTypeLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.jewelType, "LEFT" }, { -5, 0, 0, 16 }, "Jewel Type:") updateLastAnchor(controls.jewelType) elseif slot and not isAbyssalJewelSlot and context.slotTbl.slotName ~= "Watcher's Eye" then - local selFunc = function() + local function normalizeInfluenceSelections(changedControl) + local changedDropdown = changedControl == 1 and controls.influence1 or changedControl == 2 and controls.influence2 or nil + local otherDropdown = changedControl == 1 and controls.influence2 or changedControl == 2 and controls.influence1 or nil + + if changedDropdown and otherDropdown then + if isIgnoredSelection(changedDropdown.selIndex) and isNoInfluenceSelection(otherDropdown.selIndex) then + changedDropdown:SetSel(INFLUENCE_NONE_INDEX) + return + elseif isNoInfluenceSelection(changedDropdown.selIndex) and isIgnoredSelection(otherDropdown.selIndex) then + otherDropdown:SetSel(INFLUENCE_NONE_INDEX) + return + elseif isSpecificInfluenceSelection(changedDropdown.selIndex) and changedDropdown.selIndex == otherDropdown.selIndex then + changedDropdown:SetSel(INFLUENCE_ANY_INDEX) + return + end + end + + if isIgnoredSelection(controls.influence1.selIndex) and isNoInfluenceSelection(controls.influence2.selIndex) then + controls.influence1:SetSel(INFLUENCE_NONE_INDEX) + elseif isNoInfluenceSelection(controls.influence1.selIndex) and isIgnoredSelection(controls.influence2.selIndex) then + controls.influence2:SetSel(INFLUENCE_NONE_INDEX) + end + -- influenced items can't have eldritch implicits if controls.includeEldritch and isEldritchModSlot then - local hasInfluence1 = controls.influence1 and controls.influence1:GetSelValue() ~= "None" - local hasInfluence2 = controls.influence2 and controls.influence2:GetSelValue() ~= "None" + local hasInfluence1 = controls.influence1 and not isIgnoredSelection(controls.influence1.selIndex) and not isNoInfluenceSelection(controls.influence1.selIndex) + local hasInfluence2 = controls.influence2 and not isIgnoredSelection(controls.influence2.selIndex) and not isNoInfluenceSelection(controls.influence2.selIndex) controls.includeEldritch.enabled = not hasInfluence1 and not hasInfluence2 end end controls.influence1 = new("DropDownControl"):DropDownControl({ "TOPLEFT", lastItemAnchor, "BOTTOMLEFT" }, { 0, 5, 100, 18 }, - influenceDropdownNames, selFunc) - controls.influence1:SetSel(self.lastInfluence1 or 1) + influenceDropdownNames, function() normalizeInfluenceSelections(1) end) + controls.influence1:SetSel(self.lastInfluence1 or INFLUENCE_IGNORE_INDEX) controls.influence1Label = new("LabelControl"):LabelControl({"RIGHT",controls.influence1,"LEFT"}, {-5, 0, 0, 16}, "^7Influence 1:") controls.influence2 = new("DropDownControl"):DropDownControl({ "TOPLEFT", controls.influence1, "BOTTOMLEFT" }, { 0, 5, 100, 18 }, - influenceDropdownNames, selFunc) - controls.influence2:SetSel(self.lastInfluence2 or 1) - selFunc() + influenceDropdownNames, function() normalizeInfluenceSelections(2) end) + controls.influence2:SetSel(self.lastInfluence2 or INFLUENCE_IGNORE_INDEX) + normalizeInfluenceSelections() controls.influence2Label = new("LabelControl"):LabelControl({ "RIGHT", controls.influence2, "LEFT" }, { -5, 0, 0, 16 }, "^7Influence 2:") updateLastAnchor(controls.influence2, 46) @@ -1364,12 +1497,12 @@ Remove: %s will be removed from the search results.]], term, term, term) if controls.influence1 then self.lastInfluence1, options.influence1 = controls.influence1.selIndex, controls.influence1.selIndex else - options.influence1 = 1 + options.influence1 = INFLUENCE_IGNORE_INDEX end if controls.influence2 then self.lastInfluence2, options.influence2 = controls.influence2.selIndex, controls.influence2.selIndex else - options.influence2 = 1 + options.influence2 = INFLUENCE_IGNORE_INDEX end if controls.jewelType then self.lastJewelType = controls.jewelType.selIndex From 04cc4074cc5daf74be3d51b39a8409127b9ca125 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Tue, 21 Apr 2026 01:32:35 +0200 Subject: [PATCH 2/5] fix(trade): preserve every valid influence pair in the query UI The influence callback was over-normalizing Ignore+None and None+specific pairs, forcing them back to None/None and locking users out of states the query state resolver already supports (exactCount=1, exactCount=1 plus that specific). Users stuck at None/None could not return to an unfiltered pair. Drop the UI normalization. The callback keeps only the eldritch side-effect it was carrying. All pair combinations now flow through to resolveInfluenceQueryState as intended. Same specific on both sides (Shaper/Shaper) is redundant at the item level, so resolveInfluenceQueryState treats the second slot as None: the pair now generates the same query as Shaper/None (exactly 1 of that type, pseudo_has_influence capped at 1). Without the None constraint the duplicate specific would have silently produced a looser query than the paired form. Test coverage added asserts state equality and query-cost equality with the paired form. Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/System/TestTradeQueryGenerator_spec.lua | 21 +++++++++ src/Classes/TradeQueryGenerator.lua | 49 +++++++------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index 6d9c829da7..131e6a04f6 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -215,6 +215,27 @@ describe("TradeQueryGenerator", function() it("hasAnyInfluenceModId is pseudo.pseudo_has_influence_count", function() assert.are.equal(mock_queryGen._hasAnyInfluenceModId, "pseudo.pseudo_has_influence_count") end) + + -- Same specific influence on both sides is redundant at the item level and must + -- collapse to the / None semantic (exactly 1 of that type), not + -- double-count and not collapse to / Ignore. Verify via the + -- needsHasInfluenceFilter gate that builds the pseudo_has_influence cap — without + -- it, the query would match items with any number of influences including Shaper. + it("Shaper+Shaper produces the full Shaper+None query state", function() + local dup = resolve(SHAPER, SHAPER) + local paired = resolve(SHAPER, NONE) + assert.are.equal(dup.exactCount, paired.exactCount) + assert.are.equal(dup.hasNoneConstraint, paired.hasNoneConstraint) + assert.are.equal(#dup.specificInfluenceModIds, #paired.specificInfluenceModIds) + assert.are.equal(dup.specificInfluenceModIds[1], paired.specificInfluenceModIds[1]) + assert.are.equal(cost(dup), cost(paired)) + assert.are.equal(needs(dup), needs(paired)) + end) + + it("Shaper+Shaper needs the pseudo_has_influence cap to enforce exactly 1", function() + local state = resolve(SHAPER, SHAPER) + assert.is_true(needs(state)) + end) end) describe("Eldritch mod weights", function() diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index 39b63904bd..ca63a3af71 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -197,15 +197,20 @@ local function resolveInfluenceQueryState(selection1, selection2) ignoreSelectionCount = ignoreSelectionCount + 1 elseif isNoInfluenceSelection(selectionIndex) then noneSelectionCount = noneSelectionCount + 1 + elseif isSpecificInfluenceSelection(selectionIndex) then + local influenceModId = hasInfluenceModIds[selectionIndex - INFLUENCE_ANY_INDEX] + if not seenSpecificInfluenceModIds[influenceModId] then + seenSpecificInfluenceModIds[influenceModId] = true + t_insert(state.specificInfluenceModIds, influenceModId) + positiveSelectionCount = positiveSelectionCount + 1 + else + -- Same specific on both sides is redundant at the item level; treat the + -- second slot as None so the pair behaves exactly like / None + -- (exactly 1 of that type, pseudo_has_influence capped at 1). + state.hasNoneConstraint = true + end else positiveSelectionCount = positiveSelectionCount + 1 - if isSpecificInfluenceSelection(selectionIndex) then - local influenceModId = hasInfluenceModIds[selectionIndex - INFLUENCE_ANY_INDEX] - if not seenSpecificInfluenceModIds[influenceModId] then - seenSpecificInfluenceModIds[influenceModId] = true - t_insert(state.specificInfluenceModIds, influenceModId) - end - end end end @@ -1361,29 +1366,7 @@ Remove: %s will be removed from the search results.]], term, term, term) controls.jewelTypeLabel = new("LabelControl"):LabelControl({ "RIGHT", controls.jewelType, "LEFT" }, { -5, 0, 0, 16 }, "Jewel Type:") updateLastAnchor(controls.jewelType) elseif slot and not isAbyssalJewelSlot and context.slotTbl.slotName ~= "Watcher's Eye" then - local function normalizeInfluenceSelections(changedControl) - local changedDropdown = changedControl == 1 and controls.influence1 or changedControl == 2 and controls.influence2 or nil - local otherDropdown = changedControl == 1 and controls.influence2 or changedControl == 2 and controls.influence1 or nil - - if changedDropdown and otherDropdown then - if isIgnoredSelection(changedDropdown.selIndex) and isNoInfluenceSelection(otherDropdown.selIndex) then - changedDropdown:SetSel(INFLUENCE_NONE_INDEX) - return - elseif isNoInfluenceSelection(changedDropdown.selIndex) and isIgnoredSelection(otherDropdown.selIndex) then - otherDropdown:SetSel(INFLUENCE_NONE_INDEX) - return - elseif isSpecificInfluenceSelection(changedDropdown.selIndex) and changedDropdown.selIndex == otherDropdown.selIndex then - changedDropdown:SetSel(INFLUENCE_ANY_INDEX) - return - end - end - - if isIgnoredSelection(controls.influence1.selIndex) and isNoInfluenceSelection(controls.influence2.selIndex) then - controls.influence1:SetSel(INFLUENCE_NONE_INDEX) - elseif isNoInfluenceSelection(controls.influence1.selIndex) and isIgnoredSelection(controls.influence2.selIndex) then - controls.influence2:SetSel(INFLUENCE_NONE_INDEX) - end - + local function refreshInfluenceDependentControls() -- influenced items can't have eldritch implicits if controls.includeEldritch and isEldritchModSlot then local hasInfluence1 = controls.influence1 and not isIgnoredSelection(controls.influence1.selIndex) and not isNoInfluenceSelection(controls.influence1.selIndex) @@ -1392,14 +1375,14 @@ Remove: %s will be removed from the search results.]], term, term, term) end end controls.influence1 = new("DropDownControl"):DropDownControl({ "TOPLEFT", lastItemAnchor, "BOTTOMLEFT" }, { 0, 5, 100, 18 }, - influenceDropdownNames, function() normalizeInfluenceSelections(1) end) + influenceDropdownNames, refreshInfluenceDependentControls) controls.influence1:SetSel(self.lastInfluence1 or INFLUENCE_IGNORE_INDEX) controls.influence1Label = new("LabelControl"):LabelControl({"RIGHT",controls.influence1,"LEFT"}, {-5, 0, 0, 16}, "^7Influence 1:") controls.influence2 = new("DropDownControl"):DropDownControl({ "TOPLEFT", controls.influence1, "BOTTOMLEFT" }, { 0, 5, 100, 18 }, - influenceDropdownNames, function() normalizeInfluenceSelections(2) end) + influenceDropdownNames, refreshInfluenceDependentControls) controls.influence2:SetSel(self.lastInfluence2 or INFLUENCE_IGNORE_INDEX) - normalizeInfluenceSelections() + refreshInfluenceDependentControls() controls.influence2Label = new("LabelControl"):LabelControl({ "RIGHT", controls.influence2, "LEFT" }, { -5, 0, 0, 16 }, "^7Influence 2:") updateLastAnchor(controls.influence2, 46) From 4d73bdd8607c959450df584f338f9f261b91460e Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Tue, 21 Apr 2026 01:33:00 +0200 Subject: [PATCH 3/5] feat(trade): document influence-filter combinations via tooltip Add a shared tooltip on both Influence fields listing the nine distinct pair semantics (no filter, no influences, exactly 1, at least 1, exactly 2, plus the specific variants). Ordered by the number of required influences so users can scan to the constraint they want. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Classes/TradeQueryGenerator.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index ca63a3af71..dfe34a7991 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -1374,14 +1374,29 @@ Remove: %s will be removed from the search results.]], term, term, term) controls.includeEldritch.enabled = not hasInfluence1 and not hasInfluence2 end end + local influenceTooltipText = table.concat({ + "^7Influence filter (both fields combine):", + "Ignore / Ignore: no filter", + "None / None: no influences", + "Any / None: exactly 1 influence", + " / None: exactly 1, that specific", + "Any / Ignore: at least 1 influence", + " / Ignore: at least 1, that specific", + "Any / Any: exactly 2 influences", + " / Any: exactly 2, including that specific", + " / : exactly 2, both specifics", + }, "\n") + controls.influence1 = new("DropDownControl"):DropDownControl({ "TOPLEFT", lastItemAnchor, "BOTTOMLEFT" }, { 0, 5, 100, 18 }, influenceDropdownNames, refreshInfluenceDependentControls) controls.influence1:SetSel(self.lastInfluence1 or INFLUENCE_IGNORE_INDEX) + controls.influence1.tooltipText = influenceTooltipText controls.influence1Label = new("LabelControl"):LabelControl({"RIGHT",controls.influence1,"LEFT"}, {-5, 0, 0, 16}, "^7Influence 1:") controls.influence2 = new("DropDownControl"):DropDownControl({ "TOPLEFT", controls.influence1, "BOTTOMLEFT" }, { 0, 5, 100, 18 }, influenceDropdownNames, refreshInfluenceDependentControls) controls.influence2:SetSel(self.lastInfluence2 or INFLUENCE_IGNORE_INDEX) + controls.influence2.tooltipText = influenceTooltipText refreshInfluenceDependentControls() controls.influence2Label = new("LabelControl"):LabelControl({ "RIGHT", controls.influence2, "LEFT" }, { -5, 0, 0, 16 }, "^7Influence 2:") From 5a06e11f86b01686004dd0f10dc6f5dbf1c41e89 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Tue, 21 Apr 2026 01:53:33 +0200 Subject: [PATCH 4/5] refactor(trade): rename influence filter cost to count "Cost" was the only occurrence of the term in this file: the rest of the filter-budget path uses num_extra as the running count of structural filter slots consumed before weighted mods fill the remainder. Align with that convention: getInfluenceFilterCost becomes countInfluenceFilters and the intermediate influenceFilterCost variable is dropped in favour of a direct assignment to num_extra. Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/System/TestTradeQueryGenerator_spec.lua | 14 +++++++------- src/Classes/TradeQueryGenerator.lua | 8 +++----- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index 131e6a04f6..afbab55b96 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -160,7 +160,7 @@ describe("TradeQueryGenerator", function() local SHAPER = ANY + 1 -- 4 local ELDER = ANY + 2 -- 5 local resolve = mock_queryGen._resolveInfluenceQueryState - local cost = mock_queryGen._getInfluenceFilterCost + local count = mock_queryGen._countInfluenceFilters local needs = mock_queryGen._needsHasInfluenceFilter -- None: uses pseudo_has_influence=0 (1 slot instead of 6-slot NOT filter) @@ -168,7 +168,7 @@ describe("TradeQueryGenerator", function() local state = resolve(NONE, IGNORE) assert.are.equal(state.exactCount, 0) assert.is_true(state.hasNoneConstraint) - assert.are.equal(cost(state), 1) + assert.are.equal(count(state), 1) assert.is_true(needs(state)) end) @@ -178,7 +178,7 @@ describe("TradeQueryGenerator", function() assert.are.equal(state.exactCount, 1) assert.is_true(state.hasNoneConstraint) assert.are.equal(#state.specificInfluenceModIds, 1) - assert.are.equal(cost(state), 2) + assert.are.equal(count(state), 2) assert.is_true(needs(state)) end) @@ -188,7 +188,7 @@ describe("TradeQueryGenerator", function() assert.are.equal(state.exactCount, 2) assert.is_false(state.hasNoneConstraint) assert.are.equal(#state.specificInfluenceModIds, 2) - assert.are.equal(cost(state), 2) + assert.are.equal(count(state), 2) assert.is_false(needs(state)) end) @@ -197,7 +197,7 @@ describe("TradeQueryGenerator", function() local state = resolve(ANY, IGNORE) assert.are.equal(state.minCount, 1) assert.are.equal(state.exactCount, nil) - assert.are.equal(cost(state), 1) + assert.are.equal(count(state), 1) assert.is_true(needs(state)) end) @@ -207,7 +207,7 @@ describe("TradeQueryGenerator", function() assert.are.equal(state.exactCount, 2) assert.is_false(state.hasNoneConstraint) assert.are.equal(#state.specificInfluenceModIds, 1) - assert.are.equal(cost(state), 2) + assert.are.equal(count(state), 2) assert.is_true(needs(state)) end) @@ -228,7 +228,7 @@ describe("TradeQueryGenerator", function() assert.are.equal(dup.hasNoneConstraint, paired.hasNoneConstraint) assert.are.equal(#dup.specificInfluenceModIds, #paired.specificInfluenceModIds) assert.are.equal(dup.specificInfluenceModIds[1], paired.specificInfluenceModIds[1]) - assert.are.equal(cost(dup), cost(paired)) + assert.are.equal(count(dup), count(paired)) assert.are.equal(needs(dup), needs(paired)) end) diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index dfe34a7991..51e8f8ff9d 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -240,7 +240,7 @@ local function needsHasInfluenceFilter(influenceState) return influenceState.minCount ~= nil end -local function getInfluenceFilterCost(influenceState) +local function countInfluenceFilters(influenceState) local cost = #influenceState.specificInfluenceModIds if needsHasInfluenceFilter(influenceState) then cost = cost + 1 @@ -1035,9 +1035,7 @@ function TradeQueryGeneratorClass:FinishQuery() local options = self.calcContext.options local influenceState = resolveInfluenceQueryState(options.influence1, options.influence2) - local influenceFilterCost = getInfluenceFilterCost(influenceState) - - local num_extra = influenceFilterCost + local num_extra = countInfluenceFilters(influenceState) if not options.includeMirrored then num_extra = num_extra + 1 end @@ -1234,7 +1232,7 @@ end -- Test accessors for influence query state logic (not used in production paths) TradeQueryGeneratorClass._resolveInfluenceQueryState = resolveInfluenceQueryState -TradeQueryGeneratorClass._getInfluenceFilterCost = getInfluenceFilterCost +TradeQueryGeneratorClass._countInfluenceFilters = countInfluenceFilters TradeQueryGeneratorClass._needsHasInfluenceFilter = needsHasInfluenceFilter TradeQueryGeneratorClass._hasAnyInfluenceModId = hasAnyInfluenceModId TradeQueryGeneratorClass._INFLUENCE_IGNORE_INDEX = INFLUENCE_IGNORE_INDEX From 70a0cd92b8db935a45e395e387845287d536bced Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Tue, 21 Apr 2026 02:02:59 +0200 Subject: [PATCH 5/5] refactor(trade): expose a single buildInfluenceFilters helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three helpers resolveInfluenceQueryState / needsHasInfluenceFilter / countInfluenceFilters were each exported to the test mock so the spec could assert on intermediate state. That surface tested the shape of the computation rather than its outcome. Introduce buildInfluenceFilters(selection1, selection2) that returns the exact query fragments ExecuteQuery needs — the and-group filters, the top-level NOT stats (only populated for "no influences"), and the total filter-slot count. ExecuteQuery now computes the influence filters and the slot budget in a single call and inlines the fragments, dropping ~20 lines of duplicated query-building logic. Tests are rewritten to exercise the helper directly and assert on the resulting filter table, giving genuine end-to-end coverage of the nine pair combinations. Only _buildInfluenceFilters, _hasAnyInfluenceModId and the INFLUENCE_*_INDEX constants remain as test accessors; the three internal-state helpers are no longer reachable from outside the module. Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/System/TestTradeQueryGenerator_spec.lua | 127 ++++++++++--------- src/Classes/TradeQueryGenerator.lua | 62 +++++---- 2 files changed, 103 insertions(+), 86 deletions(-) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index afbab55b96..9df3b950c1 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -153,88 +153,89 @@ describe("TradeQueryGenerator", function() end) end) - describe("Influence query state", function() + describe("Influence query fragments", function() local IGNORE = mock_queryGen._INFLUENCE_IGNORE_INDEX -- 1 local NONE = mock_queryGen._INFLUENCE_NONE_INDEX -- 2 local ANY = mock_queryGen._INFLUENCE_ANY_INDEX -- 3 local SHAPER = ANY + 1 -- 4 local ELDER = ANY + 2 -- 5 - local resolve = mock_queryGen._resolveInfluenceQueryState - local count = mock_queryGen._countInfluenceFilters - local needs = mock_queryGen._needsHasInfluenceFilter - - -- None: uses pseudo_has_influence=0 (1 slot instead of 6-slot NOT filter) - it("None uses 1-slot pseudo_has_influence=0", function() - local state = resolve(NONE, IGNORE) - assert.are.equal(state.exactCount, 0) - assert.is_true(state.hasNoneConstraint) - assert.are.equal(count(state), 1) - assert.is_true(needs(state)) + local build = mock_queryGen._buildInfluenceFilters + local HAS_INFLUENCE = mock_queryGen._hasAnyInfluenceModId -- "pseudo.pseudo_has_influence_count" + local HAS_SHAPER = "pseudo.pseudo_has_shaper_influence" + + it("Ignore / Ignore produces no filters", function() + local andGroup, topStats, slots = build(IGNORE, IGNORE) + assert.are.equal(#andGroup, 0) + assert.are.equal(#topStats, 0) + assert.are.equal(slots, 0) end) - -- Shaper+None: needs pseudo_has_influence=1 to cap at 1 influence (avoids Shaper+Elder matches) - it("Shaper+None uses 2-slot filter (specific + pseudo_has_influence=1)", function() - local state = resolve(SHAPER, NONE) - assert.are.equal(state.exactCount, 1) - assert.is_true(state.hasNoneConstraint) - assert.are.equal(#state.specificInfluenceModIds, 1) - assert.are.equal(count(state), 2) - assert.is_true(needs(state)) + it("None / None emits a NOT clause at the top level (no influences)", function() + local andGroup, topStats, slots = build(NONE, NONE) + assert.are.equal(#andGroup, 0) + assert.are.equal(#topStats, 1) + assert.are.same(topStats[1], { type = "not", filters = { { id = HAS_INFLUENCE } } }) + assert.are.equal(slots, 1) end) - -- Shaper+Elder: 2 named influences, no None → no pseudo_has_influence needed (saves 1 slot) - it("Shaper+Elder uses 2-slot filter (specific mods only, no pseudo_has_influence)", function() - local state = resolve(SHAPER, ELDER) - assert.are.equal(state.exactCount, 2) - assert.is_false(state.hasNoneConstraint) - assert.are.equal(#state.specificInfluenceModIds, 2) - assert.are.equal(count(state), 2) - assert.is_false(needs(state)) + it("Any / Ignore caps min=1 (at least one influence)", function() + local andGroup, topStats, slots = build(ANY, IGNORE) + assert.are.equal(#topStats, 0) + assert.are.same(andGroup, { { id = HAS_INFLUENCE, value = { min = 1 } } }) + assert.are.equal(slots, 1) end) - -- Any+Ignore: minCount=1 → pseudo_has_influence min=1 (1 slot) - it("Any uses 1-slot pseudo_has_influence min=1", function() - local state = resolve(ANY, IGNORE) - assert.are.equal(state.minCount, 1) - assert.are.equal(state.exactCount, nil) - assert.are.equal(count(state), 1) - assert.is_true(needs(state)) + it("Shaper / None caps exactly 1 of that specific", function() + local andGroup, topStats, slots = build(SHAPER, NONE) + assert.are.equal(#topStats, 0) + assert.are.same(andGroup, { + { id = HAS_INFLUENCE, value = { min = 1, max = 1 } }, + { id = HAS_SHAPER }, + }) + assert.are.equal(slots, 2) end) - -- Any+Shaper: exactCount=2 with one unnamed slot → needs pseudo_has_influence=2 - it("Any+Shaper uses 2-slot filter (specific + pseudo_has_influence=2)", function() - local state = resolve(ANY, SHAPER) - assert.are.equal(state.exactCount, 2) - assert.is_false(state.hasNoneConstraint) - assert.are.equal(#state.specificInfluenceModIds, 1) - assert.are.equal(count(state), 2) - assert.is_true(needs(state)) + it("Shaper / Ignore requires the specific without a count cap", function() + local andGroup, topStats, slots = build(SHAPER, IGNORE) + assert.are.equal(#topStats, 0) + assert.are.same(andGroup, { { id = HAS_SHAPER } }) + assert.are.equal(slots, 1) end) - -- pseudo_has_influence mod ID is correct - it("hasAnyInfluenceModId is pseudo.pseudo_has_influence_count", function() - assert.are.equal(mock_queryGen._hasAnyInfluenceModId, "pseudo.pseudo_has_influence_count") + it("Any / Any caps exactly 2 influences", function() + local andGroup, topStats, slots = build(ANY, ANY) + assert.are.equal(#topStats, 0) + assert.are.same(andGroup, { { id = HAS_INFLUENCE, value = { min = 2, max = 2 } } }) + assert.are.equal(slots, 1) end) - -- Same specific influence on both sides is redundant at the item level and must - -- collapse to the / None semantic (exactly 1 of that type), not - -- double-count and not collapse to / Ignore. Verify via the - -- needsHasInfluenceFilter gate that builds the pseudo_has_influence cap — without - -- it, the query would match items with any number of influences including Shaper. - it("Shaper+Shaper produces the full Shaper+None query state", function() - local dup = resolve(SHAPER, SHAPER) - local paired = resolve(SHAPER, NONE) - assert.are.equal(dup.exactCount, paired.exactCount) - assert.are.equal(dup.hasNoneConstraint, paired.hasNoneConstraint) - assert.are.equal(#dup.specificInfluenceModIds, #paired.specificInfluenceModIds) - assert.are.equal(dup.specificInfluenceModIds[1], paired.specificInfluenceModIds[1]) - assert.are.equal(count(dup), count(paired)) - assert.are.equal(needs(dup), needs(paired)) + it("Shaper / Any caps exactly 2 including Shaper", function() + local andGroup, topStats, slots = build(SHAPER, ANY) + assert.are.equal(#topStats, 0) + assert.are.same(andGroup, { + { id = HAS_INFLUENCE, value = { min = 2, max = 2 } }, + { id = HAS_SHAPER }, + }) + assert.are.equal(slots, 2) end) - it("Shaper+Shaper needs the pseudo_has_influence cap to enforce exactly 1", function() - local state = resolve(SHAPER, SHAPER) - assert.is_true(needs(state)) + it("Shaper / Elder requires both specifics without a count cap", function() + local andGroup, topStats, slots = build(SHAPER, ELDER) + assert.are.equal(#topStats, 0) + assert.are.equal(#andGroup, 2) + assert.are.equal(slots, 2) + end) + + -- Same specific on both sides is redundant at the item level and must produce + -- the exact same filter set as / None (exactly 1 of that type). + -- Without the None-constraint deduplication, it would silently fall back to the + -- / Ignore form and fail to cap the influence count. + it("Shaper / Shaper produces the same filters as Shaper / None", function() + local dupAnd, dupStats, dupSlots = build(SHAPER, SHAPER) + local pairedAnd, pairedStats, pairedSlots = build(SHAPER, NONE) + assert.are.same(dupAnd, pairedAnd) + assert.are.same(dupStats, pairedStats) + assert.are.equal(dupSlots, pairedSlots) end) end) diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index 51e8f8ff9d..79831436d2 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -241,11 +241,39 @@ local function needsHasInfluenceFilter(influenceState) end local function countInfluenceFilters(influenceState) - local cost = #influenceState.specificInfluenceModIds + local count = #influenceState.specificInfluenceModIds if needsHasInfluenceFilter(influenceState) then - cost = cost + 1 + count = count + 1 end - return cost + return count +end + +-- Builds the influence-related trade query fragments from the two dropdown +-- selections. Returns three values so callers can plug the fragments into the +-- final query and size the remaining filter budget in a single call: +-- andGroup: filters to append inside the top-level AND group +-- topStats: filters to append directly to queryTable.query.stats +-- (only non-empty when the pair represents "no influences", which +-- requires a NOT clause rather than a value range) +-- slotCount: total filter slots the influence fragments will consume +local function buildInfluenceFilters(selection1, selection2) + local state = resolveInfluenceQueryState(selection1, selection2) + local andGroup = { } + local topStats = { } + if needsHasInfluenceFilter(state) then + if state.exactCount == 0 then + -- "has 0 influences" cannot be queried with a value range; use NOT instead. + t_insert(topStats, { type = "not", filters = { { id = hasAnyInfluenceModId } } }) + elseif state.exactCount ~= nil then + t_insert(andGroup, { id = hasAnyInfluenceModId, value = { min = state.exactCount, max = state.exactCount } }) + else + t_insert(andGroup, { id = hasAnyInfluenceModId, value = { min = state.minCount } }) + end + end + for _, modId in ipairs(state.specificInfluenceModIds) do + t_insert(andGroup, { id = modId }) + end + return andGroup, topStats, countInfluenceFilters(state) end ---@class TradeQueryGenerator @@ -1034,8 +1062,8 @@ function TradeQueryGeneratorClass:FinishQuery() end local options = self.calcContext.options - local influenceState = resolveInfluenceQueryState(options.influence1, options.influence2) - local num_extra = countInfluenceFilters(influenceState) + local influenceAndGroup, influenceTopStats, influenceSlotCount = buildInfluenceFilters(options.influence1, options.influence2) + local num_extra = influenceSlotCount if not options.includeMirrored then num_extra = num_extra + 1 end @@ -1120,22 +1148,13 @@ function TradeQueryGeneratorClass:FinishQuery() local andFilters = { type = "and", filters = { } } local options = self.calcContext.options - if needsHasInfluenceFilter(influenceState) then - if influenceState.exactCount == 0 then - -- "has 0 influences" cannot be queried with a value range; use NOT instead - t_insert(queryTable.query.stats, { type = "not", filters = { { id = hasAnyInfluenceModId } } }) - elseif influenceState.exactCount ~= nil then - t_insert(andFilters.filters, { id = hasAnyInfluenceModId, value = { min = influenceState.exactCount, max = influenceState.exactCount } }) - else - t_insert(andFilters.filters, { id = hasAnyInfluenceModId, value = { min = influenceState.minCount } }) - end - filters = filters + 1 + for _, stat in ipairs(influenceTopStats) do + t_insert(queryTable.query.stats, stat) end - - for _, modId in ipairs(influenceState.specificInfluenceModIds) do - t_insert(andFilters.filters, { id = modId }) - filters = filters + 1 + for _, filter in ipairs(influenceAndGroup) do + t_insert(andFilters.filters, filter) end + filters = filters + influenceSlotCount if #andFilters.filters > 0 then t_insert(queryTable.query.stats, andFilters) @@ -1230,10 +1249,7 @@ function TradeQueryGeneratorClass:FinishQuery() main:ClosePopup() end --- Test accessors for influence query state logic (not used in production paths) -TradeQueryGeneratorClass._resolveInfluenceQueryState = resolveInfluenceQueryState -TradeQueryGeneratorClass._countInfluenceFilters = countInfluenceFilters -TradeQueryGeneratorClass._needsHasInfluenceFilter = needsHasInfluenceFilter +TradeQueryGeneratorClass._buildInfluenceFilters = buildInfluenceFilters TradeQueryGeneratorClass._hasAnyInfluenceModId = hasAnyInfluenceModId TradeQueryGeneratorClass._INFLUENCE_IGNORE_INDEX = INFLUENCE_IGNORE_INDEX TradeQueryGeneratorClass._INFLUENCE_NONE_INDEX = INFLUENCE_NONE_INDEX