/*
** Command & Conquer Generals Zero Hour(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. //
// //
////////////////////////////////////////////////////////////////////////////////
// AIUpdate.cpp //
// Implementation of generic AI mechanisms
// Author: Michael S. Booth, 2001-2002
// Subsequently : John Ahlquist 2002 and a cast of thousands.
#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine
#define DEFINE_LOCOMOTORSET_NAMES // for TheLocomotorSetNames[]
#define DEFINE_AUTOACQUIRE_NAMES
#include "Common/ActionManager.h"
#include "Common/GameState.h"
#include "Common/CRCDebug.h"
#include "Common/GlobalData.h"
#include "Common/Player.h"
#include "Common/PlayerList.h"
#include "Common/RandomValue.h"
#include "Common/Team.h"
#include "Common/ThingFactory.h"
#include "Common/ThingTemplate.h"
#include "Common/Upgrade.h"
#include "Common/PerfTimer.h"
#include "Common/UnitTimings.h"
#include "Common/Xfer.h"
#include "Common/XferCRC.h"
#include "GameClient/ControlBar.h"
#include "GameClient/Drawable.h"
#include "GameClient/InGameUI.h" // useful for printing quick debug strings when we need to
#include "GameLogic/AI.h"
#include "GameLogic/AIPathfind.h"
#include "GameLogic/Locomotor.h"
#include "GameLogic/Module/AIUpdate.h"
#include "GameLogic/Module/BodyModule.h"
#include "GameLogic/Module/ContainModule.h"
#include "GameLogic/Module/PhysicsUpdate.h"
#include "GameLogic/Module/ProneUpdate.h"
#include "GameLogic/Module/DeliverPayloadAIUpdate.h"
#include "GameLogic/Module/HackInternetAIUpdate.h"
#include "GameLogic/Module/HordeUpdate.h"
#include "GameLogic/Object.h"
#include "GameLogic/PartitionManager.h"
#include "GameLogic/PolygonTrigger.h"
#include "GameLogic/ScriptEngine.h"
#include "GameLogic/TurretAI.h"
#include "GameLogic/Weapon.h"
#include "Common/Radar.h" // For TheRadar
#define SLEEPY_AI
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes")
#endif
//-------------------------------------------------------------------------------------------------
AIUpdateModuleData::AIUpdateModuleData()
{
//m_locomotorTemplates -- nothing to do
for (int i = 0; i < MAX_TURRETS; i++)
m_turretData[i] = NULL;
m_autoAcquireEnemiesWhenIdle = 0;
m_moodAttackCheckRate = LOGICFRAMES_PER_SECOND * 2;
#ifdef ALLOW_SURRENDER
m_surrenderDuration = LOGICFRAMES_PER_SECOND * 120;
#endif
m_forbidPlayerCommands = FALSE;
m_turretsLinked = FALSE;
}
//-------------------------------------------------------------------------------------------------
AIUpdateModuleData::~AIUpdateModuleData()
{
for (int i = 0; i < MAX_TURRETS; i++)
{
if (m_turretData[i])
{
TurretAIData* td = const_cast(m_turretData[i]);
if (td)
td->deleteInstance();
}
}
}
//-------------------------------------------------------------------------------------------------
const LocomotorTemplateVector* AIUpdateModuleData::findLocomotorTemplateVector(LocomotorSetType t) const
{
if (m_locomotorTemplates.empty())
return NULL;
LocomotorTemplateMap::const_iterator it = m_locomotorTemplates.find(t);
if (it == m_locomotorTemplates.end())
{
return NULL;
}
else
{
return &(*it).second;
}
}
//-------------------------------------------------------------------------------------------------
/*static*/ void AIUpdateModuleData::buildFieldParse(MultiIniFieldParse& p)
{
ModuleData::buildFieldParse(p);
static const FieldParse dataFieldParse[] =
{
{ "Turret", AIUpdateModuleData::parseTurret, NULL, offsetof(AIUpdateModuleData, m_turretData[0]) },
{ "AltTurret", AIUpdateModuleData::parseTurret, NULL, offsetof(AIUpdateModuleData, m_turretData[1]) },
{ "AutoAcquireEnemiesWhenIdle", INI::parseBitString32, TheAutoAcquireEnemiesNames, offsetof(AIUpdateModuleData, m_autoAcquireEnemiesWhenIdle) },
{ "MoodAttackCheckRate", INI::parseDurationUnsignedInt, NULL, offsetof(AIUpdateModuleData, m_moodAttackCheckRate) },
#ifdef ALLOW_SURRENDER
{ "SurrenderDuration", INI::parseDurationUnsignedInt, NULL, offsetof(AIUpdateModuleData, m_surrenderDuration) },
#endif
{ "ForbidPlayerCommands", INI::parseBool, NULL, offsetof(AIUpdateModuleData, m_forbidPlayerCommands) },
{ "TurretsLinked", INI::parseBool, NULL, offsetof( AIUpdateModuleData, m_turretsLinked ) },
{ 0, 0, 0, 0 }
};
p.add(dataFieldParse);
}
//-------------------------------------------------------------------------------------------------
/*static*/ void AIUpdateModuleData::parseTurret(INI* ini, void *instance, void * store, const void* /*userData*/)
{
if (*(TurretAIData**)store)
{
DEBUG_CRASH(("Only one turret to a customer, for now"));
throw INI_INVALID_DATA;
}
TurretAIData* td = newInstance(TurretAIData);
ini->initFromINIMultiProc(td, td->buildFieldParse);
*(TurretAIData**)store = td;
}
//-------------------------------------------------------------------------------------------------
/*static*/ void AIUpdateModuleData::parseLocomotorSet(INI* ini, void *instance, void * /*store*/, const void* /*userData*/)
{
ThingTemplate *tt = (ThingTemplate *)instance;
AIUpdateModuleData *self = tt->friend_getAIModuleInfo();
if (!self)
{
DEBUG_CRASH( ("Attempted to specify a locomotor for object %s without an AIUpdate block.", tt->getName().str() ) );
throw INI_INVALID_DATA;
}
LocomotorSetType set = (LocomotorSetType)INI::scanIndexList(ini->getNextToken(), TheLocomotorSetNames);
if (!self->m_locomotorTemplates[set].empty())
{
if (ini->getLoadType() != INI_LOAD_CREATE_OVERRIDES)
{
DEBUG_CRASH(("re-specifying a LocomotorSet is no longer allowed\n"));
throw INI_INVALID_DATA;
}
}
self->m_locomotorTemplates[set].clear();
for (const char* locoName = ini->getNextToken(); locoName; locoName = ini->getNextTokenOrNull())
{
if (!*locoName || !stricmp(locoName, "None"))
continue;
NameKeyType locoKey = NAMEKEY(locoName);
const LocomotorTemplate* lt = TheLocomotorStore->findLocomotorTemplate(locoKey);
if (!lt)
{
DEBUG_CRASH(("Locomotor %s not found!\n",locoName));
throw INI_INVALID_DATA;
}
self->m_locomotorTemplates[set].push_back(lt);
}
}
//-------------------------------------------------------------------------------------------------
// subclasses may want to override this, to use a subclass of AIStateMachine.
AIStateMachine* AIUpdateInterface::makeStateMachine()
{
return newInstance(AIStateMachine)( getObject(), "AIUpdateInterfaceMachine");
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
AIUpdateInterface::AIUpdateInterface( Thing *thing, const ModuleData* moduleData ) :
UpdateModule( thing, moduleData )
{
int i;
m_priorWaypointID = 0xfacade;
m_currentWaypointID = 0xfacade;
m_stateMachine = NULL;
m_nextEnemyScanTime = 0;
m_currentVictimID = INVALID_ID;
m_desiredSpeed = FAST_AS_POSSIBLE;
m_lastCommandSource = CMD_FROM_AI;
m_guardMode = GUARDMODE_NORMAL;
m_guardTargetType[0] = m_guardTargetType[1] = GUARDTARGET_NONE;
m_locationToGuard.zero();
m_objectToGuard = INVALID_ID;
m_areaToGuard = NULL;
m_attackInfo = NULL;
m_waypointCount = 0;
m_waypointIndex = 0;
m_completedWaypoint = NULL;
m_path = NULL;
m_requestedVictimID = INVALID_ID;
m_requestedDestination.zero();
m_requestedDestination2.zero();
m_pathTimestamp = 0;
m_ignoreObstacleID = INVALID_ID;
m_pathExtraDistance = 0;
m_pathfindGoalCell.x = m_pathfindGoalCell.y = -1;
m_pathfindCurCell.x = m_pathfindCurCell.y = -1;
m_blockedFrames = 0;
m_curMaxBlockedSpeed = 0;
m_bumpSpeedLimit = FAST_AS_POSSIBLE;
m_ignoreCollisionsUntil = 0;
m_queueForPathFrame = 0;
m_finalPosition.zero();
m_repulsor1 = INVALID_ID;
m_repulsor2 = INVALID_ID;
m_nextGoalPathIndex = -1;
m_moveOutOfWay1 = INVALID_ID;
m_moveOutOfWay2 = INVALID_ID;
m_locomotorSet.clear();
m_curLocomotor = NULL;
m_curLocomotorSet = LOCOMOTORSET_INVALID;
m_locomotorGoalType = NONE;
m_locomotorGoalData.zero();
for (i = 0; i < MAX_TURRETS; i++)
m_turretAI[i] = NULL;
m_turretSyncFlag = TURRET_INVALID;
m_attitude = AI_NORMAL;
m_nextMoodCheckTime = 0;
#ifdef ALLOW_DEMORALIZE
m_demoralizedFramesLeft = 0;
#endif
#ifdef ALLOW_SURRENDER
m_surrenderedFramesLeft = 0;
m_surrenderedPlayerIndex = -1;
#endif
m_crateCreated = INVALID_ID;
m_tmpInt = 0;
m_doFinalPosition = FALSE;
m_waitingForPath = FALSE;
m_isAttackPath = FALSE;
m_isFinalGoal = FALSE;
m_isApproachPath = FALSE;
m_isSafePath = FALSE;
m_movementComplete = FALSE;
m_isMoving = FALSE;
m_isBlocked = FALSE;
m_isBlockedAndStuck = FALSE;
m_upgradedLocomotors = FALSE;
m_canPathThroughUnits = FALSE;
m_randomlyOffsetMoodCheck = FALSE;
m_isAiDead = FALSE;
m_isRecruitable = TRUE; // Things default to being recruitable.
m_executingWaypointQueue = FALSE;
m_retryPath = FALSE;
m_isInUpdate = FALSE;
m_fixLocoInPostProcess = FALSE;
// ---------------------------------------------
for (i = 0; i < MAX_TURRETS; i++)
{
if (getAIUpdateModuleData()->m_turretData[i])
{
m_turretAI[i] = newInstance(TurretAI)(getObject(), getAIUpdateModuleData()->m_turretData[i], (WhichTurretType)i);
}
}
chooseLocomotorSet(LOCOMOTORSET_NORMAL);
#ifdef SLEEPY_AI
setWakeFrame(getObject(), UPDATE_SLEEP_NONE);
#endif
}
#ifdef ALLOW_SURRENDER
//=============================================================================
// Object::setSurrendered, and related methods ================================
//=============================================================================
void AIUpdateInterface::setSurrendered( const Object *objWeSurrenderedTo, Bool surrendered )
{
if (surrendered)
{
Bool wasSurrendered = isSurrendered();
const AIUpdateModuleData* d = getAIUpdateModuleData();
if (m_surrenderedFramesLeft < d->m_surrenderDuration)
m_surrenderedFramesLeft = d->m_surrenderDuration;
const Player* playerWeSurrenderedTo = objWeSurrenderedTo ? objWeSurrenderedTo->getControllingPlayer() : NULL;
m_surrenderedPlayerIndex = playerWeSurrenderedTo ? playerWeSurrenderedTo->getPlayerIndex() : -1;
if (!wasSurrendered)
{
//aiIdle(CMD_FROM_AI);
// srj sez: calling aiIdle() won't work, since we are probably "effectivelyDead"...
// meaning we won't respong to aiDoCommand! so go straight to the metal here:
getStateMachine()->clear();
getStateMachine()->setState( AI_IDLE );
setLastCommandSource(CMD_FROM_AI);
// Play our sound surrendered
AudioEventRTS surrenderSound = *getObject()->getTemplate()->getVoiceSurrender();
surrenderSound.setObjectID(getObject()->getID());
TheAudio->addAudioEvent(&surrenderSound);
}
}
else
{
// GS During the act of surrendering, we dipped to 0 and then were manually set to have hit points.
// That made us alive but marked as Dead. Gotta undo that.
getObject()->setEffectivelyDead( FALSE );
m_surrenderedFramesLeft = 0;
m_surrenderedPlayerIndex = -1;
}
}
#endif
//=============================================================================
void AIUpdateInterface::setGoalPositionClipped(const Coord3D* in, CommandSourceType cmdSource)
{
if (in)
{
Coord3D tmp = *in;
if (cmdSource == CMD_FROM_PLAYER)
{
Real fudge = TheGlobalData->m_partitionCellSize * 0.5f;
if (getObject()->isKindOf(KINDOF_AIRCRAFT) && getObject()->isSignificantlyAboveTerrain() && m_curLocomotor != NULL)
{
// aircraft must stay further away from the map edges, to prevent getting "lost"
fudge = max(fudge, m_curLocomotor->getPreferredHeight());
}
Region3D mapRegion;
TheTerrainLogic->getExtent( &mapRegion );
if (tmp.x < mapRegion.lo.x + fudge)
{
tmp.x = mapRegion.lo.x + fudge;
}
if (tmp.x > mapRegion.hi.x - fudge)
{
tmp.x = mapRegion.hi.x - fudge;
}
if (tmp.y < mapRegion.lo.y + fudge)
{
tmp.y = mapRegion.lo.y + fudge;
}
if (tmp.y > mapRegion.hi.y - fudge)
{
tmp.y = mapRegion.hi.y - fudge;
}
}
getStateMachine()->setGoalPosition(&tmp);
}
else
{
getStateMachine()->setGoalPosition(NULL);
}
}
/* Called by the pathfinder when it processes the pathfind queue. Basically, it's our turn
to call use the PathfindServicesInterface to do a pathfind operation. This shouldn't be called
(and in fact is very hard to do because PathfindServicesInterace is private to the pathfinder)
except by the pathfinder during pathfind queue processing. jba */
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::doPathfind( PathfindServicesInterface *pathfinder )
{
if (!m_waitingForPath) {
return;
}
//CRCDEBUG_LOG(("AIUpdateInterface::doPathfind() for object %d\n", getObject()->getID()));
m_waitingForPath = FALSE;
if (m_isSafePath) {
destroyPath();
Coord3D pos1, pos2;
pos1.set(-1000,-1000,0);
Object *repulsor = TheGameLogic->findObjectByID(m_repulsor1);
if (repulsor) {
pos1 = *repulsor->getPosition();
}
pos2 = pos1;
repulsor = TheGameLogic->findObjectByID(m_repulsor2);
if (repulsor) {
pos2 = *repulsor->getPosition();
}
m_path = pathfinder->findSafePath(getObject(), m_locomotorSet,
getObject()->getPosition(),
&pos1, &pos2,
getObject()->getVisionRange() + TheAI->getAiData()->m_repulsedDistance);
return;
}
if (m_isApproachPath & !isDoingGroundMovement()) {
m_isApproachPath = false;
}
if (m_isApproachPath) {
destroyPath();
m_path = pathfinder->findClosestPath(getObject(), m_locomotorSet, getObject()->getPosition(),
&m_requestedDestination, m_isBlockedAndStuck, 0.2f, FALSE );
if (isDoingGroundMovement() && getPath()) {
TheAI->pathfinder()->updateGoal(getObject(), getPath()->getLastNode()->getPosition(),
getPath()->getLastNode()->getLayer());
}
return;
}
if (m_isAttackPath) {
Object *victim = NULL;
if (m_requestedVictimID != INVALID_ID) {
victim = TheGameLogic->findObjectByID(m_requestedVictimID);
}
if (computeAttackPath(pathfinder, victim, &m_requestedDestination)) {
if (getPath()) {
TheAI->pathfinder()->updateGoal(getObject(), getPath()->getLastNode()->getPosition(),
getPath()->getLastNode()->getLayer());
}
//CRCDEBUG_LOG(("AIUpdateInterface::doPathfind() - m_isAttackPath = TRUE after computeAttackPath\n"));
m_isAttackPath = TRUE;
return;
}
//CRCDEBUG_LOG(("AIUpdateInterface::doPathfind() - m_isAttackPath = FALSE after computeAttackPath()\n"));
m_isAttackPath = FALSE;
if (victim) {
m_requestedDestination = *victim->getPosition();
/* find a pathable destination near the victim.*/
TheAI->pathfinder()->adjustToPossibleDestination(getObject(), getLocomotorSet(), &m_requestedDestination);
ignoreObstacle(victim);
}
}
computePath(pathfinder, &m_requestedDestination);
if (m_isFinalGoal && isDoingGroundMovement() && getPath()) {
TheAI->pathfinder()->updateGoal(getObject(), getPath()->getLastNode()->getPosition(),
getPath()->getLastNode()->getLayer());
}
if (m_queueForPathFrame > TheGameLogic->getFrame()) {
m_waitingForPath = TRUE;
}
#ifdef SLEEPY_AI
// if we're no longer waiting for a path, make sure we wake up right away!
if (!m_waitingForPath)
{
wakeUpNow();
}
#endif
}
/* Requests a path to be found. Note that if it is possible to do it without having to use the
pathfinder (air units just move point to point) it generates the path immediately. Otherwise the path
will be processed when we get to the front of the pathfind queue. jba */
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::requestPath( Coord3D *destination, Bool isFinalGoal )
{
if (m_locomotorSet.getValidSurfaces() == 0) {
DEBUG_CRASH(("Attempting to path immobile unit."));
}
//DEBUG_LOG(("Request Frame %d, obj %s %x\n", TheGameLogic->getFrame(), getObject()->getTemplate()->getName().str(), getObject()));
m_requestedDestination = *destination;
m_isFinalGoal = isFinalGoal;
CRCDEBUG_LOG(("AIUpdateInterface::requestPath() - m_isAttackPath = FALSE for object %d\n", getObject()->getID()));
m_isAttackPath = FALSE;
m_requestedVictimID = INVALID_ID;
m_isApproachPath = FALSE;
m_isSafePath = FALSE;
if (canComputeQuickPath()) {
computeQuickPath(destination);
return;
}
m_waitingForPath = TRUE;
if (m_pathTimestamp > TheGameLogic->getFrame()-3) {
/* Requesting path very quickly. Can cause a spin. */
//DEBUG_LOG(("%d Pathfind - repathing in less than 3 frames. Waiting 1 second\n",
//TheGameLogic->getFrame()));
setQueueForPathTime(LOGICFRAMES_PER_SECOND);
// See if it has been too soon.
// jba intense debug
//DEBUG_LOG(("Info - RePathing very quickly %d, %d.\n", m_pathTimestamp, TheGameLogic->getFrame()));
if (m_path && m_isBlockedAndStuck) {
setIgnoreCollisionTime(2*LOGICFRAMES_PER_SECOND);
m_blockedFrames = 0;
m_isBlocked = FALSE;
m_isBlockedAndStuck = FALSE;
}
return;
}
TheAI->pathfinder()->queueForPath(getObject()->getID());
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::requestAttackPath( ObjectID victimID, const Coord3D* victimPos )
{
if (m_locomotorSet.getValidSurfaces() == 0) {
DEBUG_CRASH(("Attempting to path immobile unit."));
}
CRCDEBUG_LOG(("AIUpdateInterface::requestAttackPath() - m_isAttackPath = TRUE for object %d\n", getObject()->getID()));
m_requestedDestination = *victimPos;
m_requestedVictimID = victimID;
m_isAttackPath = TRUE;
m_isApproachPath = FALSE;
m_isSafePath = FALSE;
m_waitingForPath = TRUE;
if (m_pathTimestamp > TheGameLogic->getFrame()-3) {
/* Requesting path very quickly. Can cause a spin. */
//DEBUG_LOG(("%d Pathfind - repathing in less than 3 frames. Waiting 2 second\n",TheGameLogic->getFrame()));
setQueueForPathTime(2*LOGICFRAMES_PER_SECOND);
setLocomotorGoalNone();
return;
}
TheAI->pathfinder()->queueForPath(getObject()->getID());
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::requestApproachPath( Coord3D *destination )
{
if (m_locomotorSet.getValidSurfaces() == 0) {
DEBUG_CRASH(("Attempting to path immobile unit."));
}
m_requestedDestination = *destination;
m_isFinalGoal = TRUE;
CRCDEBUG_LOG(("AIUpdateInterface::requestApproachPath() - m_isAttackPath = FALSE for object %d\n", getObject()->getID()));
m_isAttackPath = FALSE;
m_requestedVictimID = INVALID_ID;
m_isApproachPath = TRUE;
m_isSafePath = FALSE;
m_waitingForPath = TRUE;
if (m_pathTimestamp > TheGameLogic->getFrame()-3) {
/* Requesting path very quickly. Can cause a spin. */
//DEBUG_LOG(("%d Pathfind - repathing in less than 3 frames. Waiting 2 second\n",TheGameLogic->getFrame()));
setQueueForPathTime(2*LOGICFRAMES_PER_SECOND);
return;
}
TheAI->pathfinder()->queueForPath(getObject()->getID());
}
//-------------------------------------------------------------------------------------------------
// Requests a safe path away from the repulsor.
void AIUpdateInterface::requestSafePath( ObjectID repulsor )
{
if (repulsor != m_repulsor1) {
m_repulsor2 = m_repulsor1; // save the prior repulsor.
}
m_repulsor1 = repulsor;
m_isFinalGoal = FALSE;
CRCDEBUG_LOG(("AIUpdateInterface::requestSafePath() - m_isAttackPath = FALSE for object %d\n", getObject()->getID()));
m_isAttackPath = FALSE;
m_requestedVictimID = INVALID_ID;
m_isApproachPath = FALSE;
m_isSafePath = TRUE;
m_waitingForPath = TRUE;
if (m_pathTimestamp > TheGameLogic->getFrame()-3) {
/* Requesting path very quickly. Can cause a spin. */
//DEBUG_LOG(("%d Pathfind - repathing in less than 3 frames. Waiting 2 second\n",TheGameLogic->getFrame()));
setQueueForPathTime(2*LOGICFRAMES_PER_SECOND);
return;
}
TheAI->pathfinder()->queueForPath(getObject()->getID());
}
enum {WAYPOINT_PATH_LIMIT=1024};
//-------------------------------------------------------------------------------------------------
//
void AIUpdateInterface::setPathFromWaypoint(const Waypoint *way, const Coord2D *offset)
{
destroyPath();
m_path = newInstance(Path);
Coord3D pos = *getObject()->getPosition();
m_path->prependNode( &pos, LAYER_GROUND );
m_path->markOptimized();
int count = 0;
while (way) {
Coord3D wayPos = *way->getLocation();
wayPos.x += offset->x;
wayPos.y += offset->y;
if (way->getLink(0) == NULL) {
TheAI->pathfinder()->snapPosition(getObject(), &wayPos);
}
m_path->appendNode( &wayPos, LAYER_GROUND );
way = way->getLink(0);
count++;
if (count>WAYPOINT_PATH_LIMIT) break;
}
m_waitingForPath = FALSE;
TheAI->pathfinder()->setDebugPath(m_path);
#ifdef SLEEPY_AI
// if we're no longer waiting for a path, make sure we wake up right away!
wakeUpNow();
#endif
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::onObjectCreated()
{
// create the behavior state machine.
// can't do this in the ctor because makeStateMachine is a protected virtual func,
// and overrides to virtual funcs don't exist in our ctor. (look it up.)
if (m_stateMachine == NULL)
{
m_stateMachine = makeStateMachine();
m_stateMachine->initDefaultState();
}
}
//-------------------------------------------------------------------------------------------------
AIUpdateInterface::~AIUpdateInterface( void )
{
m_locomotorSet.clear();
m_curLocomotor = NULL;
if( m_stateMachine ) {
m_stateMachine->halt();
m_stateMachine->deleteInstance();
}
for (int i = 0; i < MAX_TURRETS; i++)
{
if (m_turretAI[i])
m_turretAI[i]->deleteInstance();
m_turretAI[i] = NULL;
}
m_stateMachine = NULL;
// destroy the current path. (destroyPath is NULL savvy)
destroyPath();
}
//=============================================================================
void AIUpdateInterface::setTurretTargetObject(WhichTurretType tur, Object* o, Bool forceAttacking)
{
if (m_turretAI[tur])
{
m_turretAI[tur]->setTurretTargetObject(o, forceAttacking);
}
}
//=============================================================================
Object* AIUpdateInterface::getTurretTargetObject( WhichTurretType tur, Bool clearDeadTargets )
{
if( m_turretAI[ tur ] )
{
Object *obj;
Coord3D pos;
if( m_turretAI[ tur ]->friend_getTurretTarget( obj, pos, clearDeadTargets ) == TARGET_OBJECT )
{
return obj;
}
}
return NULL;
}
//=============================================================================
void AIUpdateInterface::setTurretTargetPosition(WhichTurretType tur, const Coord3D* pos)
{
if (m_turretAI[tur])
{
m_turretAI[tur]->setTurretTargetPosition(pos);
}
}
//=============================================================================
void AIUpdateInterface::setTurretEnabled(WhichTurretType tur, Bool enabled)
{
if (m_turretAI[tur])
{
m_turretAI[tur]->setTurretEnabled( enabled );
}
}
//=============================================================================
void AIUpdateInterface::recenterTurret(WhichTurretType tur)
{
if (m_turretAI[tur])
{
m_turretAI[tur]->recenterTurret();
}
}
//=============================================================================
Bool AIUpdateInterface::isTurretEnabled( WhichTurretType tur ) const
{
if( m_turretAI[ tur ] )
{
return m_turretAI[ tur ]->isTurretEnabled();
}
return FALSE;
}
//=============================================================================
Bool AIUpdateInterface::isTurretInNaturalPosition(WhichTurretType tur) const
{
if (m_turretAI[tur])
{
return m_turretAI[tur]->isTurretInNaturalPosition();
}
return FALSE;
}
//=============================================================================
Bool AIUpdateInterface::isWeaponSlotOnTurretAndAimingAtTarget(WeaponSlotType wslot, const Object* victim) const
{
for (int i = 0; i < MAX_TURRETS; i++)
{
if (m_turretAI[i] && m_turretAI[i]->isWeaponSlotOnTurret(wslot))
{
return m_turretAI[i]->isTryingToAimAtTarget(victim);
}
}
return FALSE;
}
//=============================================================================
Bool AIUpdateInterface::getTurretRotAndPitch(WhichTurretType tur, Real* turretAngle, Real* turretPitch) const
{
if (m_turretAI[tur])
{
if (turretAngle)
*turretAngle = m_turretAI[tur]->getTurretAngle();
if (turretPitch)
*turretPitch = m_turretAI[tur]->getTurretPitch();
return TRUE;
}
return FALSE;
}
//=============================================================================
Real AIUpdateInterface::getTurretTurnRate(WhichTurretType tur) const
{
return (tur != TURRET_INVALID && m_turretAI[tur] != NULL) ?
m_turretAI[tur]->getTurnRate() :
0.0f;
}
//=============================================================================
WhichTurretType AIUpdateInterface::getWhichTurretForCurWeapon() const
{
for (int i = 0; i < MAX_TURRETS; ++i)
if (m_turretAI[i] && m_turretAI[i]->isOwnersCurWeaponOnTurret())
return (WhichTurretType)i;
return TURRET_INVALID;
}
//=============================================================================
WhichTurretType AIUpdateInterface::getWhichTurretForWeaponSlot(WeaponSlotType wslot, Real* turretAngle, Real* turretPitch) const
{
for (int i = 0; i < MAX_TURRETS; ++i)
{
if (m_turretAI[i] && m_turretAI[i]->isWeaponSlotOnTurret(wslot))
{
if (turretAngle)
*turretAngle = m_turretAI[i]->getTurretAngle();
if (turretPitch)
*turretPitch = m_turretAI[i]->getTurretPitch();
return (WhichTurretType)i;
}
}
return TURRET_INVALID;
}
//=============================================================================
Real AIUpdateInterface::getCurLocomotorSpeed() const
{
if (m_curLocomotor != NULL)
return m_curLocomotor->getMaxSpeedForCondition(getObject()->getBodyModule()->getDamageState());
DEBUG_LOG(("no current locomotor!"));
return 0.0f;
}
//=============================================================================
void AIUpdateInterface::setLocomotorUpgrade(Bool set)
{
m_upgradedLocomotors = set;
if (m_curLocomotorSet == LOCOMOTORSET_NORMAL || m_curLocomotorSet == LOCOMOTORSET_NORMAL_UPGRADED)
chooseLocomotorSet(LOCOMOTORSET_NORMAL);
}
//=============================================================================
Bool AIUpdateInterface::chooseLocomotorSet(LocomotorSetType wst)
{
DEBUG_ASSERTCRASH(wst != LOCOMOTORSET_NORMAL_UPGRADED, ("never pass LOCOMOTORSET_NORMAL_UPGRADED here"));
if (wst == LOCOMOTORSET_NORMAL && m_upgradedLocomotors)
wst = LOCOMOTORSET_NORMAL_UPGRADED;
if (wst == m_curLocomotorSet)
return TRUE;
if (chooseLocomotorSetExplicit(wst))
{
chooseGoodLocomotorFromCurrentSet();
return TRUE;
}
return FALSE;
}
//=============================================================================
// this should only be called by load/save, or by chooseLocomotorSet.
// it does no sanity checking; it just jams it in.
Bool AIUpdateInterface::chooseLocomotorSetExplicit(LocomotorSetType wst)
{
const LocomotorTemplateVector* set = getAIUpdateModuleData()->findLocomotorTemplateVector(wst);
if (set)
{
m_locomotorSet.clear();
m_curLocomotor = NULL;
for (Int i = 0; i < set->size(); ++i)
{
const LocomotorTemplate* lt = set->at(i);
if (lt)
m_locomotorSet.addLocomotor(lt);
}
m_curLocomotorSet = wst;
return TRUE;
}
return FALSE;
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::chooseGoodLocomotorFromCurrentSet( void )
{
Locomotor* prevLoco = m_curLocomotor;
Locomotor* newLoco = TheAI->pathfinder()->chooseBestLocomotorForPosition(getObject()->getLayer(), &m_locomotorSet, getObject()->getPosition());
if (newLoco == NULL)
{
if (prevLoco != NULL)
{
/* due to physics, we might slight into a cell for which we have no loco
(eg, cliff) and get stuck. this is bad. as a solution, we do this.
this may look a little funny, but as a practical matter, it works well,
since the pathfinder will prevent us from doing any significant "wrong" terrain. */
newLoco = prevLoco;
}
else
{
/* this can happen for a newly-created object, which might come into being in
the middle of an obstacle. for now, we just fake it and choose a ground locomotor. */
newLoco = m_locomotorSet.findLocomotor(LOCOMOTORSURFACE_GROUND);
}
}
m_curLocomotor = newLoco;
if (prevLoco != m_curLocomotor)
{
// make sure the group's speed will be recalculated
if (getGroup())
getGroup()->recomputeGroupSpeed();
// turn off precision-z-pos anytime the loco changes, just in case,
// since it should only be enabled in very special cases
m_curLocomotor->setUsePreciseZPos(FALSE);
// ditto for no-slow-down.
m_curLocomotor->setNoSlowDownAsApproachingDest(FALSE);
// ditto for ultra-accuracy.
m_curLocomotor->setUltraAccurate(FALSE);
}
}
//----------------------------------------------------------------------------------------------------------
Object* AIUpdateInterface::checkForCrateToPickup()
{
if (m_crateCreated != INVALID_ID)
{
m_crateCreated = INVALID_ID; // we have processed it, so clear it.
Object* crate = TheGameLogic->findObjectByID(m_crateCreated);
if (crate)
{
for (BehaviorModule** m = crate->getBehaviorModules(); *m; ++m)
{
CollideModuleInterface* collide = (*m)->getCollide();
if (!collide)
continue;
if( collide->wouldLikeToCollideWith(getObject()))
{
return crate;
}
}
}
}
return NULL;
}
#ifdef ALLOW_SURRENDER
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::doSurrenderUpdateStuff()
{
RELEASE_CRASH(("Read the comment in doSurrenderUpdateStuff"));
/*
If you ever re-enable this code, you must convert it to be
properly sleepy. It is crucial that we avoid requiring a call
to AIUpdate every frame just to support this. (srj)
*/
UnsignedInt prevSurrenderedFrames = m_surrenderedFramesLeft;
if (m_surrenderedFramesLeft > 0)
--m_surrenderedFramesLeft;
if (m_surrenderedFramesLeft > 0)
getObject()->setModelConditionState( MODELCONDITION_SURRENDER );
else
getObject()->clearModelConditionState( MODELCONDITION_SURRENDER );
//
// when we leave a surrendered state we give ourselves an idle command ... why you ask? Well
// during the surrender sequence we might have started moving towards a POW truck come
// to pick us up, but now we should stop and be all normal again
//
if( prevSurrenderedFrames > 0 && m_surrenderedFramesLeft == 0 )
{
m_surrenderedPlayerIndex = -1;
aiIdle( CMD_FROM_AI );
}
}
#endif
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::setQueueForPathTime(Int frames)
{
#ifdef SLEEPY_AI
if (frames >= UPDATE_SLEEP_NONE && getWakeFrame() > UPDATE_SLEEP(frames))
{
if (m_isInUpdate)
{
// we're changing this while in our own update (probably via a move state).
// just do nothing, since update will calculate the correct sleep behavior at the end.
}
else
{
setWakeFrame(getObject(), UPDATE_SLEEP(frames));
}
}
#endif
m_queueForPathFrame = frames ? (TheGameLogic->getFrame() + frames) : 0;
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::wakeUpNow()
{
#ifdef SLEEPY_AI
if (getWakeFrame() > UPDATE_SLEEP_NONE)
{
if (m_isInUpdate)
{
// we're changing this while in our own update (probably via a move state).
// just do nothing, since update will calculate the correct sleep behavior at the end.
}
else
{
setWakeFrame(getObject(), UPDATE_SLEEP_NONE);
}
}
#endif
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::friend_notifyStateMachineChanged()
{
wakeUpNow();
}
//-------------------------------------------------------------------------------------------------
/**
* The "main loop" of the AI subsystem
*/
DECLARE_PERF_TIMER(AIUpdateInterface_update)
UpdateSleepTime AIUpdateInterface::update( void )
{
//DEBUG_LOG(("AIUpdateInterface frame %d: %08lx\n",TheGameLogic->getFrame(),getObject()));
USE_PERF_TIMER(AIUpdateInterface_update)
m_isInUpdate = TRUE;
m_completedWaypoint = NULL; // Reset so state machine update can set it if we just completed the path.
// assume we can sleep forever, unless the state machine (or turret, etc) demand otherwise
UpdateSleepTime subMachineSleep = UPDATE_SLEEP_FOREVER;
StateReturnType stRet = getStateMachine()->updateStateMachine();
if (IS_STATE_SLEEP(stRet))
{
Int frames = GET_STATE_SLEEP_FRAMES(stRet);
if (frames < subMachineSleep)
subMachineSleep = UPDATE_SLEEP(frames);
}
else
{
// it's STATE_CONTINUE, STATE_SUCCESS, or STATE_FAILURE,
// any of which will probably require next frame
subMachineSleep = UPDATE_SLEEP_NONE;
}
// note that this is all OK with sleepiness, since m_movementComplete can
// only be set via our statemachine (via friend_startingMove or friend_endMove),
// which we just called. thus we should
// never have worry about waking ourselves up when this changes, since
// if it changes the code will always flow thru here anyway. (srj)
if (m_movementComplete)
{
setQueueForPathTime(0);
// destroy path
destroyPath();
setLocomotorGoalNone();
getObject()->clearModelConditionState(MODELCONDITION_MOVING);
Coord3D goalPos;
if (TheAI->pathfinder()->goalPosition(getObject(), &goalPos))
{
// Pop to goal - This shouldn't happen (often), but make sure we got to where we're going.
Real dx = goalPos.x-getObject()->getPosition()->x;
Real dy = goalPos.y-getObject()->getPosition()->y;
if (dx*dx+dy*dy>=PATHFIND_CELL_SIZE_F*PATHFIND_CELL_SIZE_F)
{
// Too far, so just grid current pos.
goalPos = *getObject()->getPosition();
TheAI->pathfinder()->snapPosition(getObject(), &goalPos);
}
setFinalPosition(&goalPos);
TheAI->pathfinder()->updateGoal(getObject(), &goalPos, getObject()->getLayer());
}
m_movementComplete = FALSE;
ignoreObstacle(NULL);
}
UnsignedInt now = TheGameLogic->getFrame();
if (m_queueForPathFrame != 0)
{
if (now >= m_queueForPathFrame)
{
TheAI->pathfinder()->queueForPath(getObject()->getID());
setQueueForPathTime(0);
}
else
{
UnsignedInt sleepForPathDelta = m_queueForPathFrame - now;
if (sleepForPathDelta < subMachineSleep)
subMachineSleep = UPDATE_SLEEP(sleepForPathDelta);
}
}
Object *obj = getObject();
if (! obj->isEffectivelyDead() &&
! obj->isDisabledByType( DISABLED_PARALYZED ) &&
! obj->isDisabledByType( DISABLED_UNMANNED ) &&
! obj->isDisabledByType( DISABLED_EMP ) &&
! obj->isDisabledByType( DISABLED_SUBDUED ) &&
! obj->isDisabledByType( DISABLED_HACKED ) )
{
// If we are dead, don't let the turrets do anything anymore, or else they will keep attacking
for (int i = 0; i < MAX_TURRETS; ++i)
{
if (m_turretAI[i])
{
UpdateSleepTime tmp = m_turretAI[i]->updateTurretAI();
if (tmp < subMachineSleep)
subMachineSleep = tmp;
}
}
}
// must do death check outside of the state machine update, to avoid corruption
if (isAiInDeadState() && !(getStateMachine()->getCurrentStateID() == AI_DEAD) )
{
/// @todo Yikes! If we are not interruptable, and we die, what do we do? (MSB)
getStateMachine()->clear();
getStateMachine()->setState( AI_DEAD );
getStateMachine()->lock("AIUpdateInterface::update");
// strangely, dead things need to NOT sleep at all. (but they don't stay dead for long,
// so this is not too bad.)
subMachineSleep = UPDATE_SLEEP_NONE;
}
// do this objects movement
UpdateSleepTime tmp = doLocomotor();
if (tmp < subMachineSleep)
subMachineSleep = tmp;
#ifdef ALLOW_DEMORALIZE
RELEASE_CRASH(("If ALLOW_DEMORALIZE is ever defined, this code must be redone to do proper SLEEPY updates. (srj)"));
// update the demoralized frames if present
if( m_demoralizedFramesLeft > 0 )
{
setDemoralized( m_demoralizedFramesLeft - 1 );
}
#endif
#ifdef ALLOW_SURRENDER
RELEASE_CRASH(("If ALLOW_SURRENDER is ever defined, this code must be redone to do proper SLEEPY updates. (srj)"));
doSurrenderUpdateStuff();
#endif
m_isInUpdate = FALSE;
if (m_completedWaypoint != NULL)
{
// sleep NONE here so that it will get reset next frame.
// this happen infrequently, so it shouldn't be an issue.
return UPDATE_SLEEP_NONE;
}
else
{
#ifdef SLEEPY_AI
return subMachineSleep;
#else
return UPDATE_SLEEP_NONE;
#endif
}
}
//-------------------------------------------------------------------------------------------------
/**
* Append waypoint to queue for later movement
*/
Bool AIUpdateInterface::queueWaypoint( const Coord3D *pos )
{
if (m_waypointCount < MAX_WAYPOINTS)
{
m_waypointQueue[ m_waypointCount++ ] = *pos;
return TRUE;
}
return FALSE;
}
//-------------------------------------------------------------------------------------------------
/**
* Start moving along the waypoint path in the queue
*/
void AIUpdateInterface::executeWaypointQueue( void )
{
// the dead don't listen very well
if (isAiInDeadState())
return;
// m_actionStack->clear();
if (m_waypointCount > 0)
{
m_waypointIndex = 0;
m_executingWaypointQueue = TRUE;
}
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::clearWaypointQueue( void )
{
m_waypointCount = 0;
m_executingWaypointQueue = FALSE;
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::markAsDead()
{
m_isAiDead = TRUE;
getObject()->setEffectivelyDead(TRUE);
wakeUpNow(); // wake us up immediately so that our anim plays promptly!
}
//-------------------------------------------------------------------------------------------------
/* Returns TRUE if this ai has a higher path priority than the other one.
The way to have a higher priority is:
1. If the paths were assigned when both units were in the same ai group, we use the path priority assigned.
2. If not, the unit that is in front has the higher priority.
3. If exactly tied (usually beacause both units got unfortunately snapped to the same location), ObjectID is used
to break the tie.
*/
Bool AIUpdateInterface::hasHigherPathPriority(AIUpdateInterface *otherAI) const
{
Object *other = otherAI->getObject();
// Dozers have highest priority.
if (getObject()->isKindOf(KINDOF_DOZER) && !other->isKindOf(KINDOF_DOZER)) {
return TRUE;
}
if (!getObject()->isKindOf(KINDOF_DOZER) && other->isKindOf(KINDOF_DOZER)) {
return FALSE;
}
// Vehicles always have higher priority than infantry.
if (getObject()->isKindOf(KINDOF_VEHICLE) && other->isKindOf(KINDOF_INFANTRY)) {
return TRUE;
}
if (getObject()->isKindOf(KINDOF_INFANTRY) && other->isKindOf(KINDOF_VEHICLE)) {
return FALSE;
}
// The paths aren't of the same group, so see which unit is in front.
Coord3D ourDir = *getObject()->getUnitDirectionVector2D();
Coord3D otherDir = *other->getUnitDirectionVector2D();
if (ourDir.x*otherDir.x + ourDir.y*otherDir.y <= 0) {
return getObject()->getID() < other->getID();
}
Coord2D combinedDir;
combinedDir.x = ourDir.x + otherDir.x;
combinedDir.y = ourDir.y + otherDir.y;
Coord2D vectorToOther;
vectorToOther.x = other->getPosition()->x - getObject()->getPosition()->x;
vectorToOther.y = other->getPosition()->y - getObject()->getPosition()->y;
// Dot product is our directions projected onto each other.
Real dotProduct = combinedDir.x*vectorToOther.x + combinedDir.y*vectorToOther.y;
if (dotProduct>0) return FALSE; // other is ahead of us along our directional vector.
if (dotProduct<0) return TRUE; // We are ahead of other.
// Exactly equal. Use object id's to break the tie.
return getObject()->getID() < other->getID();
}
//-------------------------------------------------------------------------------------------------
/* Returns max speed we can have and not run into unit that is blocking us.
*/
Real AIUpdateInterface::calculateMaxBlockedSpeed(Object *other) const
{
Coord3D ourDir = *getObject()->getUnitDirectionVector2D();
Coord3D otherDir = *other->getUnitDirectionVector2D();
// Dot product is our directions projected onto each other.
Coord2D vectorToOther;
vectorToOther.x = other->getPosition()->x - getObject()->getPosition()->x;
vectorToOther.y = other->getPosition()->y - getObject()->getPosition()->y;
vectorToOther.normalize();
Real dotProduct = vectorToOther.x*otherDir.x + vectorToOther.y*otherDir.y;
if (dotProduct<0) return 0; // They are running into us.
Real speedFactor = dotProduct;
PhysicsBehavior *otherPhysics = other->getPhysics();
if (!otherPhysics) {
return m_curMaxBlockedSpeed;
}
Coord3D otherVel = *otherPhysics->getVelocity();
otherVel.z = 0;
// Calculate how fast other is moving away from us...
Real awaySpeed = otherVel.length() * speedFactor;
// Now calculate the amount we are moving relative to towards them...
dotProduct = vectorToOther.x*ourDir.x + vectorToOther.y*ourDir.y;
if (dotProduct<=0) {
// Unexpected - we are moving away. Shouldn't be blocked...
return m_curMaxBlockedSpeed;
}
Real maxSpeed = awaySpeed / dotProduct;
if (other->getFormationID()!=NO_FORMATION_ID && getObject()->getFormationID()==other->getFormationID()) {
maxSpeed *= 0.55f; // don't let formations crowd each other.
}
if (maxSpeed>m_curMaxBlockedSpeed) return m_curMaxBlockedSpeed;
return maxSpeed;
}
//-------------------------------------------------------------------------------------------------
Bool AIUpdateInterface::blockedBy(Object *other)
/* Returns TRUE if we are blocked from moving by the other object.*/
{
Coord3D goalPos = *getStateMachine()->getGoalPosition();
Object *obj = getObject();
Coord3D pos = *obj->getPosition();
ICoord2D goalCell = *getPathfindGoalCell();
// If we are near our final goal, don't get stuck.
if (goalCell.x>0 && goalCell.y>0) {
Real dx = fabs(goalPos.x-pos.x);
Real dy = fabs(goalPos.y-pos.y);
if (dxcanCrushOrSquish(other, TEST_CRUSH_OR_SQUISH);
if (canCrush) return FALSE; // just run over them.
AIUpdateInterface* aiOther = other->getAI();
if (!aiOther->isDoingGroundMovement()) {
return FALSE; // Can't be blocked if the other is airborne.
}
if (!aiOther) return FALSE; // Ignore it.
if (getCurLocomotor() && getCurLocomotor()->isMovingBackwards()) {
return false; // don't collide.
}
Bool otherMoving = ( aiOther->m_locomotorGoalType != NONE );
Coord3D otherPos = *other->getPosition();
Real dx = pos.x-otherPos.x;
Real dy = pos.y-otherPos.y;
Real curDSqr = dx*dx+dy*dy;
if (obj->isKindOf(KINDOF_INFANTRY) && other->isKindOf(KINDOF_INFANTRY)) {
// Infantry doesn't tend to impede other infantry...
#ifdef INFANTRY_MOVES_THROUGH_INFANTRY
if (!otherMoving) {
return FALSE; // Infantry can run through other infantry.
}
return FALSE;
#else
// If we are crossing, just pass through.
Coord3D ourDir = *obj->getUnitDirectionVector2D();
Coord3D theirDir = *other->getUnitDirectionVector2D();
// Dot product is our directions projected onto each other.
Real dotProduct = ourDir.x*theirDir.x + ourDir.y*theirDir.y;
if (dotProduct<=0.25) return FALSE; // we are not moving in the same direction.
#endif
}
if (curDSqr < PATHFIND_CELL_SIZE_F*PATHFIND_CELL_SIZE_F*0.0001f) {
// Somehow 2 units ended up on the same grid.
// Lowest path priority wins.
return (hasHigherPathPriority(aiOther));
}
// we've been blocked for a while. If we're crossing, just move through.
Coord3D ourDir = *obj->getUnitDirectionVector2D();
Coord3D theirDir = *other->getUnitDirectionVector2D();
// Dot product is our directions projected onto each other.
Real dotProduct = ourDir.x*theirDir.x + ourDir.y*theirDir.y;
if (getNumFramesBlocked()>LOGICFRAMES_PER_SECOND) {
if (dotProduct<=0.0f) return FALSE; // we are not moving in the same direction.
}
Real collisionAngle = ThePartitionManager->getRelativeAngle2D( obj, &otherPos );
Real otherAngle = ThePartitionManager->getRelativeAngle2D( other, &pos );
//DEBUG_LOG(("Collision angle %.2f, %.2f, %s, %x %s\n", collisionAngle*180/PI, otherAngle*180/PI, obj->getTemplate()->getName().str(), obj, other->getTemplate()->getName().str()));
Real angleLimit = PI/4; // 45 degrees.
if (collisionAngle>PI/2 || collisionAngle<-PI/2) {
return FALSE; // we're moving away.
}
if (!otherMoving) angleLimit *= 0.75f;
if (collisionAngle>angleLimit || collisionAngle<-angleLimit) {
if (dotProduct<=0.0f) return FALSE; // we are not moving in the same direction.
if (otherMoving && (otherAngle>angleLimit || otherAngle<-angleLimit) ) {
// See if we're running into each other.
Coord3D ourDir = *obj->getUnitDirectionVector2D();
Coord3D theirDir = *other->getUnitDirectionVector2D();
dx += ourDir.x - theirDir.x;
dy += ourDir.y - theirDir.y;
if (curDSqr>dx*dx+dy*dy) {
if (hasHigherPathPriority(aiOther)) {
// Lowest path priority wins.
return FALSE;
}
} else {
//DEBUG_LOG(("Moving Away From EachOther\n"));
return FALSE; // moving away, so no need for corrective action.
}
} else {
return FALSE; // Off angle, and they're not moving, so we aren't moving into each other.
}
}
if (!aiOther->isAiInDeadState())
{
return TRUE;
}
return FALSE;
}
//-------------------------------------------------------------------------------------------------
Bool AIUpdateInterface::needToRotate(void)
/* Returns TRUE if we need to rotate to point in our path's direcion.*/
{
if (isWaitingForPath())
return TRUE; // new path will probably require rotation.
if (this->getCurLocomotor() && this->getCurLocomotor()->getWanderWidthFactor()>0.0f)
return FALSE; // wanderers don't need to rotate.
Real deltaAngle = 0;
if (getPath())
{
ClosestPointOnPathInfo info;
CRCDEBUG_LOG(("AIUpdateInterface::needToRotate() - calling computePointOnPath() for object %d\n", getObject()->getID()));
getPath()->computePointOnPath(getObject(), m_locomotorSet, *getObject()->getPosition(), info);
deltaAngle = ThePartitionManager->getRelativeAngle2D( getObject(), &info.posOnPath );
}
if (fabs(deltaAngle)>PI/30)
{
return TRUE;
}
return FALSE;
}
//-------------------------------------------------------------------------------------------------
/* Returns TRUE if the physics collide should apply the force. Normally not.
Also determines whether objects are blocked, and if so, if they are stuck. jba.*/
Bool AIUpdateInterface::processCollision(PhysicsBehavior *physics, Object *other)
{
#ifdef DO_UNIT_TIMINGS
return false;
#endif
if (m_ignoreCollisionsUntil > TheGameLogic->getFrame())
return FALSE;
if (m_canPathThroughUnits)
return FALSE;
AIUpdateInterface* aiOther = other->getAI();
if (aiOther == NULL)
return FALSE;
Bool selfMoving = isMoving();
Bool otherMoving = ( aiOther && aiOther->isMoving() );
if (!isDoingGroundMovement()) return FALSE;
if (!aiOther->isDoingGroundMovement()) return FALSE;
if (selfMoving)
{
Bool blocked = blockedBy(other);
if (blocked)
{
if (getObject()->isKindOf(KINDOF_INFANTRY))
{
// Panic bounces around.
if (getStateMachine()->getCurrentStateID() == AI_PANIC)
{
return TRUE; // just bounce off of other humans.
}
}
m_isBlocked = TRUE; // we are blocked.
if (otherMoving && aiOther->isWaitingForPath())
{
return FALSE; // let them get their path;
}
Real maxSpeed = calculateMaxBlockedSpeed(other);
if (maxSpeed < m_curMaxBlockedSpeed)
{
m_curMaxBlockedSpeed = maxSpeed;
}
if (!aiOther->isMovingAwayFrom(getObject())) {
if (other->isKindOf(KINDOF_INFANTRY) && !getObject()->isKindOf(KINDOF_INFANTRY))
{
//Kris: Patch 1.01 -- November 5, 2003
//Prevent busy units from being told to move out of the way!
if( other->testStatus( OBJECT_STATUS_IS_USING_ABILITY ) || other->getAI() && other->getAI()->isBusy() )
{
return FALSE;
}
aiOther->aiMoveAwayFromUnit(getObject(), CMD_FROM_AI);
return FALSE;
}
#define dont_MOVE_AROUND // It just causes more problems than it fixes. jba.
#ifdef MOVE_AROUND
if (m_curLocomotor!=NULL && (other->isKindOf(KINDOF_INFANTRY)==getObject()->isKindOf(KINDOF_INFANTRY))) {
Real myMaxSpeed = m_curLocomotor->getMaxSpeedForCondition(getObject()->getBodyModule()->getDamageState());
Locomotor *hisLoco = aiOther->getCurLocomotor();
if (hisLoco) {
Real hisMaxSpeed = hisLoco->getMaxSpeedForCondition(other->getBodyModule()->getDamageState());
if (hisMaxSpeed > 0.05 && hisMaxSpeed < 0.6f*myMaxSpeed) {
aiOther->aiMoveAwayFromUnit(getObject(), CMD_FROM_AI);
return FALSE;
}
}
}
#endif
}
//DEBUG_LOG(("Blocked %s, %x, %s\n", getObject()->getTemplate()->getName().str(), getObject(), other->getTemplate()->getName().str()));
if (m_blockedFrames==0) m_blockedFrames = 1;
if (!needToRotate())
{
// If we are already pointing in the right direction, we may be stuck.
if (!otherMoving)
{
// Intense logging jba
// DEBUG_LOG(("Blocked&Stuck !otherMoving\n"));
m_isBlockedAndStuck = TRUE;
return FALSE;
}
// See if other is blocked by us.
if (aiOther->blockedBy(getObject()))
{
if (!aiOther->needToRotate())
{
// Deadlocked.
if (!hasHigherPathPriority(aiOther))
{
// get out of his way.
aiMoveAwayFromUnit(aiOther->getObject(), CMD_FROM_AI);
//m_isBlockedAndStuck = TRUE;
// Intense logging jba.
// DEBUG_LOG(("Blocked&Stuck other is blockedByUs, has higher priority\n"));
}
}
}
else
{
// Just wait.
}
}
else
{
// We are rotating, so don't accumulate blocked frames.
m_blockedFrames = 1;
}
}
}
else
{
if (isAiInDeadState())
{
// Dead infantry get pushed around by crushers.
if (getObject()->isKindOf(KINDOF_INFANTRY) && other->canCrushOrSquish(getObject(), TEST_SQUISH_ONLY))
{
return TRUE;
}
}
Coord3D otherPos = *other->getPosition();
Real dx = getObject()->getPosition()->x - otherPos.x;
Real dy = getObject()->getPosition()->y - otherPos.y;
Real curDSqr = dx*dx+dy*dy;
if (!otherMoving && curDSqr < PATHFIND_CELL_SIZE_F*PATHFIND_CELL_SIZE_F*0.25f)
{
if (this->getCurrentStateID() == AI_BUSY) {
return false;
}
if (getObject()->testStatus(OBJECT_STATUS_IS_USING_ABILITY)) {
return false; // we are doing a special ability. Shouldn't move at this time. jba.
}
// jba intense debug
//DEBUG_LOG(("*****Units ended up on top of each other. Shouldn't happen.\n"));
if (isIdle()) {
Coord3D safePosition = *getObject()->getPosition();
TheAI->pathfinder()->adjustToPossibleDestination(getObject(), getLocomotorSet(), &safePosition);
aiMoveToPosition( &safePosition, CMD_FROM_AI );
}
if (aiOther->isIdle()) {
TheAI->pathfinder()->adjustToPossibleDestination(other, aiOther->getLocomotorSet(),
&otherPos);
aiOther->aiMoveToPosition( &otherPos, CMD_FROM_AI);
}
}
}
return FALSE;
}
//-------------------------------------------------------------------------------------------------
/**
* See if we can do a quick path without pathfinding.
*/
Bool AIUpdateInterface::canComputeQuickPath( void )
{
/* Basically, if a unit is moving through the air, we can quick path. jba. */
Bool landBound = FALSE;
// Note - if a truck happens to pop into the air and gets a move to command, it still
// needs to pathfind. So only skip pathfinding for airborne things that can fly... jba.
if (!(m_locomotorSet.getValidSurfaces() & LOCOMOTORSURFACE_AIR))
{
landBound = TRUE;
}
Bool unitIsFlyingThroughTheAir = FALSE;
if (landBound) {
unitIsFlyingThroughTheAir = FALSE; // Land bound units never fly.
} else {
if (!isDoingGroundMovement()) {
// If it can fly, and it isn't moving on the ground, we're flying.
unitIsFlyingThroughTheAir = TRUE;
}
}
return unitIsFlyingThroughTheAir;
}
//-------------------------------------------------------------------------------------------------
/**
* Create a quick path. (Just places the start & end point as the path). jba.
*/
Bool AIUpdateInterface::computeQuickPath( const Coord3D *destination )
{
// for now, quick path objects don't pathfind, generally airborne units
// build a trivial one-node path containing destination
// First, see if our path already goes to the destination.
if (m_path) {
PathNode *closeNode = NULL;
closeNode = m_path->getLastNode();
if (closeNode && closeNode->getNextOptimized()==NULL) {
Real dxSqr = destination->x - closeNode->getPosition()->x;
dxSqr *= dxSqr;
Real dySqr = destination->y - closeNode->getPosition()->y;
dySqr *= dySqr;
Real dzSqr = destination->z - closeNode->getPosition()->z;
dzSqr *= dzSqr;
if (dxSqr+dySqr+dzSqr<0.25f) {
return TRUE;
}
}
}
// destroy previous path
destroyPath();
if (getObject()->isKindOf(KINDOF_AIRCRAFT) && !getObject()->isKindOf(KINDOF_PROJECTILE)) {
m_path = TheAI->pathfinder()->getAircraftPath(getObject(), destination);
} else {
m_path = newInstance(Path);
m_path->prependNode( destination, LAYER_GROUND );
Coord3D pos = *getObject()->getPosition();
pos.z = destination->z;
m_path->prependNode( &pos, getObject()->getLayer() );
m_path->getFirstNode()->setNextOptimized(m_path->getFirstNode()->getNext());
if (TheGlobalData->m_debugAI==AI_DEBUG_PATHS)
{
TheAI->pathfinder()->setDebugPath(m_path);
}
}
// timestamp when the path was created
m_pathTimestamp = TheGameLogic->getFrame();
m_blockedFrames = 0;
m_isBlockedAndStuck = FALSE;
return TRUE;
}
//-------------------------------------------------------------------------------------------------
/**
* Invoke the pathfinder to compute a path to the desired location.
*/
Bool AIUpdateInterface::computePath( PathfindServicesInterface *pathServices, Coord3D *destination )
{
if (!m_isBlockedAndStuck) {
destroyPath();
}
if (canComputeQuickPath())
{
return computeQuickPath(destination);
}
m_retryPath = false;
Region3D extent;
TheTerrainLogic->getMaximumPathfindExtent(&extent);
if (!extent.isInRegionNoZ(destination)) {
// We're going off the map.
Coord3D pos = *getObject()->getPosition();
if (!extent.isInRegionNoZ(&pos)) {
// We're starting off the map. Since we're off the map, we can't pathfind so just build a path.
return computeQuickPath(destination);
}
}
// Special case of exit factory. jba.
if ((m_stateMachine->getCurrentStateID() == AI_FOLLOW_EXITPRODUCTION_PATH) && canPathThroughUnits()) {
Bool ok = computeQuickPath(destination);
if (ok) {
TheAI->pathfinder()->moveAlliesAwayFromDestination(getObject(), *destination);
setCanPathThroughUnits(false);
setGoalPositionClipped(destination, CMD_FROM_AI);
return ok;
}
}
Path *theNewPath = NULL;
TheAI->pathfinder()->setIgnoreObstacleID( getIgnoredObstacleID() );
Coord3D originalDestination = *destination;
// sanity check - if destination cell is invalid, don't bother pathing
LocomotorSurfaceTypeMask surfaces = m_locomotorSet.getValidSurfaces();
if (!m_isFinalGoal && TheAI->pathfinder()->isLinePassable( getObject(), surfaces,
getObject()->getLayer(), *getObject()->getPosition(), originalDestination, false, true)) {
return computeQuickPath(destination);
}
PathfindLayerEnum destinationLayer = TheTerrainLogic->getLayerForDestination(destination);
if (TheAI->pathfinder()->validMovementPosition( getObject()->getCrusherLevel()>0, destinationLayer, m_locomotorSet, destination ) == FALSE)
{
theNewPath = NULL;
}
else
{
// compute a ground-based path
if (m_isBlockedAndStuck) {
theNewPath = pathServices->patchPath( getObject(), m_locomotorSet,
getPath(), m_isBlockedAndStuck);
} else {
theNewPath = pathServices->findPath( getObject(), m_locomotorSet, getObject()->getPosition(),
destination);
}
}
if (theNewPath==NULL && m_path==NULL) {
Real pathCostFactor = 0.0f;
theNewPath = pathServices->findClosestPath( getObject(), m_locomotorSet, getObject()->getPosition(),
destination, m_isBlockedAndStuck, pathCostFactor, FALSE );
m_retryPath = true;
}
TheAI->pathfinder()->setIgnoreObstacleID( INVALID_ID );
if (theNewPath) {
// destroy previous path
destroyPath();
m_path = theNewPath;
if (getCurLocomotor() && getCurLocomotor()->isUltraAccurate()) {
// Move exactly to the destination. Normal ground pathfinding moves to a gridded location.
theNewPath->updateLastNode(&originalDestination);
}
setLocomotorGoalPositionOnPath();
if( !getObject()->isKindOf(KINDOF_NO_COLLIDE))// If I don't collide with things, I don't need to tell them to get out of the way
TheAI->pathfinder()->moveAllies(getObject(), theNewPath);
} else {
// Keep using the old path.
if (m_path && m_isBlockedAndStuck) {
destroyPath();
// Stop and wait one second.
setQueueForPathTime(LOGICFRAMES_PER_SECOND);
Coord3D goalPos;
Object *obj = getObject();
goalPos = *obj->getPosition();
TheAI->pathfinder()->snapPosition(obj, &goalPos);
setFinalPosition(&goalPos);
setLocomotorGoalNone();
m_blockedFrames = 0;
m_isBlocked = FALSE;
m_isBlockedAndStuck = FALSE;
}
}
// timestamp when the path was created
m_pathTimestamp = TheGameLogic->getFrame();
m_blockedFrames = 0;
m_isBlockedAndStuck = FALSE;
if (m_path)
return TRUE;
return FALSE;
}
//-------------------------------------------------------------------------------------------------
/**
* Invoke the pathfinder to compute a path to attack the current victim.
*/
Bool AIUpdateInterface::computeAttackPath( PathfindServicesInterface *pathServices, const Object *victim, const Coord3D* victimPos )
{
//CRCDEBUG_LOG(("AIUpdateInterface::computeAttackPath() for object %d\n", getObject()->getID()));
// See if it has been too soon.
if (m_pathTimestamp >= TheGameLogic->getFrame()-2)
{
// jba intense debug
//CRCDEBUG_LOG(("Info - RePathing very quickly %d, %d.\n", m_pathTimestamp, TheGameLogic->getFrame()));
if (m_path && m_isBlockedAndStuck)
{
setIgnoreCollisionTime(2*LOGICFRAMES_PER_SECOND);
m_blockedFrames = 0;
m_isBlocked = FALSE;
m_isBlockedAndStuck = FALSE;
return TRUE;
}
}
Bool landBound = FALSE;
// Note - if a truck happens to pop into the air and gets a move to command, it still
// needs to pathfind. So only skip pathfinding for airborne things that can fly... jba.
if (!(m_locomotorSet.getValidSurfaces() & LOCOMOTORSURFACE_AIR))
{
landBound = TRUE;
}
Object* source = getObject();
if (!victim && !victimPos)
{
//CRCDEBUG_LOG(("AIUpdateInterface::computeAttackPath() - victim is NULL\n"));
return FALSE;
}
PathfindLayerEnum victimLayer = LAYER_GROUND;
if (victim) {
victimLayer = victim->getLayer();
}
Weapon *weapon = source->getCurrentWeapon();
if (!weapon)
{
DEBUG_CRASH(("no weapon in AIUpdateInterface::computeAttackPath"));
return FALSE;
}
// is our weapon within attack range?
// if so, just return TRUE with no path.
if (victim != NULL)
{
if (weapon->isWithinAttackRange(source, victim))
{
Bool viewBlocked = FALSE;
if (isDoingGroundMovement() && !victim->isSignificantlyAboveTerrain())
{
viewBlocked = TheAI->pathfinder()->isAttackViewBlockedByObstacle(source, *source->getPosition(), victim, *victim->getPosition());
}
if (!viewBlocked)
{
destroyPath();
//CRCDEBUG_LOG(("AIUpdateInterface::computeAttackPath() - target is in range and visible\n"));
return TRUE;
}
}
}
else if (victimPos != NULL)
{
if (weapon->isWithinAttackRange(source, victimPos))
{
Bool viewBlocked = FALSE;
if (isDoingGroundMovement())
{
viewBlocked = TheAI->pathfinder()->isAttackViewBlockedByObstacle(source, *source->getPosition(), NULL, *victimPos);
}
if (!viewBlocked) {
destroyPath();
//CRCDEBUG_LOG(("AIUpdateInterface::computeAttackPath() target pos is in range and visible\n"));
return TRUE;
}
}
}
// Contact weapon
if (weapon->isContactWeapon())
{
// Weapon is basically a contact weapon, like a car bomb. The approach target logic
// has been modified to let it approach the object, so just approach the target position. jba.
Coord3D tmp = *victimPos;
destroyPath();
if (this->getCurLocomotor())
{
getCurLocomotor()->setNoSlowDownAsApproachingDest(TRUE);
}
Bool ok = computePath(pathServices, &tmp);
if (m_path==NULL) return false;
Real dx, dy;
dx = victimPos->x - m_path->getLastNode()->getPosition()->x;
dy = victimPos->y - m_path->getLastNode()->getPosition()->y;
if (sqr(dx)+sqr(dy) < sqr(PATHFIND_CELL_SIZE_F*3)) {
if (m_path)
{
m_path->updateLastNode(victimPos); // jam in the coordinates of the target.
}
}
dx = source->getPosition()->x - m_path->getLastNode()->getPosition()->x;
dy = source->getPosition()->y - m_path->getLastNode()->getPosition()->y;
if (sqr(dx)+sqr(dy) < sqr(PATHFIND_CELL_SIZE_F)) {
// Very short path - we can't get to the goal.
destroyPath();
return false;
}
//CRCDEBUG_LOG(("AIUpdateInterface::computeAttackPath() is contact weapon\n"));
return ok;
}
Coord3D localVictimPos;
if (victim != NULL)
{
if (victim->isKindOf(KINDOF_BRIDGE))
{
TBridgeAttackInfo info;
TheTerrainLogic->getBridgeAttackPoints(victim, &info);
Real distSqr1 = ThePartitionManager->getDistanceSquared( source, &info.attackPoint1, FROM_BOUNDINGSPHERE_3D );
Real distSqr2 = ThePartitionManager->getDistanceSquared( source, &info.attackPoint2, FROM_BOUNDINGSPHERE_3D );
if (distSqr2getPosition();
}
}
else
{
localVictimPos = *victimPos;
}
localVictimPos.z = TheTerrainLogic->getLayerHeight( localVictimPos.x, localVictimPos.y, victimLayer );
if (getObject()->isAboveTerrain() && !landBound)
{
// for now, airborne objects don't pathfind
// build a trivial one-node path containing destination
weapon->computeApproachTarget(getObject(), victim, &localVictimPos, 0, localVictimPos);
//DEBUG_ASSERTCRASH(weapon->isGoalPosWithinAttackRange(getObject(), &localVictimPos, victim, victimPos, NULL),
// ("position we just calced is not acceptable\n"));
// First, see if our path already goes to the destination.
if (m_path)
{
PathNode *startNode, *closeNode = NULL;
startNode = m_path->getFirstNode();
closeNode = startNode->getNextOptimized();
if (closeNode && closeNode->getNextOptimized()==NULL) {
Real dxSqr = localVictimPos.x - closeNode->getPosition()->x;
dxSqr *= dxSqr;
Real dySqr = localVictimPos.y - closeNode->getPosition()->y;
dySqr *= dySqr;
if (dxSqr+dySqr<0.25f)
{
return TRUE;
}
}
}
// destroy previous path
destroyPath();
m_path = newInstance(Path);
m_path->prependNode( &localVictimPos, LAYER_GROUND );
Coord3D pos = *getObject()->getPosition();
pos.z = localVictimPos.z;
m_path->prependNode( &pos, LAYER_GROUND );
m_path->getFirstNode()->setNextOptimized(m_path->getFirstNode()->getNext());
if (TheGlobalData->m_debugAI==AI_DEBUG_PATHS)
{
TheAI->pathfinder()->setDebugPath(m_path);
}
}
else
{
// destroy previous path
destroyPath();
TheAI->pathfinder()->setIgnoreObstacleID( getIgnoredObstacleID() );
// compute a ground-based path
m_path = pathServices->findAttackPath( getObject(), m_locomotorSet, getObject()->getPosition(),
victim, &localVictimPos, weapon);
if (m_path) {
Coord3D goal = *m_path->getLastNode()->getPosition();
if (!weapon->isGoalPosWithinAttackRange(getObject(), &goal, victim, &localVictimPos)) {
// We didn't actually find a path we can attack from. [8/14/2003]
// If the move is a short distance, just do a find closest path to our current
// position. This will unstack us if we are on top of another unit. jba.
Coord3D objPos = *getObject()->getPosition();
goal.sub(&objPos);
if (goal.length()<3*PATHFIND_CELL_SIZE_F) {
destroyPath();
TheAI->pathfinder()->adjustDestination(getObject(), m_locomotorSet, &objPos);
m_path = pathServices->findClosestPath(getObject(), m_locomotorSet, getObject()->getPosition(),
&objPos, false, 0.2f, true );
}
if (m_path==NULL) {
return false;
}
}
goal = *m_path->getLastNode()->getPosition();
TheAI->pathfinder()->updateGoal(getObject(), &goal, TheTerrainLogic->getLayerForDestination(&goal));
if (m_path->getBlockedByAlly())
{
if( !getObject()->isKindOf(KINDOF_NO_COLLIDE))// If I don't collide with things, I don't need to tell them to get out of the way
TheAI->pathfinder()->moveAllies(getObject(), m_path);
}
}
TheAI->pathfinder()->setIgnoreObstacleID( INVALID_ID );
}
// timestamp when the path was created
m_pathTimestamp = TheGameLogic->getFrame();
m_blockedFrames = 0;
m_isBlockedAndStuck = FALSE;
//CRCDEBUG_LOG(("AIUpdateInterface::computeAttackPath() done\n"));
if (m_path)
return TRUE;
return FALSE;
}
//-------------------------------------------------------------------------------------------------
/**
* Destroy the current path, and set it to NULL
*/
void AIUpdateInterface::destroyPath( void )
{
// destroy previous path
if (m_path)
m_path->deleteInstance();
m_path = NULL;
m_waitingForPath = FALSE; // we no longer need it.
//CRCDEBUG_LOG(("AIUpdateInterface::destroyPath() - m_isAttackPath = FALSE for object %d\n", getObject()->getID()));
m_isAttackPath = FALSE;
setLocomotorGoalNone();
}
//-------------------------------------------------------------------------------------------------
/**
* This is used by the internal move to state to indicate that a move started.
*/
void AIUpdateInterface::friend_startingMove(void)
{
m_movementComplete = FALSE; // we aren't finished moving.
m_isMoving = TRUE;
m_blockedFrames = 0;
m_isBlockedAndStuck = FALSE;
}
//-------------------------------------------------------------------------------------------------
/**
* This is used by the internal move to state to indicate that a move completed.
*/
void AIUpdateInterface::friend_endingMove()
{
m_movementComplete = TRUE;
m_isMoving = FALSE;
}
//-------------------------------------------------------------------------------------------------
/**
* This is used by the jetai to set a specific path.
*/
void AIUpdateInterface::friend_setPath(Path *path)
{
destroyPath();
m_path = path;
}
//-------------------------------------------------------------------------------------------------
/**
* This is used by the guard tunnel network state to set a target object.
*/
void AIUpdateInterface::friend_setGoalObject(Object *obj)
{
Bool locked = getStateMachine()->isLocked();
getStateMachine()->unlock();
getStateMachine()->setGoalObject(obj);
if (locked) {
getStateMachine()->lock("Friend_setGlobalObject re-locking");
}
}
//-------------------------------------------------------------------------------------------------
/** Is there a path at all that exists from us to the destination location */
//-------------------------------------------------------------------------------------------------
Bool AIUpdateInterface::isPathAvailable( const Coord3D *destination ) const
{
// sanity
if( destination == NULL )
return FALSE;
const Coord3D *myPos = getObject()->getPosition();
return TheAI->pathfinder()->clientSafeQuickDoesPathExist( m_locomotorSet, myPos, destination );
} // end isPathAvailable
//-------------------------------------------------------------------------------------------------
/** Is there a path (computed using the less accurate but quick method )
* at all that exists from us to the destination location */
//-------------------------------------------------------------------------------------------------
Bool AIUpdateInterface::isQuickPathAvailable( const Coord3D *destination ) const
{
// sanity
if( destination == NULL )
return FALSE;
const Coord3D *myPos = getObject()->getPosition();
return TheAI->pathfinder()->clientSafeQuickDoesPathExistForUI( m_locomotorSet, myPos, destination );
} // end isQuickPathAvailable
//-------------------------------------------------------------------------------------------------
Bool AIUpdateInterface::isValidLocomotorPosition(const Coord3D* pos) const
{
return TheAI->pathfinder()->validMovementPosition( getObject()->getCrusherLevel()>0, getObject()->getLayer(), m_locomotorSet, pos );
}
//-------------------------------------------------------------------------------------------------
DECLARE_PERF_TIMER(doLocomotor)
/**
* Compute drive forces
*/
UpdateSleepTime AIUpdateInterface::doLocomotor( void )
{
USE_PERF_TIMER(doLocomotor)
if (getObject()->isKindOf(KINDOF_IMMOBILE))
return UPDATE_SLEEP_FOREVER;
chooseGoodLocomotorFromCurrentSet();
if (m_isBlocked)
{
++m_blockedFrames;
}
else
{
m_blockedFrames = 0;
}
m_isBlocked = FALSE;
Bool blocked = m_blockedFrames > 0;
Bool requiresConstantCalling = TRUE; // assume the worst.
if (m_curLocomotor)
{
m_curLocomotor->setPhysicsOptions(getObject());
if (isAiInDeadState() && !m_curLocomotor->getLocomotorWorksWhenDead())
{
// now it's over, I'm dead, and I haven't done anything that I want,
// or, I'm still alive, and there's nothing I want to do
}
else
{
switch (m_locomotorGoalType)
{
case POSITION_EXPLICIT:
{
Real speed = m_desiredSpeed;
Real myMaxSpeed = m_curLocomotor->getMaxSpeedForCondition(getObject()->getBodyModule()->getDamageState());
if( speed == FAST_AS_POSSIBLE || speed > myMaxSpeed )
speed = myMaxSpeed;
m_curLocomotor->locoUpdate_moveTowardsPosition(getObject(),
m_locomotorGoalData, 0.0f, speed, &blocked);
m_doFinalPosition = FALSE;
}
break;
case POSITION_ON_PATH:
{
if (!getPath())
{
if (m_waitingForPath)
{
return UPDATE_SLEEP_FOREVER; // Can't move till we get our path.
}
DEBUG_LOG(("Dead %d, obj %s %x\n", isAiInDeadState(), getObject()->getTemplate()->getName().str(), getObject()));
#ifdef STATE_MACHINE_DEBUG
DEBUG_LOG(("Waiting %d, state %s\n", m_waitingForPath, getStateMachine()->getCurrentStateName().str()));
m_stateMachine->setDebugOutput(1);
#endif
DEBUG_CRASH(("must have a path here (doLocomotor)"));
break;
}
Coord3D goalPos;
Real onPathDistToGoal;
if (!isDoingGroundMovement())
{
// airborne locomotor. Get the goal and distance direct to the goal, don't consider obstacles.
onPathDistToGoal = getPath()->computeFlightDistToGoal(getObject()->getPosition(), goalPos);
}
else
{
// Compute the actual goal position along the path to move towards. Consider
// obstacles, and follow the intermediate path points.
ClosestPointOnPathInfo info;
CRCDEBUG_LOG(("AIUpdateInterface::doLocomotor() - calling computePointOnPath() for %s\n",
DescribeObject(getObject()).str()));
getPath()->computePointOnPath(getObject(), m_locomotorSet, *getObject()->getPosition(), info);
onPathDistToGoal = info.distAlongPath;
goalPos = info.posOnPath;
// layer is a possible bridge in the path. Check & set the layer if applicable.
TheAI->pathfinder()->updateLayer(getObject(), info.layer);
}
Real speed = m_desiredSpeed;
Real myMaxSpeed = m_curLocomotor->getMaxSpeedForCondition(getObject()->getBodyModule()->getDamageState());
if( speed == FAST_AS_POSSIBLE || speed > myMaxSpeed )
speed = myMaxSpeed;
if (blocked && speed>m_curMaxBlockedSpeed)
{
speed = m_curMaxBlockedSpeed;
if (m_bumpSpeedLimit>speed) {
m_bumpSpeedLimit = speed;
}
m_bumpSpeedLimit *= 0.95f;
speed = m_bumpSpeedLimit;
}
else
{
blocked = FALSE;
if (m_bumpSpeedLimitm_bumpSpeedLimit) {
speed = m_bumpSpeedLimit;
}
}
m_curLocomotor->locoUpdate_moveTowardsPosition(getObject(), goalPos,
onPathDistToGoal+getPathExtraDistance(), speed, &blocked);
m_doFinalPosition = FALSE;
}
break;
case ANGLE:
{
m_curLocomotor->locoUpdate_moveTowardsAngle(getObject(), m_locomotorGoalData.x);
m_doFinalPosition = FALSE;
}
break;
case NONE:
{
if (m_doFinalPosition)
{
Coord3D pos = *getObject()->getPosition();
Bool onGround = !getObject()->isAboveTerrain() && getObject()->getLayer() == LAYER_GROUND;
Real dx = m_finalPosition.x - pos.x;
Real dy = m_finalPosition.y - pos.y;
Real dSqr = dx*dx+dy*dy;
const Real DARN_CLOSE = 0.25f;
if (dSqr < DARN_CLOSE)
{
m_doFinalPosition = FALSE;
if (onGround)
m_finalPosition.z = TheTerrainLogic->getGroundHeight( m_finalPosition.x, m_finalPosition.y );
else
m_finalPosition.z = pos.z;
getObject()->setPosition(&m_finalPosition);
}
else
{
Real dist = sqrtf(dSqr);
if (dist<1) dist = 1;
pos.x += 2*PATHFIND_CELL_SIZE_F*dx/(dist*LOGICFRAMES_PER_SECOND);
pos.y += 2*PATHFIND_CELL_SIZE_F*dy/(dist*LOGICFRAMES_PER_SECOND);
if (onGround)
pos.z = TheTerrainLogic->getGroundHeight( pos.x, pos.y );
getObject()->setPosition(&pos);
}
}
requiresConstantCalling = m_curLocomotor->locoUpdate_maintainCurrentPosition(getObject());
}
break;
}
}
if (!blocked && m_blockedFrames>1)
{
m_blockedFrames = 1;
}
// After our movement for the frame, update our AirborneTarget flag.
if(getObject()->getHeightAboveTerrain() > m_curLocomotor->getAirborneTargetingHeight() )
getObject()->setStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_AIRBORNE_TARGET ) );
else
getObject()->clearStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_AIRBORNE_TARGET ) );
m_curMaxBlockedSpeed = FAST_AS_POSSIBLE;
}
if (m_curLocomotor != NULL
&& m_locomotorGoalType == NONE
&& m_doFinalPosition == FALSE
&& m_isBlocked == FALSE
&& requiresConstantCalling == FALSE)
{
return UPDATE_SLEEP_FOREVER;
}
else
{
return UPDATE_SLEEP_NONE;
}
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::setLocomotorGoalPositionOnPath()
{
m_locomotorGoalType = POSITION_ON_PATH;
m_locomotorGoalData.zero();
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::setLocomotorGoalPositionExplicit(const Coord3D& newPos)
{
m_locomotorGoalType = POSITION_EXPLICIT;
m_locomotorGoalData = newPos;
#ifdef _DEBUG
if (_isnan(m_locomotorGoalData.x) || _isnan(m_locomotorGoalData.y) || _isnan(m_locomotorGoalData.z))
{
DEBUG_CRASH(("NAN in setLocomotorGoalPositionExplicit"));
}
#endif
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::setLocomotorGoalOrientation(Real angle)
{
m_locomotorGoalType = ANGLE;
m_locomotorGoalData.x = angle;
#ifdef _DEBUG
if (_isnan(m_locomotorGoalData.x) || _isnan(m_locomotorGoalData.y) || _isnan(m_locomotorGoalData.z))
{
DEBUG_CRASH(("NAN in setLocomotorGoalOrientation"));
}
#endif
}
//-------------------------------------------------------------------------------------------------
void AIUpdateInterface::setLocomotorGoalNone()
{
m_locomotorGoalType = NONE;
}
//-------------------------------------------------------------------------------------------------
Bool AIUpdateInterface::isDoingGroundMovement(void) const
{
if (getObject()->isDisabledByType( DISABLED_UNMANNED )
&& getObject()->isKindOf( KINDOF_PRODUCED_AT_HELIPAD ) )
{
return TRUE; // an unmanned helicopter gets grounded, eventually.
}
if (m_locomotorSet.getValidSurfaces() == LOCOMOTORSURFACE_AIR)
{
return FALSE; // air only loco.
}
if (m_curLocomotor == NULL)
{
return FALSE; // No loco, so we aren't moving.
}
// Cur loco is air, so not ground.
if (m_curLocomotor->getLegalSurfaces() & LOCOMOTORSURFACE_AIR)
{
return FALSE;
}
// We are held, so not moving on ground.
if( getObject()->isDisabledByType( DISABLED_HELD ) )
{
return FALSE;
}
// if we're airborne and "allowed to fall", we are probably deliberately in midair
// due to rappel or accident...
const PhysicsBehavior* physics = getObject()->getPhysics();
if (getObject()->isAboveTerrain() && physics != NULL && physics->getAllowToFall())
{
return FALSE;
}
// After all exceptions, we must be doing ground movement.
//DEBUG_ASSERTLOG(getObject()->isSignificantlyAboveTerrain(), ("Object %s is significantly airborne but also doing ground movement. What?\n",getObject()->getTemplate()->getName().str()));
return TRUE;
}
//-------------------------------------------------------------------------------------------------
/** Some aircraft (comanche in particular, which hover) shouldn't stack destinations.
Others, like missles, should stack destinations. AdjustDestination in pathfinder unstacks
destinations, and this routine identifies non-ground units that should unstack. */
Bool AIUpdateInterface::isAircraftThatAdjustsDestination(void) const
{
if (m_curLocomotor == NULL)
{
return FALSE; // No loco, so we aren't moving.
}
if (m_curLocomotor->getAppearance() == LOCO_HOVER)
{
return TRUE; // Hover adjusts.
}
if (m_curLocomotor->getAppearance() == LOCO_WINGS)
{
return TRUE; // wings adjusts.
}
if (m_curLocomotor->getAppearance() == LOCO_THRUST)
{
return FALSE; // thrust doesn't adjust.
}
return FALSE;
}
//-------------------------------------------------------------------------------------------------
Bool AIUpdateInterface::getTreatAsAircraftForLocoDistToGoal() const
{
Bool treatAsAircraft = !isDoingGroundMovement();
if (getPathExtraDistance() > PATHFIND_CLOSE_ENOUGH)
{
// We are following a waypoint or other multiple point path, so use the "easy" success criteria.
treatAsAircraft = TRUE;
}
if (m_curLocomotor && m_curLocomotor->getAppearance() == LOCO_HOVER)
{
// Hovercrafts are very sloppy. So use aircraft tests for distance to goal. jba.
treatAsAircraft = TRUE;
}
return treatAsAircraft;
}
//-------------------------------------------------------------------------------------------------
Real AIUpdateInterface::getLocomotorDistanceToGoal()
{
switch (m_locomotorGoalType)
{
case POSITION_EXPLICIT:
DEBUG_CRASH(("not yet implemented"));
return 0.0f;
case POSITION_ON_PATH:
if (!getPath())
{
DEBUG_CRASH(("must have a path here (getLocomotorDistanceToGoal)"));
return 0.0f;
}
else if (!m_curLocomotor)
{
//DEBUG_LOG(("no locomotor here, so no dist. (this is ok.)\n"));
return 0.0f;
}
else if( m_curLocomotor->isCloseEnoughDist3D() || getObject()->isKindOf(KINDOF_PROJECTILE))
{
const Object *me = getObject();
const Coord3D *dest = getGoalPosition();
if (m_path->getLastNode()) {
dest = m_path->getLastNode()->getPosition();
}
Real distance = ThePartitionManager->getDistanceSquared( me, dest, FROM_CENTER_3D );
return sqrt( distance );// Other paths return dots of normalized vectors, so one sqrt ain't so bad
}
else
{
Coord3D goalPos;
Bool treatAsAircraft = getTreatAsAircraftForLocoDistToGoal();
Real dist;
if (treatAsAircraft)
{
// airborne locomotor. Get the goal and distance direct to the goal, don't consider obstacles.
dist = getPath()->computeFlightDistToGoal(getObject()->getPosition(), goalPos);
} else {
// Ground based locomotor.
ClosestPointOnPathInfo info;
CRCDEBUG_LOG(("AIUpdateInterface::getLocomotorDistanceToGoal() - calling computePointOnPath() for object %d\n", getObject()->getID()));
getPath()->computePointOnPath(getObject(), m_locomotorSet, *getObject()->getPosition(), info);
goalPos = info.posOnPath;
dist = info.distAlongPath;
}
if (m_path->getLastNode()) {
goalPos = *m_path->getLastNode()->getPosition();
}
// We are trying to get to goal. So,
// If the actual distance is farther, then use the actual distance so we get there.
Real dx = goalPos.x - getObject()->getPosition()->x;
Real dy = goalPos.y - getObject()->getPosition()->y;
Real distSqr = dx*dx + dy*dy;
if (treatAsAircraft)
{
if (sqr(dist) > distSqr)
{
return sqrt(distSqr);
}
else
{
return dist;
}
}
if (distisMobile() == FALSE)
return;
chooseLocomotorSet(LOCOMOTORSET_NORMAL);
getStateMachine()->clear();
getStateMachine()->setGoalWaypoint(NULL);
Object *obj = getObject();
Object *other = NULL;
Team *team = obj->getTeam();
for (DLINK_ITERATOR