From 47d8d77f9766a610c6416d3359b25a3a64479941 Mon Sep 17 00:00:00 2001 From: oat Date: Sat, 25 Jul 2026 19:42:00 +0700 Subject: [PATCH 1/2] Handle object-form mods from the trade API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trade API migrated item mods from plain strings to objects carrying a description and flags. Commit 2d097695b adapted explicitMods only, so any item whose runeMods/enchantMods/implicitMods were populated crashed with "attempt to call method 'gsub' (a nil value)" — escapeGGGString was handed a table. Use the same format-agnostic idiom already ported into ImportTab.lua, and apply it to pseudoMods too, which crashed the same way one line later on :match. That guard also fixes a pre-existing crash on an empty pseudoMods array, which previously indexed nil rather than falling back to "0". --- src/Classes/TradeQueryRequests.lua | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua index 01312bf016..6668fd1a75 100644 --- a/src/Classes/TradeQueryRequests.lua +++ b/src/Classes/TradeQueryRequests.lua @@ -398,23 +398,23 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) item.explicitMods = item.explicitMods or { } t_insert(rawLines, "Implicits: " .. (#item.enchantMods + #item.runeMods + #item.implicitMods)) - for _, modLine in ipairs(item.enchantMods) do - t_insert(rawLines, "{enchant}" .. escapeGGGString(modLine)) + for _, itemMod in ipairs(item.enchantMods) do + t_insert(rawLines, "{enchant}" .. escapeGGGString(itemMod.description or itemMod)) end - for _, modLine in ipairs(item.runeMods) do - t_insert(rawLines, "{enchant}{rune}" .. escapeGGGString(modLine)) + for _, itemMod in ipairs(item.runeMods) do + t_insert(rawLines, "{enchant}{rune}" .. escapeGGGString(itemMod.description or itemMod)) end - for _, modLine in ipairs(item.implicitMods) do - t_insert(rawLines, escapeGGGString(modLine)) + for _, itemMod in ipairs(item.implicitMods) do + t_insert(rawLines, escapeGGGString(itemMod.description or itemMod)) end - for _, modLine in ipairs(item.explicitMods) do + for _, itemMod in ipairs(item.explicitMods) do local s = "" - for flagName, flag in pairs(modLine.flags or {}) do + for flagName, flag in pairs(itemMod.flags or {}) do if flag then s = s .. string.format("{%s}", flagName) end end - t_insert(rawLines, s .. escapeGGGString(modLine.description)) + t_insert(rawLines, s .. escapeGGGString(itemMod.description or itemMod)) end if item.mirrored then t_insert(rawLines, "Mirrored") @@ -428,6 +428,9 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) t_insert(rawLines, "Sanctified") end + local pseudoMod = item.pseudoMods and item.pseudoMods[1] + pseudoMod = pseudoMod and (pseudoMod.description or pseudoMod) + table.insert(items, { amount = trade_entry.listing.price.amount, currency = trade_entry.listing.price.currency, @@ -435,7 +438,7 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) item_string = table.concat(rawLines, "\n"), 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 = pseudoMod and pseudoMod:match("Sum: (.+)") or "0", id = trade_entry.id }) end From bd28bd9d9db98b0367e5af623a133d407b241b42 Mon Sep 17 00:00:00 2001 From: oat Date: Sat, 25 Jul 2026 19:42:09 +0700 Subject: [PATCH 2/2] Make "Find best" return the trade URL without listing items Register the generated search so the short shareable link can be built, then stop. The item listings are no longer fetched, parsed or sorted, so the row stays on [Find best][url][Price Item] instead of flipping to the result dropdown. "Price Item" remains available on that URL for when the items are actually wanted. The URL is set before the error branch so an empty search still yields a link to loosen in the browser. This orphans the Augment/Anoint Behaviour dropdowns in the Find best dialog: their only consumer was the post-fetch item rewriting removed here. Left in place rather than wired into "Price Item", which fires on whatever URL is in the box and would rewrite hand-pasted results based on a dropdown set during an unrelated search. --- src/Classes/TradeQuery.lua | 70 ++++++++------------------------------ 1 file changed, 14 insertions(+), 56 deletions(-) diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 261457739e..c12d044023 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -1022,70 +1022,28 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro controls["uri"..context.row_idx]:SetText(url, true) return end + -- Register the search so we get a short shareable link, but stop there: the item + -- listings are never fetched. Press "Price Item" on the resulting URL to pull them. context.controls["priceButton"..context.row_idx].label = "Searching..." self.lastQueries[row_idx] = query - self.tradeQueryRequests:SearchWithQueryWeightAdjusted(self.pbRealm, self.pbLeague, query, - function(items, errMsg) - if errMsg then - self:SetNotice(context.controls.pbNotice, colorCodes.NEGATIVE .. errMsg) - context.controls["priceButton"..context.row_idx].label = "Price Item" - return - else - self:SetNotice(context.controls.pbNotice, "") - end - - local selectedSlot = getSelectedSlot() - local itemsSafe = self:FilterToSafeItems(items, selectedSlot and selectedSlot.slotName) - - if self.tradeQueryGenerator.lastAugmentBehaviour == "Copy Current" or self.tradeQueryGenerator.lastAnointBehaviour == "Copy Current" then - for i, _ in ipairs(itemsSafe) do - local item = new("Item", itemsSafe[i].item_string) - -- avoid interacting with badly parsed stuff - if item.base and item.type then - self.itemsTab:CopyAnointsAndAugments(item, true, true, context.slotTbl.slotName) - itemsSafe[i].item_string = item:BuildRaw() - end - end - elseif self.tradeQueryGenerator.lastAugmentBehaviour == "Remove" then - for item_idx, _ in ipairs(itemsSafe) do - local item = new("Item", itemsSafe[item_idx].item_string) - -- sockets are kept as-is so the user can see e.g. exceptional or corrupted sockets - local validRunes = self.itemsTab:GetValidRunesForItem(item) - for rune_idx, _ in ipairs(item.runes or {}) do - if not self.itemsTab:IsSocketBoundRune(item, item.runes[rune_idx], validRunes) then - item.runes[rune_idx] = "None" - end - end - item:UpdateRunes() - itemsSafe[item_idx].item_string = item:BuildRaw() - end - elseif self.tradeQueryGenerator.lastAnointBehaviour == "Remove" then - for i, _ in ipairs(itemsSafe) do - local item = new("Item", itemsSafe[i].item_string) - item.enchantModLines = {} - itemsSafe[i].item_string = item:BuildRaw() - end - end - - self.resultTbl[context.row_idx] = itemsSafe - self:UpdateControlsWithItems(context.row_idx) - context.controls["priceButton"..context.row_idx].label = "Price Item" - end, - { - callbackQueryId = function(queryId) - local url = self.tradeQueryRequests:buildUrl(self.hostName .. "trade2/search", self.pbRealm, self.pbLeague, queryId) - controls["uri"..context.row_idx]:SetText(url, true) - end - } - ) + self.tradeQueryRequests:PerformSearch(self.pbRealm, self.pbLeague, query, function(response, errMsg) + context.controls["priceButton"..context.row_idx].label = "Price Item" + -- Set the URL even on error, so an empty search still gives a link to loosen in the browser + if response and response.id then + local url = self.tradeQueryRequests:buildUrl(self.hostName .. "trade2/search", self.pbRealm, self.pbLeague, response.id) + controls["uri"..context.row_idx]:SetText(url, true) + end + self:SetNotice(context.controls.pbNotice, errMsg and (colorCodes.NEGATIVE .. errMsg) or "") + end) end) end) controls["bestButton"..row_idx].shown = function() return not self.resultTbl[row_idx] end controls["bestButton"..row_idx].enabled = function() return self.pbLeague end controls["bestButton"..row_idx].tooltipText = [[Creates a weighted search to find the highest Stat Value items for this slot. -Note that even if you are authenticated, you can click this button again to show the search link. +The trade link is put in the box to the right; the items themselves are not fetched. +Control + click the link to open the search in your web-browser. If you have additional requirements that the trade tool doesn't cover (e.g. Adorned Magic jewels), -you can add them, copy the link here, and press "Price Item" to evaluate the items.]] +you can add them, then press "Price Item" to evaluate the items inside Path of Building.]] controls["bestButton" .. row_idx].onHover = function() local button = controls["bestButton" .. row_idx]