Skip to content

Commit bab63d4

Browse files
authored
bugfix(contain): Restore retail compatibility after crash fix in OpenContain::processDamageToContained (#2427)
1 parent a6fdd0a commit bab63d4

4 files changed

Lines changed: 116 additions & 91 deletions

File tree

Generals/Code/GameEngine/Include/GameLogic/Module/OpenContain.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ class OpenContain : public UpdateModule,
205205
virtual Bool hasObjectsWantingToEnterOrExit() const override;
206206

207207
virtual void processDamageToContained(Real percentDamage) override; ///< Do our % damage to units now.
208+
#if RETAIL_COMPATIBLE_CRC
209+
void processDamageToContainedInternal(Object* const* objects, size_t size, Real percentDamage);
210+
#endif
208211

209212
virtual void enableLoadSounds( Bool enable ) override { m_loadSoundsEnabled = enable; }
210213

Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,68 +1296,78 @@ void OpenContain::orderAllPassengersToExit( CommandSourceType commandSource )
12961296
}
12971297
}
12981298

1299+
#if RETAIL_COMPATIBLE_CRC
1300+
12991301
//-------------------------------------------------------------------------------------------------
1300-
void OpenContain::processDamageToContained(Real percentDamage)
1302+
void OpenContain::processDamageToContainedInternal(Object* const* objects, size_t size, Real percentDamage)
13011303
{
13021304
const bool killContained = percentDamage == 1.0f;
13031305

1306+
for (size_t i = 0; i < size; ++i)
1307+
{
1308+
Object* object = objects[i];
1309+
1310+
// Calculate the damage to be inflicted on each unit.
1311+
Real damage = object->getBodyModule()->getMaxHealth() * percentDamage;
1312+
1313+
DamageInfo damageInfo;
1314+
damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
1315+
damageInfo.in.m_deathType = DEATH_BURNED;
1316+
damageInfo.in.m_sourceID = getObject()->getID();
1317+
damageInfo.in.m_amount = damage;
1318+
object->attemptDamage( &damageInfo );
1319+
1320+
if( !object->isEffectivelyDead() && killContained )
1321+
object->kill(); // in case we are carrying flame proof troops we have been asked to kill
1322+
1323+
// TheSuperHackers @info Calls to Object::attemptDamage and Object::kill may not remove
1324+
// the occupant from the host container straight away. Instead it would be removed when the
1325+
// Object deletion is finalized in a Game Logic update. This will lead to strange behavior
1326+
// where the occupant will be removed after death with a delay. This behavior cannot be
1327+
// changed without breaking retail compatibility.
1328+
}
1329+
}
1330+
1331+
#endif // RETAIL_COMPATIBLE_CRC
1332+
1333+
//-------------------------------------------------------------------------------------------------
1334+
void OpenContain::processDamageToContained(Real percentDamage)
1335+
{
13041336
#if RETAIL_COMPATIBLE_CRC
13051337

1306-
const ContainedItemsList* items = getContainedItemsList();
1307-
if( items )
1338+
DEBUG_ASSERTCRASH(m_containListSize == m_containList.size(), ("contain list size doesn't match size of container"));
1339+
1340+
// TheSuperHackers @bugfix Caball009 11/03/2026 Use a temporary copy of the contain list to iterate over,
1341+
// because causing damage to the occupants may remove some or all elements from the list
1342+
// while iterating over it, which may be unsafe.
1343+
1344+
constexpr const UnsignedInt smallContainerSize = 16;
1345+
if (m_containListSize < smallContainerSize)
13081346
{
1309-
ContainedItemsList::const_iterator it = items->begin();
1310-
const size_t listSize = items->size();
1347+
Object* containCopy[smallContainerSize];
1348+
std::copy(m_containList.begin(), m_containList.end(), containCopy);
13111349

1312-
while( it != items->end() )
1313-
{
1314-
Object *object = *it++;
1315-
1316-
//Calculate the damage to be inflicted on each unit.
1317-
Real damage = object->getBodyModule()->getMaxHealth() * percentDamage;
1318-
1319-
DamageInfo damageInfo;
1320-
damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
1321-
damageInfo.in.m_deathType = DEATH_BURNED;
1322-
damageInfo.in.m_sourceID = getObject()->getID();
1323-
damageInfo.in.m_amount = damage;
1324-
object->attemptDamage( &damageInfo );
1325-
1326-
if( !object->isEffectivelyDead() && killContained )
1327-
object->kill(); // in case we are carrying flame proof troops we have been asked to kill
1328-
1329-
// TheSuperHackers @info Calls to Object::attemptDamage and Object::kill will not remove
1330-
// the occupant from the host container straight away. Instead it will be removed when the
1331-
// Object deletion is finalized in a Game Logic update. This will lead to strange behavior
1332-
// where the occupant will be removed after death with a delay. This behavior cannot be
1333-
// changed without breaking retail compatibility.
1334-
1335-
// TheSuperHackers @bugfix xezon 05/06/2025 Stop iterating when the list was cleared.
1336-
// This scenario can happen if the killed occupant(s) apply deadly damage on death
1337-
// to the host container, which then attempts to remove all remaining occupants
1338-
// on the death of the host container. This is reproducible by destroying a
1339-
// GLA Battle Bus with at least 2 half damaged GLA Terrorists inside.
1340-
if (listSize != items->size())
1341-
{
1342-
DEBUG_ASSERTCRASH( listSize == 0, ("List is expected empty") );
1343-
break;
1344-
}
1345-
}
1350+
processDamageToContainedInternal(containCopy, m_containListSize, percentDamage);
1351+
}
1352+
else
1353+
{
1354+
const std::vector<Object*> containCopy(m_containList.begin(), m_containList.end());
1355+
1356+
processDamageToContainedInternal(&containCopy[0], containCopy.size(), percentDamage);
13461357
}
13471358

13481359
#else
13491360

13501361
// TheSuperHackers @bugfix xezon 05/06/2025 Temporarily empty the m_containList
1351-
// to prevent a potential child call to catastrophically modify the m_containList.
1352-
// This scenario can happen if the killed occupant(s) apply deadly damage on death
1353-
// to the host container, which then attempts to remove all remaining occupants
1354-
// on the death of the host container. This is reproducible by destroying a
1355-
// GLA Battle Bus with at least 2 half damaged GLA Terrorists inside.
1362+
// because causing damage to the occupants may remove some or all elements from the list
1363+
// while iterating over it, which may be unsafe.
13561364

13571365
// Caveat: While the m_containList is empty, it will not be possible to apply damage
13581366
// on death of a unit to another unit in the host container. If this functionality
13591367
// is desired, then this implementation needs to be revisited.
13601368

1369+
const bool killContained = percentDamage == 1.0f;
1370+
13611371
ContainedItemsList list;
13621372
m_containList.swap(list);
13631373
m_containListSize = 0;

GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OpenContain.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ class OpenContain : public UpdateModule,
219219
virtual Bool hasObjectsWantingToEnterOrExit() const override;
220220

221221
virtual void processDamageToContained(Real percentDamage) override; ///< Do our % damage to units now.
222+
#if RETAIL_COMPATIBLE_CRC
223+
void processDamageToContainedInternal(Object* const* objects, size_t size, Real percentDamage);
224+
#endif
222225

223226
virtual Bool isWeaponBonusPassedToPassengers() const override;
224227
virtual WeaponBonusConditionFlags getWeaponBonusPassedToPassengers() const override;

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,71 +1462,80 @@ void OpenContain::orderAllPassengersToHackInternet( CommandSourceType commandSou
14621462
}
14631463
}
14641464

