Skip to content

Commit 20c350d

Browse files
vaisestLocalIdentity
andauthored
Trade: static stat data, better error handling, fix radius jewel (PathOfBuildingCommunity#2020)
* Update trader for 0.5: regenerate mods and switch to using local stat json * Fix newlines in querymods.lua * Defer loading of trade stat data and fix tests * Check that trader result items parse base item before adding them * Fix weight generation for radius jewels again * regen data files * Download trade site stats automatically and convert it to Lua * Disable spell checker and sort to try and minimise diff problems * Cspell fixes * Try to fix querymods.lua rune category inconsistencies * Regenerate all data fter having properly exported game data * Use stat transform in trader, fixes lower is better stats not counting * Fix modCache * Stop download stats using async download DownloadPage is async and could download the trade stats after query mods was generated so it could use stale data * Fix jewel corruptions --------- Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent d8c954b commit 20c350d

12 files changed

Lines changed: 67886 additions & 16356 deletions

spec/System/TestTradeQueryGenerator_spec.lua

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ describe("TradeQueryGenerator", function()
55
-- Pass: Mod line maps correctly to trade stat entry without error
66
-- Fail: Mapping fails (e.g., no match found), indicating incomplete stat parsing for curse mods, potentially missing curse-enabling items in queries
77
it("handles special curse case", function()
8-
local mod = { tradeHashes = {[30642521] = {"You can apply an additional Curse"}} }
9-
local tradeStatsParsed = { result = { [2] = { entries = { { text = "You can apply # additional Curses", id = "explicit.stat_30642521" } } } } }
10-
mock_queryGen.modData = { Explicit = true }
11-
mock_queryGen:ProcessMod(mod, tradeStatsParsed, 1)
8+
local mod = { tradeHashes = {[30642521] = {"You can apply an additional Curse"}}, type = "Prefix", weightKey = {}, weightVal = {} }
9+
mock_queryGen.modData = { Explicit = {} }
10+
mock_queryGen:ProcessMod(mod)
1211
-- Simplified assertion; in full impl, check modData
1312
assert.is_true(true)
1413
end)

src/Classes/TradeHelpers.lua

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,30 +32,23 @@ function M.modLineValue(line)
3232
return tonumber(line:match("%-?[%d]+%.?[%d]*"))
3333
end
3434

35-
-- Helper: fetch and cache the trade API stats
36-
local _tradeStats = nil
37-
local _tradeStatsFetched = false
38-
-- contains data for stats which have options, like allocates #
39-
local optionTradeStatMap = {}
40-
--- @return table
41-
local function getTradeStatsLookup()
35+
local _tradeStats
36+
37+
---@return table? tradeStats
38+
function M.getTradeStats()
4239
if _tradeStats then return _tradeStats end
43-
local tradeStats = ""
44-
local easy = common.curl.easy()
45-
if not easy then return nil end
46-
easy:setopt_url("https://www.pathofexile.com/api/trade2/data/stats")
47-
easy:setopt_useragent("Path of Building/" .. (launch.versionNumber or ""))
48-
easy:setopt_writefunction(function(d)
49-
tradeStats = tradeStats .. d
50-
return true
51-
end)
52-
local ok = easy:perform()
53-
easy:close()
54-
if not ok or tradeStats == "" then return {} end
55-
local parsed = dkjson.decode(tradeStats)
56-
_tradeStats = parsed.result
57-
58-
for _, cat in ipairs(_tradeStats) do
40+
_tradeStats = LoadModule("Data/TradeSiteStats")
41+
return _tradeStats
42+
end
43+
44+
local _optionTradeStatMap
45+
46+
---@param tradeStats table table of data from https://www.pathofexile.com/api/trade2/data/stats
47+
---@return table optionTradeStatMap table containing helper data for matching trade option filters
48+
local function getOptionTradeStatMap(tradeStats)
49+
if _optionTradeStatMap then return _optionTradeStatMap end
50+
local optionTradeStatMap = {}
51+
for _, cat in ipairs(tradeStats) do
5952
if cat.id == "enchant" or cat.id == "explicit" or cat.id == "implicit" then
6053
optionTradeStatMap[cat.id] = {}
6154
for _, entry in ipairs(cat.entries) do
@@ -72,7 +65,8 @@ local function getTradeStatsLookup()
7265
end
7366
end
7467
end
75-
return _tradeStats
68+
_optionTradeStatMap = optionTradeStatMap
69+
return _optionTradeStatMap
7670
end
7771

7872
-- Map source types used in OpenBuySimilarPopup to trade API category labels
@@ -120,7 +114,7 @@ function M.shouldBeInverted(tradeId, modLine, modType)
120114
if not inverseKey then
121115
return false
122116
end
123-
for _, category in ipairs(getTradeStatsLookup()) do
117+
for _, category in ipairs(M.getTradeStats() or {}) do
124118
if category.id == modType then
125119
for _, stat in ipairs(category.entries) do
126120
if tradeId == stat.id then
@@ -132,7 +126,9 @@ function M.shouldBeInverted(tradeId, modLine, modType)
132126
end
133127

134128
-- test for inverted mod
135-
if inverseKey and ((invertedLine == formattedTradeSiteText) or (invertedLine:gsub("^%+", "") == formattedTradeSiteText)) then
129+
if inverseKey
130+
and ((invertedLine == formattedTradeSiteText)
131+
or (invertedLine:gsub("^%+", "") == formattedTradeSiteText)) then
136132
return true
137133
end
138134

@@ -153,30 +149,27 @@ function M.formatDatabaseText(text)
153149
-- (123-124) -> #
154150
text = text:gsub("%(%d+%-%d+%)", "#")
155151
text = text:gsub("%d+", "#")
156-
-- remove radius jewel text. the same description is used for regular and
157-
-- radius jewels in the exports
158-
text = text:gsub("^Notable Passive Skills in Radius also grant ", "")
159-
text = text:gsub("^Small Passive Skills in Radius also grant ", "")
160152
return text
161153
end
162154

155+
163156
-- Helper: find the trade stat ID for a mod line
164-
--- @param item table
165-
--- @param modLine string
166-
--- @param modType string
167-
--- @param isDesecrated boolean
168-
--- the
169-
--- @return number? hash returned for most mods
170-
--- @return string? optionTradeId returned if the mod is an option. e.g. Allocates X
171-
--- @return number value returned if the mod is an option and uses values. e.g. timeless jewel
157+
---@param item table
158+
---@param modLine string
159+
---@param modType string
160+
---@param isDesecrated boolean
161+
---@return number? hash returned for most mods
162+
---@return string? optionTradeId returned if the mod is an option. e.g. Allocates X
163+
---@return number? value returned if the mod is an option and uses values. e.g. timeless jewel
172164
function M.findTradeHash(item, modLine, modType, isDesecrated)
173165
local formattedLine = M.formatDatabaseText(modLine)
174166
-- the data export splits some mods into different parts, even though they
175167
-- are technically just one stat. we handle that here
176168
local isUnique = item.rarity == "UNIQUE" or item.rarity == "RELIC"
177169
local function findStat(dbMod, ignoreWeights)
178170
local excludeTags = (not isUnique) and { default = true } or nil
179-
if not ignoreWeights and #(dbMod.weightKey or {}) > 0 and not (item:GetModSpawnWeight(dbMod, nil, excludeTags) > 0) then
171+
if not ignoreWeights and #(dbMod.weightKey or {}) > 0
172+
and not (item:GetModSpawnWeight(dbMod, nil, excludeTags) > 0) then
180173
return nil
181174
end
182175
for tradeHash, description in pairs(dbMod.tradeHashes) do
@@ -196,10 +189,9 @@ function M.findTradeHash(item, modLine, modType, isDesecrated)
196189
end
197190
end
198191

199-
-- initialise optionTradeStatMap
200-
if not _tradeStats then
201-
getTradeStatsLookup()
202-
end
192+
local tradeStats = M.getTradeStats()
193+
local optionTradeStatMap = getOptionTradeStatMap(tradeStats)
194+
if not tradeStats or not optionTradeStatMap then return end
203195

204196
for _, v in ipairs(optionTradeStatMap[modType] or {}) do
205197
if v.pattern then
@@ -211,7 +203,6 @@ function M.findTradeHash(item, modLine, modType, isDesecrated)
211203
return nil, v.tradeId
212204
end
213205
end
214-
215206

216207
-- desecrate-only mods
217208
if isDesecrated then
@@ -230,8 +221,8 @@ function M.findTradeHash(item, modLine, modType, isDesecrated)
230221
return tradeHashMaybe
231222
end
232223
end
233-
-- most implicit and explicit applicable to the type
234-
elseif modType ~= "implicit" or modType ~= "explicit" then
224+
-- most implicit and explicit applicable to the type
225+
elseif modType == "implicit" or modType == "explicit" then
235226
for _, dbMod in pairs(item.affixes) do
236227
local tradeHashMaybe = findStat(dbMod)
237228
if tradeHashMaybe then

src/Classes/TradeQuery.lua

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, ba
777777
self.onlyWeightedBaseOutput[row_idx][result_index] = onlyWeightedBaseOutput
778778
self.lastComparedWeightList[row_idx][result_index] = self.statSortSelectionList
779779
end
780-
780+
781781
local slotName = self.slotTables[row_idx].nodeId and "Jewel " .. tostring(self.slotTables[row_idx].nodeId) or self.slotTables[row_idx].slotName
782782
if slotName == "Megalomaniac" then
783783
local addedNodes = {}
@@ -787,7 +787,7 @@ function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, ba
787787
addedNodes[node] = true
788788
end
789789
end
790-
790+
791791
local output = self:ReduceOutput(calcFunc({ addNodes = addedNodes }))
792792
local weight = self.tradeQueryGenerator.WeightedRatioOutputs(baseOutput, output, self.statSortSelectionList)
793793
result.evaluation = {{ output = output, weight = weight }}
@@ -968,31 +968,43 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro
968968
self:SetNotice(context.controls.pbNotice, "")
969969
end
970970

971+
-- ensure we only take in items that parse properly to avoid crash issues.
972+
local itemsSafe = {}
973+
for _, entry in ipairs(items) do
974+
local item = new("Item", entry.item_string)
975+
if item.base then
976+
t_insert(itemsSafe, entry)
977+
end
978+
end
979+
971980
if self.tradeQueryGenerator.lastAugmentBehaviour == "Copy Current" or self.tradeQueryGenerator.lastAnointBehaviour == "Copy Current" then
972-
for i, _ in ipairs(items) do
973-
local item = new("Item", items[i].item_string)
974-
self.itemsTab:CopyAnointsAndAugments(item, true, true, context.slotTbl.slotName)
975-
items[i].item_string = item:BuildRaw()
981+
for i, _ in ipairs(itemsSafe) do
982+
local item = new("Item", itemsSafe[i].item_string)
983+
-- avoid interacting with badly parsed stuff
984+
if item.base and item.type then
985+
self.itemsTab:CopyAnointsAndAugments(item, true, true, context.slotTbl.slotName)
986+
itemsSafe[i].item_string = item:BuildRaw()
987+
end
976988
end
977989
elseif self.tradeQueryGenerator.lastAugmentBehaviour == "Remove" then
978-
for item_idx, _ in ipairs(items) do
979-
local item = new("Item", items[item_idx].item_string)
990+
for item_idx, _ in ipairs(itemsSafe) do
991+
local item = new("Item", itemsSafe[item_idx].item_string)
980992
-- sockets are kept as-is so the user can see e.g. exceptional or corrupted sockets
981993
for rune_idx, _ in ipairs(item.runes or {}) do
982994
item.runes[rune_idx] = "None"
983995
end
984996
item:UpdateRunes()
985-
items[item_idx].item_string = item:BuildRaw()
997+
itemsSafe[item_idx].item_string = item:BuildRaw()
986998
end
987999
elseif self.tradeQueryGenerator.lastAnointBehaviour == "Remove" then
988-
for i, _ in ipairs(items) do
989-
local item = new("Item", items[i].item_string)
1000+
for i, _ in ipairs(itemsSafe) do
1001+
local item = new("Item", itemsSafe[i].item_string)
9901002
item.enchantModLines = {}
991-
items[i].item_string = item:BuildRaw()
1003+
itemsSafe[i].item_string = item:BuildRaw()
9921004
end
9931005
end
9941006

995-
self.resultTbl[context.row_idx] = items
1007+
self.resultTbl[context.row_idx] = itemsSafe
9961008
self:UpdateControlsWithItems(context.row_idx)
9971009
context.controls["priceButton"..context.row_idx].label = "Price Item"
9981010
end,

0 commit comments

Comments
 (0)