Check version
Check for duplicates
Check for support
What platform are you running Path of Building on?
Windows
What is the value from the calculation in-game?
Per the stat semantics the game adds both radius components: base 20, plus the level-scaled radius growth (+11 at effective gem level 23), plus +1 per nearby enemy up to 10 (+1 at the config's 1 nearby enemy) — so the build below should show AreaOfEffectRadius 32 (v2.66.2 values).
What is the value from the calculation in Path of Building?
PoB shows 31 or 21 for the same build depending on the app launch — skillData.radiusExtra has two writers (Cleave's own cleave_+1_base_radius_per_nearby_enemy_up_to_10 and the global active_skill_base_radius_+) and only the one that lands last in pairs() order survives; that order changes per process under LuaJIT's string-hash seeding. Both displayed values drop one real component. Observed 7×31 / 1×21 over 8 fresh launches on the v2.66.2 source archive (5/3 split on v2.65.0; seed-dependent, not 50/50).
How to reproduce the issue
- Import the build code below (Duelist, single Cleave gem L23/q20, Rusted Sword, config "# of nearby enemies" = 1).
- Note Calcs-tab AreaOfEffectRadius.
- Close and relaunch PoB, reload the same build; repeat a few times — the value flips between launches.
Headless: save the attached driver into a stock checkout's src/ and run luajit cleave_radius_repro.lua cleave_radius.xml several times (one process per run).
PoB for PoE1 build code
eNplU01T20AMPSe_YmfvbSD0QDs2TGJKmhkSmJi2x45iK8mW_XD3w5B_X-3aJpTepJWe9PTebHb9oiRr0TphdM7PP55xhroytdD7nH9_vP1wya-vxtkD-MP9bh6EjJWr8ShLMZPYosz5Z4J5sHv0P4ZRF7_obQu6Fj7na6ORs0qCc2tQmPObgFI4zxm4CnVdnCpdqwKhS1M9oV9YExpixlkr8HllauopZndFyVkD2h_Q6BX8NnZh6gH8-i702_dJpL1UjbE-hQXIyqWofBJSOuaoskDl5sebhzLnO5AOOdX7hkRqVnnRYsoTPjFDDVuJtMfbQPudNHTz3NRHNrPKBMvfQyMqDh5ltG8QcXrB2Z8AUvgjJWf_jdWkT9lgRfdLhLY7aJRN0sR4Rhe5GD5aRAbdwgQ57w-hhHnhJal4AKvROXKOmt_4Nr2MVgXn0X4Dd7g1VsHJ12nv45J4ffrHv_hCrDVZRLLwnhwtTNwio2SAJ4V7ajEu0b-yizkTdZdvwJISX9j6frOa3Y03kVDNymdj6zQ5Nnc3kdxJnJz_RGiMZmSJoOoyTUoOp-6kTGH0Tuy7dboJA1IF6UUjBdo1gt0ev2pUAkkcHdQW7WnQgM8m7__EX-yiDBU=
Screenshots
Versions
Found and mechanism-verified on v2.65.0; reproduced on an unmodified
v2.66.2 source archive (current release, its own runtime/lua51.dll).
All four sites unchanged in v2.66.2: the merge loop
(Modules/CalcActiveSkill.lua:61), the last-wins extraction (:633-638),
the global writer (Data/SkillStatMap.lua:567), and both Cleave /
Vaal Cleave statMap entries (Data/Skills/act_str.lua:1805, 1977).
Cleave's per-nearby stat VALUE was rebalanced between the versions, so
the two displayed values differ per version — the flip is the same.
Reproduction
- Duelist, any one-handed axe/sword, a single Cleave gem at effective
level ≥ 22 (e.g. level-21+ gem, or 20 + "+1 to socketed gems"),
any quality; Configuration: "# of nearby enemies" ≥ 1. (Import code
and build XML below — exactly this build, L23 q20.)
- Note Calcs-tab AreaOfEffectRadius.
- Close and relaunch PoB, reload the same build. Repeat a few times:
the radius flips across launches. The split is seed-dependent, not
50/50 (observed 5/3 over 8 launches on v2.65.0; 7/1 on v2.66.2).
Headless, from a stock checkout's src/ (driver below; run it several
times — the coin is per-process). Verbatim from the v2.66.2 archive run:
run 1 : AreaOfEffectRadius=31 skillData.radiusExtra=11
run 2 : AreaOfEffectRadius=31 skillData.radiusExtra=11
run 3 : AreaOfEffectRadius=31 skillData.radiusExtra=11
run 4 : AreaOfEffectRadius=31 skillData.radiusExtra=11
run 5 : AreaOfEffectRadius=31 skillData.radiusExtra=11
run 6 : AreaOfEffectRadius=21 skillData.radiusExtra=1
run 7 : AreaOfEffectRadius=31 skillData.radiusExtra=11
run 8 : AreaOfEffectRadius=31 skillData.radiusExtra=11
On v2.66.2 with this build the two writers evaluate to 11 (global,
level-scaled at L23) and 1 (own, +1 × 1 nearby enemy); the game adds
both, so the expected radius is 20 + 11 + 1 = 32 — PoB shows 31 or
21 depending on the launch. (On v2.65.0 the same build flips 30 ↔ 31.)
Mechanism (to the line)
- Cleave's own statMap maps
cleave_+1_base_radius_per_nearby_enemy_up_to_10 →
skill("radiusExtra", …, Multiplier:NearbyEnemies, limit 10, limitTotal); the tag is evaluated at SkillData extraction (on v2.66.2
data: 1 × nearby, so 1 at nearby=1; on v2.65.0 data the stat value
was larger and the limit capped it at 10).
- The global SkillStatMap maps
active_skill_base_radius_+ →
skill("radiusExtra", nil) — level-scaled, 11 at effective L23
(10 at L20/21 on v2.65.0, which is why low-level Cleave showed no
flicker there: both writers agreed).
calcs.mergeSkillInstanceMods iterates for stat, statValue in pairs(stats) (CalcActiveSkill.lua:61); extraction is last-wins:
activeSkill.skillData[value.key] = value.value
(CalcActiveSkill.lua:633-638). Which writer lands last = pairs()
order = per-process LuaJIT hash seed. One coin per launch.
- Vaal Cleave shares the same statMap pair (same defect). A data scan of
all 1,489 skill blocks (including all 212 transfigured variants, with
SkillPart-tag co-application semantics) finds exactly one more
structural site — StalkingPustule radius — which is unreachable in
practice (its own-map stat never materializes for the item-granted
version). Cleave of Rage is clean (its radius stat maps to an
AreaOfEffect modifier, not a SkillData write).
Why tests never see this (worth stating in the fix's regression test)
Determinism here is conditional on identical execution history: any
harness that compares two runs with the same process/seed/history gets
the same pairs() order on both sides and passes — including A/B tests
of a candidate fix, which flicker identically on both legs. The coin
only shows across process boundaries. A regression test for this class
must compare across fresh processes (we run k-fresh-process
byte-comparisons; that is what caught it).
Fix directions (mindful of the hash-weight constraint from #9947)
You noted in #9947 that per-lookup key weight sank the build-unique
cache-key approach there. Both directions below are zero-cost per
lookup:
- Determinism: iterate the stats table in sorted key order in
mergeSkillInstanceMods (one table.sort of ~40 short strings per
skill instantiation; no change to any lookup path). This pins ONE
branch reproducibly — we run exactly this patch in our harness and
the entire snapshot corpus is byte-identical to unpatched PoB except
the three collision sites.
- Correctness: the game adds both components (base 20 + level
growth + per-nearby up-to-10), so beyond determinism the two writers
should ACCUMULATE into radiusExtra (or use distinct keys summed at
read), not overwrite. That is a game-behavior call we leave to you;
determinism alone at least makes the displayed value stable and
testable.
cleave_radius_repro.lua (repro driver — save into src/ and run)
-- cleave_radius_repro.lua
-- Repro for: Cleave/Vaal Cleave AreaOfEffectRadius is launch-dependent --
-- two statMap writers race for skillData.radiusExtra; pairs() order decides
-- which lands last, and LuaJIT's per-process string-hash seed decides
-- pairs() order (PoB v2.65.0).
-- Run from a stock PathOfBuilding checkout's src/ directory, SEVERAL times
-- (one process per run -- the coin is per-process):
-- luajit cleave_radius_repro.lua cleave_radius.xml
-- Expected: AreaOfEffectRadius flips across runs (v2.66.2 data: 31 <-> 21;
-- v2.65.0 data: 31 <-> 30). The split is seed-dependent, not 50/50.
-- Both values are wrong vs the game, which adds both components; see the
-- issue text.
-- capture our arg BEFORE booting: PoB itself consumes arg[1] as an
-- optional import URL (Modules/Main.lua), so it must not see our path
local pathBuild = assert(arg[1], "arg1: cleave_radius.xml")
arg[1] = nil
package.path = package.path .. ";../runtime/lua/?.lua;../runtime/lua/?/init.lua"
package.cpath = package.cpath .. ";../runtime/?.dll"
dofile("HeadlessWrapper.lua")
local f = assert(io.open(pathBuild, "r"), "cannot open " .. tostring(pathBuild))
local xml = f:read("*a")
f:close()
loadBuildFromXML(xml, "cleave-radius-repro")
wipeGlobalCache()
build.calcsTab:BuildOutput()
local o = build.calcsTab.mainOutput
local skillData = build.calcsTab.mainEnv.player.mainSkill.skillData
-- rerun this command several times: the pair flips across processes
print(string.format("AreaOfEffectRadius=%s skillData.radiusExtra=%s",
tostring(o.AreaOfEffectRadius), tostring(skillData.radiusExtra)))
cleave_radius.xml (minimal repro build — constructed, no player data)
<?xml version="1.0" encoding="UTF-8"?>
<PathOfBuilding>
<Build level="90" targetVersion="3_0" bandit="None" className="Duelist" ascendClassName="None" mainSocketGroup="1" viewMode="CALCS" pantheonMajorGod="None" pantheonMinorGod="None"/>
<Import/>
<Calcs/>
<Skills sortGemsByDPS="false">
<Skill mainActiveSkillCalcs="1" enabled="true" slot="Body Armour" mainActiveSkill="1">
<Gem level="23" quality="20" enabled="true" nameSpec="Cleave"/>
</Skill>
</Skills>
<Tree activeSpec="1">
<Spec title="harness" treeVersion="3_28" clusterHashFormatVersion="2" classId="4" ascendClassId="0" nodes="">
</Spec>
</Tree>
<Items activeItemSet="1">
<Item id="1">
Rarity: NORMAL
Rusted Sword
</Item>
<Slot name="Weapon 1" itemId="1"/>
</Items>
<Config>
<Input name="multiplierNearbyEnemies" number="1"/>
</Config>
</PathOfBuilding>
Check version
Check for duplicates
Check for support
What platform are you running Path of Building on?
Windows
What is the value from the calculation in-game?
What is the value from the calculation in Path of Building?
How to reproduce the issue
PoB for PoE1 build code
Screenshots
Versions
Found and mechanism-verified on v2.65.0; reproduced on an unmodified
v2.66.2 source archive (current release, its own runtime/lua51.dll).
All four sites unchanged in v2.66.2: the merge loop
(
Modules/CalcActiveSkill.lua:61), the last-wins extraction (:633-638),the global writer (
Data/SkillStatMap.lua:567), and both Cleave /Vaal Cleave statMap entries (
Data/Skills/act_str.lua:1805, 1977).Cleave's per-nearby stat VALUE was rebalanced between the versions, so
the two displayed values differ per version — the flip is the same.
Reproduction
level ≥ 22 (e.g. level-21+ gem, or 20 + "+1 to socketed gems"),
any quality; Configuration: "# of nearby enemies" ≥ 1. (Import code
and build XML below — exactly this build, L23 q20.)
the radius flips across launches. The split is seed-dependent, not
50/50 (observed 5/3 over 8 launches on v2.65.0; 7/1 on v2.66.2).
Headless, from a stock checkout's
src/(driver below; run it severaltimes — the coin is per-process). Verbatim from the v2.66.2 archive run:
On v2.66.2 with this build the two writers evaluate to 11 (global,
level-scaled at L23) and 1 (own, +1 × 1 nearby enemy); the game adds
both, so the expected radius is 20 + 11 + 1 = 32 — PoB shows 31 or
21 depending on the launch. (On v2.65.0 the same build flips 30 ↔ 31.)
Mechanism (to the line)
cleave_+1_base_radius_per_nearby_enemy_up_to_10→skill("radiusExtra", …, Multiplier:NearbyEnemies, limit 10, limitTotal); the tag is evaluated at SkillData extraction (on v2.66.2data: 1 × nearby, so 1 at nearby=1; on v2.65.0 data the stat value
was larger and the limit capped it at 10).
active_skill_base_radius_+→skill("radiusExtra", nil)— level-scaled, 11 at effective L23(10 at L20/21 on v2.65.0, which is why low-level Cleave showed no
flicker there: both writers agreed).
calcs.mergeSkillInstanceModsiteratesfor stat, statValue in pairs(stats)(CalcActiveSkill.lua:61); extraction is last-wins:activeSkill.skillData[value.key] = value.value(CalcActiveSkill.lua:633-638). Which writer lands last =
pairs()order = per-process LuaJIT hash seed. One coin per launch.
all 1,489 skill blocks (including all 212 transfigured variants, with
SkillPart-tag co-application semantics) finds exactly one more
structural site — StalkingPustule
radius— which is unreachable inpractice (its own-map stat never materializes for the item-granted
version). Cleave of Rage is clean (its radius stat maps to an
AreaOfEffect modifier, not a SkillData write).
Why tests never see this (worth stating in the fix's regression test)
Determinism here is conditional on identical execution history: any
harness that compares two runs with the same process/seed/history gets
the same
pairs()order on both sides and passes — including A/B testsof a candidate fix, which flicker identically on both legs. The coin
only shows across process boundaries. A regression test for this class
must compare across fresh processes (we run k-fresh-process
byte-comparisons; that is what caught it).
Fix directions (mindful of the hash-weight constraint from #9947)
You noted in #9947 that per-lookup key weight sank the build-unique
cache-key approach there. Both directions below are zero-cost per
lookup:
mergeSkillInstanceMods(onetable.sortof ~40 short strings perskill instantiation; no change to any lookup path). This pins ONE
branch reproducibly — we run exactly this patch in our harness and
the entire snapshot corpus is byte-identical to unpatched PoB except
the three collision sites.
growth + per-nearby up-to-10), so beyond determinism the two writers
should ACCUMULATE into radiusExtra (or use distinct keys summed at
read), not overwrite. That is a game-behavior call we leave to you;
determinism alone at least makes the displayed value stable and
testable.
cleave_radius_repro.lua (repro driver — save into src/ and run)
cleave_radius.xml (minimal repro build — constructed, no player data)