/* ** Command & Conquer Generals(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . */ //////////////////////////////////////////////////////////////////////////////// // // // (c) 2001-2003 Electronic Arts Inc. // // // //////////////////////////////////////////////////////////////////////////////// // GameClient/Eva.cpp ///////////////////////////////////////////////////////////////////////////// #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "GameClient/Eva.h" #include "Common/Player.h" #include "Common/PlayerList.h" #include "GameLogic/GameLogic.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif //------------------------------------------------------------------------------------------------- const char *TheEvaMessageNames[] = { "LOWPOWER", "INSUFFICIENTFUNDS", "SUPERWEAPONDETECTED_PARTICLECANNON", "SUPERWEAPONDETECTED_NUKE", "SUPERWEAPONDETECTED_SCUDSTORM", "SUPERWEAPONLAUNCHED_PARTICLECANNON", "SUPERWEAPONLAUNCHED_NUKE", "SUPERWEAPONLAUNCHED_SCUDSTORM", "BUILDINGLOST", "BASEUNDERATTACK", "ALLYUNDERATTACK", "BEACONDETECTED", "UNITLOST", "GENERALLEVELUP", "VEHICLESTOLEN", "BUILDINGSTOLEN", "CASHSTOLEN", "UPGRADECOMPLETE", "BUILDINGBEINGSTOLEN", "EVA_INVALID", }; //------------------------------------------------------------------------------ INI::parseEvaEvent void INI::parseEvaEvent( INI* ini ) { AsciiString name; // read the name const char* c = ini->getNextToken(); name.set( c ); EvaCheckInfo *check = TheEva->newEvaCheckInfo( name ); if (!check) { // could be null because it already exists. return; } // parse the ini definition ini->initFromINI( check, check->getFieldParse() ); } //----------------------------------------------------------------------------------- EvaSideSounds static void parseSideSoundsList( INI *ini, void *instance, void *store, const void* userData ) { std::vector *sounds = (std::vector*) store; EvaSideSounds newSounds; ini->initFromINI( &newSounds, newSounds.getFieldParse() ); // This could be made more efficient, but to be honest, it shouldn't be that slow. sounds->push_back(newSounds); } //----------------------------------------------------------------------------------- EvaSideSounds const FieldParse EvaSideSounds::s_evaSideSounds[] = { { "Side", INI::parseAsciiString, NULL, offsetof( EvaSideSounds, m_side) }, { "Sounds", INI::parseSoundsList, NULL, offsetof( EvaSideSounds, m_soundNames) }, { 0, 0, 0, 0 }, }; //------------------------------------------------------------------------------------ EvaCheckInfo EvaCheckInfo::EvaCheckInfo() : m_message(EVA_COUNT), m_priority(0), // lowest of all priorities m_framesBetweenChecks(900), // 30 seconds at 30 fps m_framesToExpire(150) // 5 seconds at 30 fps { } //------------------------------------------------------------------------------------------------- const FieldParse EvaCheckInfo::s_evaEventInfo[] = { { "Priority", INI::parseUnsignedInt, NULL, offsetof( EvaCheckInfo, m_priority ) }, { "TimeBetweenChecksMS", INI::parseDurationUnsignedInt, NULL, offsetof( EvaCheckInfo, m_framesBetweenChecks ) }, { "ExpirationTimeMS", INI::parseDurationUnsignedInt, NULL, offsetof( EvaCheckInfo, m_framesToExpire) }, { "SideSounds", parseSideSoundsList, NULL, offsetof( EvaCheckInfo, m_evaSideSounds ) }, { 0, 0, 0, 0 }, }; //------------------------------------------------------------------------------------------------- const ShouldPlayFunc Eva::s_shouldPlayFuncs[] = { Eva::shouldPlayLowPower, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, Eva::shouldPlayGenericHandler, NULL, }; //------------------------------------------------------------------------------------------------- EvaCheck::EvaCheck() : m_evaInfo(NULL), m_triggeredOnFrame(TRIGGEREDON_NOT), m_timeForNextCheck(NEXT_CHECK_NOW), m_alreadyPlayed(FALSE) { } //------------------------------------------------------------------------------------------------- Eva::Eva() : m_localPlayer(NULL), m_previousBuildingCount(0), m_previousUnitCount(0), m_enabled(TRUE) { for (Int i = 0; i < EVA_COUNT; ++i) { m_shouldPlay[i] = FALSE; } } //------------------------------------------------------------------------------------------------- Eva::~Eva() { EvaCheckInfoPtrVecIt it; for (it = m_allCheckInfos.begin(); it != m_allCheckInfos.end(); ++it) { if (*it) (*it)->deleteInstance(); } } //------------------------------------------------------------------------------------------------- void Eva::init() { // parse the INI here, etc. INI ini; ini.load( AsciiString( "Data\\INI\\Eva.ini" ), INI_LOAD_OVERWRITE, NULL); } //------------------------------------------------------------------------------------------------- void Eva::reset() { m_previousUnitCount = 0; m_previousBuildingCount = 0; // remove all pending counters, etc, here. EvaCheckVecIt it; for (it = m_checks.begin(); it != m_checks.end(); /* empty */) { it = m_checks.erase(it); } // remove all things flagged as "need to play" for (Int i = 0; i < EVA_COUNT; ++i) { m_shouldPlay[i] = FALSE; } // If we were previously disabled, re-enable ourselves. m_enabled = TRUE; } //------------------------------------------------------------------------------------------------- void Eva::update() { if (!m_enabled) { return; } m_localPlayer = ThePlayerList->getLocalPlayer(); UnsignedInt frame = TheGameLogic->getFrame(); // Don't update for the first few frames. This way, we don't have to deal with our initial power // being 0, etc. if (frame < 2) { return; } for (Int mesg = (Int)EVA_FIRST; mesg < (Int)EVA_COUNT; ++mesg) { if (isTimeForCheck((EvaMessage)mesg, frame)) { if (messageShouldPlay((EvaMessage)mesg, frame)) { playMessage((EvaMessage)mesg, frame); } } } processPlayingMessages(frame); m_localPlayer = NULL; // Reset all of the flags that have been set to true that haven't actually been probed, because // they will need to trigger again to be valid messages. for (Int i = EVA_FIRST; i < EVA_COUNT; ++i) { m_shouldPlay[i] = FALSE; } } //------------------------------------------------------------------------------------------------- EvaMessage Eva::nameToMessage(const AsciiString& name) { for (Int i = EVA_FIRST; i < EVA_COUNT; ++i) { if (name.compareNoCase(TheEvaMessageNames[i]) == 0) { return (EvaMessage) i; } } DEBUG_CRASH(("Invalid requested Eva message translation :%s: jkmcd", name.str())); return EVA_COUNT; } //------------------------------------------------------------------------------------------------- AsciiString Eva::messageToName(EvaMessage message) { if (message >= EVA_FIRST && message < EVA_COUNT) return TheEvaMessageNames[message]; DEBUG_CRASH(("Invalid requested Eva message translation. jkmcd")); return AsciiString::TheEmptyString; } //------------------------------------------------------------------------------------------------- EvaCheckInfo *Eva::newEvaCheckInfo(AsciiString name) { EvaMessage mesg = nameToMessage(name); // Only return a new one if there isn't an existing one. EvaCheckInfoPtrVecIt it; for (it = m_allCheckInfos.begin(); it != m_allCheckInfos.end(); ++it) { if (*it && (*it)->m_message == mesg) return NULL; } EvaCheckInfo *checkInfo = newInstance(EvaCheckInfo); m_allCheckInfos.push_back(checkInfo); checkInfo->m_message = mesg; return checkInfo; } //------------------------------------------------------------------------------------------------- const EvaCheckInfo *Eva::getEvaCheckInfo(AsciiString name) { EvaMessage mesg = nameToMessage(name); // Only return a new one if there isn't an existing one. EvaCheckInfoPtrVecIt it; for (it = m_allCheckInfos.begin(); it != m_allCheckInfos.end(); ++it) { if (*it && (*it)->m_message == mesg) return *it; } return NULL; } //------------------------------------------------------------------------------------------------- void Eva::setShouldPlay(EvaMessage messageToPlay) { m_shouldPlay[messageToPlay] = TRUE; } //------------------------------------------------------------------------------------------------- void Eva::setEvaEnabled(Bool enabled) { // clear out any waiting messages. for (Int i = EVA_FIRST; i < EVA_COUNT; ++i) { m_shouldPlay[i] = FALSE; } m_enabled = enabled; } //------------------------------------------------------------------------------------------------- Bool Eva::isTimeForCheck(EvaMessage messageToTest, UnsignedInt currentFrame) const { EvaCheckVec::const_iterator it; for (it = m_checks.begin(); it != m_checks.end(); ++it) { if (it->m_evaInfo->m_message == messageToTest) { return FALSE; } } return TRUE; } //------------------------------------------------------------------------------------------------- Bool Eva::messageShouldPlay(EvaMessage messageToTest, UnsignedInt currentFrame) const { if (m_localPlayer == NULL) { return FALSE; } m_messageBeingTested = messageToTest; return s_shouldPlayFuncs[messageToTest](m_localPlayer); } //------------------------------------------------------------------------------------------------- Bool Eva::shouldPlayLowPower( Player *localPlayer ) { // @todo make eva sensitive to whether player can do anything about it... // "Low power, Low power, Low power, yadda yadda yadda..." //const ThingTemplate *chinaReactorTemplate = findTemplate(; //const ThingTemplate *americanReactorTemplate; //if ( chinaReactorTemplate && americanReactorTemplate ) //{ // if ( ! (localPlayer->canBuild(chinaReactorTemplate) || localPlayer->canBuild(americanReactorTemplate)) ) // return FALSE //} return !localPlayer->getEnergy()->hasSufficientPower(); } //------------------------------------------------------------------------------------------------- Bool Eva::shouldPlayGenericHandler( Player * ) { if (TheEva->m_shouldPlay[TheEva->m_messageBeingTested]) { TheEva->m_shouldPlay[TheEva->m_messageBeingTested] = FALSE; return TRUE; } return FALSE; } //------------------------------------------------------------------------------------------------- void Eva::playMessage(EvaMessage messageToTest, UnsignedInt currentFrame) { EvaCheck check; check.m_evaInfo = getEvaCheckInfo(Eva::messageToName(messageToTest)); if (!check.m_evaInfo) { return; } check.m_timeForNextCheck = currentFrame + check.m_evaInfo->m_framesBetweenChecks; check.m_triggeredOnFrame = currentFrame; check.m_alreadyPlayed = FALSE; m_checks.push_back(check); } //------------------------------------------------------------------------------------------------- void Eva::processPlayingMessages(UnsignedInt currentFrame) { // First pass, remove all the objects that can check after this frame. EvaCheckVecIt it; for (it = m_checks.begin(); it != m_checks.end(); /* empty */) { // These are requests that will be available next frame because they've played. if (it->m_timeForNextCheck <= currentFrame + 1 && it->m_alreadyPlayed) { it = m_checks.erase(it); continue; } // These are requests that never got a chance to play and have since expired. if (it->m_triggeredOnFrame + it->m_evaInfo->m_framesToExpire <= currentFrame && !it->m_alreadyPlayed) { it = m_checks.erase(it); continue; } ++it; } // It's possible, although unlikely, that we removed everything in the list. if (m_checks.begin() == m_checks.end()) { return; } // If we're currently playing some audio, we're done. if (m_evaSpeech.isCurrentlyPlaying()) { return; } // Okay. No one is currently playing anything, so lets find an event and trigger it. EvaCheckVecIt storedIt = m_checks.end(); UnsignedInt highestPriority = 0; for (it = m_checks.begin(); it != m_checks.end(); ++it) { if (it->m_evaInfo->m_priority > highestPriority && !it->m_alreadyPlayed) { storedIt = it; highestPriority = it->m_evaInfo->m_priority; } } // There wasn't anything waiting to play. if (storedIt == m_checks.end()) { return; } // We've got a winner! AsciiString side = ThePlayerList->getLocalPlayer()->getSide(); Int numSides = storedIt->m_evaInfo->m_evaSideSounds.size(); for (Int i = 0; i < numSides; ++i) { if (side.compareNoCase(storedIt->m_evaInfo->m_evaSideSounds[i].m_side) == 0) { // Its this one. if (storedIt->m_evaInfo->m_evaSideSounds[i].m_soundNames.size() > 0) { Int soundToPlay = GameClientRandomValue(0, storedIt->m_evaInfo->m_evaSideSounds[i].m_soundNames.size() - 1); m_evaSpeech.setEventName(storedIt->m_evaInfo->m_evaSideSounds[i].m_soundNames[soundToPlay]); } else { // clear it. m_evaSpeech.setEventName(AsciiString::TheEmptyString); } } } // Update the entry storedIt->m_alreadyPlayed = true; storedIt->m_timeForNextCheck = currentFrame + storedIt->m_evaInfo->m_framesBetweenChecks; // Now that we correctly filter messages, we need to set the player index for who should hear the // sound to the local player. m_evaSpeech.setPlayerIndex(m_localPlayer->getPlayerIndex()); m_evaSpeech.setPlayingHandle(TheAudio->addAudioEvent(&m_evaSpeech)); } //------------------------------------------------------------------------------------------------- Eva *TheEva = NULL;