Skip to content

Commit a41bd52

Browse files
author
LocalIdentity
committed
Merge branch 'dev' into fix-radiant-faith+foulborn-choir-of-the-storm
2 parents dcd9f9f + 2416cd4 commit a41bd52

169 files changed

Lines changed: 214815 additions & 1709 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
# Changelog
22

3+
## [v2.60.0](https://github.com/PathOfBuildingCommunity/PathOfBuilding-PoE2/tree/v2.60.0) (2026/01/28)
4+
5+
[Full Changelog](https://github.com/PathOfBuildingCommunity/PathOfBuilding/compare/v2.59.1...v2.60.0)
6+
7+
8+
## What's Changed
9+
### New to Path of Building
10+
- Add 3.27 Phrecia Tree [\#9437](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9437) ([ShadowTrolll](https://github.com/ShadowTrolll))
11+
- Add support for newer unveils on Bitterbind Point [\#9357](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9357) ([Peechey](https://github.com/Peechey))
12+
- Add "aoe" filtering for gem search [\#9433](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9433) ([Blitz54](https://github.com/Blitz54))
13+
### Fixed Crashes
14+
- Fix crash on adding support gems and importing items to many builds [\#9340](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9340) ([LocalIdentity](https://github.com/LocalIdentity))
15+
- Fix Radius Jewels in Shared Items Crashing on Load [\#9349](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9349) ([Peechey](https://github.com/Peechey))
16+
- Fix Crash when sorting gems while using Foulborn Gruthkel's Pelt [\#9376](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9376) ([LocalIdentity](https://github.com/LocalIdentity))
17+
### User Interface
18+
- Fix Foulborn Icons showing on tree nodes, and foil items not importing type [\#9363](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9363) ([Blitz54](https://github.com/Blitz54))
19+
### Fixed Calculations
20+
- Fix Spellslinger gaining generic damage instead of Spell damage [\#9352](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9352) ([LocalIdentity](https://github.com/LocalIdentity))
21+
- Fix Foulborn Choir of the Storm's Overcapped Mod not applying to Total Mana [\#9351](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9351) ([Peechey](https://github.com/Peechey))
22+
### Fixed Behaviours
23+
- Fix Party Tab max Fortify override not working [\#9342](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9342) ([LocalIdentity](https://github.com/LocalIdentity))
24+
- Fix Utula's working with The Tides of Time [\#9436](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9436) ([Blitz54](https://github.com/Blitz54))
25+
### Accuracy Improvements
26+
- Updated Lori's Lantern text to match in-game description [\#9418](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9418) ([EminGul](https://github.com/EminGul))
27+
- Fix flavour text for some Uniques [\#9434](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9434) ([Blitz54](https://github.com/Blitz54))
28+
- Fix Sirus Meteor and Maven Memory game damage values [\#9372](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/9372) ([LocalIdentity](https://github.com/LocalIdentity))
29+
30+
331
## [v2.59.2](https://github.com/PathOfBuildingCommunity/PathOfBuilding-PoE2/tree/v2.59.2) (2025/11/23)
432

533
[Full Changelog](https://github.com/PathOfBuildingCommunity/PathOfBuilding/compare/v2.59.1...v2.59.2)

changelog.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
VERSION[2.60.0][2026/01/28]
2+
3+
--- New to Path of Building ---
4+
* Add 3.27 Phrecia Tree (ShadowTrolll)
5+
* Add support for newer unveils on Bitterbind Point (Peechey)
6+
* Add "aoe" filtering for gem search (Blitz54)
7+
8+
--- Fixed Crashes ---
9+
* Fix crash on adding support gems and importing items to many builds (LocalIdentity)
10+
* Fix Radius Jewels in Shared Items Crashing on Load (Peechey)
11+
* Fix Crash when sorting gems while using Foulborn Gruthkel's Pelt (LocalIdentity)
12+
13+
--- User Interface ---
14+
* Fix Foulborn Icons showing on tree nodes, and foil items not importing type (Blitz54)
15+
16+
--- Fixed Calculations ---
17+
* Fix Spellslinger gaining generic damage instead of Spell damage (LocalIdentity)
18+
* Fix Foulborn Choir of the Storm's Overcapped Mod not applying to Total Mana (Peechey)
19+
20+
--- Fixed Behaviours ---
21+
* Fix Party Tab max Fortify override not working (LocalIdentity)
22+
* Fix Utula's working with The Tides of Time (Blitz54)
23+
24+
--- Accuracy Improvements ---
25+
* Updated Lori's Lantern text to match in-game description (EminGul)
26+
* Fix flavour text for some Uniques (Blitz54)
27+
* Fix Sirus Meteor and Maven Memory game damage values (LocalIdentity)
28+
129
VERSION[2.59.2][2025/11/23]
230

331
--- Fixed Crashes ---

manifest.xml

Lines changed: 154 additions & 28 deletions
Large diffs are not rendered by default.

runtime/lua/dkjson.lua

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,26 @@ local function escapeutf8 (uchar)
130130
end
131131
end
132132

133+
local function sortedkeys(tbl)
134+
local keys = {}
135+
for k in pairs(tbl) do
136+
keys[#keys + 1] = k
137+
end
138+
table.sort(keys, function(a, b)
139+
local ta, tb = type(a), type(b)
140+
if ta == tb then
141+
return a < b
142+
end
143+
if ta == "number" then
144+
return true
145+
elseif tb == "number" then
146+
return false
147+
end
148+
return ta < tb
149+
end)
150+
return keys
151+
end
152+
133153
local function fsub (str, pattern, repl)
134154
-- gsub always builds a new string in a buffer, even when no match
135155
-- exists. First using find should be more efficient when most strings
@@ -333,7 +353,10 @@ encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, s
333353
end
334354
end
335355
else -- unordered
336-
for k,v in pairs (value) do
356+
local keys = sortedkeys(value)
357+
for i = 1, #keys do
358+
local k = keys[i]
359+
local v = value[k]
337360
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
338361
if not buflen then return nil, msg end
339362
prev = true -- add a seperator before the next element

spec/System/TestItemMods_spec.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,4 +585,31 @@ describe("TetsItemMods", function()
585585

586586
assert.are.equals(0.86, build.calcsTab.calcsOutput.LightningEffMult)
587587
end)
588+
589+
it("Max charges with conditional mod", function() -- see #9442
590+
build.skillsTab:PasteSocketGroup("Grace 20/20 Default 1\n")
591+
runCallback("OnFrame")
592+
593+
local baseFrenzyChargesMax = build.calcsTab.calcsOutput.FrenzyChargesMax
594+
local baseEnduranceChargesMax = build.calcsTab.calcsOutput.EnduranceChargesMax
595+
596+
build.configTab.input.customMods = [[
597+
+1 to Maximum Frenzy Charges while affected by Grace
598+
]]
599+
build.configTab:BuildModList()
600+
runCallback("OnFrame")
601+
602+
assert.are.equals(baseFrenzyChargesMax + 1, build.calcsTab.calcsOutput.FrenzyChargesMax)
603+
assert.are.equals(baseEnduranceChargesMax, build.calcsTab.calcsOutput.EnduranceChargesMax)
604+
605+
build.configTab.input.customMods = [[
606+
Your Maximum Endurance Charges is equal to your Maximum Frenzy Charges
607+
+1 to Maximum Frenzy Charges while affected by Grace
608+
]]
609+
build.configTab:BuildModList()
610+
runCallback("OnFrame")
611+
612+
assert.are.equals(baseFrenzyChargesMax + 1, build.calcsTab.calcsOutput.FrenzyChargesMax)
613+
assert.are.equals(baseEnduranceChargesMax + 1, build.calcsTab.calcsOutput.EnduranceChargesMax)
614+
end)
588615
end)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
describe("TradeQuery Currency Conversion", function()
2+
local mock_tradeQuery = new("TradeQuery", { itemsTab = {} })
3+
4+
-- test case for commit: "Skip callback on errors to prevent incomplete conversions"
5+
describe("FetchCurrencyConversionTable", function()
6+
-- Pass: Callback not called on error
7+
-- Fail: Callback called, indicating partial data risk
8+
it("skips callback on error", function()
9+
local orig_launch = launch
10+
local spy = { called = false }
11+
launch = {
12+
DownloadPage = function(url, callback, opts)
13+
callback(nil, "test error")
14+
end
15+
}
16+
mock_tradeQuery:FetchCurrencyConversionTable(function()
17+
spy.called = true
18+
end)
19+
launch = orig_launch
20+
assert.is_false(spy.called)
21+
end)
22+
end)
23+
24+
describe("ConvertCurrencyToChaos", function()
25+
-- Pass: Ceils amount to integer (e.g., 4.9 -> 5)
26+
-- Fail: Wrong value or nil, indicating broken rounding/baseline logic, causing inaccurate chaos totals
27+
it("handles chaos currency", function()
28+
mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 1 } }
29+
mock_tradeQuery.pbLeague = "league"
30+
local result = mock_tradeQuery:ConvertCurrencyToChaos("chaos", 4.9)
31+
assert.are.equal(result, 5)
32+
end)
33+
34+
-- Pass: Returns nil without crash
35+
-- Fail: Crashes or wrong value, indicating unhandled currencies, corrupting price conversions
36+
it("returns nil for unmapped", function()
37+
local result = mock_tradeQuery:ConvertCurrencyToChaos("exotic", 10)
38+
assert.is_nil(result)
39+
end)
40+
end)
41+
42+
describe("PriceBuilderProcessPoENinjaResponse", function()
43+
-- Pass: Processes without error, restoring map
44+
-- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions
45+
it("handles unmapped currency", function()
46+
local orig_conv = mock_tradeQuery.currencyConversionTradeMap
47+
mock_tradeQuery.currencyConversionTradeMap = { div = "id" }
48+
local resp = { exotic = 10 }
49+
mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp)
50+
-- No crash expected
51+
assert.is_true(true)
52+
mock_tradeQuery.currencyConversionTradeMap = orig_conv
53+
end)
54+
end)
55+
56+
describe("GetTotalPriceString", function()
57+
-- Pass: Sums and formats correctly (e.g., "5 chaos, 10 div")
58+
-- Fail: Wrong string (e.g., unsorted/missing sums), indicating aggregation bug, misleading users on totals
59+
it("aggregates prices", function()
60+
mock_tradeQuery.totalPrice = { { currency = "chaos", amount = 5 }, { currency = "div", amount = 10 } }
61+
local result = mock_tradeQuery:GetTotalPriceString()
62+
assert.are.equal(result, "5 chaos, 10 div")
63+
end)
64+
end)
65+
end)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
describe("TradeQueryGenerator", function()
2+
local mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} })
3+
4+
describe("ProcessMod", function()
5+
-- Pass: Mod line maps correctly to trade stat entry without error
6+
-- Fail: Mapping fails (e.g., no match found), indicating incomplete stat parsing for curse mods, potentially missing curse-enabling items in queries
7+
it("handles special curse case", function()
8+
local mod = { "You can apply an additional Curse" }
9+
local tradeStatsParsed = { result = { [2] = { entries = { { text = "You can apply # additional Curses", id = "id" } } } } }
10+
mock_queryGen.modData = { Explicit = true }
11+
mock_queryGen:ProcessMod(mod, tradeStatsParsed, 1)
12+
-- Simplified assertion; in full impl, check modData
13+
assert.is_true(true)
14+
end)
15+
end)
16+
17+
describe("WeightedRatioOutputs", function()
18+
-- Pass: Returns 0, avoiding math errors
19+
-- Fail: Returns NaN/inf or crashes, indicating unhandled infinite values, causing evaluation failures in infinite-scaling builds
20+
it("handles infinite base", function()
21+
local baseOutput = { TotalDPS = math.huge }
22+
local newOutput = { TotalDPS = 100 }
23+
local statWeights = { { stat = "TotalDPS", weightMult = 1 } }
24+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
25+
assert.are.equal(result, 0)
26+
end)
27+
28+
-- Pass: Returns capped value (100), preventing division issues
29+
-- Fail: Returns inf/NaN, indicating unhandled zero base, leading to invalid comparisons in low-output builds
30+
it("handles zero base", function()
31+
local baseOutput = { TotalDPS = 0 }
32+
local newOutput = { TotalDPS = 100 }
33+
local statWeights = { { stat = "TotalDPS", weightMult = 1 } }
34+
data.misc.maxStatIncrease = 1000
35+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
36+
assert.are.equal(result, 100)
37+
end)
38+
end)
39+
40+
describe("Filter prioritization", function()
41+
-- Pass: Limits mods to MAX_FILTERS (2 in test), preserving top priorities
42+
-- Fail: Exceeds limit, indicating over-generation of filters, risking API query size errors or rate limits
43+
it("respects MAX_FILTERS", function()
44+
local orig_max = _G.MAX_FILTERS
45+
_G.MAX_FILTERS = 2
46+
mock_queryGen.modWeights = { { weight = 10, tradeModId = "id1" }, { weight = 5, tradeModId = "id2" } }
47+
table.sort(mock_queryGen.modWeights, function(a, b)
48+
return math.abs(a.weight) > math.abs(b.weight)
49+
end)
50+
local prioritized = {}
51+
for i, entry in ipairs(mock_queryGen.modWeights) do
52+
if #prioritized < _G.MAX_FILTERS then
53+
table.insert(prioritized, entry)
54+
end
55+
end
56+
assert.are.equal(#prioritized, 2)
57+
_G.MAX_FILTERS = orig_max
58+
end)
59+
end)
60+
end)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
describe("TradeQueryRateLimiter", function()
2+
describe("ParseHeader", function()
3+
-- Pass: Extracts keys/values correctly
4+
-- Fail: Nil/malformed values, indicating regex failure, breaking policy updates from API
5+
it("parses basic headers", function()
6+
local limiter = new("TradeQueryRateLimiter")
7+
local headers = limiter:ParseHeader("X-Rate-Limit-Policy: test\nRetry-After: 5\nContent-Type: json")
8+
assert.are.equal(headers["x-rate-limit-policy"], "test")
9+
assert.are.equal(headers["retry-after"], "5")
10+
assert.are.equal(headers["content-type"], "json")
11+
end)
12+
end)
13+
14+
describe("ParsePolicy", function()
15+
-- Pass: Extracts rules/limits/states accurately
16+
-- Fail: Wrong buckets/windows, indicating parsing bug, enforcing incorrect rates
17+
it("parses full policy", function()
18+
local limiter = new("TradeQueryRateLimiter")
19+
local header = "X-Rate-Limit-Policy: trade-search-request-limit\nX-Rate-Limit-Rules: Ip,Account\nX-Rate-Limit-Ip: 8:10:60,15:60:120\nX-Rate-Limit-Ip-State: 7:10:60,14:60:120\nX-Rate-Limit-Account: 2:5:60\nX-Rate-Limit-Account-State: 1:5:60\nRetry-After: 10"
20+
local policies = limiter:ParsePolicy(header)
21+
local policy = policies["trade-search-request-limit"]
22+
assert.are.equal(policy.ip.limits[10].request, 8)
23+
assert.are.equal(policy.ip.limits[10].timeout, 60)
24+
assert.are.equal(policy.ip.state[10].request, 7)
25+
assert.are.equal(policy.account.limits[5].request, 2)
26+
end)
27+
end)
28+
29+
describe("UpdateFromHeader", function()
30+
-- Pass: Reduces limits (e.g., 5 -> 4)
31+
-- Fail: Unchanged limits, indicating margin ignored, risking user over-requests
32+
it("applies margin to limits", function()
33+
local limiter = new("TradeQueryRateLimiter")
34+
limiter.limitMargin = 1
35+
local header = "X-Rate-Limit-Policy: test\nX-Rate-Limit-Rules: Ip\nX-Rate-Limit-Ip: 5:10:60\nX-Rate-Limit-Ip-State: 4:10:60"
36+
limiter:UpdateFromHeader(header)
37+
assert.are.equal(limiter.policies["test"].ip.limits[10].request, 4)
38+
end)
39+
end)
40+
41+
describe("NextRequestTime", function()
42+
-- Pass: Delays past timestamp
43+
-- Fail: Allows immediate request, indicating ignored cooldowns, causing 429 errors
44+
it("blocks on retry-after", function()
45+
local limiter = new("TradeQueryRateLimiter")
46+
local now = os.time()
47+
limiter.policies["test"] = {}
48+
limiter.retryAfter["test"] = now + 10
49+
local nextTime = limiter:NextRequestTime("test", now)
50+
assert.is_true(nextTime > now)
51+
end)
52+
53+
-- Pass: Calculates delay from timestamps
54+
-- Fail: Allows request in limit, indicating state misread, over-throttling or bans
55+
it("blocks on window limit", function()
56+
local limiter = new("TradeQueryRateLimiter")
57+
local now = os.time()
58+
limiter.policies["test"] = { ["ip"] = { ["limits"] = { ["10"] = { ["request"] = 1, ["timeout"] = 60 } }, ["state"] = { ["10"] = { ["request"] = 1, ["timeout"] = 0 } } } }
59+
limiter.requestHistory["test"] = { timestamps = {now - 5} }
60+
limiter.lastUpdate["test"] = now - 5
61+
local nextTime = limiter:NextRequestTime("test", now)
62+
assert.is_true(nextTime > now)
63+
end)
64+
end)
65+
66+
describe("AgeOutRequests", function()
67+
-- Pass: Removes old stamps, decrements to 1
68+
-- Fail: Stale data persists, indicating aging bug, perpetual blocking
69+
it("cleans up timestamps and decrements", function()
70+
local limiter = new("TradeQueryRateLimiter")
71+
limiter.policies["test"] = { ["ip"] = { ["state"] = { ["10"] = { ["request"] = 2, ["timeout"] = 0, ["decremented"] = nil } } } }
72+
limiter.requestHistory["test"] = { timestamps = {os.time() - 15, os.time() - 5}, maxWindow=10, lastCheck=os.time() - 10 }
73+
limiter:AgeOutRequests("test", os.time())
74+
assert.are.equal(limiter.policies["test"].ip.state["10"].request, 1)
75+
assert.are.equal(#limiter.requestHistory["test"].timestamps, 1)
76+
end)
77+
end)
78+
end)

0 commit comments

Comments
 (0)