diff --git a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index c002ff8f23..87dc94cb71 100644 --- a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -57,7 +57,6 @@ #include "GameLogic/ObjectCreationList.h" #include "GameLogic/ObjectIter.h" //#include "GameLogic/PartitionManager.h" -#include "GameLogic/AI.h" #include "GameLogic/Module/AIUpdate.h" #include "GameLogic/Module/BodyModule.h" #include "GameLogic/Module/OpenContain.h" @@ -206,6 +205,15 @@ static void doSetRallyPoint( Object *obj, const Coord3D& pos ) } +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +static Player *getMessagePlayer(GameMessage *msg) +{ + return ThePlayerList->getNthPlayer( msg->getPlayerIndex() ); +} + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ static Object * getSingleObjectFromSelection(const AIGroup *currentlySelectedGroup) { if( currentlySelectedGroup && !currentlySelectedGroup->isEmpty() ) @@ -350,7 +358,7 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) DEBUG_ASSERTCRASH(msg != nullptr && msg != (GameMessage*)0xdeadbeef, ("bad msg")); #endif - Player *msgPlayer = ThePlayerList->getNthPlayer( msg->getPlayerIndex() ); + Player *msgPlayer = getMessagePlayer(msg); if (msgPlayer == nullptr) { DEBUG_CRASH(("logicMessageDispatcher: Processing message from unknown player (player index '%d')", msg->getPlayerIndex())); @@ -420,1594 +428,339 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) GameMessage::Type msgType = msg->getType(); switch( msgType ) { - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_NEW_GAME: { -#if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @fix stephanmeesters 11/03/2026 - // Make sure we're ready to start a new game. This prevents an issue where an infinite disconnect screen - // can be force-triggered in an online match by using cheats. - if ( isInGame() || isClearingGameData() || isLoadingMap() ) - { - DEBUG_CRASH( ("Called MSG_NEW_GAME while game is not ready (inGame=%d, clearingData=%d, loadingMap=%d)", - isInGame(), isClearingGameData(), isLoadingMap()) ); - break; - } -#endif - - //DEBUG_ASSERTCRASH(msg->getArgumentCount() == 1 || msg->getArgumentCount() == 2, ("%d arguments to MSG_NEW_GAME", msg->getArgumentCount())); - GameMode gameMode = (GameMode)msg->getArgument( 0 )->integer; - Int rankPoints = 0; - GameDifficulty diff = DIFFICULTY_NORMAL; - if ( msg->getArgumentCount() >= 2 ) - diff = (GameDifficulty)msg->getArgument( 1 )->integer; - if ( msg->getArgumentCount() >= 3 ) - rankPoints = msg->getArgument( 2 )->integer; - - if ( msg->getArgumentCount() >= 4 ) - { - Int maxFPS = msg->getArgument( 3 )->integer; - if (maxFPS < 1 || maxFPS > 1000) - maxFPS = TheGlobalData->m_framesPerSecondLimit; - DEBUG_LOG(("Setting max FPS limit to %d FPS", maxFPS)); - TheFramePacer->setFramesPerSecondLimit(maxFPS); - TheWritableGlobalData->m_useFpsLimit = true; - } - - // prepare for new game - prepareNewGame( gameMode, diff, rankPoints ); - - // start new game - startNewGame( FALSE ); - + onNewGame(msg); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_CLEAR_GAME_DATA: { - -#if defined(RTS_DEBUG) - if (TheDisplay && TheGlobalData->m_dumpAssetUsage) - TheDisplay->dumpAssetUsage(TheGlobalData->m_mapName.str()); -#endif - - if (currentlySelectedGroup) - { -#if RETAIL_COMPATIBLE_AIGROUP - TheAI->destroyGroup(currentlySelectedGroup); -#else - currentlySelectedGroup->removeAll(); -#endif - } - currentlySelectedGroup = nullptr; - clearGameData(); + onClearGameData(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_META_BEGIN_PATH_BUILD: { - DEBUG_LOG(("META: begin path build")); - DEBUG_ASSERTCRASH(!theBuildPlan, ("mismatched theBuildPlan")); - - if (theBuildPlan == false) - { - theBuildPlan = true; - thePlanSubjectCount = 0; - } - + onBeginPathBuild(msg); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_META_END_PATH_BUILD: { - DEBUG_LOG(("META: end path build")); - DEBUG_ASSERTCRASH(theBuildPlan, ("mismatched theBuildPlan")); - - // tell everyone who participated in the plan to move - for( int i=0; igetAIUpdateInterface(); - if (ai) - ai->executeWaypointQueue(); - } - - theBuildPlan = false; - thePlanSubjectCount = 0; - + onEndPathBuild(msg); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_SET_RALLY_POINT: { - Object *obj = findObjectByID( msg->getArgument( 0 )->objectID ); - Coord3D dest = msg->getArgument( 1 )->location; - - if (obj) - { -#if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @fix stephanmeesters 11/03/2026 Validate the owner of the source object - if ( obj->getControllingPlayer() != msgPlayer ) - { - DEBUG_CRASH( ("MSG_SET_RALLY_POINT: Player '%ls' attempted to set the rally point of object '%s' owned by player '%ls'.", - msgPlayer->getPlayerDisplayName().str(), - obj->getTemplate()->getName().str(), - obj->getControllingPlayer()->getPlayerDisplayName().str()) ); - break; - } -#endif - - doSetRallyPoint( obj, dest ); - } - + onSetRallyPoint(msg); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_WEAPON: { - WeaponSlotType weaponSlot = (WeaponSlotType)msg->getArgument( 0 )->integer; - Int maxShotsToFire = msg->getArgument( 1 )->integer; - - // lock it just till the weapon is empty or the attack is "done" - if( currentlySelectedGroup && currentlySelectedGroup->setWeaponLockForGroup( weaponSlot, LOCKED_TEMPORARILY )) - { - currentlySelectedGroup->groupAttackPosition( nullptr, maxShotsToFire, CMD_FROM_PLAYER ); - } - + onDoWeapon(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_COMBATDROP_AT_OBJECT: { - Object *targetObject = findObjectByID( msg->getArgument( 0 )->objectID ); - - // issue command for either single object or for selected group - if( currentlySelectedGroup && targetObject ) - currentlySelectedGroup->groupCombatDrop( targetObject, - *targetObject->getPosition(), - CMD_FROM_PLAYER ); - -/* - if( sourceObject && targetObject ) - { - AIUpdateInterface* sourceAI = sourceObject->getAIUpdateInterface(); - if (sourceAI) - { - sourceAI->aiCombatDrop( targetObject, *targetObject->getPosition(), CMD_FROM_PLAYER ); - } - } -*/ - + onCombatdropAtObject(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_COMBATDROP_AT_LOCATION: { - Coord3D targetLoc = msg->getArgument( 0 )->location; - - if( currentlySelectedGroup ) - currentlySelectedGroup->groupCombatDrop( nullptr, targetLoc, CMD_FROM_PLAYER ); - -/* - if( sourceObject ) - { - AIUpdateInterface* sourceAI = sourceObject->getAIUpdateInterface(); - if (sourceAI) - { - sourceAI->aiCombatDrop( nullptr, targetLoc, CMD_FROM_PLAYER ); - } - } -*/ - + onCombatdropAtLocation(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_WEAPON_AT_OBJECT: { - // Lock the weapon choice to the right weapon, then give an attack command - - WeaponSlotType weaponSlot = (WeaponSlotType)msg->getArgument( 0 )->integer; - Object *targetObject = findObjectByID( msg->getArgument( 1 )->objectID ); - Int maxShotsToFire = msg->getArgument( 2 )->integer; - - // sanity - if( targetObject == nullptr ) - break; - - - // issue command for either single object or for selected group - if( currentlySelectedGroup ) - { - // lock it just till the weapon is empty or the attack is "done" - if (currentlySelectedGroup->setWeaponLockForGroup( weaponSlot, LOCKED_TEMPORARILY )) - currentlySelectedGroup->groupAttackObject( targetObject, maxShotsToFire, CMD_FROM_PLAYER ); - } + onDoWeaponAtObject(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_SWITCH_WEAPONS: { - // use the selected group - WeaponSlotType weaponSlot = (WeaponSlotType)msg->getArgument( 0 )->integer; - // lock until un-switched, or switched to something else. - if( currentlySelectedGroup ) - currentlySelectedGroup->setWeaponLockForGroup( weaponSlot, LOCKED_PERMANENTLY ); + onDoSwitchWeapons(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_SET_MINE_CLEARING_DETAIL: { - if( currentlySelectedGroup ) - { - currentlySelectedGroup->setMineClearingDetail(true); - } + onSetMineClearingDetail(msg, currentlySelectedGroup); break; } - case GameMessage::MSG_ENABLE_RETALIATION_MODE: { -#if RETAIL_COMPATIBLE_CRC - //Logically turns on or off retaliation mode for a specified player. - const Int playerIndex = msg->getArgument( 0 )->integer; - const Bool enableRetaliation = msg->getArgument( 1 )->boolean; - - Player *player = ThePlayerList->getNthPlayer( playerIndex ); - if( player ) - { - DEBUG_ASSERTCRASH(player == msgPlayer, - ("Retaliation mode of player '%ls' was illegally set by player '%ls'. Before: '%d', after: '%d'.", - player->getPlayerDisplayName().str(), msgPlayer->getPlayerDisplayName().str(), - player->isLogicalRetaliationModeEnabled(), enableRetaliation) ); - - player->setLogicalRetaliationModeEnabled( enableRetaliation ); - } -#else - // TheSuperHackers @fix stephanmeesters 08/03/2026 Ensure that players can only set their own retaliation mode. - const Bool enableRetaliation = msg->getArgument( 0 )->boolean; - msgPlayer->setLogicalRetaliationModeEnabled( enableRetaliation ); -#endif + onEnableRetaliationMode(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_WEAPON_AT_LOCATION: { - WeaponSlotType weaponSlot = (WeaponSlotType)msg->getArgument( 0 )->integer; - Coord3D targetLoc = msg->getArgument( 1 )->location; - Int maxShotsToFire = msg->getArgument( 2 )->integer; - - // issue command for either single object or for selected group - if( currentlySelectedGroup ) - { - // lock it just till the weapon is empty or the attack is "done" - if (currentlySelectedGroup->setWeaponLockForGroup( weaponSlot, LOCKED_TEMPORARILY )) - currentlySelectedGroup->groupAttackPosition( &targetLoc, maxShotsToFire, CMD_FROM_PLAYER ); - - - } - + onDoWeaponAtLocation(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_SPECIAL_POWER: { - - // first argument is the special power ID - UnsignedInt specialPowerID = msg->getArgument( 0 )->integer; - - // Command button options -- special power may care about variance options - UnsignedInt options = msg->getArgument( 1 )->integer; - - // check for possible specific source, ignoring selection. - ObjectID sourceID = msg->getArgument(2)->objectID; - Object* source = findObjectByID(sourceID); - if (source != nullptr) - { -#if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @fix stephanmeesters 01/03/2026 Validate the origin of the source object - if ( source->getControllingPlayer() != msgPlayer ) - { - DEBUG_CRASH( ("MSG_DO_SPECIAL_POWER: Player '%ls' attempted to control the object '%s' owned by player '%ls'.", - msgPlayer->getPlayerDisplayName().str(), - source->getTemplate()->getName().str(), - source->getControllingPlayer()->getPlayerDisplayName().str()) ); - break; - } -#endif - - AIGroupPtr theGroup = TheAI->createGroup(); - theGroup->add(source); - theGroup->groupDoSpecialPower( specialPowerID, options ); -#if RETAIL_COMPATIBLE_AIGROUP - TheAI->destroyGroup(theGroup); -#else - theGroup->removeAll(); -#endif - } - else - { - //Use the selected group! - if( currentlySelectedGroup ) - { - currentlySelectedGroup->groupDoSpecialPower( specialPowerID, options ); - } - } + onDoSpecialPower(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_SPECIAL_POWER_AT_LOCATION: { - const Bool hasAngle = msg->getArgumentCount() >= 6; - Int argumentIndex = 0; - - // first argument is the special power ID - UnsignedInt specialPowerID = msg->getArgument( argumentIndex++ )->integer; - - // Location argument 2 is destination - Coord3D targetCoord = msg->getArgument( argumentIndex++ )->location; - - // Angle argument 3 is the orientation of the special power (if applicable) - Real angle = hasAngle ? msg->getArgument( argumentIndex++ )->real : INVALID_ANGLE; - - // Object in way -- if applicable (some specials care, others don't) - ObjectID objectID = msg->getArgument( argumentIndex++ )->objectID; - Object *objectInWay = findObjectByID( objectID ); - - // Command button options -- special power may care about variance options - UnsignedInt options = msg->getArgument( argumentIndex++ )->integer; - - // check for possible specific source, ignoring selection. - ObjectID sourceID = msg->getArgument( argumentIndex++ )->objectID; - Object* source = findObjectByID( sourceID ); - if (source != nullptr) - { -#if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @fix stephanmeesters 01/03/2026 Validate the origin of the source object - if ( source->getControllingPlayer() != msgPlayer ) - { - DEBUG_CRASH( ("MSG_DO_SPECIAL_POWER_AT_LOCATION: Player '%ls' attempted to control the object '%s' owned by player '%ls'.", - msgPlayer->getPlayerDisplayName().str(), - source->getTemplate()->getName().str(), - source->getControllingPlayer()->getPlayerDisplayName().str()) ); - break; - } -#endif - - AIGroupPtr theGroup = TheAI->createGroup(); - theGroup->add(source); - theGroup->groupDoSpecialPowerAtLocation( specialPowerID, &targetCoord, angle, objectInWay, options ); -#if RETAIL_COMPATIBLE_AIGROUP - TheAI->destroyGroup(theGroup); -#else - theGroup->removeAll(); -#endif - } - else - { - //Use the selected group! - if( currentlySelectedGroup ) - { - currentlySelectedGroup->groupDoSpecialPowerAtLocation( specialPowerID, &targetCoord, angle, objectInWay, options ); - } - } + onDoSpecialPowerAtLocation(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_SPECIAL_POWER_AT_OBJECT: { - // first argument is the special power ID - UnsignedInt specialPowerID = msg->getArgument( 0 )->integer; - - // argument 2 is target object - ObjectID targetID = msg->getArgument(1)->objectID; - Object *target = findObjectByID( targetID ); - if( !target ) - { - break; - } - - // Command button options -- special power may care about variance options - UnsignedInt options = msg->getArgument( 2 )->integer; - - // check for possible specific source, ignoring selection. - ObjectID sourceID = msg->getArgument(3)->objectID; - Object* source = findObjectByID(sourceID); - if (source != nullptr) - { -#if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @fix stephanmeesters 01/03/2026 Validate the origin of the source object - if ( source->getControllingPlayer() != msgPlayer ) - { - DEBUG_CRASH( ("MSG_DO_SPECIAL_POWER_AT_OBJECT: Player '%ls' attempted to control the object '%s' owned by player '%ls'.", - msgPlayer->getPlayerDisplayName().str(), - source->getTemplate()->getName().str(), - source->getControllingPlayer()->getPlayerDisplayName().str()) ); - break; - } -#endif - - AIGroupPtr theGroup = TheAI->createGroup(); - theGroup->add(source); - theGroup->groupDoSpecialPowerAtObject( specialPowerID, target, options ); -#if RETAIL_COMPATIBLE_AIGROUP - TheAI->destroyGroup(theGroup); -#else - theGroup->removeAll(); -#endif - } - else - { - if( currentlySelectedGroup ) - { - currentlySelectedGroup->groupDoSpecialPowerAtObject( specialPowerID, target, options ); - } - } + onDoSpecialPowerAtObject(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_ATTACKMOVETO: { - Coord3D dest = msg->getArgument( 0 )->location; - - if (currentlySelectedGroup) - { - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - currentlySelectedGroup->groupAttackMoveToPosition( &dest, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); - } - + onDoAttackmoveto(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_FORCEMOVETO: { - Coord3D dest = msg->getArgument( 0 )->location; - - if (currentlySelectedGroup) - { - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - currentlySelectedGroup->groupMoveToPosition( &dest, false, CMD_FROM_PLAYER ); - } - + onDoForcemoveto(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- // MSG_DO_SALVAGE is intentionally set up to mimic the moveto. case GameMessage::MSG_DO_SALVAGE: case GameMessage::MSG_DO_MOVETO: { - Coord3D dest = msg->getArgument( 0 )->location; - - if( currentlySelectedGroup ) - { - //DEBUG_LOG(("GameLogicDispatch - got a MSG_DO_MOVETO command")); - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - currentlySelectedGroup->groupMoveToPosition( &dest, false, CMD_FROM_PLAYER ); - } - + onDoMoveto(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_ADD_WAYPOINT: { - Coord3D dest = msg->getArgument( 0 )->location; - - if( currentlySelectedGroup ) - { - //DEBUG_LOG(("GameLogicDispatch - got a MSG_DO_MOVETO command")); - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - currentlySelectedGroup->groupMoveToPosition( &dest, true, CMD_FROM_PLAYER ); - } - + onAddWaypoint(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_GUARD_POSITION: { - Coord3D loc = msg->getArgument( 0 )->location; - GuardMode gm = (GuardMode)msg->getArgument( 1 )->integer; - if (currentlySelectedGroup) - { - currentlySelectedGroup->groupGuardPosition(&loc, gm, CMD_FROM_PLAYER); - } - + onDoGuardPosition(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_GUARD_OBJECT: { - Object* obj = findObjectByID( msg->getArgument( 0 )->objectID ); - if (!obj) - break; - - GuardMode gm = (GuardMode)msg->getArgument( 1 )->integer; - if (currentlySelectedGroup) - { - currentlySelectedGroup->groupGuardObject(obj, gm, CMD_FROM_PLAYER); - } - + onDoGuardObject(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_STOP: { - if (currentlySelectedGroup) - { - currentlySelectedGroup->groupIdle(CMD_FROM_PLAYER); - } - + onDoStop(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_SCATTER: { - if (currentlySelectedGroup) - { - currentlySelectedGroup->groupScatter(CMD_FROM_PLAYER); - } - + onDoScatter(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_CREATE_FORMATION: { - if (currentlySelectedGroup) - { - currentlySelectedGroup->groupCreateFormation(CMD_FROM_PLAYER); - } - + onCreateFormation(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_CLEAR_INGAME_POPUP_MESSAGE: { - - if( TheInGameUI ) - { - TheInGameUI->clearPopupMessageData(); - } + onClearIngamePopupMessage(msg); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_CHEER: { - //All selected units play cheer animation. - if( currentlySelectedGroup ) - { - currentlySelectedGroup->groupCheer( CMD_FROM_PLAYER ); - } + onDoCheer(msg, currentlySelectedGroup); break; } #if defined(RTS_DEBUG) || defined (_ALLOW_DEBUG_CHEATS_IN_RELEASE) - //--------------------------------------------------------------------------------------------- + case GameMessage::MSG_DEBUG_KILL_SELECTION: { - //All selected units die - if( currentlySelectedGroup ) - { - const VecObjectID& selectedObjects = currentlySelectedGroup->getAllIDs(); - for (VecObjectID::const_iterator it = selectedObjects.begin(); it != selectedObjects.end(); ++it) - { - Object *obj = findObjectByID(*it); - if (obj) - { - obj->kill(); - } - } - } + onDebugKillSelection(msg, currentlySelectedGroup); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DEBUG_HURT_OBJECT: { - Object* objToHurt = TheGameLogic->findObjectByID( msg->getArgument( 0 )->objectID ); - if (objToHurt) - { - DamageInfo damageInfo; - damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE; - damageInfo.in.m_deathType = DEATH_NORMAL; - damageInfo.in.m_sourceID = INVALID_ID; - damageInfo.in.m_amount = objToHurt->getBodyModule()->getMaxHealth() / 10.0f; - objToHurt->attemptDamage( &damageInfo ); - } + onDebugHurtObject(msg); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DEBUG_KILL_OBJECT: { - Object* objToHurt = TheGameLogic->findObjectByID( msg->getArgument( 0 )->objectID ); - if (objToHurt) - { - objToHurt->kill(); - } + onDebugKillObject(msg); break; } #endif -#ifdef ALLOW_SURRENDER - //--------------------------------------------------------------------------------------------- - case GameMessage::MSG_DO_SURRENDER: + case GameMessage::MSG_ENTER: { - //All selected units surrender - if( currentlySelectedGroup ) - { - Object* objWeSurrenderedTo = TheGameLogic->findObjectByID( msg->getArgument( 0 )->objectID ); - Bool surrender = msg->getArgument( 1 )->boolean; - currentlySelectedGroup->groupSurrender( objWeSurrenderedTo, surrender, CMD_FROM_PLAYER ); - } + onEnter(msg, currentlySelectedGroup); break; } -#endif - - //--------------------------------------------------------------------------------------------- - case GameMessage::MSG_ENTER: + case GameMessage::MSG_EXIT: { - Object *enter = findObjectByID( msg->getArgument( 1 )->objectID ); - - // sanity - if( enter == nullptr ) - break; - - if( currentlySelectedGroup ) - { - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - currentlySelectedGroup->groupEnter( enter, CMD_FROM_PLAYER ); - } - + onExit(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- - case GameMessage::MSG_EXIT: - { - Object *objectWantingToExit = findObjectByID( msg->getArgument( 0 )->objectID ); -#if RETAIL_COMPATIBLE_AIGROUP - Object *objectContainingExiter = getSingleObjectFromSelection(currentlySelectedGroup); -#else - Object *objectContainingExiter = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); -#endif - - // sanity - if( objectWantingToExit == nullptr ) - break; - - if( objectContainingExiter == nullptr ) - break; - - // sanity, the player must actually control this object - if( objectWantingToExit->getControllingPlayer() != msgPlayer ) - break; - - objectWantingToExit->releaseWeaponLock(LOCKED_TEMPORARILY); // release any temporary locks. - - // exit whatever objectWantingToExit is INSIDE of - AIUpdateInterface *ai = objectWantingToExit->getAIUpdateInterface(); - if( ai ) - ai->aiExit( objectContainingExiter, CMD_FROM_PLAYER ); - // Just like Enter, Exit needs to know the thing to exit. This can no longer be assumed because of the Tunnel system. - // If you do not specify the thing to Exit, it will Exit the thing it thinks it is in. For a tunnel network, - // that will be the specific Tunnel it entered. (Scripts can talk directly to the guy to say Get Out Regardless) - - break; - - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_EVACUATE: { - // issue command for either single object or for selected group -// AIGroup *group = TheAI->findGroup( *selectedGroupID ); - if( currentlySelectedGroup ) - { - //Coord3D pos; - //Bool hasArgs = FALSE; - //hasArgs = (msg->getArgumentCount() > 0); - - //if (hasArgs) - // pos = msg->getArgument(0)->location; - - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - - // evacuate message is for the selected group - //if (hasArgs) - // currentlySelectedGroup->groupMoveToAndEvacuate( &pos, CMD_FROM_PLAYER ); - //else - currentlySelectedGroup->groupEvacuate( CMD_FROM_PLAYER ); - -// no, this is bad, don't do here, do when POSTING message -// pickAndPlayUnitVoiceResponse( TheInGameUI->getAllSelectedDrawables(), GameMessage::MSG_EVACUATE ); - - } - + onEvacuate(msg, currentlySelectedGroup); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_EXECUTE_RAILED_TRANSPORT: { - - // issue command to currently selected objects - if( currentlySelectedGroup ) - currentlySelectedGroup->groupExecuteRailedTransport( CMD_FROM_PLAYER ); - + onExecuteRailedTransport(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_INTERNET_HACK: { -// ObjectID sourceID = msg->getArgument( 0 )->objectID; - if( currentlySelectedGroup ) - { - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - currentlySelectedGroup->groupHackInternet( CMD_FROM_PLAYER ); - } + onInternetHack(msg, currentlySelectedGroup); break; } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_GET_REPAIRED: { - Object *repairDepot = findObjectByID( msg->getArgument( 0 )->objectID ); - - // sanity - if( repairDepot == nullptr ) - break; - - // tell the currently selected group to go get repaired - if( currentlySelectedGroup ) - currentlySelectedGroup->groupGetRepaired( repairDepot, CMD_FROM_PLAYER ); - + onGetRepaired(msg, currentlySelectedGroup); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_DOCK: { - Object *dockBuilding = findObjectByID( msg->getArgument( 0 )->objectID ); - - // sanity - if( dockBuilding == nullptr ) - break; - - // tell the currently selected group to go get repaired - if( currentlySelectedGroup ) - currentlySelectedGroup->groupDock( dockBuilding, CMD_FROM_PLAYER ); - + onDock(msg, currentlySelectedGroup); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_GET_HEALED: { - Object *healDest = findObjectByID( msg->getArgument( 0 )->objectID ); - - // sanity - if( healDest == nullptr ) - break; - - // tell the currently selected group to enter the building for healing - if( currentlySelectedGroup ) - currentlySelectedGroup->groupGetHealed( healDest, CMD_FROM_PLAYER ); - + onGetHealed(msg, currentlySelectedGroup); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_REPAIR: { - Object *repairTarget = findObjectByID( msg->getArgument( 0 )->objectID ); - - // sanity - if( repairTarget == nullptr ) - break; - - // - // tell the currently selected group of objects to go repair the target object, note - // that only one of them will actually go ahead and do the repair - // - if( currentlySelectedGroup ) - currentlySelectedGroup->groupRepair( repairTarget, CMD_FROM_PLAYER ); - + onDoRepair(msg, currentlySelectedGroup); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_RESUME_CONSTRUCTION: { - Object *constructTarget = findObjectByID( msg->getArgument( 0 )->objectID ); - - // sanity - if( constructTarget == nullptr ) - break; - - // - // tell the currently selected group of objects to resume construction on - // the target object, note that only one of them will go off and resume construction - // on the target - // - if( currentlySelectedGroup ) - currentlySelectedGroup->groupResumeConstruction( constructTarget, CMD_FROM_PLAYER ); - -// no, this is bad, don't do here, do when POSTING message -// pickAndPlayUnitVoiceResponse( TheInGameUI->getAllSelectedDrawables(), msg->getType() ); - + onResumeConstruction(msg, currentlySelectedGroup); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_SPECIAL_POWER_OVERRIDE_DESTINATION: { - const Coord3D *loc = &msg->getArgument( 0 )->location; - SpecialPowerType spType = (SpecialPowerType)msg->getArgument( 1 )->integer; - - ObjectID sourceID = msg->getArgument(2)->objectID; - Object* source = findObjectByID(sourceID); - if (source != nullptr) - { -#if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @fix stephanmeesters 01/03/2026 Validate the origin of the source object - if ( source->getControllingPlayer() != msgPlayer ) - { - DEBUG_CRASH( ("MSG_DO_SPECIAL_POWER_OVERRIDE_DESTINATION: Player '%ls' attempted to control the object '%s' owned by player '%ls'.", - msgPlayer->getPlayerDisplayName().str(), - source->getTemplate()->getName().str(), - source->getControllingPlayer()->getPlayerDisplayName().str()) ); - break; - } -#endif - - AIGroupPtr theGroup = TheAI->createGroup(); - theGroup->add(source); - theGroup->groupOverrideSpecialPowerDestination( spType, loc, CMD_FROM_PLAYER ); -#if RETAIL_COMPATIBLE_AIGROUP - TheAI->destroyGroup(theGroup); -#else - theGroup->removeAll(); -#endif - } - else - { - if( currentlySelectedGroup ) - { - currentlySelectedGroup->groupOverrideSpecialPowerDestination( spType, loc, CMD_FROM_PLAYER ); - } - } - + onDoSpecialPowerOverrideDestination(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_ATTACK_OBJECT: { - Object *enemy = findObjectByID( msg->getArgument( 0 )->objectID ); - - // Check enemy, as it is possible that he died this frame. - if (enemy) - { - if (currentlySelectedGroup) - { - - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - currentlySelectedGroup->groupAttackObject( enemy, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); - - } - - } - + onDoAttackObject(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_FORCE_ATTACK_OBJECT: { - Object *enemy = findObjectByID( msg->getArgument( 0 )->objectID ); - - // Check enemy, as it is possible that he died this frame. - if (enemy) - { - if (currentlySelectedGroup) - { - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. - currentlySelectedGroup->groupForceAttackObject( enemy, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); - } - - } - + onDoForceAttackObject(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_FORCE_ATTACK_GROUND: { - const Coord3D *pos = &msg->getArgument( 0 )->location; - - if (currentlySelectedGroup) - { - - ///////////////////////////////////////////////////////////////////// - //Lorenzen sez: unclear, yet how to solve this for all cases - //Kris: This code was added to allow the toxin tractor to force attack - // while contaminating. When this change was made, it was causing - // rangers and scud launchers to reset to primary weapon mode whenever - // force attacking while not idle. I fixed this by enforcing the - // temporary and permanent modes that are already set when attempting - // the new lock. In this case, the temp lock attempt will fail whenever - // a permanent lock is in effect, thus fixing the ranger and scud and - // allowing the tox tractor to work as well. - Bool forceAttackRequiresPrimaryWeapon = !currentlySelectedGroup->isIdle(); - if ( forceAttackRequiresPrimaryWeapon ) - { - currentlySelectedGroup->setWeaponLockForGroup( PRIMARY_WEAPON, LOCKED_TEMPORARILY ); - currentlySelectedGroup->groupAttackPosition( pos, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); - } - else - /////////////////////////////////////////////////////////////////// - { - currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); - currentlySelectedGroup->groupAttackPosition( pos, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); - } - - - } - + onDoForceAttackGround(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_QUEUE_UPGRADE: { - const UpgradeTemplate *upgradeT = TheUpgradeCenter->findUpgradeByKey( (NameKeyType)(msg->getArgument( 1 )->integer) ); - if (!upgradeT) // sanity - break; - - if (currentlySelectedGroup) - currentlySelectedGroup->queueUpgrade( upgradeT ); - + onQueueUpgrade(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_CANCEL_UPGRADE: { -#if RETAIL_COMPATIBLE_AIGROUP - Object *producer = getSingleObjectFromSelection(currentlySelectedGroup); -#else - Object *producer = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); -#endif - const UpgradeTemplate *upgradeT = TheUpgradeCenter->findUpgradeByKey( (NameKeyType)(msg->getArgument( 0 )->integer) ); - - // sanity - if( producer == nullptr || upgradeT == nullptr ) - break; - - // the player must actually control the producer object - if( producer->getControllingPlayer() != msgPlayer ) - break; - - // producer must have a production update - ProductionUpdateInterface *pu = producer->getProductionUpdateInterface(); - if( pu == nullptr ) - break; - - // cancel the upgrade - pu->cancelUpgrade( upgradeT ); - + onCancelUpgrade(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_QUEUE_UNIT_CREATE: { -#if RETAIL_COMPATIBLE_AIGROUP - Object *producer = getSingleObjectFromSelection(currentlySelectedGroup); -#else - Object *producer = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); -#endif - const ThingTemplate *whatToCreate; - ProductionID productionID; - - // get data from the message - whatToCreate = TheThingFactory->findByTemplateID( msg->getArgument( 0 )->integer ); - productionID = (ProductionID)msg->getArgument( 1 )->integer; - - // sanity - if ( producer == nullptr || whatToCreate == nullptr ) - break; - - // get the production interface for the producer - ProductionUpdateInterface *pu = producer->getProductionUpdateInterface(); - if( pu == nullptr ) - { - - DEBUG_CRASH( ("MSG_QUEUE_UNIT_CREATE: Producer '%s' doesn't have a unit production interface", - producer->getTemplate()->getName().str()) ); - break; - - } - - // queue the build - pu->queueCreateUnit( whatToCreate, productionID ); - + onQueueUnitCreate(msg, currentlySelectedGroup); break; - } - - //------------------------------------------------------------------------------------------------- case GameMessage::MSG_CANCEL_UNIT_CREATE: { -#if RETAIL_COMPATIBLE_AIGROUP - Object *producer = getSingleObjectFromSelection(currentlySelectedGroup); -#else - Object *producer = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); -#endif - ProductionID productionID = (ProductionID)msg->getArgument( 0 )->integer; - - // sanity - if( producer == nullptr ) - break; - - // sanity, the player must control the producer - if( producer->getControllingPlayer() != msgPlayer ) - break; - - // get the unit production interface - ProductionUpdateInterface *pu = producer->getProductionUpdateInterface(); - if( pu == nullptr ) - break; - - // cancel the production - pu->cancelUnitCreate( productionID ); - + onCancelUnitCreate(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DOZER_CONSTRUCT: case GameMessage::MSG_DOZER_CONSTRUCT_LINE: { - const ThingTemplate *place; - Coord3D loc; - Real angle; - - // get player, what to place, and location -#if RETAIL_COMPATIBLE_AIGROUP - Object *constructorObject = getSingleObjectFromSelection(currentlySelectedGroup); -#else - Object *constructorObject = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); -#endif - place = TheThingFactory->findByTemplateID( msg->getArgument( 0 )->integer ); - loc = msg->getArgument( 1 )->location; - angle = msg->getArgument( 2 )->real; - - if( place == nullptr || constructorObject == nullptr ) - break; //These are not crashes, as the object may have died before this message came in - - if( msg->getType() == GameMessage::MSG_DOZER_CONSTRUCT ) - { - - TheBuildAssistant->buildObjectNow( constructorObject, place, &loc, angle, - constructorObject->getControllingPlayer() ); - - } - else - { - Coord3D locEnd; - - // get the end of the line location in the world - locEnd = msg->getArgument( 3 )->location; - - // place the line of structures, the end location being present will make it happen - TheBuildAssistant->buildObjectLineNow( constructorObject, place, &loc, &locEnd, angle, - constructorObject->getControllingPlayer() ); - - } - - // place the sound for putting a building down - - static AudioEventRTS placeBuilding("PlaceBuilding"); - placeBuilding.setObjectID(constructorObject->getID()); - TheAudio->addAudioEvent( &placeBuilding ); - - -// no, this is bad, don't do here, do when POSTING message -// pickAndPlayUnitVoiceResponse( TheInGameUI->getAllSelectedDrawables(), msg->getType() ); - + onDozerConstruct(msg, currentlySelectedGroup); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DOZER_CANCEL_CONSTRUCT: { - - // get the building to cancel construction on -#if RETAIL_COMPATIBLE_AIGROUP - Object *building = getSingleObjectFromSelection(currentlySelectedGroup); -#else - Object *building = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); -#endif - if( building == nullptr ) - break; - - // the player sending this message must actually control this building - if( building->getControllingPlayer() != msgPlayer ) - break; - - // Check to make sure it is actually under construction - if( !building->testStatus(OBJECT_STATUS_UNDER_CONSTRUCTION) ) - break; - - // OK, refund the money to the player, unless it is a rebuilding Hole. - if( !building->testStatus(OBJECT_STATUS_RECONSTRUCTING)) - { - Money *money = msgPlayer->getMoney(); - UnsignedInt amount = building->getTemplate()->calcCostToBuild( msgPlayer ); - money->deposit( amount, TRUE, FALSE ); - } - - // - // Destroy the building ... killing the - // building will automatically cause the dozer also cancel its own building - // behavior and it will go on its merry way doing other assigned tasks - // - building->kill(); - + onDozerCancelConstruct(msg, currentlySelectedGroup); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_SELL: { - - // use the selected group - if( currentlySelectedGroup ) - currentlySelectedGroup->groupSell( CMD_FROM_PLAYER ); - + onSell(msg, currentlySelectedGroup); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_TOGGLE_OVERCHARGE: { - - // use the selected group - if( currentlySelectedGroup ) - currentlySelectedGroup->groupToggleOvercharge( CMD_FROM_PLAYER ); - + onToggleOvercharge(msg, currentlySelectedGroup); break; - } #ifdef ALLOW_SURRENDER - // -------------------------------------------------------------------------------------------- + case GameMessage::MSG_DO_SURRENDER: + { + onDoSurrender(msg, currentlySelectedGroup); + break; + } case GameMessage::MSG_PICK_UP_PRISONER: { - Object *prisoner = TheGameLogic->findObjectByID( msg->getArgument( 0 )->objectID ); - - if( prisoner ) - { - - // use selected group - if( currentlySelectedGroup ) - currentlySelectedGroup->groupPickUpPrisoner( prisoner, CMD_FROM_PLAYER ); - - } - + onPickUpPrisoner(msg, currentlySelectedGroup); break; - } -#endif - -#ifdef ALLOW_SURRENDER - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_RETURN_TO_PRISON: { - - // use selected group - if( currentlySelectedGroup ) - currentlySelectedGroup->groupReturnToPrison( nullptr, CMD_FROM_PLAYER ); - + onReturnToPrison(msg, currentlySelectedGroup); break; - } #endif - //--------------------------------------------------------------------------------------------- // No sound does exactly the same logical processing as the usual message. Just double them up. case GameMessage::MSG_CREATE_SELECTED_GROUP_NO_SOUND: case GameMessage::MSG_CREATE_SELECTED_GROUP: { - Bool createNewGroup = msg->getArgument( 0 )->boolean; - Bool firstObject = TRUE; - - for (Int i = 1; i < msg->getArgumentCount(); ++i) { - Object *obj = findObjectByID( msg->getArgument( i )->objectID ); - if (!obj) { - continue; - } - - selectObject(obj, createNewGroup && firstObject, msgPlayer->getPlayerMask()); - firstObject = FALSE; - } - + onCreateSelectedGroup(msg); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_REMOVE_FROM_SELECTED_GROUP: { - for (Int i = 0; i < msg->getArgumentCount(); ++i) { - ObjectID objID = msg->getArgument(i)->objectID; - Object *objToRemove = findObjectByID(objID); - if (!objToRemove) { - continue; - } - - deselectObject(objToRemove, msgPlayer->getPlayerMask()); - } - + onRemoveFromSelectedGroup(msg); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DESTROY_SELECTED_GROUP: { - msgPlayer->setCurrentlySelectedAIGroup(nullptr); - + onDestroySelectedGroup(msg); break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_SELECTED_GROUP_COMMAND: { - break; - } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_PLACE_BEACON: { - if (msgPlayer->getPlayerTemplate() == nullptr) - break; - Coord3D pos = msg->getArgument( 0 )->location; - Region3D r; - TheTerrainLogic->getExtent(&r); - if (!r.isInRegionNoZ(&pos)) - pos = TheTerrainLogic->findClosestEdgePoint(&pos); - const ThingTemplate *thing = TheThingFactory->findTemplate( msgPlayer->getPlayerTemplate()->getBeaconTemplate() ); - if (thing && !TheVictoryConditions->hasSinglePlayerBeenDefeated(msgPlayer)) - { - // how many does this player have active? - Int count; - msgPlayer->countObjectsByThingTemplate( 1, &thing, false, &count ); - DEBUG_LOG(("Player already has %d beacons active", count)); - if (count >= TheMultiplayerSettings->getMaxBeaconsPerPlayer()) - { - if (msgPlayer == ThePlayerList->getLocalPlayer()) - { - // tell the user - TheInGameUI->message( TheGameText->fetch("GUI:TooManyBeacons") ); - - // play a sound - static AudioEventRTS aSound("BeaconPlacementFailed"); - aSound.setPosition(&pos); - aSound.setPlayerIndex(msgPlayer->getPlayerIndex()); - TheAudio->addAudioEvent(&aSound); - } - - break; - } - Object *object = TheThingFactory->newObject( thing, msgPlayer->getDefaultTeam() ); - object->setPosition( &pos ); - object->setProducer(nullptr); - - if (msgPlayer->getRelationship( ThePlayerList->getLocalPlayer()->getDefaultTeam() ) == ALLIES || ThePlayerList->getLocalPlayer()->isPlayerObserver()) - { - // tell the user - UnicodeString s; - s.format(TheGameText->fetch("GUI:BeaconPlaced"), msgPlayer->getPlayerDisplayName().str()); - TheInGameUI->message( s ); - - // play a sound - static AudioEventRTS aSound("BeaconPlaced"); - aSound.setPlayerIndex(msgPlayer->getPlayerIndex()); - aSound.setPosition(&pos); - TheAudio->addAudioEvent(&aSound); - - // beacons are a rare event; play a nifty radar event thingy - TheRadar->createEvent( object->getPosition(), RADAR_EVENT_INFORMATION ); - - if (ThePlayerList->getLocalPlayer()->getRelationship(msgPlayer->getDefaultTeam()) == ALLIES) - TheEva->setShouldPlay(EVA_BeaconDetected); - - TheControlBar->markUIDirty(); // check if we should grey out the button - } - else - { - - Int updateCount = 0; - static NameKeyType nameKeyClientUpdate = NAMEKEY("BeaconClientUpdate"); - ClientUpdateModule ** clientModules = object->getDrawable()->getClientUpdateModules(); - if (clientModules) - { - while (*clientModules) - { - if ((*clientModules)->getModuleNameKey() == nameKeyClientUpdate) - { - (*(BeaconClientUpdate **)clientModules)->hideBeacon(); - ++updateCount; - } - - ++clientModules; - } - } - DEBUG_ASSERTCRASH(updateCount == 1, ("Saw %d update modules for the beacon!", updateCount)); - - } - } - else - { - // tell the user - TheInGameUI->message( TheGameText->fetch("GUI:BeaconPlacementFailed") ); - - // play a sound - static AudioEventRTS aSound("BeaconPlacementFailed"); - aSound.setPosition(&pos); - aSound.setPlayerIndex(msgPlayer->getPlayerIndex()); - TheAudio->addAudioEvent(&aSound); - } + onPlaceBeacon(msg); break; } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_REMOVE_BEACON: { - AIGroupPtr allSelectedObjects = TheAI->createGroup(); -#if RETAIL_COMPATIBLE_AIGROUP - msgPlayer->getCurrentSelectionAsAIGroup(allSelectedObjects); // need to act on all objects, so we can hide teammates' beacons. -#else - msgPlayer->getCurrentSelectionAsAIGroup(allSelectedObjects.Peek()); // need to act on all objects, so we can hide teammates' beacons. -#endif - if( allSelectedObjects ) - { - const VecObjectID& selectedObjects = allSelectedObjects->getAllIDs(); - for (VecObjectID::const_iterator it = selectedObjects.begin(); it != selectedObjects.end(); ++it) - { - Object *beacon = findObjectByID(*it); - if (beacon) - { - // TheSuperHackers @bugfix Prevent runtime crashing when a beacon is no longer associated with an initialized player. - const PlayerTemplate *playerTemplate = beacon->getControllingPlayer()->getPlayerTemplate(); - if (!playerTemplate) - continue; - - const ThingTemplate *thing = TheThingFactory->findTemplate( playerTemplate->getBeaconTemplate() ); - if (thing && thing->isEquivalentTo(beacon->getTemplate())) - { - if (beacon->getControllingPlayer() == msgPlayer) - { - destroyObject(beacon); // the owner is telling it to go away. such is life. - - TheControlBar->markUIDirty(); // check if we should un-grey out the button - } - else if (msgPlayer == ThePlayerList->getLocalPlayer()) - { - Drawable *beaconDrawable = beacon->getDrawable(); - if (beaconDrawable) - { - - static NameKeyType nameKeyClientUpdate = NAMEKEY("BeaconClientUpdate"); - ClientUpdateModule ** clientModules = beaconDrawable->getClientUpdateModules(); - if (clientModules) - { - while (*clientModules) - { - if ((*clientModules)->getModuleNameKey() == nameKeyClientUpdate) - (*(BeaconClientUpdate **)clientModules)->hideBeacon(); - - ++clientModules; - } - } - } - } - } - } - } -#if RETAIL_COMPATIBLE_AIGROUP - if (allSelectedObjects->isEmpty()) - { - TheAI->destroyGroup(allSelectedObjects); - allSelectedObjects = nullptr; - } -#endif - } + onRemoveBeacon(msg); break; } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_SET_BEACON_TEXT: { - if( currentlySelectedGroup ) - { - const VecObjectID& selectedObjects = currentlySelectedGroup->getAllIDs(); - for (VecObjectID::const_iterator it = selectedObjects.begin(); it != selectedObjects.end(); ++it) - { - Object *beacon = findObjectByID(*it); - if (beacon) - { - Drawable *beaconDrawable = beacon->getDrawable(); - if (beaconDrawable) - { - UnicodeString s; - for( int i=0; igetArgumentCount(); i++ ) - { - s.concat( msg->getArgument(i)->wChar ); - } - - if (s.isEmpty()) - beaconDrawable->clearCaptionText(); - else - beaconDrawable->setCaptionText(s); - } - } - } - } + onSetBeaconText(msg, currentlySelectedGroup); break; } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_SELF_DESTRUCT: { - if (msg->getArgument(0)->boolean) - { - // transfer control to any living ally - Int i=0; - for (; igetPlayerCount(); ++i) - { - if (i != msgPlayer->getPlayerIndex()) - { - Player *otherPlayer = ThePlayerList->getNthPlayer(i); - if (msgPlayer->getRelationship(otherPlayer->getDefaultTeam()) == ALLIES && - otherPlayer->getRelationship(msgPlayer->getDefaultTeam()) == ALLIES) - { - if (TheVictoryConditions->hasSinglePlayerBeenDefeated(otherPlayer)) - continue; - - // a living ally! hooray! - otherPlayer->transferAssetsFromThat(msgPlayer); - msgPlayer->killPlayer(); // just to be safe (and to kill beacons etc that don't transfer) - break; - } - } - } - if (i == ThePlayerList->getPlayerCount()) - { - // didn't find any allies. die, loner! - msgPlayer->killPlayer(); - } - } - else - { - msgPlayer->killPlayer(); - } - // There is no reason to do any notification here, it now takes place in the victory conditions. - // bonehead. + onSelfDestruct(msg); break; } - - // -------------------------------------------------------------------------------------------- case GameMessage::MSG_SET_REPLAY_CAMERA: { - if (TheRecorder->isPlaybackMode() && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == msgPlayer) - { - if (TheTacticalView->isCameraMovementFinished()) - { - const Coord3D pos = msg->getArgument( 0 )->location; - const Real angle = msg->getArgument( 1 )->real; - const Real pitch = msg->getArgument( 2 )->real; - const Real zoom = msg->getArgument( 3 )->real; - const Mouse::MouseCursor mouseCursor = static_cast(msg->getArgument( 4 )->integer); - const ICoord2D mousePos = msg->getArgument( 5 )->pixel; - - // TheSuperHackers @info Definitely call in user mode to ensure the camera operates with auto-zoom - // over terrain elevations, because the Replay Camera does not store the absolute camera location, - // but key parameters relative to the terrain height at the camera pivot. - TheTacticalView->userSetPosition(pos); - TheTacticalView->userSetAngle(angle); - TheTacticalView->userSetPitch(pitch); - TheTacticalView->userSetZoom(zoom); - - // TheSuperHackers @fix Make sure there is no scrolling ever. - const Coord2D scroll = {0, 0}; - TheTacticalView->userScrollBy(&scroll); - - if (msg->getArgumentCount() >= 8) - { - // TheSuperHackers @feature Override all the settings above with real camera position and view direction. - // This ensures that the camera looks EXACTLY like it was at the time of recording, no matter how the - // View is configured or tweaked. Note that the above settings are still required to set regardless, because - // when the replay camera is exited, then the pivot position and angles will be needed to build the camera - // where it was left off. - const Coord3D camPos = msg->getArgument( 6 )->location; - const Coord3D camDir = msg->getArgument( 7 )->location; - - TheTacticalView->setUserControlled(false); - TheTacticalView->set3DCameraLookAt(camPos, camDir, 0.0f); - } - - // TheSuperHackers @fix Lock the new location to avoid user input from changing the camera in this frame. - TheTacticalView->lockUserControlUntilFrame( getFrame() + 1 ); - - if (!TheLookAtTranslator->hasMouseMovedRecently()) - { - TheMouse->setCursor( mouseCursor ); - TheMouse->setPosition( mousePos.x, mousePos.y ); - TheLookAtTranslator->setCurrentPos( mousePos ); - } - } - } + onSetReplayCamera(msg); break; } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_CREATE_TEAM0: case GameMessage::MSG_CREATE_TEAM1: case GameMessage::MSG_CREATE_TEAM2: @@ -2019,10 +772,9 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) case GameMessage::MSG_CREATE_TEAM8: case GameMessage::MSG_CREATE_TEAM9: { - msgPlayer->processCreateTeamGameMessage(msg->getType() - GameMessage::MSG_CREATE_TEAM0, msg); + onCreateTeam(msg); break; } - case GameMessage::MSG_SELECT_TEAM0: case GameMessage::MSG_SELECT_TEAM1: case GameMessage::MSG_SELECT_TEAM2: @@ -2034,10 +786,9 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) case GameMessage::MSG_SELECT_TEAM8: case GameMessage::MSG_SELECT_TEAM9: { - msgPlayer->processSelectTeamGameMessage(msg->getType() - GameMessage::MSG_SELECT_TEAM0); + onSelectTeam(msg); break; } - case GameMessage::MSG_ADD_TEAM0: case GameMessage::MSG_ADD_TEAM1: case GameMessage::MSG_ADD_TEAM2: @@ -2049,74 +800,19 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) case GameMessage::MSG_ADD_TEAM8: case GameMessage::MSG_ADD_TEAM9: { - msgPlayer->processAddTeamGameMessage(msg->getType() - GameMessage::MSG_ADD_TEAM0); + onAddTeam(msg); break; } - - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_LOGIC_CRC: { - if (TheNetwork) - { - Int slotIndex = -1; - for (Int i=0; igetPlayerType() == PLAYER_HUMAN && TheNetwork->getPlayerName(i) == msgPlayer->getPlayerDisplayName()) - { - slotIndex = i; - break; - } - } - - if (slotIndex < 0 || !TheNetwork->isPlayerConnected(slotIndex)) - break; - - if (msgPlayer->isLocalPlayer()) - { -#if defined(RTS_DEBUG) - // don't even put this in release, cause someone might hack it. - if (!TheDebugIgnoreSyncErrors) - { -#endif - m_shouldValidateCRCs = TRUE; -#if defined(RTS_DEBUG) - } -#endif - } - - UnsignedInt newCRC = msg->getArgument(0)->integer; - //DEBUG_LOG(("Received CRC of %8.8X from %ls on frame %d", newCRC, - //msgPlayer->getPlayerDisplayName().str(), m_frame)); - m_cachedCRCs[msgPlayer->getPlayerIndex()] = newCRC; - } - else if (TheRecorder && TheRecorder->isPlaybackMode()) - { - UnsignedInt newCRC = msg->getArgument(0)->integer; - //DEBUG_LOG(("Saw CRC of %X from player %d. Our CRC is %X. Arg count is %d", - //newCRC, msgPlayer->getPlayerIndex(), getCRC(), msg->getArgumentCount())); - - TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), (msg->getArgument(1)->boolean)); - } + onLogicCrc(msg); break; - } - - //--------------------------------------------------------------------------------------------- case GameMessage::MSG_PURCHASE_SCIENCE: { - ScienceType science = (ScienceType)msg->getArgument( 0 )->integer; - - // sanity - if( science == SCIENCE_INVALID ) - break; - - msgPlayer->attemptToPurchaseScience(science); - + onPurchaseScience(msg); break; - } - } #if RETAIL_COMPATIBLE_AIGROUP @@ -2155,3 +851,1589 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) } } + +bool GameLogic::onNewGame(GameMessage *msg) +{ +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 11/03/2026 + // Make sure we're ready to start a new game. This prevents an issue where an infinite disconnect screen + // can be force-triggered in an online match by using cheats. + if ( isInGame() || isClearingGameData() || isLoadingMap() ) + { + DEBUG_CRASH( ("Called MSG_NEW_GAME while game is not ready (inGame=%d, clearingData=%d, loadingMap=%d)", + isInGame(), isClearingGameData(), isLoadingMap()) ); + + return false; + } +#endif + + //DEBUG_ASSERTCRASH(msg->getArgumentCount() == 1 || msg->getArgumentCount() == 2, ("%d arguments to MSG_NEW_GAME", msg->getArgumentCount())); + GameMode gameMode = (GameMode)msg->getArgument( 0 )->integer; + Int rankPoints = 0; + GameDifficulty diff = DIFFICULTY_NORMAL; + if ( msg->getArgumentCount() >= 2 ) + diff = (GameDifficulty)msg->getArgument( 1 )->integer; + if ( msg->getArgumentCount() >= 3 ) + rankPoints = msg->getArgument( 2 )->integer; + + if ( msg->getArgumentCount() >= 4 ) + { + Int maxFPS = msg->getArgument( 3 )->integer; + if (maxFPS < 1 || maxFPS > 1000) + maxFPS = TheGlobalData->m_framesPerSecondLimit; + DEBUG_LOG(("Setting max FPS limit to %d FPS", maxFPS)); + TheFramePacer->setFramesPerSecondLimit(maxFPS); + TheWritableGlobalData->m_useFpsLimit = true; + } + + // prepare for new game + prepareNewGame( gameMode, diff, rankPoints ); + + // start new game + startNewGame( FALSE ); + + return true; +} + +bool GameLogic::onClearGameData(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ +#if defined(RTS_DEBUG) + if (TheDisplay && TheGlobalData->m_dumpAssetUsage) + TheDisplay->dumpAssetUsage(TheGlobalData->m_mapName.str()); +#endif + + if (currentlySelectedGroup) + { +#if RETAIL_COMPATIBLE_AIGROUP + TheAI->destroyGroup(currentlySelectedGroup); +#else + currentlySelectedGroup->removeAll(); +#endif + } + currentlySelectedGroup = nullptr; + clearGameData(); + + return true; +} + +bool GameLogic::onBeginPathBuild(GameMessage *msg) +{ + DEBUG_LOG(("META: begin path build")); + DEBUG_ASSERTCRASH(!theBuildPlan, ("mismatched theBuildPlan")); + + if (theBuildPlan == false) + { + theBuildPlan = true; + thePlanSubjectCount = 0; + } + + return true; +} + +bool GameLogic::onEndPathBuild(GameMessage *msg) +{ + DEBUG_LOG(("META: end path build")); + DEBUG_ASSERTCRASH(theBuildPlan, ("mismatched theBuildPlan")); + + // tell everyone who participated in the plan to move + for( int i=0; igetAIUpdateInterface(); + if (ai) + ai->executeWaypointQueue(); + } + + theBuildPlan = false; + thePlanSubjectCount = 0; + + return true; +} + +bool GameLogic::onSetRallyPoint(GameMessage *msg) +{ + Object *obj = findObjectByID( msg->getArgument( 0 )->objectID ); + Coord3D dest = msg->getArgument( 1 )->location; + + if (!obj) + return false; + +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 11/03/2026 Validate the owner of the source object + Player *msgPlayer = getMessagePlayer(msg); + if ( obj->getControllingPlayer() != msgPlayer ) + { + DEBUG_CRASH( ("MSG_SET_RALLY_POINT: Player '%ls' attempted to set the rally point of object '%s' owned by player '%ls'.", + msgPlayer->getPlayerDisplayName().str(), + obj->getTemplate()->getName().str(), + obj->getControllingPlayer()->getPlayerDisplayName().str()) ); + return false; + } +#endif + + doSetRallyPoint( obj, dest ); + + return true; +} + +bool GameLogic::onDoWeapon(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + WeaponSlotType weaponSlot = (WeaponSlotType)msg->getArgument( 0 )->integer; + Int maxShotsToFire = msg->getArgument( 1 )->integer; + + // lock it just till the weapon is empty or the attack is "done" + if( currentlySelectedGroup && currentlySelectedGroup->setWeaponLockForGroup( weaponSlot, LOCKED_TEMPORARILY )) + { + currentlySelectedGroup->groupAttackPosition( nullptr, maxShotsToFire, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onCombatdropAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *targetObject = findObjectByID( msg->getArgument( 0 )->objectID ); + + // issue command for either single object or for selected group + if( currentlySelectedGroup && targetObject ) + currentlySelectedGroup->groupCombatDrop( targetObject, + *targetObject->getPosition(), + CMD_FROM_PLAYER ); + + /* + if( sourceObject && targetObject ) + { + AIUpdateInterface* sourceAI = sourceObject->getAIUpdateInterface(); + if (sourceAI) + { + sourceAI->aiCombatDrop( targetObject, *targetObject->getPosition(), CMD_FROM_PLAYER ); + } + } + */ + + return true; +} + +bool GameLogic::onCombatdropAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Coord3D targetLoc = msg->getArgument( 0 )->location; + + if( currentlySelectedGroup ) + currentlySelectedGroup->groupCombatDrop( nullptr, targetLoc, CMD_FROM_PLAYER ); + + /* + if( sourceObject ) + { + AIUpdateInterface* sourceAI = sourceObject->getAIUpdateInterface(); + if (sourceAI) + { + sourceAI->aiCombatDrop( nullptr, targetLoc, CMD_FROM_PLAYER ); + } + } + */ + + return true; +} + +bool GameLogic::onDoWeaponAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + // Lock the weapon choice to the right weapon, then give an attack command + + WeaponSlotType weaponSlot = (WeaponSlotType)msg->getArgument( 0 )->integer; + Object *targetObject = findObjectByID( msg->getArgument( 1 )->objectID ); + Int maxShotsToFire = msg->getArgument( 2 )->integer; + + // sanity + if( targetObject == nullptr ) + return false; + + // issue command for either single object or for selected group + if( currentlySelectedGroup ) + { + // lock it just till the weapon is empty or the attack is "done" + if (currentlySelectedGroup->setWeaponLockForGroup( weaponSlot, LOCKED_TEMPORARILY )) + currentlySelectedGroup->groupAttackObject( targetObject, maxShotsToFire, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onDoSwitchWeapons(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + // use the selected group + WeaponSlotType weaponSlot = (WeaponSlotType)msg->getArgument( 0 )->integer; + // lock until un-switched, or switched to something else. + if( currentlySelectedGroup ) + currentlySelectedGroup->setWeaponLockForGroup( weaponSlot, LOCKED_PERMANENTLY ); + + return true; +} + +bool GameLogic::onSetMineClearingDetail(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + if( currentlySelectedGroup ) + { + currentlySelectedGroup->setMineClearingDetail(true); + } + + return true; +} + +bool GameLogic::onEnableRetaliationMode(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Player *msgPlayer = getMessagePlayer(msg); + +#if RETAIL_COMPATIBLE_CRC + (void)msgPlayer; + //Logically turns on or off retaliation mode for a specified player. + const Int playerIndex = msg->getArgument( 0 )->integer; + const Bool enableRetaliation = msg->getArgument( 1 )->boolean; + + Player *player = ThePlayerList->getNthPlayer( playerIndex ); + if( player ) + { + DEBUG_ASSERTCRASH(player == msgPlayer, + ("Retaliation mode of player '%ls' was illegally set by player '%ls'. Before: '%d', after: '%d'.", + player->getPlayerDisplayName().str(), msgPlayer->getPlayerDisplayName().str(), + player->isLogicalRetaliationModeEnabled(), enableRetaliation) ); + + player->setLogicalRetaliationModeEnabled( enableRetaliation ); + } +#else + // TheSuperHackers @fix stephanmeesters 08/03/2026 Ensure that players can only set their own retaliation mode. + const Bool enableRetaliation = msg->getArgument( 0 )->boolean; + msgPlayer->setLogicalRetaliationModeEnabled( enableRetaliation ); +#endif + + return true; +} + +bool GameLogic::onDoWeaponAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + WeaponSlotType weaponSlot = (WeaponSlotType)msg->getArgument( 0 )->integer; + Coord3D targetLoc = msg->getArgument( 1 )->location; + Int maxShotsToFire = msg->getArgument( 2 )->integer; + + // issue command for either single object or for selected group + if( currentlySelectedGroup ) + { + // lock it just till the weapon is empty or the attack is "done" + if (currentlySelectedGroup->setWeaponLockForGroup( weaponSlot, LOCKED_TEMPORARILY )) + currentlySelectedGroup->groupAttackPosition( &targetLoc, maxShotsToFire, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onDoSpecialPower(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + // first argument is the special power ID + UnsignedInt specialPowerID = msg->getArgument( 0 )->integer; + + // Command button options -- special power may care about variance options + UnsignedInt options = msg->getArgument( 1 )->integer; + + // check for possible specific source, ignoring selection. + ObjectID sourceID = msg->getArgument(2)->objectID; + Object* source = findObjectByID(sourceID); + if (source != nullptr) + { +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 01/03/2026 Validate the origin of the source object + Player *msgPlayer = getMessagePlayer(msg); + if ( source->getControllingPlayer() != msgPlayer ) + { + DEBUG_CRASH( ("MSG_DO_SPECIAL_POWER: Player '%ls' attempted to control the object '%s' owned by player '%ls'.", + msgPlayer->getPlayerDisplayName().str(), + source->getTemplate()->getName().str(), + source->getControllingPlayer()->getPlayerDisplayName().str()) ); + return false; + } +#endif + + AIGroupPtr theGroup = TheAI->createGroup(); + theGroup->add(source); + theGroup->groupDoSpecialPower( specialPowerID, options ); +#if RETAIL_COMPATIBLE_AIGROUP + TheAI->destroyGroup(theGroup); +#else + theGroup->removeAll(); +#endif + } + else + { + //Use the selected group! + if( currentlySelectedGroup ) + { + currentlySelectedGroup->groupDoSpecialPower( specialPowerID, options ); + } + } + + return true; +} + +bool GameLogic::onDoSpecialPowerAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + const Bool hasAngle = msg->getArgumentCount() >= 6; + Int argumentIndex = 0; + + // first argument is the special power ID + UnsignedInt specialPowerID = msg->getArgument( argumentIndex++ )->integer; + + // Location argument 2 is destination + Coord3D targetCoord = msg->getArgument( argumentIndex++ )->location; + + // Angle argument 3 is the orientation of the special power (if applicable) + Real angle = hasAngle ? msg->getArgument( argumentIndex++ )->real : INVALID_ANGLE; + + // Object in way -- if applicable (some specials care, others don't) + ObjectID objectID = msg->getArgument( argumentIndex++ )->objectID; + Object *objectInWay = findObjectByID( objectID ); + + // Command button options -- special power may care about variance options + UnsignedInt options = msg->getArgument( argumentIndex++ )->integer; + + // check for possible specific source, ignoring selection. + ObjectID sourceID = msg->getArgument( argumentIndex++ )->objectID; + Object* source = findObjectByID( sourceID ); + if (source != nullptr) + { +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 01/03/2026 Validate the origin of the source object + Player *msgPlayer = getMessagePlayer(msg); + if ( source->getControllingPlayer() != msgPlayer ) + { + DEBUG_CRASH( ("MSG_DO_SPECIAL_POWER_AT_LOCATION: Player '%ls' attempted to control the object '%s' owned by player '%ls'.", + msgPlayer->getPlayerDisplayName().str(), + source->getTemplate()->getName().str(), + source->getControllingPlayer()->getPlayerDisplayName().str()) ); + return false; + } +#endif + + AIGroupPtr theGroup = TheAI->createGroup(); + theGroup->add(source); + theGroup->groupDoSpecialPowerAtLocation( specialPowerID, &targetCoord, angle, objectInWay, options ); +#if RETAIL_COMPATIBLE_AIGROUP + TheAI->destroyGroup(theGroup); +#else + theGroup->removeAll(); +#endif + } + else + { + //Use the selected group! + if( currentlySelectedGroup ) + { + currentlySelectedGroup->groupDoSpecialPowerAtLocation( specialPowerID, &targetCoord, angle, objectInWay, options ); + } + } + + return true; +} + +bool GameLogic::onDoSpecialPowerAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + + // first argument is the special power ID + UnsignedInt specialPowerID = msg->getArgument( 0 )->integer; + + // argument 2 is target object + ObjectID targetID = msg->getArgument(1)->objectID; + Object *target = findObjectByID( targetID ); + if( !target ) + { + return false; + } + + // Command button options -- special power may care about variance options + UnsignedInt options = msg->getArgument( 2 )->integer; + + // check for possible specific source, ignoring selection. + ObjectID sourceID = msg->getArgument(3)->objectID; + Object* source = findObjectByID(sourceID); + if (source != nullptr) + { +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 01/03/2026 Validate the origin of the source object + Player *msgPlayer = getMessagePlayer(msg); + if ( source->getControllingPlayer() != msgPlayer ) + { + DEBUG_CRASH( ("MSG_DO_SPECIAL_POWER_AT_OBJECT: Player '%ls' attempted to control the object '%s' owned by player '%ls'.", + msgPlayer->getPlayerDisplayName().str(), + source->getTemplate()->getName().str(), + source->getControllingPlayer()->getPlayerDisplayName().str()) ); + return false; + } +#endif + + AIGroupPtr theGroup = TheAI->createGroup(); + theGroup->add(source); + theGroup->groupDoSpecialPowerAtObject( specialPowerID, target, options ); +#if RETAIL_COMPATIBLE_AIGROUP + TheAI->destroyGroup(theGroup); +#else + theGroup->removeAll(); +#endif + } + else + { + if( currentlySelectedGroup ) + { + currentlySelectedGroup->groupDoSpecialPowerAtObject( specialPowerID, target, options ); + } + } + return true; +} + +bool GameLogic::onDoAttackmoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Coord3D dest = msg->getArgument( 0 )->location; + + if (currentlySelectedGroup) + { + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + currentlySelectedGroup->groupAttackMoveToPosition( &dest, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onDoForcemoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Coord3D dest = msg->getArgument( 0 )->location; + + if (currentlySelectedGroup) + { + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + currentlySelectedGroup->groupMoveToPosition( &dest, false, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onDoMoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Coord3D dest = msg->getArgument( 0 )->location; + + if( currentlySelectedGroup ) + { + //DEBUG_LOG(("GameLogicDispatch - got a MSG_DO_MOVETO command")); + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + currentlySelectedGroup->groupMoveToPosition( &dest, false, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onAddWaypoint(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Coord3D dest = msg->getArgument( 0 )->location; + + if( currentlySelectedGroup ) + { + //DEBUG_LOG(("GameLogicDispatch - got a MSG_DO_MOVETO command")); + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + currentlySelectedGroup->groupMoveToPosition( &dest, true, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onDoGuardPosition(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Coord3D loc = msg->getArgument( 0 )->location; + GuardMode gm = (GuardMode)msg->getArgument( 1 )->integer; + if (currentlySelectedGroup) + { + currentlySelectedGroup->groupGuardPosition(&loc, gm, CMD_FROM_PLAYER); + } + + return true; +} + +bool GameLogic::onDoGuardObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object* obj = findObjectByID( msg->getArgument( 0 )->objectID ); + if (!obj) + return false; + + GuardMode gm = (GuardMode)msg->getArgument( 1 )->integer; + if (currentlySelectedGroup) + { + currentlySelectedGroup->groupGuardObject(obj, gm, CMD_FROM_PLAYER); + } + + return true; +} + +bool GameLogic::onDoStop(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + if (currentlySelectedGroup) + { + currentlySelectedGroup->groupIdle(CMD_FROM_PLAYER); + } + + return true; +} + +bool GameLogic::onDoScatter(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + if (currentlySelectedGroup) + { + currentlySelectedGroup->groupScatter(CMD_FROM_PLAYER); + } + + return true; +} + +bool GameLogic::onCreateFormation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + if (currentlySelectedGroup) + { + currentlySelectedGroup->groupCreateFormation(CMD_FROM_PLAYER); + } + + return true; +} + +bool GameLogic::onClearIngamePopupMessage(GameMessage *msg) +{ + if( TheInGameUI ) + { + TheInGameUI->clearPopupMessageData(); + } + + return true; +} + +bool GameLogic::onDoCheer(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + //All selected units play cheer animation. + if( currentlySelectedGroup ) + { + currentlySelectedGroup->groupCheer( CMD_FROM_PLAYER ); + } + + return true; +} + +#if defined(RTS_DEBUG) || defined (_ALLOW_DEBUG_CHEATS_IN_RELEASE) + +bool GameLogic::onDebugKillSelection(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + //All selected units die + if( currentlySelectedGroup ) + { + const VecObjectID& selectedObjects = currentlySelectedGroup->getAllIDs(); + for (VecObjectID::const_iterator it = selectedObjects.begin(); it != selectedObjects.end(); ++it) + { + Object *obj = findObjectByID(*it); + if (obj) + { + obj->kill(); + } + } + } + + return true; +} + +bool GameLogic::onDebugHurtObject(GameMessage *msg) +{ + Object* objToHurt = TheGameLogic->findObjectByID( msg->getArgument( 0 )->objectID ); + if (objToHurt) + { + DamageInfo damageInfo; + damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE; + damageInfo.in.m_deathType = DEATH_NORMAL; + damageInfo.in.m_sourceID = INVALID_ID; + damageInfo.in.m_amount = objToHurt->getBodyModule()->getMaxHealth() / 10.0f; + objToHurt->attemptDamage( &damageInfo ); + } + + return true; +} + +bool GameLogic::onDebugKillObject(GameMessage *msg) +{ + Object* objToHurt = TheGameLogic->findObjectByID( msg->getArgument( 0 )->objectID ); + if (objToHurt) + { + objToHurt->kill(); + } + + return true; +} + +#endif + +bool GameLogic::onEnter(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *enter = findObjectByID( msg->getArgument( 1 )->objectID ); + + // sanity + if( enter == nullptr ) + return false; + + if( currentlySelectedGroup ) + { + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + currentlySelectedGroup->groupEnter( enter, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onExit(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Player *msgPlayer = getMessagePlayer(msg); + Object *objectWantingToExit = findObjectByID( msg->getArgument( 0 )->objectID ); +#if RETAIL_COMPATIBLE_AIGROUP + Object *objectContainingExiter = getSingleObjectFromSelection(currentlySelectedGroup); +#else + Object *objectContainingExiter = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); +#endif + + // sanity + if( objectWantingToExit == nullptr ) + return false; + + if( objectContainingExiter == nullptr ) + return false; + + // sanity, the player must actually control this object + if( objectWantingToExit->getControllingPlayer() != msgPlayer ) + return false; + + objectWantingToExit->releaseWeaponLock(LOCKED_TEMPORARILY); // release any temporary locks. + + // exit whatever objectWantingToExit is INSIDE of + AIUpdateInterface *ai = objectWantingToExit->getAIUpdateInterface(); + if( ai ) + ai->aiExit( objectContainingExiter, CMD_FROM_PLAYER ); + // Just like Enter, Exit needs to know the thing to exit. This can no longer be assumed because of the Tunnel system. + // If you do not specify the thing to Exit, it will Exit the thing it thinks it is in. For a tunnel network, + // that will be the specific Tunnel it entered. (Scripts can talk directly to the guy to say Get Out Regardless) + + return true; +} + +bool GameLogic::onEvacuate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + // issue command for either single object or for selected group + // AIGroup *group = TheAI->findGroup( *selectedGroupID ); + if( currentlySelectedGroup ) + { + //Coord3D pos; + //Bool hasArgs = FALSE; + //hasArgs = (msg->getArgumentCount() > 0); + + //if (hasArgs) + // pos = msg->getArgument(0)->location; + + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + + // evacuate message is for the selected group + //if (hasArgs) + // currentlySelectedGroup->groupMoveToAndEvacuate( &pos, CMD_FROM_PLAYER ); + //else + currentlySelectedGroup->groupEvacuate( CMD_FROM_PLAYER ); + + // no, this is bad, don't do here, do when POSTING message + // pickAndPlayUnitVoiceResponse( TheInGameUI->getAllSelectedDrawables(), GameMessage::MSG_EVACUATE ); + } + + return true; +} + +bool GameLogic::onExecuteRailedTransport(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + // issue command to currently selected objects + if( currentlySelectedGroup ) + currentlySelectedGroup->groupExecuteRailedTransport( CMD_FROM_PLAYER ); + + return true; +} + +bool GameLogic::onInternetHack(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ +// ObjectID sourceID = msg->getArgument( 0 )->objectID; + if( currentlySelectedGroup ) + { + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + currentlySelectedGroup->groupHackInternet( CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onGetRepaired(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *repairDepot = findObjectByID( msg->getArgument( 0 )->objectID ); + + // sanity + if( repairDepot == nullptr ) + return false; + + // tell the currently selected group to go get repaired + if( currentlySelectedGroup ) + currentlySelectedGroup->groupGetRepaired( repairDepot, CMD_FROM_PLAYER ); + + return true; +} + +bool GameLogic::onDock(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *dockBuilding = findObjectByID( msg->getArgument( 0 )->objectID ); + + // sanity + if( dockBuilding == nullptr ) + return false; + + // tell the currently selected group to go get repaired + if( currentlySelectedGroup ) + currentlySelectedGroup->groupDock( dockBuilding, CMD_FROM_PLAYER ); + + return true; +} + +bool GameLogic::onGetHealed(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *healDest = findObjectByID( msg->getArgument( 0 )->objectID ); + + // sanity + if( healDest == nullptr ) + return false; + + // tell the currently selected group to enter the building for healing + if( currentlySelectedGroup ) + currentlySelectedGroup->groupGetHealed( healDest, CMD_FROM_PLAYER ); + + return true; +} + +bool GameLogic::onDoRepair(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *repairTarget = findObjectByID( msg->getArgument( 0 )->objectID ); + + // sanity + if( repairTarget == nullptr ) + return false; + + // + // tell the currently selected group of objects to go repair the target object, note + // that only one of them will actually go ahead and do the repair + // + if( currentlySelectedGroup ) + currentlySelectedGroup->groupRepair( repairTarget, CMD_FROM_PLAYER ); + + return true; +} + +bool GameLogic::onResumeConstruction(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *constructTarget = findObjectByID( msg->getArgument( 0 )->objectID ); + + // sanity + if( constructTarget == nullptr ) + return false; + + // + // tell the currently selected group of objects to resume construction on + // the target object, note that only one of them will go off and resume construction + // on the target + // + if( currentlySelectedGroup ) + currentlySelectedGroup->groupResumeConstruction( constructTarget, CMD_FROM_PLAYER ); + + // no, this is bad, don't do here, do when POSTING message + // pickAndPlayUnitVoiceResponse( TheInGameUI->getAllSelectedDrawables(), msg->getType() ); + + return true; +} + +bool GameLogic::onDoSpecialPowerOverrideDestination(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + + const Coord3D *loc = &msg->getArgument( 0 )->location; + SpecialPowerType spType = (SpecialPowerType)msg->getArgument( 1 )->integer; + + ObjectID sourceID = msg->getArgument(2)->objectID; + Object* source = findObjectByID(sourceID); + + if (source != nullptr) + { +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @fix stephanmeesters 01/03/2026 Validate the origin of the source object + Player *msgPlayer = getMessagePlayer(msg); + if ( source->getControllingPlayer() != msgPlayer ) + { + DEBUG_CRASH( ("MSG_DO_SPECIAL_POWER_OVERRIDE_DESTINATION: Player '%ls' attempted to control the object '%s' owned by player '%ls'.", + msgPlayer->getPlayerDisplayName().str(), + source->getTemplate()->getName().str(), + source->getControllingPlayer()->getPlayerDisplayName().str()) ); + return false; + } +#endif + + AIGroupPtr theGroup = TheAI->createGroup(); + theGroup->add(source); + theGroup->groupOverrideSpecialPowerDestination( spType, loc, CMD_FROM_PLAYER ); +#if RETAIL_COMPATIBLE_AIGROUP + TheAI->destroyGroup(theGroup); +#else + theGroup->removeAll(); +#endif + } + else + { + if( currentlySelectedGroup ) + { + currentlySelectedGroup->groupOverrideSpecialPowerDestination( spType, loc, CMD_FROM_PLAYER ); + } + } + + return true; +} + +bool GameLogic::onDoAttackObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *enemy = findObjectByID( msg->getArgument( 0 )->objectID ); + + // Check enemy, as it is possible that he died this frame. + if (enemy) + { + if (currentlySelectedGroup) + { + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + currentlySelectedGroup->groupAttackObject( enemy, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); + } + } + + return true; +} + +bool GameLogic::onDoForceAttackObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *enemy = findObjectByID( msg->getArgument( 0 )->objectID ); + + // Check enemy, as it is possible that he died this frame. + if (enemy) + { + if (currentlySelectedGroup) + { + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); // release any temporary locks. + currentlySelectedGroup->groupForceAttackObject( enemy, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); + } + } + + return true; +} + +bool GameLogic::onDoForceAttackGround(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + const Coord3D *pos = &msg->getArgument( 0 )->location; + + if (currentlySelectedGroup) + { + ///////////////////////////////////////////////////////////////////// + //Lorenzen sez: unclear, yet how to solve this for all cases + //Kris: This code was added to allow the toxin tractor to force attack + // while contaminating. When this change was made, it was causing + // rangers and scud launchers to reset to primary weapon mode whenever + // force attacking while not idle. I fixed this by enforcing the + // temporary and permanent modes that are already set when attempting + // the new lock. In this case, the temp lock attempt will fail whenever + // a permanent lock is in effect, thus fixing the ranger and scud and + // allowing the tox tractor to work as well. + Bool forceAttackRequiresPrimaryWeapon = !currentlySelectedGroup->isIdle(); + if ( forceAttackRequiresPrimaryWeapon ) + { + currentlySelectedGroup->setWeaponLockForGroup( PRIMARY_WEAPON, LOCKED_TEMPORARILY ); + currentlySelectedGroup->groupAttackPosition( pos, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); + } + else + /////////////////////////////////////////////////////////////////// + { + currentlySelectedGroup->releaseWeaponLockForGroup(LOCKED_TEMPORARILY); + currentlySelectedGroup->groupAttackPosition( pos, NO_MAX_SHOTS_LIMIT, CMD_FROM_PLAYER ); + } + } + + return true; +} + +bool GameLogic::onQueueUpgrade(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + const UpgradeTemplate *upgradeT = TheUpgradeCenter->findUpgradeByKey( (NameKeyType)(msg->getArgument( 1 )->integer) ); + if (!upgradeT) // sanity + return false; + + if (currentlySelectedGroup) + currentlySelectedGroup->queueUpgrade( upgradeT ); + + return true; +} + +bool GameLogic::onCancelUpgrade(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Player *msgPlayer = getMessagePlayer(msg); + +#if RETAIL_COMPATIBLE_AIGROUP + Object *producer = getSingleObjectFromSelection(currentlySelectedGroup); +#else + Object *producer = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); +#endif + const UpgradeTemplate *upgradeT = TheUpgradeCenter->findUpgradeByKey( (NameKeyType)(msg->getArgument( 0 )->integer) ); + + // sanity + if( producer == nullptr || upgradeT == nullptr ) + return false; + + // the player must actually control the producer object + if( producer->getControllingPlayer() != msgPlayer ) + return false; + + // producer must have a production update + ProductionUpdateInterface *pu = producer->getProductionUpdateInterface(); + if( pu == nullptr ) + return false; + + // cancel the upgrade + pu->cancelUpgrade( upgradeT ); + + return true; +} + +bool GameLogic::onQueueUnitCreate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ +#if RETAIL_COMPATIBLE_AIGROUP + Object *producer = getSingleObjectFromSelection(currentlySelectedGroup); +#else + Object *producer = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); +#endif + const ThingTemplate *whatToCreate; + ProductionID productionID; + + // get data from the message + whatToCreate = TheThingFactory->findByTemplateID( msg->getArgument( 0 )->integer ); + productionID = (ProductionID)msg->getArgument( 1 )->integer; + + // sanity + if ( producer == nullptr || whatToCreate == nullptr ) + return false; + + // get the production interface for the producer + ProductionUpdateInterface *pu = producer->getProductionUpdateInterface(); + if( pu == nullptr ) + { + DEBUG_CRASH( ("MSG_QUEUE_UNIT_CREATE: Producer '%s' doesn't have a unit production interface", + producer->getTemplate()->getName().str()) ); + return false; + } + + // queue the build + pu->queueCreateUnit( whatToCreate, productionID ); + + return true; +} + +bool GameLogic::onCancelUnitCreate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Player *msgPlayer = getMessagePlayer(msg); + +#if RETAIL_COMPATIBLE_AIGROUP + Object *producer = getSingleObjectFromSelection(currentlySelectedGroup); +#else + Object *producer = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); +#endif + ProductionID productionID = (ProductionID)msg->getArgument( 0 )->integer; + + // sanity + if( producer == nullptr ) + return false; + + // sanity, the player must control the producer + if( producer->getControllingPlayer() != msgPlayer ) + return false; + + // get the unit production interface + ProductionUpdateInterface *pu = producer->getProductionUpdateInterface(); + if( pu == nullptr ) + return false; + + // cancel the production + pu->cancelUnitCreate( productionID ); + + return true; +} + +bool GameLogic::onDozerConstruct(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + const ThingTemplate *place; + Coord3D loc; + Real angle; + + // get player, what to place, and location +#if RETAIL_COMPATIBLE_AIGROUP + Object *constructorObject = getSingleObjectFromSelection(currentlySelectedGroup); +#else + Object *constructorObject = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); +#endif + place = TheThingFactory->findByTemplateID( msg->getArgument( 0 )->integer ); + loc = msg->getArgument( 1 )->location; + angle = msg->getArgument( 2 )->real; + + if( place == nullptr || constructorObject == nullptr ) + return false; //These are not crashes, as the object may have died before this message came in + + if( msg->getType() == GameMessage::MSG_DOZER_CONSTRUCT ) + { + TheBuildAssistant->buildObjectNow( constructorObject, place, &loc, angle, + constructorObject->getControllingPlayer() ); + } + else + { + Coord3D locEnd; + + // get the end of the line location in the world + locEnd = msg->getArgument( 3 )->location; + + // place the line of structures, the end location being present will make it happen + TheBuildAssistant->buildObjectLineNow( constructorObject, place, &loc, &locEnd, angle, + constructorObject->getControllingPlayer() ); + } + + // place the sound for putting a building down + + static AudioEventRTS placeBuilding("PlaceBuilding"); + placeBuilding.setObjectID(constructorObject->getID()); + TheAudio->addAudioEvent( &placeBuilding ); + + // no, this is bad, don't do here, do when POSTING message + // pickAndPlayUnitVoiceResponse( TheInGameUI->getAllSelectedDrawables(), msg->getType() ); + + return true; +} + +bool GameLogic::onDozerCancelConstruct(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Player *msgPlayer = getMessagePlayer(msg); + + // get the building to cancel construction on +#if RETAIL_COMPATIBLE_AIGROUP + Object *building = getSingleObjectFromSelection(currentlySelectedGroup); +#else + Object *building = getSingleObjectFromSelection(currentlySelectedGroup.Peek()); +#endif + if( building == nullptr ) + return false; + + // the player sending this message must actually control this building + if( building->getControllingPlayer() != msgPlayer ) + return false; + + // Check to make sure it is actually under construction + if( !building->testStatus(OBJECT_STATUS_UNDER_CONSTRUCTION) ) + return false; + + // OK, refund the money to the player, unless it is a rebuilding Hole. + if( !building->testStatus(OBJECT_STATUS_RECONSTRUCTING)) + { + Money *money = msgPlayer->getMoney(); + UnsignedInt amount = building->getTemplate()->calcCostToBuild( msgPlayer ); + money->deposit( amount, TRUE, FALSE ); + } + + // + // Destroy the building ... killing the + // building will automatically cause the dozer also cancel its own building + // behavior and it will go on its merry way doing other assigned tasks + // + building->kill(); + + return true; +} + +bool GameLogic::onSell(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + // use the selected group + if( currentlySelectedGroup ) + currentlySelectedGroup->groupSell( CMD_FROM_PLAYER ); + + return true; +} + +bool GameLogic::onToggleOvercharge(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + // use the selected group + if( currentlySelectedGroup ) + currentlySelectedGroup->groupToggleOvercharge( CMD_FROM_PLAYER ); + + return true; +} + +#ifdef ALLOW_SURRENDER + +bool GameLogic::onDoSurrender(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + //All selected units surrender + if( currentlySelectedGroup ) + { + Object* objWeSurrenderedTo = TheGameLogic->findObjectByID( msg->getArgument( 0 )->objectID ); + Bool surrender = msg->getArgument( 1 )->boolean; + currentlySelectedGroup->groupSurrender( objWeSurrenderedTo, surrender, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onPickUpPrisoner(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + Object *prisoner = TheGameLogic->findObjectByID( msg->getArgument( 0 )->objectID ); + + if( prisoner ) + { + // use selected group + if( currentlySelectedGroup ) + currentlySelectedGroup->groupPickUpPrisoner( prisoner, CMD_FROM_PLAYER ); + } + + return true; +} + +bool GameLogic::onReturnToPrison(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + // use selected group + if( currentlySelectedGroup ) + currentlySelectedGroup->groupReturnToPrison( nullptr, CMD_FROM_PLAYER ); + + return true; +} + +#endif + +bool GameLogic::onCreateSelectedGroup(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + Bool createNewGroup = msg->getArgument( 0 )->boolean; + Bool firstObject = TRUE; + + for (Int i = 1; i < msg->getArgumentCount(); ++i) { + Object *obj = findObjectByID( msg->getArgument( i )->objectID ); + if (!obj) { + continue; + } + + selectObject(obj, createNewGroup && firstObject, msgPlayer->getPlayerMask()); + firstObject = FALSE; + } + + return true; +} + +bool GameLogic::onRemoveFromSelectedGroup(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + + for (Int i = 0; i < msg->getArgumentCount(); ++i) { + ObjectID objID = msg->getArgument(i)->objectID; + Object *objToRemove = findObjectByID(objID); + if (!objToRemove) { + continue; + } + + deselectObject(objToRemove, msgPlayer->getPlayerMask()); + } + + return true; +} + +bool GameLogic::onDestroySelectedGroup(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + msgPlayer->setCurrentlySelectedAIGroup(nullptr); + + return true; +} + +bool GameLogic::onPlaceBeacon(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + + if (msgPlayer->getPlayerTemplate() == nullptr) + return false; + + Coord3D pos = msg->getArgument( 0 )->location; + Region3D r; + TheTerrainLogic->getExtent(&r); + if (!r.isInRegionNoZ(&pos)) + pos = TheTerrainLogic->findClosestEdgePoint(&pos); + const ThingTemplate *thing = TheThingFactory->findTemplate( msgPlayer->getPlayerTemplate()->getBeaconTemplate() ); + + if (thing && !TheVictoryConditions->hasSinglePlayerBeenDefeated(msgPlayer)) + { + // how many does this player have active? + Int count; + msgPlayer->countObjectsByThingTemplate( 1, &thing, false, &count ); + DEBUG_LOG(("Player already has %d beacons active", count)); + if (count >= TheMultiplayerSettings->getMaxBeaconsPerPlayer()) + { + if (msgPlayer == ThePlayerList->getLocalPlayer()) + { + // tell the user + TheInGameUI->message( TheGameText->fetch("GUI:TooManyBeacons") ); + + // play a sound + static AudioEventRTS aSound("BeaconPlacementFailed"); + aSound.setPosition(&pos); + aSound.setPlayerIndex(msgPlayer->getPlayerIndex()); + TheAudio->addAudioEvent(&aSound); + } + + return false; + } + Object *object = TheThingFactory->newObject( thing, msgPlayer->getDefaultTeam() ); + object->setPosition( &pos ); + object->setProducer(nullptr); + + if (msgPlayer->getRelationship( ThePlayerList->getLocalPlayer()->getDefaultTeam() ) == ALLIES || ThePlayerList->getLocalPlayer()->isPlayerObserver()) + { + // tell the user + UnicodeString s; + s.format(TheGameText->fetch("GUI:BeaconPlaced"), msgPlayer->getPlayerDisplayName().str()); + TheInGameUI->message( s ); + + // play a sound + static AudioEventRTS aSound("BeaconPlaced"); + aSound.setPlayerIndex(msgPlayer->getPlayerIndex()); + aSound.setPosition(&pos); + TheAudio->addAudioEvent(&aSound); + + // beacons are a rare event; play a nifty radar event thingy + TheRadar->createEvent( object->getPosition(), RADAR_EVENT_INFORMATION ); + + if (ThePlayerList->getLocalPlayer()->getRelationship(msgPlayer->getDefaultTeam()) == ALLIES) + TheEva->setShouldPlay(EVA_BeaconDetected); + + TheControlBar->markUIDirty(); // check if we should grey out the button + } + else + { + + Int updateCount = 0; + static NameKeyType nameKeyClientUpdate = NAMEKEY("BeaconClientUpdate"); + ClientUpdateModule ** clientModules = object->getDrawable()->getClientUpdateModules(); + if (clientModules) + { + while (*clientModules) + { + if ((*clientModules)->getModuleNameKey() == nameKeyClientUpdate) + { + (*(BeaconClientUpdate **)clientModules)->hideBeacon(); + ++updateCount; + } + + ++clientModules; + } + } + DEBUG_ASSERTCRASH(updateCount == 1, ("Saw %d update modules for the beacon!", updateCount)); + + } + } + else + { + // tell the user + TheInGameUI->message( TheGameText->fetch("GUI:BeaconPlacementFailed") ); + + // play a sound + static AudioEventRTS aSound("BeaconPlacementFailed"); + aSound.setPosition(&pos); + aSound.setPlayerIndex(msgPlayer->getPlayerIndex()); + TheAudio->addAudioEvent(&aSound); + } + + return true; +} + +bool GameLogic::onRemoveBeacon(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + AIGroupPtr allSelectedObjects = TheAI->createGroup(); +#if RETAIL_COMPATIBLE_AIGROUP + msgPlayer->getCurrentSelectionAsAIGroup(allSelectedObjects); // need to act on all objects, so we can hide teammates' beacons. +#else + msgPlayer->getCurrentSelectionAsAIGroup(allSelectedObjects.Peek()); // need to act on all objects, so we can hide teammates' beacons. +#endif + if( allSelectedObjects ) + { + const VecObjectID& selectedObjects = allSelectedObjects->getAllIDs(); + for (VecObjectID::const_iterator it = selectedObjects.begin(); it != selectedObjects.end(); ++it) + { + Object *beacon = findObjectByID(*it); + if (beacon) + { + // TheSuperHackers @bugfix Prevent runtime crashing when a beacon is no longer associated with an initialized player. + const PlayerTemplate *playerTemplate = beacon->getControllingPlayer()->getPlayerTemplate(); + if (!playerTemplate) + continue; + + const ThingTemplate *thing = TheThingFactory->findTemplate( playerTemplate->getBeaconTemplate() ); + if (thing && thing->isEquivalentTo(beacon->getTemplate())) + { + if (beacon->getControllingPlayer() == msgPlayer) + { + destroyObject(beacon); // the owner is telling it to go away. such is life. + + TheControlBar->markUIDirty(); // check if we should un-grey out the button + } + else if (msgPlayer == ThePlayerList->getLocalPlayer()) + { + Drawable *beaconDrawable = beacon->getDrawable(); + if (beaconDrawable) + { + + static NameKeyType nameKeyClientUpdate = NAMEKEY("BeaconClientUpdate"); + ClientUpdateModule ** clientModules = beaconDrawable->getClientUpdateModules(); + if (clientModules) + { + while (*clientModules) + { + if ((*clientModules)->getModuleNameKey() == nameKeyClientUpdate) + (*(BeaconClientUpdate **)clientModules)->hideBeacon(); + + ++clientModules; + } + } + } + } + } + } + } +#if RETAIL_COMPATIBLE_AIGROUP + if (allSelectedObjects->isEmpty()) + { + TheAI->destroyGroup(allSelectedObjects); + allSelectedObjects = nullptr; + } +#endif + } + + return true; +} + +bool GameLogic::onSetBeaconText(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup) +{ + if( currentlySelectedGroup ) + { + const VecObjectID& selectedObjects = currentlySelectedGroup->getAllIDs(); + for (VecObjectID::const_iterator it = selectedObjects.begin(); it != selectedObjects.end(); ++it) + { + Object *beacon = findObjectByID(*it); + if (beacon) + { + Drawable *beaconDrawable = beacon->getDrawable(); + if (beaconDrawable) + { + UnicodeString s; + for( int i=0; igetArgumentCount(); i++ ) + { + s.concat( msg->getArgument(i)->wChar ); + } + + if (s.isEmpty()) + beaconDrawable->clearCaptionText(); + else + beaconDrawable->setCaptionText(s); + } + } + } + } + + return true; +} + +bool GameLogic::onSelfDestruct(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + + if (msg->getArgument(0)->boolean) + { + // transfer control to any living ally + Int i=0; + for (; igetPlayerCount(); ++i) + { + if (i != msgPlayer->getPlayerIndex()) + { + Player *otherPlayer = ThePlayerList->getNthPlayer(i); + if (msgPlayer->getRelationship(otherPlayer->getDefaultTeam()) == ALLIES && + otherPlayer->getRelationship(msgPlayer->getDefaultTeam()) == ALLIES) + { + if (TheVictoryConditions->hasSinglePlayerBeenDefeated(otherPlayer)) + continue; + + // a living ally! hooray! + otherPlayer->transferAssetsFromThat(msgPlayer); + msgPlayer->killPlayer(); // just to be safe (and to kill beacons etc that don't transfer) + break; + } + } + } + if (i == ThePlayerList->getPlayerCount()) + { + // didn't find any allies. die, loner! + msgPlayer->killPlayer(); + } + } + else + { + msgPlayer->killPlayer(); + } + + // There is no reason to do any notification here, it now takes place in the victory conditions. + // bonehead. + + return true; +} + +bool GameLogic::onSetReplayCamera(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + + if (TheRecorder->isPlaybackMode() && TheGlobalData->m_useCameraInReplay && TheControlBar->getObserverLookAtPlayer() == msgPlayer) + { + if (TheTacticalView->isCameraMovementFinished()) + { + const Coord3D pos = msg->getArgument( 0 )->location; + const Real angle = msg->getArgument( 1 )->real; + const Real pitch = msg->getArgument( 2 )->real; + const Real zoom = msg->getArgument( 3 )->real; + const Mouse::MouseCursor mouseCursor = static_cast(msg->getArgument( 4 )->integer); + const ICoord2D mousePos = msg->getArgument( 5 )->pixel; + + // TheSuperHackers @info Definitely call in user mode to ensure the camera operates with auto-zoom + // over terrain elevations, because the Replay Camera does not store the absolute camera location, + // but key parameters relative to the terrain height at the camera pivot. + TheTacticalView->userSetPosition(pos); + TheTacticalView->userSetAngle(angle); + TheTacticalView->userSetPitch(pitch); + TheTacticalView->userSetZoom(zoom); + + // TheSuperHackers @fix Make sure there is no scrolling ever. + const Coord2D scroll = {0, 0}; + TheTacticalView->userScrollBy(&scroll); + + if (msg->getArgumentCount() >= 8) + { + // TheSuperHackers @feature Override all the settings above with real camera position and view direction. + // This ensures that the camera looks EXACTLY like it was at the time of recording, no matter how the + // View is configured or tweaked. Note that the above settings are still required to set regardless, because + // when the replay camera is exited, then the pivot position and angles will be needed to build the camera + // where it was left off. + const Coord3D camPos = msg->getArgument( 6 )->location; + const Coord3D camDir = msg->getArgument( 7 )->location; + + TheTacticalView->setUserControlled(false); + TheTacticalView->set3DCameraLookAt(camPos, camDir, 0.0f); + } + + // TheSuperHackers @fix Lock the new location to avoid user input from changing the camera in this frame. + TheTacticalView->lockUserControlUntilFrame( getFrame() + 1 ); + + if (!TheLookAtTranslator->hasMouseMovedRecently()) + { + TheMouse->setCursor( mouseCursor ); + TheMouse->setPosition( mousePos.x, mousePos.y ); + TheLookAtTranslator->setCurrentPos( mousePos ); + } + } + } + + return true; +} + +bool GameLogic::onCreateTeam(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + msgPlayer->processCreateTeamGameMessage(msg->getType() - GameMessage::MSG_CREATE_TEAM0, msg); + + return true; +} + +bool GameLogic::onSelectTeam(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + msgPlayer->processSelectTeamGameMessage(msg->getType() - GameMessage::MSG_SELECT_TEAM0); + + return true; +} + +bool GameLogic::onAddTeam(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + msgPlayer->processAddTeamGameMessage(msg->getType() - GameMessage::MSG_ADD_TEAM0); + + return true; +} + +bool GameLogic::onLogicCrc(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + if (TheNetwork) + { + Int slotIndex = -1; + for (Int i=0; igetPlayerType() == PLAYER_HUMAN && TheNetwork->getPlayerName(i) == msgPlayer->getPlayerDisplayName()) + { + slotIndex = i; + break; + } + } + + if (slotIndex < 0 || !TheNetwork->isPlayerConnected(slotIndex)) + return false; + + if (msgPlayer->isLocalPlayer()) + { +#if defined(RTS_DEBUG) + // don't even put this in release, cause someone might hack it. + if (!TheDebugIgnoreSyncErrors) + { +#endif + m_shouldValidateCRCs = TRUE; +#if defined(RTS_DEBUG) + } +#endif + } + + UnsignedInt newCRC = msg->getArgument(0)->integer; + //DEBUG_LOG(("Received CRC of %8.8X from %ls on frame %d", newCRC, + //msgPlayer->getPlayerDisplayName().str(), m_frame)); + m_cachedCRCs[msgPlayer->getPlayerIndex()] = newCRC; + } + else if (TheRecorder && TheRecorder->isPlaybackMode()) + { + UnsignedInt newCRC = msg->getArgument(0)->integer; + //DEBUG_LOG(("Saw CRC of %X from player %d. Our CRC is %X. Arg count is %d", + //newCRC, msgPlayer->getPlayerIndex(), getCRC(), msg->getArgumentCount())); + + TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), (msg->getArgument(1)->boolean)); + } + return true; +} + +bool GameLogic::onPurchaseScience(GameMessage *msg) +{ + Player *msgPlayer = getMessagePlayer(msg); + ScienceType science = (ScienceType)msg->getArgument( 0 )->integer; + + // sanity + if( science == SCIENCE_INVALID ) + return false; + + msgPlayer->attemptToPurchaseScience(science); + + return true; +} diff --git a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h index 2aac21da37..d932a47fa9 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -35,6 +35,7 @@ #include "Common/STLTypedefs.h" #include "Common/ObjectStatusTypes.h" #include "GameNetwork/NetworkDefs.h" +#include "GameLogic/AI.h" #include "GameLogic/Module/UpdateModule.h" // needed for DIRECT_UPDATEMODULE_ACCESS /* @@ -286,6 +287,79 @@ class GameLogic : public SubsystemInterface, public Snapshot void remakeSleepyUpdate(); void validateSleepyUpdate() const; + bool onNewGame(GameMessage *msg); + bool onClearGameData(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onBeginPathBuild(GameMessage *msg); + bool onEndPathBuild(GameMessage *msg); + bool onSetRallyPoint(GameMessage *msg); + bool onDoWeapon(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCombatdropAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCombatdropAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoWeaponAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSwitchWeapons(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onSetMineClearingDetail(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onEnableRetaliationMode(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoWeaponAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSpecialPower(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSpecialPowerAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSpecialPowerAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoAttackmoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoForcemoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoMoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onAddWaypoint(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoGuardPosition(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoGuardObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoStop(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoScatter(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCreateFormation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onClearIngamePopupMessage(GameMessage *msg); + bool onDoCheer(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); +#if defined(RTS_DEBUG) || defined (_ALLOW_DEBUG_CHEATS_IN_RELEASE) + bool onDebugKillSelection(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDebugHurtObject(GameMessage *msg); + bool onDebugKillObject(GameMessage *msg); +#endif + bool onEnter(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onExit(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onEvacuate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onExecuteRailedTransport(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onInternetHack(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onGetRepaired(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDock(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onGetHealed(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoRepair(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onResumeConstruction(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSpecialPowerOverrideDestination(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoAttackObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoForceAttackObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoForceAttackGround(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onQueueUpgrade(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCancelUpgrade(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onQueueUnitCreate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCancelUnitCreate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDozerConstruct(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDozerCancelConstruct(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onSell(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onToggleOvercharge(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); +#ifdef ALLOW_SURRENDER + bool onDoSurrender(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onPickUpPrisoner(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onReturnToPrison(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); +#endif + bool onCreateSelectedGroup(GameMessage *msg); + bool onRemoveFromSelectedGroup(GameMessage *msg); + bool onDestroySelectedGroup(GameMessage *msg); + bool onPlaceBeacon(GameMessage *msg); + bool onRemoveBeacon(GameMessage *msg); + bool onSetBeaconText(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onSelfDestruct(GameMessage *msg); + bool onSetReplayCamera(GameMessage *msg); + bool onCreateTeam(GameMessage *msg); + bool onSelectTeam(GameMessage *msg); + bool onAddTeam(GameMessage *msg); + bool onLogicCrc(GameMessage *msg); + bool onPurchaseScience(GameMessage *msg); + private: /** diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h index 0389947212..dcb60e36ed 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -35,6 +35,7 @@ #include "Common/STLTypedefs.h" #include "Common/ObjectStatusTypes.h" #include "GameNetwork/NetworkDefs.h" +#include "GameLogic/AI.h" #include "GameLogic/Module/UpdateModule.h" // needed for DIRECT_UPDATEMODULE_ACCESS /* @@ -294,6 +295,79 @@ class GameLogic : public SubsystemInterface, public Snapshot void remakeSleepyUpdate(); void validateSleepyUpdate() const; + bool onNewGame(GameMessage *msg); + bool onClearGameData(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onBeginPathBuild(GameMessage *msg); + bool onEndPathBuild(GameMessage *msg); + bool onSetRallyPoint(GameMessage *msg); + bool onDoWeapon(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCombatdropAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCombatdropAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoWeaponAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSwitchWeapons(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onSetMineClearingDetail(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onEnableRetaliationMode(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoWeaponAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSpecialPower(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSpecialPowerAtLocation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSpecialPowerAtObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoAttackmoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoForcemoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoMoveto(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onAddWaypoint(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoGuardPosition(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoGuardObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoStop(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoScatter(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCreateFormation(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onClearIngamePopupMessage(GameMessage *msg); + bool onDoCheer(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); +#if defined(RTS_DEBUG) || defined (_ALLOW_DEBUG_CHEATS_IN_RELEASE) + bool onDebugKillSelection(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDebugHurtObject(GameMessage *msg); + bool onDebugKillObject(GameMessage *msg); +#endif + bool onEnter(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onExit(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onEvacuate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onExecuteRailedTransport(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onInternetHack(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onGetRepaired(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDock(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onGetHealed(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoRepair(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onResumeConstruction(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoSpecialPowerOverrideDestination(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoAttackObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoForceAttackObject(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDoForceAttackGround(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onQueueUpgrade(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCancelUpgrade(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onQueueUnitCreate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onCancelUnitCreate(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDozerConstruct(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onDozerCancelConstruct(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onSell(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onToggleOvercharge(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); +#ifdef ALLOW_SURRENDER + bool onDoSurrender(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onPickUpPrisoner(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onReturnToPrison(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); +#endif + bool onCreateSelectedGroup(GameMessage *msg); + bool onRemoveFromSelectedGroup(GameMessage *msg); + bool onDestroySelectedGroup(GameMessage *msg); + bool onPlaceBeacon(GameMessage *msg); + bool onRemoveBeacon(GameMessage *msg); + bool onSetBeaconText(GameMessage *msg, AIGroupPtr ¤tlySelectedGroup); + bool onSelfDestruct(GameMessage *msg); + bool onSetReplayCamera(GameMessage *msg); + bool onCreateTeam(GameMessage *msg); + bool onSelectTeam(GameMessage *msg); + bool onAddTeam(GameMessage *msg); + bool onLogicCrc(GameMessage *msg); + bool onPurchaseScience(GameMessage *msg); + static void createOptimizedTree(const ThingTemplate *thingTemplate, Coord3D *pos, Real angle); private: