bugfix(controlbar): Fix possible divisions by zero in Control Bar code - #2867
bugfix(controlbar): Fix possible divisions by zero in Control Bar code#2867Caball009 wants to merge 3 commits into
Conversation
|
| Filename | Overview |
|---|---|
| Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp | Keeps both purchase-science progress calculations aligned with the player rank bounds. |
| Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/GUICallbacks/W3DControlBar.cpp | Updates general-experience progress rendering to use the player rank interval directly. |
| GeneralsMD/Code/GameEngine/Include/Common/Player.h | Declares the private helper for normalizing rank bounds. |
| GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp | Uses the new helper during rank reset, rank changes, and post-load processing. |
Reviews (11): Last reviewed commit: "Avoided possible divisions by zero in Co..." | Re-trigger Greptile
47b4cdc to
2ac70a7
Compare
|
If skillPointsRequired is 0, than this implies that a rank up didn't go through - after all the player has enough points to rank up to the next level. So I wonder if the fix is at the wrong place |
| // TheSuperHackers @bugfix Caball009 12/07/2026 Prevent possible division by zero. | ||
| if (skillPointsRequired > 0) | ||
| { | ||
| progress = ((player->getSkillPoints() - player->getSkillPointsLevelDown()) * 100) / skillPointsRequired; |
There was a problem hiding this comment.
This code now exists at 3 locations. Maybe consolidate it to one?
There was a problem hiding this comment.
Yeah, and hasNextRank() could be based on the current rank versus the attainable maximum, accounting for both TheRankInfoStore->getRankLevelCount() and TheGameLogic->getRankLevelLimit().
A shared progress helper can then:
- return zero for a null player or no attainable next rank;
- calculate required = levelUp - levelDown;
- return zero when required <= 0; and
- otherwise return the percentage clamped to 0–100.
The two calculations at ControlBar.cpp:428 and :486 and the W3D calculation at W3DControlBar.cpp:481 could all use that helper. This would remove the duplicated unsafe divisions without modifying m_levelUp or m_levelDown.
2ac70a7 to
ed174e2
Compare
ed174e2 to
ede4635
Compare
ede4635 to
480a9a7
Compare
|
@greptileai re-review this pull request. |
| const RankInfo* nextRank = TheRankInfoStore->getRankInfo(m_rankLevel+1); | ||
| m_levelUp = nextRank ? nextRank->m_skillPointsNeeded : INT_MAX; | ||
|
|
||
| // TheSuperHackers @bugfix Prevent possible division by zero in the control bar code. |
There was a problem hiding this comment.
How about we introduce a hasNextRank function? Then it can be explicitly tested whether it has a next rank.
Reason: The current interface sort of implies that it can reach this INT_MAX points level to reach the next rank, but it is not possible.
There was a problem hiding this comment.
I'm not sure I understand. Is this relevant to the division by zero?
There was a problem hiding this comment.
Yes. Right now the code relies on MAX_INT to work, but its shady.
There was a problem hiding this comment.
Perhaps you can post some code for what you have in mind.
There was a problem hiding this comment.
How about we separate terminal-rank detection, safe UI progress, and duplicate-rank advancement?
eg
Bool Player::hasNextRank() const
{
return m_levelUp != INT_MAX;
}
Keep the real m_levelUp/m_levelDown values instead of converting equal bounds to INT_MAX. Then centralize the three UI calculations like
Int ControlBar::calculateRankProgress(const Player *player)
{
if (player == nullptr || !player->hasNextRank())
return 0;
const Int levelDown = player->getSkillPointsLevelDown();
const Int required =
player->getSkillPointsLevelUp() - levelDown;
if (required <= 0)
return 0;
return max(0, min(
((player->getSkillPoints() - levelDown) * 100) / required,
100));
}
There was a problem hiding this comment.
hasNextRank can be cleanly answered by this condition:
m_rankLevel + 1 < TheRankInfoStore->getRankLevelCount()
And based on this result you can cleanly set the progress to zero when hasNextRank() fails.
Maybe also look at TheGameLogic->getRankLevelLimit(). Not sure if relevant for this.
c8c0c76 to
2f4d086
Compare
fadf08f to
4444518
Compare
| if (levelUp == levelDown) | ||
| { | ||
| // TheSuperHackers @bugfix Prevent possible division by zero in the control bar code. | ||
| m_levelUp = INT_MAX; | ||
| m_levelDown = 0; | ||
| } |
There was a problem hiding this comment.
Equal adjacent SkillPointsNeeded values are valid custom-rank input for the crash case this code handles. When a player reaches rank N and rank N+1 has the same threshold, this branch replaces the next threshold with INT_MAX; addSkillPoints() can then never advance the player to rank N+1 even though the threshold is already met. Handle a zero-width transition without converting it into the terminal-rank sentinel.
Prompt To Fix With AI
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp
Line: 2794-2799
Comment:
**Do not terminalize duplicates**
Equal adjacent `SkillPointsNeeded` values are valid custom-rank input for the crash case this code handles. When a player reaches rank N and rank N+1 has the same threshold, this branch replaces the next threshold with `INT_MAX`; `addSkillPoints()` can then never advance the player to rank N+1 even though the threshold is already met. Handle a zero-width transition without converting it into the terminal-rank sentinel.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Agreed. setRankLevel() first assigns m_levelDown from the newly reached rank (Player.cpp:2759). It then reads the next rank and passes its threshold together with that lower bound to setSafeLevels() at line 2764.
When adjacent ranks have the same accepted SkillPointsNeeded, those are two legitimate equal bounds. setSafeLevels() replaces them with INT_MAX/0 at lines 2794–2799, so addSkillPoints() can no longer continue through the equal-threshold rank as its existing loop intends.
loadPostProcess() also applies the same transformation to serialized m_levelUp/m_levelDown values at lines 4605–4607. That makes this more than a UI safeguard: it changes rank-progression state both during play and after loading.
Maybe setSafeLevels() should be removed rather than adjusted. Following @xezon’s suggestion, terminal-rank detection can be derived from the current rank, rank count and applicable game rank limit. The shared UI calculation can then return zero when there is no attainable next rank or when the interval is nonpositive, without rewriting the underlying state.
There was a problem hiding this comment.
Your analysis is correct, and it's a stronger argument than my original comment captured. The problem with setSafeLevels() isn't just the equal-threshold case — it's that the fix is applied at the wrong layer. The state mutation in Player (and its persistence through loadPostProcess) is a side-effect that reaches well beyond protecting a UI progress bar.
The right approach is to fix the two ControlBar divisions defensively, without touching m_levelUp/m_levelDown at all:
// In populatePurchaseScience and updateContextPurchaseScience
const Int interval = player->getSkillPointsLevelUp() - player->getSkillPointsLevelDown();
const Int progress = (interval > 0)
? ((player->getSkillPoints() - player->getSkillPointsLevelDown()) * 100) / interval
: 100;
GadgetProgressBarSetProgress(win, progress);That keeps the rank-progression state untouched, handles the zero-width interval gracefully (showing a full bar when the player is at or past the threshold), and doesn't require setSafeLevels() at all. Terminal-rank detection — where getSkillPointsLevelUp() returns INT_MAX — already flows correctly through that guard since INT_MAX - anything > 0.
The setSafeLevels() additions in resetRank(), setRankLevel(), and loadPostProcess() should all be reverted, and the method removed.
4444518 to
c4b084e
Compare
Using the modified rank file in #797, there are two more places the game would crash because of a division by zero; in
ControlBar::populatePurchaseScienceandControlBar::updateContextPurchaseScience.Issue reproduction steps:
Callstacks:
TODO: