Skip to content

Fix damage conversion result depending on table iteration order - #2588

Open
Pyiner wants to merge 1 commit into
PathOfBuildingCommunity:devfrom
Pyiner:fix/conversion-apply-order
Open

Pyiner wants to merge 1 commit into
PathOfBuildingCommunity:devfrom
Pyiner:fix/conversion-apply-order

Conversation

@Pyiner

@Pyiner Pyiner commented Nov 10, 2026

Copy link
Copy Markdown

Description of the problem being solved:

In CalcOffence.lua, the second conversion step ("Apply all conversions simultaneously") loops over tempConversions with pairs(). Inside that loop, an entry for a converted type sets that type's share (= data.base), while every entry adds its conversions to other types. When a skill's own conversion and a global conversion both point at the same damage type, the addition to that type is overwritten if that type's own entry is visited afterwards.

pairs() order over string keys is not stable across LuaJIT processes (the string hash is seeded per process), so the same build can show two different results depending on the process.

Example: a skill with 20% of Physical converted to Fire, plus a global "75% of Damage Converted to Fire Damage" (e.g. Avatar of Fire). The Physical row should end up as Physical 0.2 / Fire 0.8 (skill conversion first, then 75% of the remaining 80%). Depending on iteration order it came out as either that or Physical 0.8 / Fire 0.2. In one build I measured CombinedDPS 7,725 vs 5,736 across fresh processes, about 3:5.

The fix keeps the intended "simultaneous" semantics: set every remaining share first, then add every conversion, both in dmgTypeList order, so the result no longer depends on table order.

Steps taken to verify a working solution:

  • Added a spec to TestOffence_spec.lua (skill conversion to Fire + global 75% conversion to Fire) asserting Physical row Fire = 0.8, mult = 0.2.
  • Without the fix that spec failed in 2 of 6 runs (process-dependent order). With the fix it passed 8 of 8 runs.
  • Full suite in the pathofbuilding-tests container: 904 successes / 0 failures (903 on dev without the new spec).

Link to a build that showcases this PR:

Any build with a skill that converts part of its Physical damage to Fire (e.g. Cluster Grenade's 20%) plus Avatar of Fire.

Before screenshot:

N/A (result varied between runs of the same build).

After screenshot:

N/A

🤖 Generated with Claude Code

The second conversion step applied all conversions in one pairs() loop over
tempConversions, where an entry for a converted type sets that type's share
and every entry adds its conversions to other types. When a skill's own
conversion and a global conversion both target the same type, the addition
to that type is lost if the type's own share is set after it. pairs() order
over string keys varies between LuaJIT processes, so the same build could
give two results (e.g. 20% skill Physical-to-Fire plus 75% global to Fire:
Fire 0.2 or 0.8).

Set all shares first, then add all conversions, both in dmgTypeList order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant