bugfix(battleplan): Battle plans are now transferred to the new owner on capture#2257
bugfix(battleplan): Battle plans are now transferred to the new owner on capture#2257Stubbjax wants to merge 4 commits intoTheSuperHackers:mainfrom
Conversation
|
| Filename | Overview |
|---|---|
| Generals/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp | Adds BattlePlanUpdate::onCapture() to transfer battle plan bonuses from old to new owner on building capture, guarded by #if !RETAIL_COMPATIBLE_CRC to preserve retail parity. |
| GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp | Zero Hour mirror of the same onCapture addition, identical to the Generals version — same strengths and known open discussion points. |
| Generals/Code/GameEngine/Source/Common/RTS/Player.cpp | Makes changeBattlePlan's bonus parameter const and moves mutation into a local invertedBonus copy, correctly enabling two back-to-back calls with the same pointer without data corruption. |
| GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp | Zero Hour mirror of the const refactoring to changeBattlePlan — clean, no new issues. |
Sequence Diagram
sequenceDiagram
participant Game as Game Engine
participant Obj as Object
participant BPU as BattlePlanUpdate
participant Old as oldOwner (Player)
participant New as newOwner (Player)
Game->>Obj: onCapture(oldOwner, newOwner)
Obj->>BPU: onCapture(oldOwner, newOwner)
alt RETAIL_COMPATIBLE_CRC defined
Note over BPU: No transfer — retail behavior preserved.<br/>oldOwner keeps bonus, newOwner gets nothing.
else !RETAIL_COMPATIBLE_CRC
BPU->>Old: changeBattlePlan(m_planAffectingArmy, -1, m_bonuses)
alt oldOwner's plan count drops to 0
Old->>Old: applyBattlePlanBonusesForPlayerObjects(invertedBonus)
Note over Old: Bonus removed from army
else count remains > 0
Note over Old: Other strategy center still active, no change
end
BPU->>New: changeBattlePlan(m_planAffectingArmy, +1, m_bonuses)
alt newOwner's plan count rises to 1
New->>New: applyBattlePlanBonusesForPlayerObjects(m_bonuses)
Note over New: Bonus applied to army
else count was already > 0
Note over New: Already had this plan active, no duplicate bonus
end
end
Last reviewed commit: "fix: Formatting"
Generals/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp
Outdated
Show resolved
Hide resolved
3c994e9 to
434df18
Compare
Generals/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp
Show resolved
Hide resolved
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp
Show resolved
Hide resolved
434df18 to
6e41efa
Compare
| void BattlePlanUpdate::onCapture(Player* oldOwner, Player* newOwner) | ||
| { | ||
| #if !RETAIL_COMPATIBLE_CRC | ||
| // TheSuperHackers @bugfix Stubbjax 04/11/2025 Transfer battle plan bonuses on capture. | ||
| oldOwner->changeBattlePlan(m_planAffectingArmy, -1, m_bonuses); | ||
| newOwner->changeBattlePlan(m_planAffectingArmy, 1, m_bonuses); | ||
| #endif |
There was a problem hiding this comment.
Missing null pointer guard for oldOwner
The onDelete method (line ~194) has an explicit comment noting that the controlling player can legitimately be null during game reset cycles, and guards accordingly:
// however, player CAN legitimately be null during game reset cycles
if( player && m_planAffectingArmy != PLANSTATUS_NONE )The new onCapture doesn't apply the same safety check. If oldOwner is null (e.g., capturing from a neutral entity during a reset cycle), calling oldOwner->changeBattlePlan(...) will crash. newOwner should similarly be guarded.
| void BattlePlanUpdate::onCapture(Player* oldOwner, Player* newOwner) | |
| { | |
| #if !RETAIL_COMPATIBLE_CRC | |
| // TheSuperHackers @bugfix Stubbjax 04/11/2025 Transfer battle plan bonuses on capture. | |
| oldOwner->changeBattlePlan(m_planAffectingArmy, -1, m_bonuses); | |
| newOwner->changeBattlePlan(m_planAffectingArmy, 1, m_bonuses); | |
| #endif | |
| void BattlePlanUpdate::onCapture(Player* oldOwner, Player* newOwner) | |
| { | |
| #if !RETAIL_COMPATIBLE_CRC | |
| // TheSuperHackers @bugfix Stubbjax 04/11/2025 Transfer battle plan bonuses on capture. | |
| if( oldOwner ) | |
| oldOwner->changeBattlePlan(m_planAffectingArmy, -1, m_bonuses); | |
| if( newOwner ) | |
| newOwner->changeBattlePlan(m_planAffectingArmy, 1, m_bonuses); | |
| #endif | |
| } |
The same issue is present in GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp:251.
Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp
Line: 251-257
Comment:
**Missing null pointer guard for `oldOwner`**
The `onDelete` method (line ~194) has an explicit comment noting that the controlling player *can legitimately be null* during game reset cycles, and guards accordingly:
```cpp
// however, player CAN legitimately be null during game reset cycles
if( player && m_planAffectingArmy != PLANSTATUS_NONE )
```
The new `onCapture` doesn't apply the same safety check. If `oldOwner` is null (e.g., capturing from a neutral entity during a reset cycle), calling `oldOwner->changeBattlePlan(...)` will crash. `newOwner` should similarly be guarded.
```suggestion
void BattlePlanUpdate::onCapture(Player* oldOwner, Player* newOwner)
{
#if !RETAIL_COMPATIBLE_CRC
// TheSuperHackers @bugfix Stubbjax 04/11/2025 Transfer battle plan bonuses on capture.
if( oldOwner )
oldOwner->changeBattlePlan(m_planAffectingArmy, -1, m_bonuses);
if( newOwner )
newOwner->changeBattlePlan(m_planAffectingArmy, 1, m_bonuses);
#endif
}
```
The same issue is present in `GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp:251`.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Probably not - captures happen during active gameplay between two valid players, not during reset cycles like deletion.
The onDelete null guard exists because object cleanup can happen after the player reference is already cleared during game teardown. But onCapture fires during live gameplay when a player actively captures a building, so both owners should be valid.
However, it's a low-cost safety check (just two if statements) that makes the code more defensive. The real question is: can Strategy Centers spawn as neutral buildings that can be captured, or can they only be built by players?
If they're only player-built, the guards aren't needed. If there's any scenario where a neutral Strategy Center exists and can be captured, then oldOwner could be null.
Generals/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp
Show resolved
Hide resolved
| BattlePlanBonusesData invertedBonus = *bonus; | ||
|
|
||
| invertedBonus.m_armorScalar = 1.0f / __max( bonus->m_armorScalar, 0.01f ); | ||
| invertedBonus.m_sightRangeScalar = 1.0f / __max( bonus->m_sightRangeScalar, 0.01f ); |
There was a problem hiding this comment.
While at it, could remove all the excess tabs here.
Generals/Code/GameEngine/Source/GameLogic/Object/Update/BattlePlanUpdate.cpp
Show resolved
Hide resolved
| void BattlePlanUpdate::onCapture(Player* oldOwner, Player* newOwner) | ||
| { | ||
| #if !RETAIL_COMPATIBLE_CRC | ||
| // TheSuperHackers @bugfix Stubbjax 04/11/2025 Transfer battle plan bonuses on capture. | ||
| oldOwner->changeBattlePlan(m_planAffectingArmy, -1, m_bonuses); | ||
| newOwner->changeBattlePlan(m_planAffectingArmy, 1, m_bonuses); | ||
| #endif |
| { | ||
| //First, inverse the bonuses | ||
| bonus->m_armorScalar = 1.0f / __max( bonus->m_armorScalar, 0.01f ); | ||
| bonus->m_sightRangeScalar = 1.0f / __max( bonus->m_sightRangeScalar, 0.01f ); |
There was a problem hiding this comment.
I do wonder, is this CRC safe?
This function is called like so:
player->changeBattlePlan( m_planAffectingArmy, -1, m_bonuses );
player->changeBattlePlan( PLANSTATUS_BOMBARDMENT, -1, m_bonuses );
player->changeBattlePlan( PLANSTATUS_HOLDTHELINE, -1, m_bonuses );
player->changeBattlePlan( PLANSTATUS_SEARCHANDDESTROY, -1, m_bonuses );
player->changeBattlePlan( PLANSTATUS_BOMBARDMENT, 1, m_bonuses );
player->changeBattlePlan( PLANSTATUS_HOLDTHELINE, 1, m_bonuses );
player->changeBattlePlan( PLANSTATUS_SEARCHANDDESTROY, 1, m_bonuses );
So m_bonuses will be written here by the original code.
But now with this change, m_bonuses will remain unmodified. Does this not carry the risk of mismatch? If not, why?
There was a problem hiding this comment.
I suspect it does not mismatch because
void BattlePlanUpdate::setBattlePlan( BattlePlanStatus plan )
{
...
m_bonuses->m_armorScalar = 1.0f;
m_bonuses->m_sightRangeScalar = 1.0f;
...
This resets the scalars after every removal to restore a clean slate.
It could only mismatch if this reset was not happening after a battle plan removal.
There was a problem hiding this comment.
Should be ok. Perhaps @Skyaero42 can run it over a few replays?
Needs rebase
4794155 to
2f6c72a
Compare
Will do, give me a few days. |
This change fixes an issue where battle plans are not transferred from the old owner to the new owner when a Strategy Center is captured. In addition, bonuses in effect upon surrender in team games are also now transferred to the new owner, if applicable.
In retail, capturing a Strategy Center leaves any existing bonus intact for the opponent, while giving the capturing player no bonus.