/* ** Command & Conquer Generals(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . */ //////////////////////////////////////////////////////////////////////////////// // // // (c) 2001-2003 Electronic Arts Inc. // // // //////////////////////////////////////////////////////////////////////////////// // DeployStyleAIUpdate.cpp //////////// // Author: Kris Morness, August 2002 // Desc: A standard ai update that also handles units that must deploy to attack and pack before moving. #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "Common/Player.h" #include "Common/ThingFactory.h" #include "Common/ThingTemplate.h" #include "GameClient/Drawable.h" #include "GameClient/InGameUI.h" #include "GameLogic/ExperienceTracker.h" #include "GameLogic/Locomotor.h" #include "GameLogic/Object.h" #include "GameLogic/PartitionManager.h" #include "GameLogic/Weapon.h" #include "GameLogic/Module/BodyModule.h" #include "GameLogic/Module/ContainModule.h" #include "GameLogic/Module/DeployStyleAIUpdate.h" #include "GameLogic/Module/PhysicsUpdate.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- DeployStyleAIUpdate::DeployStyleAIUpdate( Thing *thing, const ModuleData* moduleData ) : AIUpdateInterface( thing, moduleData ) { m_hasOutsideCommand = false; m_state = READY_TO_MOVE; m_frameToWakeForDeploy = 0; reset(); } //------------------------------------------------------------------------------------------------- DeployStyleAIUpdate::~DeployStyleAIUpdate( void ) { } //------------------------------------------------------------------------------------------------- Bool DeployStyleAIUpdate::isIdle() const { // we need to do this because we enter an idle state during deploy states in these cases, // but scripting relies on us never claiming to be "idle"... if (m_hasOutsideCommand) return false; return AIUpdateInterface::isIdle(); } //------------------------------------------------------------------------------------------------- void DeployStyleAIUpdate::reset() { m_designatedTargetID = INVALID_ID; m_isAttackMultiple = FALSE; m_overriddenAttack = FALSE; m_isGuardingPosition = FALSE; m_isAttackObject = FALSE; m_attackObjectID = INVALID_ID; m_isAttackPosition = FALSE; m_position.zero(); } //------------------------------------------------------------------------------------------------- void DeployStyleAIUpdate::aiDoCommand( const AICommandParms* parms ) { if (!isAllowedToRespondToAiCommands(parms)) return; if( parms->m_cmd != AICMD_IDLE && parms->m_cmd != AICMD_FOLLOW_PATH_APPEND ) { aiIdle( CMD_FROM_AI ); } if( parms->m_cmdSource != CMD_FROM_AI ) { reset(); if( parms->m_cmd != AICMD_IDLE ) { m_lastOutsideCommand.store( *parms ); m_hasOutsideCommand = TRUE; } if( m_state != DEPLOY && m_state != UNDEPLOY ) { //Only issue the command if we're not in the process of deploying/undeploying. AIUpdateInterface::aiDoCommand( parms ); } switch( parms->m_cmd ) { case AICMD_GUARD_POSITION: m_position.set( &parms->m_pos ); m_isGuardingPosition = TRUE; //fall through (no break) case AICMD_GUARD_OBJECT: case AICMD_GUARD_AREA: case AICMD_ATTACKMOVE_TO_POSITION: case AICMD_HUNT: case AICMD_ATTACKFOLLOW_WAYPOINT_PATH: case AICMD_ATTACKFOLLOW_WAYPOINT_PATH_AS_TEAM: case AICMD_ATTACK_AREA: m_isAttackMultiple = TRUE; break; case AICMD_ATTACK_OBJECT: case AICMD_FORCE_ATTACK_OBJECT: m_isAttackObject = TRUE; m_attackObjectID = parms->m_obj ? parms->m_obj->getID() : INVALID_ID; break; case AICMD_ATTACK_POSITION: m_isAttackPosition = TRUE; m_position.set( &parms->m_pos ); break; } } else { //Always process AI issued commands. AIUpdateInterface::aiDoCommand( parms ); } } //------------------------------------------------------------------------------------------------- UpdateSleepTime DeployStyleAIUpdate::update( void ) { // have to call our parent's isIdle, because we override it to never return true // when we have a pending command... Object *self = getObject(); Weapon *weapon = self->getCurrentWeapon(); Bool inRange = FALSE; Object *designatedTarget = NULL; Bool isAttacking = FALSE; if( weapon ) { if( m_isAttackPosition ) { //Handle force attacking attacking specific position inRange = weapon->isWithinAttackRange( self, &m_position ); isAttacking = TRUE; } else if( m_isAttackObject ) { //Handle attacking a specific object. designatedTarget = TheGameLogic->findObjectByID( m_attackObjectID ); if( designatedTarget && designatedTarget->isEffectivelyDead() ) { designatedTarget = NULL; } if( designatedTarget ) { inRange = weapon->isWithinAttackRange( self, designatedTarget ); isAttacking = TRUE; } } else if( m_isAttackMultiple ) { Bool newTarget = FALSE; //Handle attackmove and guard. //We are attacking in a different way... so attempt to figure out how. WhichTurretType tur = getWhichTurretForCurWeapon(); if( tur != TURRET_INVALID ) { //Get the turret's current target. designatedTarget = getTurretTargetObject( tur ); } else { //Get the current goal object (NULL if we have a turret). designatedTarget = getGoalObject(); } if( !designatedTarget ) { //If we still don't have a target, get the last known target. designatedTarget = TheGameLogic->findObjectByID( m_designatedTargetID ); } if( designatedTarget && designatedTarget->isEffectivelyDead() ) { //See if we can acquire one! designatedTarget = getNextMoodTarget( TRUE, FALSE ); newTarget = TRUE; } if( !designatedTarget && m_isGuardingPosition ) { //While guarding, acquire a target periodically. designatedTarget = getNextMoodTarget( FALSE, FALSE ); if( designatedTarget ) { inRange = weapon->isWithinAttackRange( self, designatedTarget ); isAttacking = TRUE; if( inRange ) { //Continue overriding it as long as there are enemies in range! aiAttackObject( designatedTarget, NO_MAX_SHOTS_LIMIT, CMD_FROM_AI ); m_overriddenAttack = TRUE; m_designatedTargetID = designatedTarget->getID(); } else { designatedTarget = NULL; } } else { designatedTarget = NULL; } } else if( designatedTarget ) { //Finally... we have a target -- so are we in range? inRange = weapon->isWithinAttackRange( self, designatedTarget ); isAttacking = TRUE; m_designatedTargetID = designatedTarget->getID(); if( m_overriddenAttack && newTarget && inRange ) { //Continue overriding it as long as there are enemies in range! aiAttackObject( designatedTarget, NO_MAX_SHOTS_LIMIT, CMD_FROM_AI ); } } else { //No target, we must be done or moving. m_designatedTargetID = INVALID_ID; } } } Bool remainDeployed = m_isGuardingPosition && !designatedTarget && !isMoving() && !isWaitingForPath(); UnsignedInt now = TheGameLogic->getFrame(); switch( m_state ) { case READY_TO_MOVE: if( remainDeployed || (inRange && isAttacking) ) { setMyState( DEPLOY ); } break; case READY_TO_ATTACK: if( !remainDeployed && (!inRange && isAttacking || !isAttacking && (isWaitingForPath() || getPath())) ) { WhichTurretType tur = getWhichTurretForCurWeapon(); if( tur != TURRET_INVALID ) { if( doTurretsHaveToCenterBeforePacking() ) { setMyState( ALIGNING_TURRETS ); break; } } setMyState( UNDEPLOY ); } else if( !designatedTarget && m_overriddenAttack && m_hasOutsideCommand ) { //For multiple attacks we specifically order the unit to attack one object. After //object is dead, we revert to our original command. AICommandParms parms( AICMD_MOVE_TO_POSITION, CMD_FROM_AI ); // values don't matter, will be wiped by next line m_lastOutsideCommand.reconstitute( parms ); aiDoCommand(&parms); } break; case DEPLOY: if( m_frameToWakeForDeploy != 0 && now >= m_frameToWakeForDeploy) { setMyState( READY_TO_ATTACK ); if( m_isAttackMultiple && inRange && isAttacking && designatedTarget ) { aiAttackObject( designatedTarget, NO_MAX_SHOTS_LIMIT, CMD_FROM_AI ); m_overriddenAttack = TRUE; } } break; case UNDEPLOY: if( m_frameToWakeForDeploy != 0 && now >= m_frameToWakeForDeploy) { setMyState( READY_TO_MOVE ); } break; case ALIGNING_TURRETS: { WhichTurretType tur = getWhichTurretForCurWeapon(); if( tur != TURRET_INVALID ) { if( isTurretInNaturalPosition( tur ) ) { setMyState( UNDEPLOY ); } } break; } } UpdateSleepTime mine = UPDATE_SLEEP_FOREVER; switch( m_state ) { case READY_TO_ATTACK: case READY_TO_MOVE: mine = UPDATE_SLEEP_FOREVER; // we can sleep for, well, a while break; case DEPLOY: case UNDEPLOY: mine = m_frameToWakeForDeploy > now ? UPDATE_SLEEP(m_frameToWakeForDeploy - now) : UPDATE_SLEEP_NONE; aiIdle( CMD_FROM_AI ); break; case ALIGNING_TURRETS: mine = UPDATE_SLEEP_NONE; // no sleep for us right now. aiIdle( CMD_FROM_AI ); break; } UpdateSleepTime ret = AIUpdateInterface::update(); return (mine < ret) ? mine : ret; } //------------------------------------------------------------------------------------------------- void DeployStyleAIUpdate::setMyState( DeployStateTypes stateID ) { m_state = stateID; Object *self = getObject(); switch( stateID ) { case DEPLOY: { //Tell our object to deploy (so it can continue the same attack later). aiIdle( CMD_FROM_AI ); self->clearAndSetModelConditionFlags( MAKE_MODELCONDITION_MASK( MODELCONDITION_PACKING ), MAKE_MODELCONDITION_MASK( MODELCONDITION_UNPACKING ) ); m_frameToWakeForDeploy = getUnpackTime(); //In frames //Make sure the animation matches the length of unpacking self->getDrawable()->setAnimationLoopDuration( m_frameToWakeForDeploy ); m_frameToWakeForDeploy += TheGameLogic->getFrame(); // convert to absolute frame //Play deploy sound const ThingTemplate *thing = self->getTemplate(); const AudioEventRTS* soundToPlayPtr = thing->getPerUnitSound( "Deploy" ); if( soundToPlayPtr ) { AudioEventRTS soundToPlay = *soundToPlayPtr; soundToPlay.setObjectID( self->getID() ); TheAudio->addAudioEvent( &soundToPlay ); } break; } case UNDEPLOY: { //Tell our object to pack up (so it can continue the same move later). aiIdle( CMD_FROM_AI ); self->clearAndSetModelConditionFlags( MAKE_MODELCONDITION_MASK2( MODELCONDITION_UNPACKING, MODELCONDITION_DEPLOYED ), MAKE_MODELCONDITION_MASK( MODELCONDITION_PACKING ) ); m_frameToWakeForDeploy = getPackTime(); //In frames //Make sure the animation matches the length of packing self->getDrawable()->setAnimationLoopDuration( m_frameToWakeForDeploy ); m_frameToWakeForDeploy += TheGameLogic->getFrame(); // convert to absolute frame if( doTurretsFunctionOnlyWhenDeployed() ) { WhichTurretType tur = getWhichTurretForCurWeapon(); if( tur != TURRET_INVALID ) { setTurretEnabled( tur, false ); } } //Play undeploy sound const ThingTemplate *thing = self->getTemplate(); const AudioEventRTS* soundToPlayPtr = thing->getPerUnitSound( "Undeploy" ); if( soundToPlayPtr ) { AudioEventRTS soundToPlay = *soundToPlayPtr; soundToPlay.setObjectID( self->getID() ); TheAudio->addAudioEvent( &soundToPlay ); } break; } case READY_TO_MOVE: { m_frameToWakeForDeploy = 0; //We're ready to move, so restore our command! if( m_hasOutsideCommand ) { AICommandParms parms( AICMD_MOVE_TO_POSITION, CMD_FROM_AI ); // values don't matter, will be wiped by next line m_lastOutsideCommand.reconstitute( parms ); aiDoCommand(&parms); } self->clearModelConditionFlags( MAKE_MODELCONDITION_MASK( MODELCONDITION_PACKING ) ); break; } case READY_TO_ATTACK: { m_frameToWakeForDeploy = 0; //We're ready to attack, so restore our command! if( !m_isAttackMultiple && m_hasOutsideCommand ) { AICommandParms parms( AICMD_MOVE_TO_POSITION, CMD_FROM_AI ); // values don't matter, will be wiped by next line m_lastOutsideCommand.reconstitute( parms ); aiDoCommand(&parms); } self->clearAndSetModelConditionFlags( MAKE_MODELCONDITION_MASK( MODELCONDITION_UNPACKING ), MAKE_MODELCONDITION_MASK( MODELCONDITION_DEPLOYED) ); if( doTurretsFunctionOnlyWhenDeployed() ) { WhichTurretType tur = getWhichTurretForCurWeapon(); if( tur != TURRET_INVALID ) { setTurretEnabled( tur, true ); } } break; } case ALIGNING_TURRETS: { m_frameToWakeForDeploy = 0; WhichTurretType tur = getWhichTurretForCurWeapon(); if( tur != TURRET_INVALID ) { recenterTurret( tur ); } break; } } } // ------------------------------------------------------------------------------------------------ /** CRC */ // ------------------------------------------------------------------------------------------------ void DeployStyleAIUpdate::crc( Xfer *xfer ) { // extend base class AIUpdateInterface::crc(xfer); } // end crc // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: * 1: Initial version * 2: Added support for attack move * 3: Added improved support for guard, and support for hunt AI **/ // ------------------------------------------------------------------------------------------------ void DeployStyleAIUpdate::xfer( Xfer *xfer ) { // version XferVersion currentVersion = 3; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); // extend base class AIUpdateInterface::xfer(xfer); xfer->xferBool(&m_hasOutsideCommand); xfer->xferUnsignedInt(&m_frameToWakeForDeploy); xfer->xferUser(&m_state, sizeof(m_state)); if( version >= 2 ) { xfer->xferObjectID( &m_designatedTargetID ); xfer->xferObjectID( &m_attackObjectID ); xfer->xferCoord3D( &m_position ); xfer->xferBool( &m_isAttackMultiple ); xfer->xferBool( &m_isAttackObject ); xfer->xferBool( &m_isAttackPosition ); } if( version >= 3 ) { xfer->xferBool( &m_isGuardingPosition ); xfer->xferBool( &m_overriddenAttack ); } m_lastOutsideCommand.doXfer(xfer); if( version < 2 ) { AICommandParmsStorage obsolete; obsolete.doXfer(xfer); } } // end xfer // ------------------------------------------------------------------------------------------------ /** Load post process */ // ------------------------------------------------------------------------------------------------ void DeployStyleAIUpdate::loadPostProcess( void ) { // extend base class AIUpdateInterface::loadPostProcess(); } // end loadPostProcess