diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MultiLocationSpecialPowerUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MultiLocationSpecialPowerUpdate.h index 351826f118d..4f8c6327a6f 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MultiLocationSpecialPowerUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MultiLocationSpecialPowerUpdate.h @@ -58,6 +58,13 @@ class MultiLocationSpecialPowerUpdateModuleData : public ModuleData UnsignedInt m_initialDelay; ///< delay before the first OCL fires (INI in ms, stored as frames) UnsignedInt m_delay; ///< delay between successive OCLs (INI in ms, stored as frames) + // An OCL containing an Attack nugget makes the caster attack the point with one of its own weapon + // slots, and aiAttackPosition replaces the previous order rather than queueing behind it. With a + // fixed m_delay the next point cuts the previous barrage short, so offer a mode that advances only + // once the caster has actually stopped attacking. + Bool m_waitForAttackComplete; ///< advance only when the caster stops attacking + UnsignedInt m_maxWaitPerTarget; ///< safety cap per point, so an unattackable point cannot stall the sequence + MultiLocationSpecialPowerUpdateModuleData(); static void buildFieldParse(MultiIniFieldParse& p); @@ -110,10 +117,13 @@ class MultiLocationSpecialPowerUpdate : public SpecialPowerUpdateModule const ObjectCreationList* findOCL() const; ///< science-upgraded OCL, else the default void fireOclAtLocation( const Coord3D *loc ); ///< spawn the OCL at one captured point, honoring CreateLocation + UnsignedInt getPollInterval() const; ///< frames between update() wake-ups during the sequence SpecialPowerModuleInterface* m_specialPowerModule; ///< cached paired power module (recharge/cost/timer) std::vector m_locations; ///< all captured target points (in click order) Bool m_active; ///< TRUE while the OCL spawn sequence is running Int m_spawnIndex; ///< index of the next point to spawn an OCL at + Bool m_attackSeen; ///< the caster actually entered its attack state for the point just fired + UnsignedInt m_waitDeadline; ///< logic frame at which we give up waiting on the point just fired }; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp index 24010f704e4..d56564b6510 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp @@ -322,11 +322,8 @@ void ChronoSphereUpdateModule::loadPostProcess( void ) // extend base class SpecialPowerUpdateModule::loadPostProcess(); - // If a teleport was pending when the game was saved, re-arm the wake so it still fires. - if( m_active ) - { - UnsignedInt now = TheGameLogic->getFrame(); - UnsignedInt delay = (m_teleportFrame > now) ? (m_teleportFrame - now) : 0; - setWakeFrame( getObject(), delay > 0 ? UPDATE_SLEEP( delay ) : UPDATE_SLEEP_NONE ); - } + // Do not call setWakeFrame() here. It is not safe from the xfer system: the sleepy update heap + // has not been rebuilt yet, so our index in the logic is still -1 and friend_awakenUpdateModule + // would RELEASE_CRASH. Nothing needs re-arming anyway - UpdateModule::xfer already restored our + // wake frame, and GameLogic::loadPostProcess rebuilds the heap from it. } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/MultiLocationSpecialPowerUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/MultiLocationSpecialPowerUpdate.cpp index f7a2880c943..91fd4d6d97d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/MultiLocationSpecialPowerUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/MultiLocationSpecialPowerUpdate.cpp @@ -34,6 +34,7 @@ #include "GameLogic/Object.h" #include "GameLogic/ObjectCreationList.h" #include "GameLogic/TerrainLogic.h" +#include "GameLogic/Module/AIUpdate.h" #include "GameLogic/Module/SpecialPowerModule.h" #include "GameLogic/Module/MultiLocationSpecialPowerUpdate.h" @@ -81,6 +82,8 @@ MultiLocationSpecialPowerUpdateModuleData::MultiLocationSpecialPowerUpdateModule m_createLoc = CREATE_AT_EDGE_NEAR_SOURCE; m_initialDelay = 0; m_delay = 0; + m_waitForAttackComplete = FALSE; + m_maxWaitPerTarget = 0; } //------------------------------------------------------------------------------------------------- @@ -96,6 +99,8 @@ MultiLocationSpecialPowerUpdateModuleData::MultiLocationSpecialPowerUpdateModule { "UpgradeOCL", parseMultiLocUpgradeOCL, nullptr, offsetof( MultiLocationSpecialPowerUpdateModuleData, m_upgradeOCL ) }, { "InitialDelay", INI::parseDurationUnsignedInt, nullptr, offsetof( MultiLocationSpecialPowerUpdateModuleData, m_initialDelay ) }, { "Delay", INI::parseDurationUnsignedInt, nullptr, offsetof( MultiLocationSpecialPowerUpdateModuleData, m_delay ) }, + { "WaitForAttackComplete", INI::parseBool, nullptr, offsetof( MultiLocationSpecialPowerUpdateModuleData, m_waitForAttackComplete ) }, + { "MaxWaitPerTarget", INI::parseDurationUnsignedInt, nullptr, offsetof( MultiLocationSpecialPowerUpdateModuleData, m_maxWaitPerTarget ) }, { nullptr, nullptr, nullptr, 0 } }; p.add(dataFieldParse); @@ -109,6 +114,8 @@ MultiLocationSpecialPowerUpdate::MultiLocationSpecialPowerUpdate( Thing *thing, m_locations.clear(); m_active = FALSE; m_spawnIndex = 0; + m_attackSeen = FALSE; + m_waitDeadline = 0; } //------------------------------------------------------------------------------------------------- @@ -202,6 +209,19 @@ void MultiLocationSpecialPowerUpdate::fireOclAtLocation( const Coord3D *loc ) } } +//------------------------------------------------------------------------------------------------- +// Frames between update() wake-ups. In WaitForAttackComplete mode Delay is the poll granularity and +// the minimum gap between points, rather than the whole spacing. +//------------------------------------------------------------------------------------------------- +UnsignedInt MultiLocationSpecialPowerUpdate::getPollInterval() const +{ + const MultiLocationSpecialPowerUpdateModuleData *d = getMultiLocationSpecialPowerUpdateModuleData(); + if( d->m_delay > 0 ) + return d->m_delay; + + return d->m_waitForAttackComplete ? (LOGICFRAMES_PER_SECOND / 4) : 1; +} + //------------------------------------------------------------------------------------------------- // First click: the first target point arrives here as targetPos. The rest of the points arrive // together via setSpecialPowerMultiLocations() once the final click commits. @@ -242,6 +262,8 @@ void MultiLocationSpecialPowerUpdate::setSpecialPowerMultiLocations( const std:: const MultiLocationSpecialPowerUpdateModuleData *d = getMultiLocationSpecialPowerUpdateModuleData(); m_spawnIndex = 0; m_active = TRUE; + m_attackSeen = FALSE; + m_waitDeadline = 0; setWakeFrame( getObject(), UPDATE_SLEEP( d->m_initialDelay > 0 ? d->m_initialDelay : 1 ) ); } @@ -254,11 +276,34 @@ UpdateSleepTime MultiLocationSpecialPowerUpdate::update() return UPDATE_SLEEP_FOREVER; const MultiLocationSpecialPowerUpdateModuleData *d = getMultiLocationSpecialPowerUpdateModuleData(); + const UnsignedInt pollInterval = getPollInterval(); + + // If the OCL makes us attack (an Attack nugget orders our own AI), the next point's order would + // cancel the barrage still in progress. In that mode hold here until the caster is done shooting. + if( d->m_waitForAttackComplete && m_spawnIndex > 0 && m_spawnIndex < (Int)m_locations.size() ) + { + const AIUpdateInterface *ai = getObject()->getAIUpdateInterface(); + const Bool attacking = ai != nullptr && ai->isAttacking(); + + // The AI needs a frame or two to enter the attack state after aiAttackPosition, so "not + // attacking" only means "finished" once we have actually seen it attack. + if( attacking ) + m_attackSeen = TRUE; + + const Bool finished = m_attackSeen && !attacking; + const Bool timedOut = TheGameLogic->getFrame() >= m_waitDeadline; + if( !finished && !timedOut ) + return UPDATE_SLEEP( pollInterval ); + } if( m_spawnIndex < (Int)m_locations.size() ) { fireOclAtLocation( &m_locations[m_spawnIndex] ); ++m_spawnIndex; + + m_attackSeen = FALSE; + const UnsignedInt maxWait = d->m_maxWaitPerTarget > 0 ? d->m_maxWaitPerTarget : (30 * LOGICFRAMES_PER_SECOND); + m_waitDeadline = TheGameLogic->getFrame() + maxWait; } if( m_spawnIndex >= (Int)m_locations.size() ) @@ -268,7 +313,7 @@ UpdateSleepTime MultiLocationSpecialPowerUpdate::update() return UPDATE_SLEEP_FOREVER; } - return UPDATE_SLEEP( d->m_delay > 0 ? d->m_delay : 1 ); + return UPDATE_SLEEP( pollInterval ); } // ------------------------------------------------------------------------------------------------ @@ -284,12 +329,13 @@ void MultiLocationSpecialPowerUpdate::crc( Xfer *xfer ) /** Xfer method * Version Info: * 1: Initial version - * 2: Added m_spawnIndex (timed OCL spawn sequence) */ + * 2: Added m_spawnIndex (timed OCL spawn sequence) + * 3: Added m_attackSeen / m_waitDeadline (WaitForAttackComplete sequencing) */ // ------------------------------------------------------------------------------------------------ void MultiLocationSpecialPowerUpdate::xfer( Xfer *xfer ) { // version - const XferVersion currentVersion = 2; + const XferVersion currentVersion = 3; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -311,6 +357,12 @@ void MultiLocationSpecialPowerUpdate::xfer( Xfer *xfer ) if( version >= 2 ) xfer->xferInt( &m_spawnIndex ); + + if( version >= 3 ) + { + xfer->xferBool( &m_attackSeen ); + xfer->xferUnsignedInt( &m_waitDeadline ); + } } // ------------------------------------------------------------------------------------------------ @@ -321,10 +373,9 @@ void MultiLocationSpecialPowerUpdate::loadPostProcess( void ) // extend base class SpecialPowerUpdateModule::loadPostProcess(); - // resume an in-flight spawn sequence - if( m_active && m_spawnIndex < (Int)m_locations.size() ) - { - const MultiLocationSpecialPowerUpdateModuleData *d = getMultiLocationSpecialPowerUpdateModuleData(); - setWakeFrame( getObject(), UPDATE_SLEEP( d->m_delay > 0 ? d->m_delay : 1 ) ); - } + // Do not call setWakeFrame() here to resume an in-flight spawn sequence. It is not safe from the + // xfer system: the sleepy update heap has not been rebuilt yet, so our index in the logic is + // still -1 and friend_awakenUpdateModule would RELEASE_CRASH. The sequence resumes on its own - + // UpdateModule::xfer already restored our wake frame, and GameLogic::loadPostProcess rebuilds the + // heap from it, which also preserves the sleep we had banked instead of restarting a full one. }