From be31980f835276330f87100d241eab79ea8ba715 Mon Sep 17 00:00:00 2001 From: Andrey Goder Date: Fri, 24 Jul 2026 21:21:50 -0700 Subject: [PATCH 1/3] Fix weighted trade result parsing for 3.29 --- spec/System/TestTradeQueryRequests_spec.lua | 46 +++++++++++++++++++++ src/Classes/TradeQueryRequests.lua | 4 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/spec/System/TestTradeQueryRequests_spec.lua b/spec/System/TestTradeQueryRequests_spec.lua index 0e731701f82..fb3ffd9a57b 100644 --- a/spec/System/TestTradeQueryRequests_spec.lua +++ b/spec/System/TestTradeQueryRequests_spec.lua @@ -1,4 +1,5 @@ describe("TradeQueryRequests", function() + local dkjson = require "dkjson" local mock_limiter = { NextRequestTime = function() return os.time() @@ -192,6 +193,51 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]] end) end) + describe("FetchResultBlock", function() + it("reads weighted sums from current and legacy pseudo mods", function() + local function makeTradeEntry(id, pseudoMods) + return { + id = id, + listing = { + price = { amount = 1, currency = "chaos", type = "~price" }, + whisper = "hi", + account = { name = "seller" }, + }, + item = { + extended = { text = "VGVzdCBJdGVt" }, + pseudoMods = pseudoMods, + }, + } + end + local response = dkjson.encode({ + result = { + makeTradeEntry("current", { { description = "Sum: 178", domain = "pseudo", hash = "stat.statgroup.0" } }), + makeTradeEntry("legacy", { "Sum: 42" }), + makeTradeEntry("empty", { }), + }, + }) + local fetchedItems + local callbackError + requests.requestQueue.fetch = { } + requests:FetchResultBlock("test", function(items, errMsg) + fetchedItems = items + callbackError = errMsg + end) + + local request = table.remove(requests.requestQueue.fetch, 1) + request.callback(response) + + local itemsById = { } + for _, item in ipairs(fetchedItems) do + itemsById[item.id] = item + end + assert.is_nil(callbackError) + assert.are.equal("178", itemsById.current.weight) + assert.are.equal("42", itemsById.legacy.weight) + assert.are.equal("0", itemsById.empty.weight) + end) + end) + describe("FetchResults", function() -- Pass: Fetches exactly 10 from 11, in 1 block -- Fail: Fetches wrong count/blocks, indicating batch limit violation, triggering rate limits diff --git a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua index 84f6e39770c..4bdc5e3372a 100644 --- a/src/Classes/TradeQueryRequests.lua +++ b/src/Classes/TradeQueryRequests.lua @@ -290,6 +290,8 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) end local items = {} for _, trade_entry in pairs(response.result) do + local pseudoMod = trade_entry.item.pseudoMods and trade_entry.item.pseudoMods[1] + local pseudoModLine = pseudoMod and (pseudoMod.description or pseudoMod) table.insert(items, { amount = trade_entry.listing.price.amount, currency = trade_entry.listing.price.currency, @@ -297,7 +299,7 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) item_string = escapeGGGString(common.base64.decode(trade_entry.item.extended.text)), whisper = trade_entry.listing.whisper, trader = trade_entry.listing.account.name, - weight = trade_entry.item.pseudoMods and trade_entry.item.pseudoMods[1]:match("Sum: (.+)") or "0", + weight = pseudoModLine and pseudoModLine:match("Sum: (.+)") or "0", id = trade_entry.id }) end From 54d6771519a97cdd0f6b9234cfbcf8fa97a17d3c Mon Sep 17 00:00:00 2001 From: Andrey Goder Date: Fri, 24 Jul 2026 21:53:33 -0700 Subject: [PATCH 2/3] Support flattened trade stat options --- spec/System/TestCompareBuySimilar_spec.lua | 20 ------------------ spec/System/TestTradeHelpers_spec.lua | 9 ++++---- src/Classes/TradeHelpers.lua | 24 ++++++++++++++++++---- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/spec/System/TestCompareBuySimilar_spec.lua b/spec/System/TestCompareBuySimilar_spec.lua index 15f814310ef..7c34e00db05 100644 --- a/spec/System/TestCompareBuySimilar_spec.lua +++ b/spec/System/TestCompareBuySimilar_spec.lua @@ -31,26 +31,6 @@ Corrupted]]) modEntries[1]) end) - it("matches thread of hope radius as an option", function() - local thread = new("Item", [[ -Rarity: UNIQUE -Thread of Hope -Crimson Jewel -Radius: Variable -Implicits: 0 -Only affects Passives in Massive Ring --15% to all Elemental Resistances -Passive Skills in Radius can be Allocated without being connected to your tree -Passage]]) - local modSources = { - { list = thread.explicitModLines, type = "explicit" } - } - local modEntries = bs.addModEntries(thread, modSources) - assert.equal(4, #modEntries) - assert.equal("explicit.stat_3642528642", modEntries[1].tradeIds[1]) - assert.equal(5, modEntries[1].value) - end) - it("combines mods that are the same stat", function() local lifeDiamond = new("Item", [[ Test Subject diff --git a/spec/System/TestTradeHelpers_spec.lua b/spec/System/TestTradeHelpers_spec.lua index 8b61757af1b..a563db8e23f 100644 --- a/spec/System/TestTradeHelpers_spec.lua +++ b/spec/System/TestTradeHelpers_spec.lua @@ -21,11 +21,12 @@ describe("TradeHelpers trade hash matching", function() end) describe("findTradeIdOption", function() - it("matches a '#'-valued option and returns its value", function() - local tradeId, value = tradeHelpers.findTradeIdOption("Grants Level 20 Summon Bestial Snake Skill", + it("matches a flattened option stat and returns its value", function() + local tradeId, value = tradeHelpers.findTradeIdOption( + "Passive skills in radius of Elemental Overload can be Allocated without being connected to your tree", "explicit") - assert.equal("explicit.stat_2878779644", tradeId) - assert.equal(3, value) + assert.equal("explicit.stat_2422708892", tradeId) + assert.equal(22088, value) end) it("matches an exact-text option and returns no value", function() diff --git a/src/Classes/TradeHelpers.lua b/src/Classes/TradeHelpers.lua index 5baeaaeca2d..be5292828ea 100644 --- a/src/Classes/TradeHelpers.lua +++ b/src/Classes/TradeHelpers.lua @@ -83,11 +83,23 @@ local _optionTradeStatMap ---@return table optionTradeStatMap table containing helper data for matching trade option filters local function getOptionTradeStatMap(tradeStats) if _optionTradeStatMap then return _optionTradeStatMap end - local optionTradeStatMap = {} + local optionTradeStatMap = { + exact = {}, + patterns = {}, + } for _, cat in ipairs(tradeStats) do if cat.id == "enchant" or cat.id == "explicit" or cat.id == "implicit" then for _, entry in ipairs(cat.entries) do - if entry.option and entry.text:match("#") then + local tradeId, optionId = entry.id:match("^(.-)|(.+)$") + if tradeId and optionId then + -- The 3.29 trade API expands option stats into one entry per option, + -- with the option value appended to the stat ID. + local matchKey = entry.text:gsub(" Passage", ""):lower() + optionTradeStatMap.exact[cat.id .. "\0" .. matchKey] = { + tradeId = tradeId, + value = tonumber(optionId) or optionId, + } + elseif entry.option and entry.text:match("#") then -- pob parses the passage part as a separate mod line, which -- causes trouble local matchKey = entry.text:gsub("#", "(.*)"):gsub(" Passage", ""):lower() @@ -95,7 +107,7 @@ local function getOptionTradeStatMap(tradeStats) for _, option in ipairs(entry.option.options) do option.text = option.text and option.text:lower() end - optionTradeStatMap[matchKey] = { type = cat.id, options = entry.option.options, tradeId = entry.id } + optionTradeStatMap.patterns[matchKey] = { type = cat.id, options = entry.option.options, tradeId = entry.id } end end end @@ -165,7 +177,11 @@ function M.findTradeIdOption(modLine, modType) -- reformat double-line cluster enchants modLine = modLine:gsub(".added small passive skills grant: ", " ") - for pat, entry in pairs(optionTradeStatMap) do + local exactEntry = optionTradeStatMap.exact[modType .. "\0" .. modLine] + if exactEntry then + return exactEntry.tradeId, exactEntry.value + end + for pat, entry in pairs(optionTradeStatMap.patterns) do local match = modLine:match(pat) if entry.type == modType and match then for _, option in ipairs(entry.options) do From 9d90f7bf857f69a25325067aa2157df91b9b3b61 Mon Sep 17 00:00:00 2001 From: Andrey Goder Date: Fri, 24 Jul 2026 22:01:52 -0700 Subject: [PATCH 3/3] Preserve legacy trade option matching --- spec/System/TestCompareBuySimilar_spec.lua | 20 +++++++++++++ spec/System/TestTradeHelpers_spec.lua | 7 +++++ src/Classes/TradeHelpers.lua | 35 ++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/spec/System/TestCompareBuySimilar_spec.lua b/spec/System/TestCompareBuySimilar_spec.lua index 7c34e00db05..15f814310ef 100644 --- a/spec/System/TestCompareBuySimilar_spec.lua +++ b/spec/System/TestCompareBuySimilar_spec.lua @@ -31,6 +31,26 @@ Corrupted]]) modEntries[1]) end) + it("matches thread of hope radius as an option", function() + local thread = new("Item", [[ +Rarity: UNIQUE +Thread of Hope +Crimson Jewel +Radius: Variable +Implicits: 0 +Only affects Passives in Massive Ring +-15% to all Elemental Resistances +Passive Skills in Radius can be Allocated without being connected to your tree +Passage]]) + local modSources = { + { list = thread.explicitModLines, type = "explicit" } + } + local modEntries = bs.addModEntries(thread, modSources) + assert.equal(4, #modEntries) + assert.equal("explicit.stat_3642528642", modEntries[1].tradeIds[1]) + assert.equal(5, modEntries[1].value) + end) + it("combines mods that are the same stat", function() local lifeDiamond = new("Item", [[ Test Subject diff --git a/spec/System/TestTradeHelpers_spec.lua b/spec/System/TestTradeHelpers_spec.lua index a563db8e23f..806116e4e35 100644 --- a/spec/System/TestTradeHelpers_spec.lua +++ b/spec/System/TestTradeHelpers_spec.lua @@ -29,6 +29,13 @@ describe("TradeHelpers trade hash matching", function() assert.equal(22088, value) end) + it("matches a legacy '#'-valued option and returns its value", function() + local tradeId, value = tradeHelpers.findTradeIdOption("Grants Level 20 Summon Bestial Snake Skill", + "explicit") + assert.equal("explicit.stat_2878779644", tradeId) + assert.equal(3, value) + end) + it("matches an exact-text option and returns no value", function() local tradeId, value = tradeHelpers.findTradeIdOption("Allocates Tranquility", "enchant") assert.equal("enchant.stat_2954116742", tradeId) diff --git a/src/Classes/TradeHelpers.lua b/src/Classes/TradeHelpers.lua index be5292828ea..853404e2855 100644 --- a/src/Classes/TradeHelpers.lua +++ b/src/Classes/TradeHelpers.lua @@ -79,6 +79,33 @@ end local _optionTradeStatMap +-- These option stats are still needed for legacy items, but are no longer +-- included in the trade API's 3.29 stats response. +local legacyOptionTradeStats = { + { + type = "explicit", + id = "explicit.stat_2878779644", + text = "Grants Level 20 Summon Bestial # Skill", + options = { + { id = 1, text = "rhoa" }, + { id = 2, text = "ursa" }, + { id = 3, text = "snake" }, + }, + }, + { + type = "explicit", + id = "explicit.stat_3642528642", + text = "Only affects Passives in # Ring", + options = { + { id = 1, text = "small" }, + { id = 2, text = "medium" }, + { id = 3, text = "large" }, + { id = 4, text = "very large" }, + { id = 5, text = "massive" }, + }, + }, +} + ---@param tradeStats table table of data from https://www.pathofexile.com/api/trade2/data/stats ---@return table optionTradeStatMap table containing helper data for matching trade option filters local function getOptionTradeStatMap(tradeStats) @@ -112,6 +139,14 @@ local function getOptionTradeStatMap(tradeStats) end end end + for _, entry in ipairs(legacyOptionTradeStats) do + local matchKey = entry.text:gsub("#", "(.*)"):lower() + optionTradeStatMap.patterns[matchKey] = optionTradeStatMap.patterns[matchKey] or { + type = entry.type, + options = entry.options, + tradeId = entry.id, + } + end _optionTradeStatMap = optionTradeStatMap return _optionTradeStatMap