Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Generals/Code/GameEngine/Include/Common/Player.h
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ class Player : public Snapshot
//it's possible for multiple strategy centers to have the same plan, so we need
//to keep track of that like radar. Keep in mind multiple strategy centers with
//same plan do not stack, but different strategy centers with different plans do.
void changeBattlePlan( BattlePlanStatus plan, Int delta, BattlePlanBonusesData *bonus );
void changeBattlePlan( BattlePlanStatus plan, Int delta, const BattlePlanBonusesData *bonus );
Int getNumBattlePlansActive() const { return m_bombardBattlePlans + m_holdTheLineBattlePlans + m_searchAndDestroyBattlePlans; }
Int getBattlePlansActiveSpecific( BattlePlanStatus plan ) const;
void applyBattlePlanBonusesForObject( Object *obj ) const; //New object or converted object gaining our current battle plan bonuses.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class BattlePlanUpdate : public UpdateModule, public SpecialPowerUpdateInterface
virtual void onObjectCreated() override;
virtual void onDelete() override;
virtual UpdateSleepTime update() override;
virtual void onCapture(Player* oldOwner, Player* newOwner) override;

virtual CommandOption getCommandOption() const override;
protected:
Expand Down
23 changes: 12 additions & 11 deletions Generals/Code/GameEngine/Source/Common/RTS/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2921,8 +2921,7 @@ Bool Player::doesObjectQualifyForBattlePlan( Object *obj ) const
}

//-------------------------------------------------------------------------------------------------
// note, bonus is an in-out parm.
void Player::changeBattlePlan( BattlePlanStatus plan, Int delta, BattlePlanBonusesData *bonus )
void Player::changeBattlePlan( BattlePlanStatus plan, Int delta, const BattlePlanBonusesData *bonus )
{
DUMPBATTLEPLANBONUSES(bonus, this, nullptr);
Bool addBonus = false;
Expand Down Expand Up @@ -2976,22 +2975,24 @@ void Player::changeBattlePlan( BattlePlanStatus plan, Int delta, BattlePlanBonus
else if( removeBonus )
{
//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 );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested a multiplayer match between one client @ 7cb5a62 and another @ 6e41efa and did not observe any mismatches.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

if( bonus->m_bombardment > 0 )
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);
if (invertedBonus.m_bombardment > 0)
{
bonus->m_bombardment = -1;
invertedBonus.m_bombardment = -1;
}
if( bonus->m_holdTheLine > 0 )
if (invertedBonus.m_holdTheLine > 0)
{
bonus->m_holdTheLine = -1;
invertedBonus.m_holdTheLine = -1;
}
if( bonus->m_searchAndDestroy > 0 )
if (invertedBonus.m_searchAndDestroy > 0)
{
bonus->m_searchAndDestroy = -1;
invertedBonus.m_searchAndDestroy = -1;
}

applyBattlePlanBonusesForPlayerObjects( bonus );
applyBattlePlanBonusesForPlayerObjects(&invertedBonus);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@ void BattlePlanUpdate::onObjectCreated()
enableTurret( false );
}

//-------------------------------------------------------------------------------------------------
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
Comment on lines +251 to +257
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care for this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}

//-------------------------------------------------------------------------------------------------
Bool BattlePlanUpdate::initiateIntentToDoSpecialPower(const SpecialPowerTemplate *specialPowerTemplate, const Object *targetObj, const Coord3D *targetPos, const Waypoint *way, UnsignedInt commandOptions )
{
Expand Down
2 changes: 1 addition & 1 deletion GeneralsMD/Code/GameEngine/Include/Common/Player.h
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ class Player : public Snapshot
//it's possible for multiple strategy centers to have the same plan, so we need
//to keep track of that like radar. Keep in mind multiple strategy centers with
//same plan do not stack, but different strategy centers with different plans do.
void changeBattlePlan( BattlePlanStatus plan, Int delta, BattlePlanBonusesData *bonus );
void changeBattlePlan( BattlePlanStatus plan, Int delta, const BattlePlanBonusesData *bonus );
Int getNumBattlePlansActive() const { return m_bombardBattlePlans + m_holdTheLineBattlePlans + m_searchAndDestroyBattlePlans; }
Int getBattlePlansActiveSpecific( BattlePlanStatus plan ) const;
void applyBattlePlanBonusesForObject( Object *obj ) const; //New object or converted object gaining our current battle plan bonuses.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class BattlePlanUpdate : public SpecialPowerUpdateModule
virtual void onObjectCreated() override;
virtual void onDelete() override;
virtual UpdateSleepTime update() override;
virtual void onCapture(Player* oldOwner, Player* newOwner) override;

virtual CommandOption getCommandOption() const override;
protected:
Expand Down
23 changes: 12 additions & 11 deletions GeneralsMD/Code/GameEngine/Source/Common/RTS/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3414,8 +3414,7 @@ Bool Player::doesObjectQualifyForBattlePlan( Object *obj ) const
}

//-------------------------------------------------------------------------------------------------
// note, bonus is an in-out parm.
void Player::changeBattlePlan( BattlePlanStatus plan, Int delta, BattlePlanBonusesData *bonus )
void Player::changeBattlePlan( BattlePlanStatus plan, Int delta, const BattlePlanBonusesData *bonus )
{
DUMPBATTLEPLANBONUSES(bonus, this, nullptr);
Bool addBonus = false;
Expand Down Expand Up @@ -3469,22 +3468,24 @@ void Player::changeBattlePlan( BattlePlanStatus plan, Int delta, BattlePlanBonus
else if( removeBonus )
{
//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 );
if( bonus->m_bombardment > 0 )
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);
if (invertedBonus.m_bombardment > 0)
{
bonus->m_bombardment = -1;
invertedBonus.m_bombardment = -1;
}
if( bonus->m_holdTheLine > 0 )
if (invertedBonus.m_holdTheLine > 0)
{
bonus->m_holdTheLine = -1;
invertedBonus.m_holdTheLine = -1;
}
if( bonus->m_searchAndDestroy > 0 )
if (invertedBonus.m_searchAndDestroy > 0)
{
bonus->m_searchAndDestroy = -1;
invertedBonus.m_searchAndDestroy = -1;
}

applyBattlePlanBonusesForPlayerObjects( bonus );
applyBattlePlanBonusesForPlayerObjects(&invertedBonus);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@ void BattlePlanUpdate::onObjectCreated()
enableTurret( false );
}

//-------------------------------------------------------------------------------------------------
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
}

//-------------------------------------------------------------------------------------------------
Bool BattlePlanUpdate::initiateIntentToDoSpecialPower(const SpecialPowerTemplate *specialPowerTemplate, const Object *targetObj, const Coord3D *targetPos, const Waypoint *way, UnsignedInt commandOptions )
{
Expand Down
Loading