1465-
1465+
#if RETAIL_COMPATIBLE_CRC
14661466

14671467
//-------------------------------------------------------------------------------------------------
1468-
void OpenContain::processDamageToContained(Real percentDamage)
1468+
void OpenContain::processDamageToContainedInternal(Object* const* objects, size_t size, Real percentDamage)
14691469
{
1470-
const OpenContainModuleData *data = getOpenContainModuleData();
1470+
const DeathType deathType = getOpenContainModuleData()->m_isBurnedDeathToUnits ? DEATH_BURNED : DEATH_NORMAL;
14711471
const bool killContained = percentDamage == 1.0f;
14721472

1473+
for (size_t i = 0; i < size; ++i)
1474+
{
1475+
Object* object = objects[i];
1476+
1477+
// Calculate the damage to be inflicted on each unit.
1478+
Real damage = object->getBodyModule()->getMaxHealth() * percentDamage;
1479+
1480+
DamageInfo damageInfo;
1481+
damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
1482+
damageInfo.in.m_deathType = deathType;
1483+
damageInfo.in.m_sourceID = getObject()->getID();
1484+
damageInfo.in.m_amount = damage;
1485+
object->attemptDamage( &damageInfo );
1486+
1487+
if( !object->isEffectivelyDead() && killContained )
1488+
object->kill(); // in case we are carrying flame proof troops we have been asked to kill
1489+
1490+
// TheSuperHackers @info Calls to Object::attemptDamage and Object::kill may not remove
1491+
// the occupant from the host container straight away. Instead it would be removed when the
1492+
// Object deletion is finalized in a Game Logic update. This will lead to strange behavior
1493+
// where the occupant will be removed after death with a delay. This behavior cannot be
1494+
// changed without breaking retail compatibility.
1495+
}
1496+
}
1497+
1498+
#endif // RETAIL_COMPATIBLE_CRC
1499+
1500+
//-------------------------------------------------------------------------------------------------
1501+
void OpenContain::processDamageToContained(Real percentDamage)
1502+
{
14731503
#if RETAIL_COMPATIBLE_CRC
14741504

1475-
const ContainedItemsList* items = getContainedItemsList();
1476-
if( items )
1505+
DEBUG_ASSERTCRASH(m_containListSize == m_containList.size(), ("contain list size doesn't match size of container"));
1506+
1507+
// TheSuperHackers @bugfix Caball009 11/03/2026 Use a temporary copy of the contain list to iterate over,
1508+
// because causing damage to the occupants may remove some or all elements from the list
1509+
// while iterating over it, which may be unsafe.
1510+
1511+
constexpr const UnsignedInt smallContainerSize = 16;
1512+
if (m_containListSize < smallContainerSize)
14771513
{
1478-
ContainedItemsList::const_iterator it = items->begin();
1479-
const size_t listSize = items->size();
1514+
Object* containCopy[smallContainerSize];
1515+
std::copy(m_containList.begin(), m_containList.end(), containCopy);
14801516

1481-
while( it != items->end() )
1482-
{
1483-
Object *object = *it++;
1484-
1485-
//Calculate the damage to be inflicted on each unit.
1486-
Real damage = object->getBodyModule()->getMaxHealth() * percentDamage;
1487-
1488-
DamageInfo damageInfo;
1489-
damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
1490-
damageInfo.in.m_deathType = data->m_isBurnedDeathToUnits ? DEATH_BURNED : DEATH_NORMAL;
1491-
damageInfo.in.m_sourceID = getObject()->getID();
1492-
damageInfo.in.m_amount = damage;
1493-
object->attemptDamage( &damageInfo );
1494-
1495-
if( !object->isEffectivelyDead() && killContained )
1496-
object->kill(); // in case we are carrying flame proof troops we have been asked to kill
1497-
1498-
// TheSuperHackers @info Calls to Object::attemptDamage and Object::kill will not remove
1499-
// the occupant from the host container straight away. Instead it will be removed when the
1500-
// Object deletion is finalized in a Game Logic update. This will lead to strange behavior
1501-
// where the occupant will be removed after death with a delay. This behavior cannot be
1502-
// changed without breaking retail compatibility.
1503-
1504-
// TheSuperHackers @bugfix xezon 05/06/2025 Stop iterating when the list was cleared.
1505-
// This scenario can happen if the killed occupant(s) apply deadly damage on death
1506-
// to the host container, which then attempts to remove all remaining occupants
1507-
// on the death of the host container. This is reproducible by destroying a
1508-
// GLA Battle Bus with at least 2 half damaged GLA Terrorists inside.
1509-
if (listSize != items->size())
1510-
{
1511-
DEBUG_ASSERTCRASH( listSize == 0, ("List is expected empty") );
1512-
break;
1513-
}
1514-
}
1517+
processDamageToContainedInternal(containCopy, m_containListSize, percentDamage);
1518+
}
1519+
else
1520+
{
1521+
const std::vector<Object*> containCopy(m_containList.begin(), m_containList.end());
1522+
1523+
processDamageToContainedInternal(&containCopy[0], containCopy.size(), percentDamage);
15151524
}
15161525

15171526
#else
15181527

15191528
// TheSuperHackers @bugfix xezon 05/06/2025 Temporarily empty the m_containList
1520-
// to prevent a potential child call to catastrophically modify the m_containList.
1521-
// This scenario can happen if the killed occupant(s) apply deadly damage on death
1522-
// to the host container, which then attempts to remove all remaining occupants
1523-
// on the death of the host container. This is reproducible by destroying a
1524-
// GLA Battle Bus with at least 2 half damaged GLA Terrorists inside.
1529+
// because causing damage to the occupants may remove some or all elements from the list
1530+
// while iterating over it, which may be unsafe.
15251531

15261532
// Caveat: While the m_containList is empty, it will not be possible to apply damage
15271533
// on death of a unit to another unit in the host container. If this functionality
15281534
// is desired, then this implementation needs to be revisited.
15291535

1536+
const DeathType deathType = getOpenContainModuleData()->m_isBurnedDeathToUnits ? DEATH_BURNED : DEATH_NORMAL;
1537+
const bool killContained = percentDamage == 1.0f;
1538+
15301539
ContainedItemsList list;
15311540
m_containList.swap(list);
15321541
m_containListSize = 0;
@@ -1544,7 +1553,7 @@ void OpenContain::processDamageToContained(Real percentDamage)
15441553

15451554
DamageInfo damageInfo;
15461555
damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
1547-
damageInfo.in.m_deathType = data->m_isBurnedDeathToUnits ? DEATH_BURNED : DEATH_NORMAL;
1556+
damageInfo.in.m_deathType = deathType;
15481557
damageInfo.in.m_sourceID = getObject()->getID();
15491558
damageInfo.in.m_amount = damage;
15501559
object->attemptDamage( &damageInfo );

0 commit comments

Comments
 (0)