/* ** 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. // // // //////////////////////////////////////////////////////////////////////////////// // FILE: StealthDetectorUpdate.cpp //////////////////////////////////////////////////////////////////////// // Author: Kris Morness, May 2002 // Desc: An update that checks for a status bit to stealth the owning object /////////////////////////////////////////////////////////////////////////////////////////////////// #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #define DEFINE_STEALTHLEVEL_NAMES // INCLUDES /////////////////////////////////////////////////////////////////////////////////////// #include "Common/MiscAudio.h" #include "Common/Radar.h" #include "Common/ThingTemplate.h" #include "Common/Xfer.h" #include "GameClient/Drawable.h" #include "GameClient/Eva.h" #include "GameClient/GameText.h" #include "GameClient/InGameUI.h" #include "GameClient/ParticleSys.h" #include "GameLogic/Damage.h" #include "GameLogic/Object.h" #include "GameLogic/PartitionManager.h" #include "GameLogic/Module/ContainModule.h" #include "GameLogic/Module/StealthUpdate.h" #include "GameLogic/Module/StealthDetectorUpdate.h" #include "Common/BitFlagsIO.h" #include "Common/PlayerList.h" #include "Common/Player.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif //------------------------------------------------------------------------------------------------- void StealthDetectorUpdateModuleData::buildFieldParse(MultiIniFieldParse& p) { UpdateModuleData::buildFieldParse(p); static const FieldParse dataFieldParse[] = { { "DetectionRate", INI::parseDurationUnsignedInt, NULL, offsetof( StealthDetectorUpdateModuleData, m_updateRate ) }, { "DetectionRange", INI::parseReal, NULL, offsetof( StealthDetectorUpdateModuleData, m_detectionRange ) }, { "InitiallyDisabled", INI::parseBool, NULL, offsetof( StealthDetectorUpdateModuleData, m_initiallyDisabled ) }, { "PingSound", INI::parseAudioEventRTS, NULL, offsetof( StealthDetectorUpdateModuleData, m_pingSound ) }, { "LoudPingSound", INI::parseAudioEventRTS, NULL, offsetof( StealthDetectorUpdateModuleData, m_loudPingSound ) }, { "IRBeaconParticleSysName", INI::parseParticleSystemTemplate, NULL, offsetof( StealthDetectorUpdateModuleData, m_IRBeaconParticleSysTmpl ) }, { "IRParticleSysName", INI::parseParticleSystemTemplate, NULL, offsetof( StealthDetectorUpdateModuleData, m_IRParticleSysTmpl ) }, { "IRBrightParticleSysName", INI::parseParticleSystemTemplate, NULL, offsetof( StealthDetectorUpdateModuleData, m_IRBrightParticleSysTmpl ) }, { "IRGridParticleSysName", INI::parseParticleSystemTemplate, NULL, offsetof( StealthDetectorUpdateModuleData, m_IRGridParticleSysTmpl ) }, { "IRParticleSysBone", INI::parseAsciiString, NULL, offsetof( StealthDetectorUpdateModuleData, m_IRParticleSysBone ) }, { "ExtraRequiredKindOf", KindOfMaskType::parseFromINI, NULL, offsetof( StealthDetectorUpdateModuleData, m_extraDetectKindof ) }, { "ExtraForbiddenKindOf", KindOfMaskType::parseFromINI, NULL, offsetof( StealthDetectorUpdateModuleData, m_extraDetectKindofNot ) }, { "CanDetectWhileGarrisoned", INI::parseBool, NULL, offsetof( StealthDetectorUpdateModuleData, m_canDetectWhileGarrisoned ) }, { "CanDetectWhileContained", INI::parseBool, NULL, offsetof( StealthDetectorUpdateModuleData, m_canDetectWhileTransported ) }, { 0, 0, 0, 0 } }; p.add(dataFieldParse); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- StealthDetectorUpdate::StealthDetectorUpdate( Thing *thing, const ModuleData* moduleData ) : UpdateModule( thing, moduleData ) { const StealthDetectorUpdateModuleData *data = getStealthDetectorUpdateModuleData(); m_enabled = !data->m_initiallyDisabled; // start these guys with random phasings so that we don't // have all of 'em check on the same frame. setWakeFrame(getObject(), m_enabled ? UPDATE_SLEEP(GameLogicRandomValue(1, data->m_updateRate)) : UPDATE_SLEEP_FOREVER); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- StealthDetectorUpdate::~StealthDetectorUpdate( void ) { } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- void StealthDetectorUpdate::setSDEnabled( Bool enabled ) { m_enabled = enabled; setWakeFrame(getObject(), m_enabled ? UPDATE_SLEEP_NONE : UPDATE_SLEEP_FOREVER); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- class PartitionFilterStealthedOrStealthGarrisoned : public PartitionFilter { public: PartitionFilterStealthedOrStealthGarrisoned() { } virtual Bool allow(Object *objOther); #if defined(_DEBUG) || defined(_INTERNAL) virtual const char* debugGetName() { return "PartitionFilterStealthedOrStealthGarrisoned"; } #endif }; Bool PartitionFilterStealthedOrStealthGarrisoned::allow( Object *objOther) { if( ! objOther ) return FALSE; if( objOther->getStatusBits().test( OBJECT_STATUS_STEALTHED ) ) return TRUE; ContainModuleInterface *contain = objOther->getContain(); if( contain && contain->isGarrisonable() && contain->getStealthUnitsContained() ) return TRUE; return FALSE; } //------------------------------------------------------------------------------------------------- /** The update callback. */ //------------------------------------------------------------------------------------------------- UpdateSleepTime StealthDetectorUpdate::update( void ) { const StealthDetectorUpdateModuleData *data = getStealthDetectorUpdateModuleData(); Object* self = getObject(); if (self->isEffectivelyDead()) return UPDATE_SLEEP_FOREVER; // We have to wait until we are fully constructed, but we will detect the moment we finish if( self->testStatus(OBJECT_STATUS_UNDER_CONSTRUCTION) ) return UPDATE_SLEEP_NONE; // We turn off forever the moment we are sold. if( self->testStatus(OBJECT_STATUS_SOLD) ) return UPDATE_SLEEP_FOREVER; //Are we contained by anything? Object *containedBy = self->getContainedBy(); if( containedBy ) { ContainModuleInterface *contain = containedBy->getContain(); if( contain ) { //Are we eligible to detect stealth while in a container? if( contain->isGarrisonable() ) { //We are in a garrisonable structure. if( !data->m_canDetectWhileGarrisoned ) { //But we can't detect stuff while inside. return UPDATE_SLEEP(data->m_updateRate); } } else if( !data->m_canDetectWhileTransported ) { //We are in a normal container and can't detect! return UPDATE_SLEEP(data->m_updateRate); } } } // only consider items that are currently stealthed. PartitionFilterStealthedOrStealthGarrisoned filterStealthOrStealthGarrisoned; //PartitionFilterAcceptByObjectStatus filterStatus(OBJECT_STATUS_STEALTHED, 0); PartitionFilterRelationship filterTeam(self, PartitionFilterRelationship::ALLOW_ENEMIES | PartitionFilterRelationship::ALLOW_NEUTRAL ); PartitionFilterAcceptByKindOf filterKindof(data->m_extraDetectKindof, data->m_extraDetectKindofNot); PartitionFilterSameMapStatus filterMapStatus(getObject()); PartitionFilter* filters[] = { &filterStealthOrStealthGarrisoned, &filterTeam, &filterKindof, &filterMapStatus, NULL }; Real visionRange = self->getVisionRange(); if( data->m_detectionRange > 0.0f ) { visionRange = data->m_detectionRange; } Bool foundSomeone = FALSE; SimpleObjectIterator *iter = ThePartitionManager->iterateObjectsInRange( self, visionRange, FROM_CENTER_2D, filters); MemoryPoolObjectHolder hold(iter); for (Object *them = iter->first(); them; them = iter->next()) { if ( them->isEffectivelyDead() ) continue; StealthUpdate* stealth = them->getStealth(); if ( stealth ) { // we have found someone foundSomeone = TRUE; // // if this object was not previously detected it is now being revealed and we // want to do some UI feedback // if( them->testStatus( OBJECT_STATUS_DETECTED ) == FALSE ) { // for the player revealing the stealth unit do some UI feedback if( ThePlayerList->getLocalPlayer() == self->getControllingPlayer() && self->getRelationship( them ) != ALLIES ) { Bool doFeedback = TRUE; //Kris: Aug 18, 2003 -- This whole system is no longer viable with the inclusion of so many //stealth units. It's possible to get spammed hundreds of times in a row in MD_GLA02 because //we're blindly creating radar events for each detection (unless they are mines). The best thing //to do is tryEvent for everything... doFeedback = TheRadar->tryEvent( RADAR_EVENT_STEALTH_DISCOVERED, them->getPosition() ); //OLD CODE: /* // do a radar event, for mines we only make events if there weren't other // mine events within close proximity and time to other mines // if( them->isKindOf( KINDOF_MINE ) || them->isKindOf( KINDOF_BOOBY_TRAP ) || them->isKindOf( KINDOF_DEMOTRAP ) ) doFeedback = TheRadar->tryEvent( RADAR_EVENT_STEALTH_DISCOVERED, them->getPosition() ); else TheRadar->createEvent( them->getPosition(), RADAR_EVENT_STEALTH_DISCOVERED ); */ // do audio and UI message if we need to do feedback if( doFeedback ) { // audio msg static AudioEventRTS discoveredSound = TheAudio->getMiscAudio()->m_stealthDiscoveredSound; discoveredSound.setPlayerIndex( self->getControllingPlayer()->getPlayerIndex() ); TheAudio->addAudioEvent( &discoveredSound ); // ui msg TheInGameUI->message( TheGameText->fetch( "MESSAGE:StealthDiscovered" ) ); // If revealing this unit is suppose to cause an Eva event, do it EvaMessage message = stealth->getEnemyDetectionEvaEvent(); if ( message != EVA_Invalid && TheEva != NULL ) { TheEva->setShouldPlay( message ); } } // end if } // end if // for the unit being revealed, do some UI feedback if( ThePlayerList->getLocalPlayer() == them->getControllingPlayer() && self->getRelationship( them ) != ALLIES ) { Bool doFeedback = TRUE; // // do a radar event, for mines we only make events if there weren't other // mine events within close proximity and time to other mines // if( them->isKindOf( KINDOF_MINE ) || them->isKindOf( KINDOF_BOOBY_TRAP ) || them->isKindOf( KINDOF_DEMOTRAP ) ) doFeedback = TheRadar->tryEvent( RADAR_EVENT_STEALTH_NEUTRALIZED, them->getPosition() ); else TheRadar->createEvent( them->getPosition(), RADAR_EVENT_STEALTH_NEUTRALIZED ); // do audio and UI message if we need to do feedback if( doFeedback ) { // audio msg static AudioEventRTS neutralizedSound = TheAudio->getMiscAudio()->m_stealthNeutralizedSound; neutralizedSound.setPlayerIndex( them->getControllingPlayer()->getPlayerIndex() ); TheAudio->addAudioEvent( &neutralizedSound ); // ui msg TheInGameUI->message( TheGameText->fetch( "MESSAGE:StealthNeutralized" ) ); // If revealing this unit is suppose to cause an Eva event, do it EvaMessage message = stealth->getOwnDetectionEvaEvent(); if ( message != EVA_Invalid && TheEva != NULL ) { TheEva->setShouldPlay( message ); } } // end if } // end if } // end if, them was not previously detected // updateRate PLUS 1 is necessary to ensure it stays detected 'till we are called again... stealth->markAsDetected(data->m_updateRate + 1); /** @todo srj -- evil hack here... this whole heat-vision thing is fucked. don't want it on mines but no good way to do that. hack for now. */ Drawable *theirDraw = them->getDrawable(); if ( theirDraw && !them->isKindOf(KINDOF_MINE)) { theirDraw->setSecondMaterialPassOpacity( 1.0f ); } if (data->m_IRGridParticleSysTmpl) { const ParticleSystemTemplate *gridTemplate = data->m_IRGridParticleSysTmpl; if (gridTemplate) { ParticleSystem *sys = TheParticleSystemManager->createParticleSystem( gridTemplate );//GRID if (sys) { Coord3D gridPosition = *them->getPosition(); gridPosition.z = self->getPosition()->z + 17; gridPosition.x -= ((Int)gridPosition.x)%12; gridPosition.y -= ((Int)gridPosition.y)%12; sys->setPosition( &gridPosition ); } } } }//end if them has stealthupdate else // perhaps they are garrisoning something stealthy, eh? { ContainModuleInterface *contain = them->getContain(); if( contain && contain->isGarrisonable() && contain->getStealthUnitsContained() ) { Object* rider = NULL; for(ContainedItemsList::const_iterator it = contain->getContainedItemsList()->begin(); it != contain->getContainedItemsList()->end(); ++it) { rider = *it; StealthUpdate *stealth = rider->getStealth(); if ( stealth ) { // we have found someone foundSomeone = TRUE; if( self->getControllingPlayer() != rider->getControllingPlayer() && self->getRelationship( rider ) != ALLIES ) { stealth->markAsDetected(data->m_updateRate + 2); } } } } } } const Player *localPlayer = ThePlayerList->getLocalPlayer(); //Make sure the detector is visible to the local player before we add effects or sounds. if ( self->getShroudedStatus( localPlayer->getPlayerIndex() ) <= OBJECTSHROUD_PARTIAL_CLEAR ) { if ( self->testStatus( OBJECT_STATUS_STEALTHED ) == FALSE || self->getControllingPlayer() == localPlayer ) { Drawable *myDraw = self->getDrawable(); Coord3D bonePosition = {-1.66f,5.5f,15}; if (myDraw) myDraw->getPristineBonePositions( data->m_IRParticleSysBone.str(), 0, &bonePosition, NULL, 1); const ParticleSystemTemplate *pingTemplate; if ( foundSomeone ) pingTemplate = data->m_IRBrightParticleSysTmpl; else pingTemplate = data->m_IRParticleSysTmpl; if (pingTemplate) { ParticleSystem *sys = TheParticleSystemManager->createParticleSystem( pingTemplate ); if (sys) { if (myDraw) sys->attachToDrawable( myDraw ); else sys->attachToObject( self ); sys->setPosition( &bonePosition ); } } const ParticleSystemTemplate *beaconTemplate = data->m_IRBeaconParticleSysTmpl; if (beaconTemplate) { ParticleSystem *sys = TheParticleSystemManager->createParticleSystem( beaconTemplate );//BEACON if (sys) { if (myDraw) sys->attachToDrawable( myDraw ); else sys->attachToObject( self ); sys->setPosition( &bonePosition ); } } AudioEventRTS IRPingSound; if (foundSomeone) IRPingSound = data->m_loudPingSound; else IRPingSound = data->m_pingSound; IRPingSound.setObjectID( self->getID() ); TheAudio->addAudioEvent(&IRPingSound); } } // end if doIRFX return UPDATE_SLEEP(data->m_updateRate); } // ------------------------------------------------------------------------------------------------ /** CRC */ // ------------------------------------------------------------------------------------------------ void StealthDetectorUpdate::crc( Xfer *xfer ) { // extend base class UpdateModule::crc( xfer ); } // end crc // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: * 1: Initial version */ // ------------------------------------------------------------------------------------------------ void StealthDetectorUpdate::xfer( Xfer *xfer ) { // version XferVersion currentVersion = 1; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); // extend base class UpdateModule::xfer( xfer ); // enabled xfer->xferBool( &m_enabled ); } // end xfer // ------------------------------------------------------------------------------------------------ /** Load post process */ // ------------------------------------------------------------------------------------------------ void StealthDetectorUpdate::loadPostProcess( void ) { // extend base class UpdateModule::loadPostProcess(); } // end loadPostProcess