diff --git a/src/combat.cpp b/src/combat.cpp index 90f5d56fed4..be4ad6a6450 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -319,7 +319,8 @@ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, in CLIP(predict.x, 0, world_coord(mapWidth - 1)); CLIP(predict.y, 0, world_coord(mapHeight - 1)); - (void)proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle, fireTime); + auto* psProj = proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle, fireTime); + proj_AddActiveProjectile(psProj); return true; } diff --git a/src/multistruct.cpp b/src/multistruct.cpp index d7997066966..619b18286ed 100644 --- a/src/multistruct.cpp +++ b/src/multistruct.cpp @@ -243,7 +243,9 @@ bool recvLasSat(NETQUEUE queue) } // Give enemy no quarter, unleash the lasat - (void)proj_SendProjectile(&psStruct->asWeaps[0], nullptr, player, psObj->pos, psObj, true, 0); + auto* psProj = proj_SendProjectile(&psStruct->asWeaps[0], nullptr, player, psObj->pos, psObj, true, 0); + proj_AddActiveProjectile(psProj); + psStruct->asWeaps[0].lastFired = gameTime; psStruct->asWeaps[0].ammo = 1; // abducting this field for keeping track of triggers diff --git a/src/projectile.cpp b/src/projectile.cpp index 88a1330159b..83c842dd9c1 100644 --- a/src/projectile.cpp +++ b/src/projectile.cpp @@ -29,6 +29,7 @@ #include "lib/framework/trig.h" #include "lib/framework/fixedpoint.h" #include "lib/framework/math_ext.h" +#include "lib/framework/paged_entity_container.h" #include "lib/gamelib/gtime.h" #include "lib/sound/audio_id.h" #include "lib/sound/audio.h" @@ -98,12 +99,23 @@ struct DAMAGE static const uint32_t ProjectileTrackerID = 0xdead0000; static uint32_t projectileTrackerIDIncrement = 0; -/* The list of projectiles in play */ +/* The list of projectiles in play. + * This intermediate container is needed to ensure that projectiles are always + * enumerated in a stable and predictable order, because `globalProjectileStorage` + * may insert new elements in place of old ones, which were previously destroyed, + * thus, modifying the order of iteration. */ static std::vector psProjectileList; -/* The next projectile to give out in the proj_First / proj_Next methods */ +/* The next projectile to give out in the proj_First / proj_Next methods. + * References `psProjectileList` container. */ static ProjectileIterator psProjectileNext; +/// +/// Global container to allocate and hold instances of `PROJECTILE` +/// within the Warzone's process lifetime. +/// +static PagedEntityContainer globalProjectileStorage; + /***************************************************************************/ // the last unit that did damage - used by script functions @@ -208,17 +220,23 @@ proj_InitSystem() /***************************************************************************/ +// Add allocated projectile `p` to the list of active projectiles (`psProjectileList`) +void proj_AddActiveProjectile(PROJECTILE* p) +{ + psProjectileList.emplace_back(p); +} + +/***************************************************************************/ + // Clean out all projectiles from the system, and properly decrement // all reference counts. void proj_FreeAllProjectiles() { - for (auto proj: psProjectileList) - { - delete proj; - } psProjectileList.clear(); psProjectileNext = psProjectileList.end(); + + globalProjectileStorage.clear(); } /***************************************************************************/ @@ -432,42 +450,42 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, ASSERT_OR_RETURN(nullptr, psStats != nullptr, "Invalid weapon stats"); ASSERT_OR_RETURN(nullptr, psTarget == nullptr || !psTarget->died, "Aiming at dead target!"); - PROJECTILE *psProj = new PROJECTILE(ProjectileTrackerID + ++projectileTrackerIDIncrement, player); + PROJECTILE proj(ProjectileTrackerID + ++projectileTrackerIDIncrement, player); /* get muzzle offset */ if (psAttacker == nullptr) { // if there isn't an attacker just start at the target position // NB this is for the script function to fire the las sats - psProj->src = target; + proj.src = target; } else if (psAttacker->type == OBJ_DROID && weapon_slot >= 0) { - calcDroidMuzzleLocation((DROID *)psAttacker, &psProj->src, weapon_slot); + calcDroidMuzzleLocation((DROID *)psAttacker, &proj.src, weapon_slot); /*update attack runs for VTOL droid's each time a shot is fired*/ updateVtolAttackRun((DROID *)psAttacker, weapon_slot); } else if (psAttacker->type == OBJ_STRUCTURE && weapon_slot >= 0) { - calcStructureMuzzleLocation((STRUCTURE *)psAttacker, &psProj->src, weapon_slot); + calcStructureMuzzleLocation((STRUCTURE *)psAttacker, &proj.src, weapon_slot); } else // incase anything wants a projectile { - psProj->src = psAttacker->pos; + proj.src = psAttacker->pos; } /* Initialise the structure */ - psProj->psWStats = psStats; + proj.psWStats = psStats; - psProj->pos = psProj->src; - psProj->dst = target; + proj.pos = proj.src; + proj.dst = target; - psProj->bVisible = false; + proj.bVisible = false; // Must set ->psDest and ->expectedDamageCaused before first call to setProjectileDestination(). - psProj->psDest = nullptr; - psProj->expectedDamageCaused = objGuessFutureDamage(psStats, player, psTarget); - setProjectileDestination(psProj, psTarget); // Updates expected damage of psProj->psDest, using psProj->expectedDamageCaused. + proj.psDest = nullptr; + proj.expectedDamageCaused = objGuessFutureDamage(psStats, player, psTarget); + setProjectileDestination(&proj, psTarget); // Updates expected damage of proj.psDest, using proj.expectedDamageCaused. /* When we have been created by penetration (spawned from another projectile), @@ -476,26 +494,26 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, if (psAttacker && psAttacker->type == OBJ_PROJECTILE) { PROJECTILE *psOldProjectile = (PROJECTILE *)psAttacker; - psProj->born = psOldProjectile->born; - psProj->src = psOldProjectile->src; + proj.born = psOldProjectile->born; + proj.src = psOldProjectile->src; - psProj->prevSpacetime.time = psOldProjectile->time; // Have partially ticked already. - psProj->time = gameTime; - psProj->prevSpacetime.time -= psProj->prevSpacetime.time == psProj->time; // Times should not be equal, for interpolation. + proj.prevSpacetime.time = psOldProjectile->time; // Have partially ticked already. + proj.time = gameTime; + proj.prevSpacetime.time -= proj.prevSpacetime.time == proj.time; // Times should not be equal, for interpolation. - setProjectileSource(psProj, psOldProjectile->psSource); - psProj->psDamaged = psOldProjectile->psDamaged; + setProjectileSource(&proj, psOldProjectile->psSource); + proj.psDamaged = psOldProjectile->psDamaged; // TODO Should finish the tick, when penetrating. } else { - psProj->born = fireTime; // Born at the start of the tick. + proj.born = fireTime; // Born at the start of the tick. - psProj->prevSpacetime.time = fireTime; - psProj->time = psProj->prevSpacetime.time; + proj.prevSpacetime.time = fireTime; + proj.time = proj.prevSpacetime.time; - setProjectileSource(psProj, psAttacker); + setProjectileSource(&proj, psAttacker); } if (psTarget) @@ -504,22 +522,22 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int minHeight = std::min(std::max(maxHeight + 2 * LINE_OF_FIRE_MINIMUM - areaOfFire(psAttacker, psTarget, weapon_slot, true), 0), maxHeight); scoreUpdateVar(WD_SHOTS_ON_TARGET); - psProj->dst.z = psTarget->pos.z + minHeight + gameRand(std::max(maxHeight - minHeight, 1)); + proj.dst.z = psTarget->pos.z + minHeight + gameRand(std::max(maxHeight - minHeight, 1)); /* store visible part (LOCK ON this part for homing :) */ - psProj->partVisible = maxHeight - minHeight; + proj.partVisible = maxHeight - minHeight; } else { - psProj->dst.z = target.z + LINE_OF_FIRE_MINIMUM; + proj.dst.z = target.z + LINE_OF_FIRE_MINIMUM; scoreUpdateVar(WD_SHOTS_OFF_TARGET); } - Vector3i deltaPos = psProj->dst - psProj->src; + Vector3i deltaPos = proj.dst - proj.src; /* roll never set */ - psProj->rot.roll = 0; + proj.rot.roll = 0; - psProj->rot.direction = iAtan2(deltaPos.xy()); + proj.rot.direction = iAtan2(deltaPos.xy()); // Get target distance, horizontal distance only. @@ -527,57 +545,57 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, if (proj_Direct(psStats)) { - psProj->rot.pitch = iAtan2(deltaPos.z, dist); + proj.rot.pitch = iAtan2(deltaPos.z, dist); } else { /* indirect */ - projCalcIndirectVelocities(dist, deltaPos.z, psStats->flightSpeed, &psProj->vXY, &psProj->vZ, min_angle); - psProj->rot.pitch = iAtan2(psProj->vZ, psProj->vXY); + projCalcIndirectVelocities(dist, deltaPos.z, psStats->flightSpeed, &proj.vXY, &proj.vZ, min_angle); + proj.rot.pitch = iAtan2(proj.vZ, proj.vXY); } - psProj->state = PROJ_INFLIGHT; + proj.state = PROJ_INFLIGHT; // If droid or structure, set muzzle pitch. if (psAttacker != nullptr && weapon_slot >= 0) { if (psAttacker->type == OBJ_DROID) { - ((DROID *)psAttacker)->asWeaps[weapon_slot].rot.pitch = psProj->rot.pitch; + ((DROID *)psAttacker)->asWeaps[weapon_slot].rot.pitch = proj.rot.pitch; } else if (psAttacker->type == OBJ_STRUCTURE) { - ((STRUCTURE *)psAttacker)->asWeaps[weapon_slot].rot.pitch = psProj->rot.pitch; + ((STRUCTURE *)psAttacker)->asWeaps[weapon_slot].rot.pitch = proj.rot.pitch; } } - /* put the projectile object in the global list */ - psProjectileList.push_back(psProj); + /* put the projectile object in the global list, obtain the stable address for it. */ + PROJECTILE& stableProj = globalProjectileStorage.emplace(std::move(proj)); /* play firing audio */ // only play if either object is visible, i know it's a bit of a hack, but it avoids the problem // of having to calculate real visibility values for each projectile. - if (bVisible || gfxVisible(psProj)) + if (bVisible || gfxVisible(&stableProj)) { // note that the projectile is visible - psProj->bVisible = true; + stableProj.bVisible = true; if (psStats->iAudioFireID != NO_SOUND) { - if (psProj->psSource) + if (stableProj.psSource) { /* firing sound emitted from source */ - audio_PlayObjDynamicTrack(psProj->psSource, psStats->iAudioFireID, nullptr); + audio_PlayObjDynamicTrack(stableProj.psSource, psStats->iAudioFireID, nullptr); /* GJ HACK: move howitzer sound with shell */ if (psStats->weaponSubClass == WSC_HOWITZERS) { - audio_PlayObjDynamicTrack(psProj, ID_SOUND_HOWITZ_FLIGHT, nullptr); + audio_PlayObjDynamicTrack(&stableProj, ID_SOUND_HOWITZ_FLIGHT, nullptr); } } //don't play the sound for a LasSat in multiPlayer else if (!(bMultiPlayer && psStats->weaponSubClass == WSC_LAS_SAT)) { - audio_PlayObjStaticTrack(psProj, psStats->iAudioFireID); + audio_PlayObjStaticTrack(&stableProj, psStats->iAudioFireID); } } } @@ -588,11 +606,11 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, counterBatteryFire(castBaseObject(psAttacker), psTarget); } - syncDebugProjectile(psProj, '*'); + syncDebugProjectile(&stableProj, '*'); - CHECK_PROJECTILE(psProj); + CHECK_PROJECTILE(&stableProj); - return psProj; + return &stableProj; } /***************************************************************************/ @@ -1397,23 +1415,21 @@ PROJECTILE* PROJECTILE::update() void proj_UpdateAll() { WZ_PROFILE_SCOPE(proj_UpdateAll); - static std::unordered_set spawnedProjectiles; + + static std::vector spawnedProjectiles; spawnedProjectiles.reserve(psProjectileList.size()); spawnedProjectiles.clear(); - // Update all projectiles. Penetrating projectiles may add to psProjectileList. + // Update all projectiles. + // Penetrating projectiles may spawn additional projectiles, + // which will be returned from `PROJECTILE::update()`. + // These need to be added separately to `psProjectileList` later. for (PROJECTILE* p : psProjectileList) { - // Don't process penetrating projectiles, which were spawned - // within the same `proj_UpdateAll()` invocation. - if (spawnedProjectiles.count(p) != 0) - { - continue; - } PROJECTILE* spawned = p->update(); if (spawned) { - spawnedProjectiles.emplace(spawned); + spawnedProjectiles.emplace_back(spawned); } } @@ -1424,9 +1440,16 @@ void proj_UpdateAll() { return false; } - delete p; + auto it = globalProjectileStorage.find(*p); + ASSERT(it != globalProjectileStorage.end(), "Invalid projectile, not found in global storage"); + globalProjectileStorage.erase(it); return true; }), psProjectileList.end()); + + // Add spawned penetrating projectiles, + // which were collected earlier during the update procedure. + psProjectileList.reserve(psProjectileList.size() + spawnedProjectiles.size()); + std::move(spawnedProjectiles.begin(), spawnedProjectiles.end(), std::back_inserter(psProjectileList)); } /***************************************************************************/ diff --git a/src/projectile.h b/src/projectile.h index 66f1c6827f7..67b9efc8ffa 100644 --- a/src/projectile.h +++ b/src/projectile.h @@ -54,6 +54,8 @@ bool proj_Shutdown(); ///< Shut down projectile subsystem. PROJECTILE *proj_GetFirst(); ///< Get first projectile in the list. PROJECTILE *proj_GetNext(); ///< Get next projectile in the list. +void proj_AddActiveProjectile(PROJECTILE* p); ///< Add allocated projectile `p` to the list of active projectiles + void proj_FreeAllProjectiles(); ///< Free all projectiles in the list. void setExpGain(int player, int gain); @@ -64,13 +66,13 @@ int32_t projCalcIndirectVelocities(const int32_t dx, const int32_t dz, int32_t v /** Send a single projectile against the given target. * Returns a non-null pointer to the newly-created projectile in the case of penetrating projectiles. - * The projectile is automatically added to `psProjectileList` global list. */ + * The returned projectile needs to be manually added `psProjectileList` global list. */ PROJECTILE* proj_SendProjectile(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, bool bVisible, int weapon_slot); /** Send a single projectile against the given target * with a minimum shot angle. * Returns a non-null pointer to the newly-created projectile in the case of penetrating projectiles. - * The projectile is automatically added to `psProjectileList` global list. */ + * The returned projectile needs to be manually added `psProjectileList` global list. */ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, bool bVisible, int weapon_slot, int min_angle, unsigned fireTime); /** Return whether a weapon is direct or indirect. */ diff --git a/src/projectiledef.h b/src/projectiledef.h index 2de5fd8bd67..8f0563612e4 100644 --- a/src/projectiledef.h +++ b/src/projectiledef.h @@ -44,7 +44,7 @@ struct PROJECTILE : public SIMPLE_OBJECT // Returns non-empty pointer if `update()` has spawned an additional projectile, // which will be true for penetrating projectiles. - // The newly-created projectile will be added to `psProjectileList` automatically. + // The newly-created projectile needs to be manually added to `psProjectileList`. PROJECTILE* update(); UBYTE state; ///< current projectile state diff --git a/src/wzapi.cpp b/src/wzapi.cpp index bccba532ad8..a7fbd75a942 100644 --- a/src/wzapi.cpp +++ b/src/wzapi.cpp @@ -3295,7 +3295,8 @@ wzapi::no_return_value wzapi::fireWeaponAtLoc(WZAPI_PARAMS(std::string weaponNam WEAPON sWeapon; sWeapon.nStat = weaponIndex; - (void)proj_SendProjectile(&sWeapon, nullptr, player, target, nullptr, true, 0); + auto* psProj = proj_SendProjectile(&sWeapon, nullptr, player, target, nullptr, true, 0); + proj_AddActiveProjectile(psProj); return {}; } @@ -3317,7 +3318,8 @@ wzapi::no_return_value wzapi::fireWeaponAtObj(WZAPI_PARAMS(std::string weaponNam WEAPON sWeapon; sWeapon.nStat = weaponIndex; - (void)proj_SendProjectile(&sWeapon, nullptr, player, target, psObj, true, 0); + auto* psProj = proj_SendProjectile(&sWeapon, nullptr, player, target, psObj, true, 0); + proj_AddActiveProjectile(psProj); return {}; }