Skip to content

fix: prevent OverflowException in rune skill power for high-stat casters (PLD-1390)#3293

Open
ipdae wants to merge 5 commits into
developmentfrom
bugfix/PLD-1390/rune-skill-overflow
Open

fix: prevent OverflowException in rune skill power for high-stat casters (PLD-1390)#3293
ipdae wants to merge 5 commits into
developmentfrom
bugfix/PLD-1390/rune-skill-overflow

Conversation

@ipdae

@ipdae ipdae commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes PLD-1390. A Caster-stat-referencing rune skill computed its power as (int)Math.Round(stat * SkillValue) in Player.SetRuneSkills. In C# a decimal -> int cast always performs an overflow check (regardless of checked/unchecked), so any character whose referenced stat is large enough that stat * SkillValue exceeds int.MaxValue (~2.1B) threw System.OverflowException. That aborted every battle action calling SetRuneSkills (HackAndSlash, Sweep, Raid, AdventureBoss, InfiniteTower) and blocked gameplay for the affected avatar.

The whole downstream pipeline is already long-based — SkillFactory.GetV1(long power), Skill.Power, SkillCustomField.BuffValue, and the AttackSkill/ArenaAttackSkill damage calc (SafeDecimalToInt64). The int local in SetRuneSkills was the only narrowing point. This widens power to long via NumberConversionHelper.SafeDecimalToInt64.

Root cause

Lib9c/Model/Character/Player.cs (SetRuneSkills):

var power = 0;                                          // int
power = (int)Math.Round(value * optionInfo.SkillValue); // decimal -> int, always overflow-checked

Reproduction

Real failing tx: 1ab27944f05a195c1d5a1906513bd15e7c2e97253ba077e9bd0272965abf556a

  • avatar ATK = 629,550,973
  • rune 10003 lv200: SkillValue = 4.47, ATK% Caster
  • (int)Math.Round(629,550,973 × 4.47) = 2,814,092,849 > int.MaxValueOverflowException

RuneSkillOverflowTest reproduces this against the real values (local RuneOptionSheet.csv already contains rune 10003 lv1–300, so no PatchTableSheet needed) and asserts the power is now the full long value.

Notes / out of scope

  • The obsolete Player.SetRuneV1 (used by StageSimulatorV3/RaidSimulatorV2 for legacy action versions) has the same cast but is intentionally left unchanged to preserve historical replay determinism.
  • ArenaCharacter.SetRuneSkills already clamps with SafeDecimalToInt32 (no crash); it is unchanged here, so PvE now yields the full long power while arena still caps at int.MaxValue. Whether to unify the two (and the related SafeDecimalToInt32 clamps in the damage/DoT/vampiric paths) is a separate balance/consensus decision tracked in PLD-1390.
  • This changes on-chain execution (previously-failing txs would now succeed); it must ship behind the team's standard hardfork/version gating so historical replay stays deterministic.

Test

  • Full test suite passed on pre-push (3692 passed, 6 skipped).

🤖 Generated with Claude Code

ipdae and others added 2 commits July 1, 2026 17:02
…casters

Rune skills with a Caster stat reference computed their power as
`(int)Math.Round(stat * SkillValue)`. Because a C# `decimal -> int`
cast always performs an overflow check (regardless of checked/unchecked
context), any character whose referenced stat is large enough that
`stat * SkillValue` exceeds int.MaxValue threw System.OverflowException,
which aborted every battle action that calls SetRuneSkills (HackAndSlash,
Sweep, Raid, AdventureBoss, InfiniteTower) and blocked gameplay.

Widen `power` to `long` and use NumberConversionHelper.SafeDecimalToInt64,
matching the rest of the pipeline which is already long-based
(SkillFactory.GetV1(long power), Skill.Power, SkillCustomField.BuffValue,
and the AttackSkill/ArenaAttackSkill damage calc via SafeDecimalToInt64).

Reproduced from the real failing tx
1ab27944f05a195c1d5a1906513bd15e7c2e97253ba077e9bd0272965abf556a
(ATK 629,550,973 + rune 10003 lv200, SkillValue 4.47 ->
(int)Math.Round(2,814,092,849) overflow). Added RuneSkillOverflowTest
as a regression test.

The obsolete Player.SetRuneV1 (used by StageSimulatorV3/RaidSimulatorV2
for legacy action versions) has the same cast but is intentionally left
unchanged to preserve historical replay determinism.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying lib9c with  Cloudflare Pages  Cloudflare Pages

Latest commit: 32a4e22
Status: ✅  Deploy successful!
Preview URL: https://10dc6c88.lib9c.pages.dev
Branch Preview URL: https://bugfix-pld-1390-rune-skill-o.lib9c.pages.dev

View logs

ipdae and others added 3 commits July 1, 2026 18:18
Document the constructor and the two test methods so the PR does not
increase the no-docs count enforced by the count-no-docs lint gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ArenaCharacter.SetRuneSkills clamped rune skill power to int.MaxValue via
SafeDecimalToInt32, while the PvE Player.SetRuneSkills fix widened it to
long. For a high-stat caster this made PvE and arena compute different
rune skill power (PvE full value, arena capped at ~2.1B). Widen the
active arena SetRuneSkills to long/SafeDecimalToInt64 as well so both
paths agree and match the long-based damage pipeline. The obsolete
ArenaCharacter.SetRuneV1 is left unchanged for historical determinism.

Add an arena regression test alongside the PvE one, and drop the
per-member XML doc comments on the test (no longer needed now that the
count-no-docs gate excludes test projects, #3295).

Co-Authored-By: Claude Opus 4.8 (1M context) <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