/* ** Command & Conquer Generals(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . */ //////////////////////////////////////////////////////////////////////////////// // // // (c) 2001-2003 Electronic Arts Inc. // // // //////////////////////////////////////////////////////////////////////////////// // FILE: ControlBarCommand.cpp //////////////////////////////////////////////////////////////////// // Author: Colin Day, March 2002 // Desc: Methods specific to the control bar unit commands /////////////////////////////////////////////////////////////////////////////////////////////////// // USER INCLUDES ////////////////////////////////////////////////////////////////////////////////// #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "Common/NameKeyGenerator.h" #include "Common/ThingTemplate.h" #include "Common/ThingFactory.h" #include "Common/Player.h" #include "Common/PlayerList.h" #include "Common/SpecialPower.h" #include "Common/Upgrade.h" #include "Common/BuildAssistant.h" #include "GameLogic/GameLogic.h" #include "GameLogic/Module/BattlePlanUpdate.h" #include "GameLogic/Module/DozerAIUpdate.h" #include "GameLogic/Module/OverchargeBehavior.h" #include "GameLogic/Module/ProductionUpdate.h" #include "GameLogic/Module/SpecialPowerModule.h" #include "GameLogic/Module/TransportContain.h" #include "GameLogic/Module/MobNexusContain.h" #include "GameLogic/Module/SpecialAbilityUpdate.h" #include "GameLogic/Module/BattlePlanUpdate.h" #include "GameLogic/Module/VeterancyGainCreate.h" #include "GameLogic/Module/HackInternetAIUpdate.h" #include "GameLogic/Weapon.h" #include "GameClient/InGameUI.h" #include "GameClient/Drawable.h" #include "GameClient/ControlBar.h" #include "GameClient/GameWindow.h" #include "GameClient/GameWindowManager.h" #include "GameClient/GadgetPushButton.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif // PRIVATE DATA /////////////////////////////////////////////////////////////////////////////////// static GameWindow *commandWindows[ MAX_COMMANDS_PER_SET ]; Bool commandWindowsInitialized = FALSE; static Color BuildClockColor = GameMakeColor(0,0,0,100); // STATIC DATA STORAGE //////////////////////////////////////////////////////////////////////////// ControlBar::ContainEntry ControlBar::m_containData[ MAX_COMMANDS_PER_SET ]; //------------------------------------------------------------------------------------------------- /** Note, this iterate callback assumes that the inventory exit buttons appear in a * continuous order in the layout of the command set */ //------------------------------------------------------------------------------------------------- struct PopulateInvButtonData { Int currIndex; ///< index that represents the control we're talking about Int maxIndex; ///< this is the last valid control we can use GameWindow **controls; ///< the controls Object *transport; ///< the transport }; //------------------------------------------------------------------------------------------------- /** Used for the callback iterator on transport contents to do the actual GUI fill */ //------------------------------------------------------------------------------------------------- void ControlBar::populateInvDataCallback( Object *obj, void *userData ) { PopulateInvButtonData *data = (PopulateInvButtonData *)userData; // // if we're beyond the max the GUI can support, design needs to change the parameters // of the transport object to carry less things // if( data->currIndex > data->maxIndex ) { DEBUG_ASSERTCRASH( 0, ("There is not enough GUI slots to hold the # of items inside a '%s'\n", data->transport->getTemplate()->getName().str()) ); return; } // end if // get the window control that we're going to put our smiling faces in GameWindow *control = data->controls[ data->currIndex ]; DEBUG_ASSERTCRASH( control, ("populateInvDataCallback: Control not found\n") ); // assign our control and object id to the transport data m_containData[ data->currIndex ].control = control; m_containData[ data->currIndex ].objectID = obj->getID(); data->currIndex++; // fill out the control enabled, hilite, and pushed images const Image *image; image = obj->getTemplate()->getButtonImage(); GadgetButtonSetEnabledImage( control, image ); //No longer used //image = TheMappedImageCollection->findImageByName( obj->getTemplate()->getInventoryImageName( INV_IMAGE_HILITE ) ); //GadgetButtonSetHiliteImage( control, image ); //image = TheMappedImageCollection->findImageByName( obj->getTemplate()->getInventoryImageName( INV_IMAGE_PUSHED ) ); //GadgetButtonSetHiliteSelectedImage( control, image ); //Show the contained object's veterancy symbol! image = calculateVeterancyOverlayForObject( obj ); GadgetButtonDrawOverlayImage( control, image ); // enable the control control->winEnable( TRUE ); } // end populateInvDataCallback //------------------------------------------------------------------------------------------------- /** Transports have an extra special manipulation of the user interface. They get to look * at the available command set, and any of the commands that are TransportExit commands *AND* * there is actually an object to represent that slot contained in the transport, the * inventory picture of the contained object will be displayed in the window control for * that TransportExit command. Also, transports will HIDE any TransportExit controls found * in the command set that represent slots that *DO NOT EXIST* for the transport (that is, * the transport can only hold 4 things, but the GUI has buttons for 8 things). For slots * that are empty but present in the transport the UI will show a disabled button to show * the user that there is an open "slot" */ //------------------------------------------------------------------------------------------------- void ControlBar::doTransportInventoryUI( Object *transport, const CommandSet *commandSet ) { /// @todo srj -- remove hard-coding here, please //static const CommandButton *exitCommand = findCommandButton( "Command_TransportExit" ); // sanity if( transport == NULL || commandSet == NULL ) return; // get the transport contain module ContainModuleInterface *contain = transport->getContain(); // sanity if( contain == NULL ) return; // how many slots do we have inside the transport Int transportMax = contain->getContainMax(); // // first, hide any windows in 'm_commandWindows' that correspond to TransportExit commands // within the 'commandSet' that are overflow slots (the ui could be showing more inventory // exit button slots than there are slots in the transport) // // The extra slots bit means that a tank that takes up three slots will make two transport // buttons disappear off the end to show he takes up more room. transportMax = transportMax - contain->getExtraSlotsInUse(); Int firstInventoryIndex = -1; Int lastInventoryIndex = -1; Int inventoryCommandCount = 0; const CommandButton *commandButton; for( Int i = 0; i < MAX_COMMANDS_PER_SET; i++ ) { // get command button commandButton = commandSet->getCommandButton(i); // is this an inventory exit command if( commandButton && commandButton->getCommandType() == GUI_COMMAND_EXIT_CONTAINER ) { // record the index of the control for the first inventory exit command we found if( firstInventoryIndex == -1 ) firstInventoryIndex = i; // // since we're assuming all inventory exit commands appear in a continuous order, // we need to also need to keep track of what is the last valid inventory commadn index // lastInventoryIndex = i; // increment our count of available inventory exit commands found for the set inventoryCommandCount++; // show the window, but disable by default unless something is actually loaded in there m_commandWindows[ i ]->winHide( FALSE ); m_commandWindows[ i ]->winEnable( FALSE ); //Clear any potential veterancy rank, or else we'll see it when it's empty! GadgetButtonDrawOverlayImage( m_commandWindows[ i ], NULL ); //Unmanned vehicles don't have any commands available -- in fact they are hidden! if( transport->isDisabledByType( DISABLED_UNMANNED ) ) { m_commandWindows[ i ]->winHide( TRUE ); } // if we've counted more UI spots than the transport can hold, hide this command window if( inventoryCommandCount > transportMax ) m_commandWindows[ i ]->winHide( TRUE ); // // set the inventory exit command into the window (even if it's one of the hidden ones // it's OK cause we'll never see it to click it // setControlCommand( m_commandWindows[ i ], commandButton ); } // end if } // end for i // After Every change to the m_commandWIndows, we need to show fill in the missing blanks with the images // removed from multiplayer branch //showCommandMarkers(); // // now, iterate the contained items of the transport and for each one we find we will // populate a user interface button with its inventory picture and store the inventory // data inside the control bar so we can respond to the button when its clicked // if( lastInventoryIndex >= 0 ) // just for sanity { PopulateInvButtonData data; data.controls = m_commandWindows; data.currIndex = firstInventoryIndex; data.maxIndex = lastInventoryIndex; data.transport = transport; contain->iterateContained( populateInvDataCallback, &data, FALSE ); } // end if // // save the last recorded inventory count so we know when we have to redo the gui when // something exits or enters // m_lastRecordedInventoryCount = contain->getContainCount(); } // end doTransportInventoryUI //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- void ControlBar::populateCommand( Object *obj ) { const CommandSet *commandSet; Int i; Player *player = obj->getControllingPlayer(); // reset contain data resetContainData(); // reset the build queue data resetBuildQueueData(); // get command set commandSet = TheControlBar->findCommandSet( obj->getCommandSetString() ); // if no command set match is found hide all the buttons if( commandSet == NULL ) { // hide all the buttons for( i = 0; i < MAX_COMMANDS_PER_SET; i++ ) m_commandWindows[ i ]->winHide( TRUE ); // nothing left to do return; } // end if // transports do extra special things with the user interface buttons if( obj->getContain() && obj->getContain()->isDisplayedOnControlBar() ) doTransportInventoryUI( obj, commandSet ); // populate the button with commands defined const CommandButton *commandButton; for( i = 0; i < MAX_COMMANDS_PER_SET; i++ ) { // get command button commandButton = commandSet->getCommandButton(i); // if button is not present, just hide the window if( commandButton == NULL ) { // hide window on interface m_commandWindows[ i ]->winHide( TRUE ); } // end if else { //Script only command -- don't show it in the UI. if( BitTest( commandButton->getOptions(), SCRIPT_ONLY ) ) { m_commandWindows[ i ]->winHide( TRUE ); continue; } // // inventory exit commands were a special case already taken care above ... we needed // to iterage through the containment for transport objects and fill out any available // inventory exit buttons on the UI // if( commandButton->getCommandType() != GUI_COMMAND_EXIT_CONTAINER ) { // make sure the window is not hidden m_commandWindows[ i ]->winHide( FALSE ); // enable by default m_commandWindows[ i ]->winEnable( TRUE ); // populate the visible button with data from the command button setControlCommand( m_commandWindows[ i ], commandButton ); // // commands that require sciences we don't have are hidden so they never show up // cause we can never pick "another" general technology throughout the game // if( BitTest( commandButton->getOptions(), NEED_SPECIAL_POWER_SCIENCE ) ) { const SpecialPowerTemplate *power = commandButton->getSpecialPowerTemplate(); if( power && power->getRequiredScience() != SCIENCE_INVALID ) { if( player->hasScience( power->getRequiredScience() ) == FALSE ) { //Hide the power m_commandWindows[ i ]->winHide( TRUE ); } else { //The player does have the special power! Now determine if the images require //enhancement based on upgraded versions. This is determined by the command //button specifying a vector of sciences in the command button. Int bestIndex = -1; ScienceType science; for( Int scienceIndex = 0; scienceIndex < commandButton->getScienceVec().size(); ++scienceIndex ) { science = commandButton->getScienceVec()[ scienceIndex ]; //Keep going until we reach the end or don't have the required science! if( player->hasScience( science ) ) { bestIndex = scienceIndex; } else { break; } } if( bestIndex != -1 ) { //Now get the best sciencetype. science = commandButton->getScienceVec()[ bestIndex ]; //Now we have to search through the command buttons to find a matching purchase science button. for( const CommandButton *command = m_commandButtons; command; command = command->getNext() ) { if( command->getCommandType() == GUI_COMMAND_PURCHASE_SCIENCE ) { //All purchase sciences specify a single science. if( command->getScienceVec().empty() ) { DEBUG_CRASH( ("Commandbutton %s is a purchase science button without any science! Please add it.", command->getName().str() ) ); } else if( command->getScienceVec()[0] == science ) { commandButton->copyImagesFrom( command, true ); } } } } } } } // end if } // end else } // end else } // end for i // After Every change to the m_commandWIndows, we need to show fill in the missing blanks with the images // removed from multiplayer branch //showCommandMarkers(); // // for objects that have a production exit interface, we may have a rally point set. // if we do, we want to show that rally point in the world // ExitInterface *exit = obj->getObjectExitInterface(); if( exit ) { // // if a rally point is set, show the rally point, if we don't have it set hide any rally // point we might have visible // showRallyPoint( exit->getRallyPoint() ); } // end if // // to avoid a one frame delay where windows may become enabled/disabled, run the update // at once to get it all in the correct state immediately // updateContextCommand(); } // end populateCommand //------------------------------------------------------------------------------------------------- /** reset transport data */ //------------------------------------------------------------------------------------------------- void ControlBar::resetContainData( void ) { Int i; for( i = 0; i < MAX_COMMANDS_PER_SET; i++ ) { m_containData[ i ].control = NULL; m_containData[ i ].objectID = INVALID_ID; } // end for i } // end resetTransportData //------------------------------------------------------------------------------------------------- /** reset the build queue data we use to die queue entires to control */ //------------------------------------------------------------------------------------------------- void ControlBar::resetBuildQueueData( void ) { Int i; for( i = 0; i < MAX_BUILD_QUEUE_BUTTONS; i++ ) { m_queueData[ i ].control = NULL; m_queueData[ i ].type = PRODUCTION_INVALID; m_queueData[ i ].productionID = PRODUCTIONID_INVALID; m_queueData[ i ].upgradeToResearch = NULL; } // end for i } // end resetBuildQueue //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- void ControlBar::populateBuildQueue( Object *producer ) { /// @todo srj -- remove hard-coding here, please static const CommandButton *cancelUnitCommand = findCommandButton( "Command_CancelUnitCreate" ); /// @todo srj -- remove hard-coding here, please static const CommandButton *cancelUpgradeCommand = findCommandButton( "Command_CancelUpgradeCreate" ); static NameKeyType buildQueueIDs[ MAX_BUILD_QUEUE_BUTTONS ]; static Bool idsInitialized = FALSE; Int i; // reset the build queue data resetBuildQueueData(); // get name key ids for the build queue buttons if( idsInitialized == FALSE ) { AsciiString buttonName; for( i = 0; i < MAX_BUILD_QUEUE_BUTTONS; i++ ) { buttonName.format( "ControlBar.wnd:ButtonQueue%02d", i + 1 ); buildQueueIDs[ i ] = TheNameKeyGenerator->nameToKey( buttonName ); } // end for i idsInitialized = TRUE; } // end if // get window pointers to all the buttons for the build queue for( i = 0; i < MAX_BUILD_QUEUE_BUTTONS; i++ ) { // get window commented out cause I believe we already set this. We'll see in a few minutes m_queueData[ i ].control = TheWindowManager->winGetWindowFromId( m_contextParent[ CP_BUILD_QUEUE ], buildQueueIDs[ i ] ); // disable window by default m_queueData[ i ].control->winEnable( FALSE ); //Clear the status because this button doesn't use it -- and if it's set, it'll //become invisible meaning the image that was there will be showed. m_queueData[ i ].control->winClearStatus( WIN_STATUS_USE_OVERLAY_STATES ); // set the text of the window to nothing by default GadgetButtonSetText( m_queueData[ i ].control, UnicodeString( L"" ) ); //Clear any potential veterancy rank, or else we'll see it when it's empty! GadgetButtonDrawOverlayImage( m_queueData[ i ].control, NULL ); } // end for i // step through each object being built and set the image data for the buttons ProductionUpdateInterface *pu = producer->getProductionUpdateInterface(); if( pu == NULL ) return; // sanity const ProductionEntry *production; Int windowIndex = 0; const Image *image; for( production = pu->firstProduction(); production; production = pu->nextProduction( production ) ) { // don't go above how many queue windows we have if( windowIndex >= MAX_BUILD_QUEUE_BUTTONS ) break; // exit for // set the command into the queue button if( production->getProductionType() == PRODUCTION_UNIT ) { // set the control command setControlCommand( m_queueData[ windowIndex ].control, cancelUnitCommand ); m_queueData[ windowIndex ].type = PRODUCTION_UNIT; m_queueData[ windowIndex ].productionID = production->getProductionID(); // set the images m_queueData[ windowIndex ].control->winEnable( TRUE ); m_queueData[ windowIndex ].control->winSetStatus( WIN_STATUS_USE_OVERLAY_STATES ); image = production->getProductionObject()->getButtonImage(); GadgetButtonSetEnabledImage( m_queueData[ windowIndex ].control, image ); //No longer used. //image = TheMappedImageCollection->findImageByName( production->getProductionObject()->getInventoryImageName( INV_IMAGE_HILITE ) ); //GadgetButtonSetHiliteSelectedImage( m_queueData[ windowIndex ].control, image ); //image = TheMappedImageCollection->findImageByName( production->getProductionObject()->getInventoryImageName( INV_IMAGE_PUSHED ) ); //GadgetButtonSetHiliteImage( m_queueData[ windowIndex ].control, image ); //Show the veterancy rank of the object being constructed in the queue const Image *image = calculateVeterancyOverlayForThing( production->getProductionObject() ); GadgetButtonDrawOverlayImage( m_queueData[ windowIndex ].control, image ); // // note we're not setting a disabled image into the queue button ... when there is // nothing in the queue we set the button to disabled, we want to leave the disabled // queue button graphic we already have in place // // image = TheMappedImageCollection->findImageByName( production->getProductionObject()->getInventoryImageName( INV_IMAGE_DISABLED ) ); // GadgetButtonSetDisabledImage( m_queueData[ windowIndex ].control, image ); } // end if else { const UpgradeTemplate *ut = production->getProductionUpgrade(); // set the control command setControlCommand( m_queueData[ windowIndex ].control, cancelUpgradeCommand ); m_queueData[ windowIndex ].type = PRODUCTION_UPGRADE; m_queueData[ windowIndex ].upgradeToResearch = production->getProductionUpgrade(); // set the images m_queueData[ windowIndex ].control->winEnable( TRUE ); m_queueData[ windowIndex ].control->winSetStatus( WIN_STATUS_USE_OVERLAY_STATES ); image = ut->getButtonImage(); GadgetButtonSetEnabledImage( m_queueData[ windowIndex ].control, image ); //No longer used //image = TheMappedImageCollection->findImageByName( ut->getQueueImageName( UpgradeTemplate::UPGRADE_HILITE ) ); //GadgetButtonSetHiliteSelectedImage( m_queueData[ windowIndex ].control, image ); //image = TheMappedImageCollection->findImageByName( ut->getQueueImageName( UpgradeTemplate::UPGRADE_PUSHED ) ); //GadgetButtonSetHiliteImage( m_queueData[ windowIndex ].control, image ); // // note we're not setting a disabled image into the queue button ... when there is // nothing in the queue we set the button to disabled, we want to leave the disabled // queue button graphic we already have in place // // image = TheMappedImageCollection->findImageByName( ut->getQueueImageName( UpgradeTemplate::UPGRADE_DISABLED ) ); // GadgetButtonSetDisabledImage( m_queueData[ windowIndex ].control, image ); } // end else // we have filled up this window now windowIndex++; } // end for // // save the count of things being produced in the build queue, when it changes we will // repopulate the queue to visually show the change // m_displayedQueueCount = pu->getProductionCount(); } // end populateBuildQueue //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- void ControlBar::updateContextCommand( void ) { Object *obj = NULL; Int i; // get object if( m_currentSelectedDrawable ) obj = m_currentSelectedDrawable->getObject(); // // the contents of objects are ususally showed on the UI, when those contents change // we always to update the UI // ContainModuleInterface *contain = obj ? obj->getContain() : NULL; if( contain && contain->getContainMax() > 0 && m_lastRecordedInventoryCount != contain->getContainCount() ) { // record this ast the last known number m_lastRecordedInventoryCount = contain->getContainCount(); // re-evaluate the UI because something has changed evaluateContextUI(); } // end if, transport // get production update for those objects that have one ProductionUpdateInterface *pu = obj ? obj->getProductionUpdateInterface() : NULL; // // when we have a production update, we show the build queue when there is actually // something in the queue, otherwise we show the selection portrait for the object ... so if // the queue is visible we need to check to see if we should hide it and show the portrait, // and if the queue is hidden, we need to check and see if it should become shown // if( m_contextParent[ CP_BUILD_QUEUE ]->winIsHidden() == TRUE ) { if( pu && pu->firstProduction() != NULL ) { // don't show the portrait image setPortraitByObject( NULL ); // show the build queue m_contextParent[ CP_BUILD_QUEUE ]->winHide( FALSE ); populateBuildQueue( obj ); } // end if } // end if else { if( pu && pu->firstProduction() == NULL ) { // hide the build queue m_contextParent[ CP_BUILD_QUEUE ]->winHide( TRUE ); // show the portrait image setPortraitByObject( obj ); } // end if } // end else // update a visible production queue if( m_contextParent[ CP_BUILD_QUEUE ]->winIsHidden() == FALSE ) { // when the build queue is enabled, the selected portrait cannot be shown setPortraitByObject( NULL ); // // when showing a production queue, when the production count changes of the producer // object (the thing we have selected for the control bar) we will repopulate the // windows to visually show the new production linup // if( pu ) { // update the whole queue as necessary if( pu->getProductionCount() != m_displayedQueueCount ) populateBuildQueue( obj ); // // update the build percentage on the first thing (the thing that's being built) // in the queue // const ProductionEntry *produce = pu->firstProduction(); if( produce ) { static NameKeyType winID = TheNameKeyGenerator->nameToKey( "ControlBar.wnd:ButtonQueue01" ); GameWindow *win = TheWindowManager->winGetWindowFromId( m_contextParent[ CP_BUILD_QUEUE ], winID ); DEBUG_ASSERTCRASH( win, ("updateContextCommand: Unable to find first build queue button\n") ); // UnicodeString text; // // text.format( L"%.0f%%", produce->getPercentComplete() ); // GadgetButtonSetText( win, text ); GadgetButtonDrawInverseClock(win,produce->getPercentComplete(), m_buildUpClockColor); } // end if } // end if } // end if // evaluate each command on whether or not it should be enabled for( i = 0; i < MAX_COMMANDS_PER_SET; i++ ) { GameWindow *win; const CommandButton *command; // get the window win = m_commandWindows[ i ]; // only consider commands for windows that are actually shown //`tbd: fix the bug here, that is that if we don't change the unit, we won't attempt to show // these. if( win->winIsHidden() == TRUE ) continue; // get the command from the control command = (const CommandButton *)GadgetButtonGetData(win); //command = (const CommandButton *)win->winGetUserData(); if( command == NULL ) continue; // ignore transport/structure inventory commands, they are handled elsewhere if( command->getCommandType() == GUI_COMMAND_EXIT_CONTAINER ) { win->winSetStatus( WIN_STATUS_ALWAYS_COLOR ); //Don't let these buttons render in grayscale ever! continue; } else { win->winClearStatus( WIN_STATUS_NOT_READY ); win->winClearStatus( WIN_STATUS_ALWAYS_COLOR ); } // is the command available CommandAvailability availability = getCommandAvailability( command, obj, win ); // enable/disable the window control switch( availability ) { case COMMAND_HIDDEN: win->winHide( TRUE ); break; case COMMAND_RESTRICTED: win->winEnable( FALSE ); break; case COMMAND_NOT_READY: win->winEnable( FALSE ); win->winSetStatus( WIN_STATUS_NOT_READY ); break; case COMMAND_CANT_AFFORD: win->winEnable( FALSE ); win->winSetStatus( WIN_STATUS_ALWAYS_COLOR ); break; default: win->winEnable( TRUE ); break; } //Determine by the production type of this button, whether or not the created object //will have a veterancy rank const Image *image = calculateVeterancyOverlayForThing( command->getThingTemplate() ); GadgetButtonDrawOverlayImage( win, image ); // // for check-like commands we will keep the push button "pushed" or "unpushed" depending // on the current running status of the command // if( BitTest( command->getOptions(), CHECK_LIKE )) { // sanity, check like commands should have windows that are check like as well DEBUG_ASSERTCRASH( BitTest( win->winGetStatus(), WIN_STATUS_CHECK_LIKE ), ("updateContextCommand: Error, gadget window for command '%s' is not check-like!\n", command->getName().str()) ); if( availability == COMMAND_ACTIVE ) GadgetCheckLikeButtonSetVisualCheck( win, TRUE ); else GadgetCheckLikeButtonSetVisualCheck( win, FALSE ); } // end if } // end for i // After Every change to the m_commandWIndows, we need to show fill in the missing blanks with the images // removed from multiplayer branch //showCommandMarkers(); // // if we have a build tooltip layout, update it with the new data. // repopulateBuildTooltipLayout(); } // end updatecontextCommand //------------------------------------------------------------------------------------------------- const Image* ControlBar::calculateVeterancyOverlayForThing( const ThingTemplate *thingTemplate ) { VeterancyLevel level = LEVEL_REGULAR; if( !thingTemplate ) { return NULL; } Player *player = ThePlayerList->getLocalPlayer(); if( !player ) { return NULL; } //See if the thingTemplate has a VeterancyGainCreate //This is HORROR CODE and needs to be optimized! const VeterancyGainCreateModuleData *data = NULL; AsciiString modName; const ModuleInfo& mi = thingTemplate->getBehaviorModuleInfo(); for( Int modIdx = 0; modIdx < mi.getCount(); ++modIdx ) { modName = mi.getNthName(modIdx); if( !modName.compare( "VeterancyGainCreate" ) ) { data = (const VeterancyGainCreateModuleData*)mi.getNthData( modIdx ); break; } } //It does, so see if the player has that upgrade if( data && player->hasScience( data->m_scienceRequired ) ) { //We do! So now check to see what the veterancy level would be. level = data->m_startingLevel; } //Return the appropriate image (including NULL if no veterancy levels) switch( level ) { case LEVEL_VETERAN: return m_rankVeteranIcon; case LEVEL_ELITE: return m_rankEliteIcon; case LEVEL_HEROIC: return m_rankHeroicIcon; } return NULL;; } //------------------------------------------------------------------------------------------------- const Image* ControlBar::calculateVeterancyOverlayForObject( const Object *obj ) { if( !obj ) { return NULL; } VeterancyLevel level = obj->getVeterancyLevel(); //Return the appropriate image (including NULL if no veterancy levels) switch( level ) { case LEVEL_VETERAN: return m_rankVeteranIcon; case LEVEL_ELITE: return m_rankEliteIcon; case LEVEL_HEROIC: return m_rankHeroicIcon; } return NULL;; } //------------------------------------------------------------------------------------------------- static Int getRappellerCount(Object* obj) { Int num = 0; const ContainedItemsList* items = obj->getContain() ? obj->getContain()->getContainedItemsList() : NULL; if (items) { for (ContainedItemsList::const_iterator it = items->begin(); it != items->end(); ++it ) { if ((*it)->isKindOf(KINDOF_CAN_RAPPEL)) { ++num; } } } return num; } //------------------------------------------------------------------------------------------------- /** What's the status between 'obj' and the 'command' at present. Can we do it? Are * we already doing it? Can ya dig it? */ //------------------------------------------------------------------------------------------------- CommandAvailability ControlBar::getCommandAvailability( const CommandButton *command, Object *obj, GameWindow *win, Bool forceDisabledEvaluation ) const { if (command->getCommandType() == GUI_COMMAND_SPECIAL_POWER_FROM_COMMAND_CENTER) { if (ThePlayerList && ThePlayerList->getLocalPlayer()) obj = ThePlayerList->getLocalPlayer()->findNaturalCommandCenter(); else obj = NULL; } if (obj == NULL) return COMMAND_HIDDEN; // probably better than crashing.... Player *player = obj->getControllingPlayer(); if (obj->testScriptStatusBit(OBJECT_STATUS_SCRIPT_DISABLED) || obj->testScriptStatusBit(OBJECT_STATUS_SCRIPT_UNPOWERED)) { // if the object status is disabled or unpowered, you cannot do anything to it. return COMMAND_HIDDEN; } //Unmanned vehicles don't have any commands available -- in fact they are hidden! if( obj->isDisabledByType( DISABLED_UNMANNED ) ) { return COMMAND_HIDDEN; } //It's possible for command buttons to be a single use only type of a button -- like detonating a nuke from a convoy truck. if( obj->hasSingleUseCommandBeenUsed() ) { return COMMAND_RESTRICTED; } //Other disabled objects are unable to use buttons -- so gray them out. Bool disabled = obj->isDisabled(); // if we are only disabled by being underpowered, and this button doesn't care, well, fix it if (disabled && BitTest(command->getOptions(), IGNORES_UNDERPOWERED) && obj->getDisabledFlags().test(DISABLED_UNDERPOWERED) && obj->getDisabledFlags().count() == 1) { disabled = false; } if (disabled && !forceDisabledEvaluation) { GUICommandType commandType = command->getCommandType(); if( commandType != GUI_COMMAND_SELL && commandType != GUI_COMMAND_EVACUATE && commandType != GUI_COMMAND_EXIT_CONTAINER && commandType != GUI_COMMAND_BEACON_DELETE && commandType != GUI_COMMAND_SET_RALLY_POINT && commandType != GUI_COMMAND_SWITCH_WEAPON ) { if( getCommandAvailability( command, obj, win, TRUE ) == COMMAND_HIDDEN ) { return COMMAND_HIDDEN; } return COMMAND_RESTRICTED; } } // if the command requires an upgrade and we don't have it we can't do it if( BitTest( command->getOptions(), NEED_UPGRADE ) ) { const UpgradeTemplate *upgradeT = command->getUpgradeTemplate(); if (upgradeT) { // upgrades come in the form of player upgrades and object upgrades if( upgradeT->getUpgradeType() == UPGRADE_TYPE_PLAYER ) { if( player->hasUpgradeComplete( upgradeT ) == FALSE ) return COMMAND_RESTRICTED; } else if( upgradeT->getUpgradeType() == UPGRADE_TYPE_OBJECT && obj->hasUpgrade( upgradeT ) == FALSE ) { return COMMAND_RESTRICTED; } } } ProductionUpdateInterface *pu = obj->getProductionUpdateInterface(); if( pu && pu->firstProduction() && BitTest( command->getOptions(), NOT_QUEUEABLE ) ) { //This button is designated so that it is incapable of building this upgrade/object //when anything is in the production queue. return COMMAND_RESTRICTED; } Bool queueMaxed = pu ? ( pu->getProductionCount() == MAX_BUILD_QUEUE_BUTTONS ) : FALSE; switch( command->getCommandType() ) { case GUI_COMMAND_DOZER_CONSTRUCT: { // if the command is a dozer construct task and the object dozer is building anything // this command is not available if(command->getThingTemplate()) { BuildableStatus bStatus = command->getThingTemplate()->getBuildable(); if (bStatus == BSTATUS_NO || (bStatus == BSTATUS_ONLY_BY_AI && obj->getControllingPlayer()->getPlayerType() != PLAYER_COMPUTER)) return COMMAND_HIDDEN; } // sanity, non dozer object if( obj->isKindOf( KINDOF_DOZER ) == FALSE ) return COMMAND_RESTRICTED; // get the dozer ai update interface DozerAIInterface* dozerAI = NULL; if( obj->getAIUpdateInterface() == NULL ) return COMMAND_RESTRICTED; dozerAI = obj->getAIUpdateInterface()->getDozerAIInterface(); DEBUG_ASSERTCRASH( dozerAI != NULL, ("Something KINDOF_DOZER must have a Dozer-like AIUpdate") ); if( dozerAI == NULL ) return COMMAND_RESTRICTED; // if building anything at all right now we can't build another if( dozerAI->isTaskPending( DOZER_TASK_BUILD ) == TRUE ) return COMMAND_RESTRICTED; // return whether or not the player can build this thing if( player->canBuild( command->getThingTemplate() ) == FALSE ) return COMMAND_RESTRICTED; if( !player->canAffordBuild( command->getThingTemplate() ) ) { return COMMAND_RESTRICTED;//COMMAND_CANT_AFFORD; } break; } case GUI_COMMAND_SELL: { // if this is a sell command, is the object marked as "This cannot be sold?" // if so, remove the button, otherwise, its available if (obj->testScriptStatusBit(OBJECT_STATUS_SCRIPT_UNSELLABLE)) return COMMAND_HIDDEN; break; } case GUI_COMMAND_UNIT_BUILD: { // command is a unit build if(command->getThingTemplate()) { BuildableStatus bStatus = command->getThingTemplate()->getBuildable(); if (bStatus == BSTATUS_NO || (bStatus == BSTATUS_ONLY_BY_AI && obj->getControllingPlayer()->getPlayerType() != PLAYER_COMPUTER)) return COMMAND_HIDDEN; } if( queueMaxed ) { return COMMAND_RESTRICTED; } // return whether or not the player can build this thing //NOTE: Player::canBuild() only checks prerequisites! if( player->canBuild( command->getThingTemplate() ) == FALSE ) return COMMAND_RESTRICTED; CanMakeType makeType = TheBuildAssistant->canMakeUnit( obj, command->getThingTemplate() ); if( makeType == CANMAKE_MAXED_OUT_FOR_PLAYER || makeType == CANMAKE_PARKING_PLACES_FULL ) { //Disable the button if the player has a max amount of these units in build queue or existance. return COMMAND_RESTRICTED; } if( makeType == CANMAKE_NO_MONEY ) { return COMMAND_RESTRICTED; //COMMAND_CANT_AFFORD; } break; } case GUI_COMMAND_PLAYER_UPGRADE: { if( queueMaxed ) { return COMMAND_RESTRICTED; } // if we can build it, we must also NOT already have it or be building it if( player->hasUpgradeComplete( command->getUpgradeTemplate() ) == TRUE || player->hasUpgradeInProduction( command->getUpgradeTemplate() ) == TRUE ) return COMMAND_CANT_AFFORD;//COMMAND_RESTRICTED; // if this is an upgrade create we must be able to build it. if( TheUpgradeCenter->canAffordUpgrade( player, command->getUpgradeTemplate() ) == FALSE ) return COMMAND_RESTRICTED;//COMMAND_CANT_AFFORD; break; } case GUI_COMMAND_OBJECT_UPGRADE: { if( queueMaxed ) { return COMMAND_RESTRICTED; } // no production update, can't possibly do this command if( pu == NULL ) { DEBUG_CRASH(("Objects that have Object-Level Upgrades must also have ProductionUpdate. Just cuz.")); return COMMAND_RESTRICTED; } // // if this object already has this upgrade, or is researching it already in the queue // we will disable the button so you can't build another one // if( obj->hasUpgrade( command->getUpgradeTemplate() ) == TRUE || pu->isUpgradeInQueue( command->getUpgradeTemplate() ) == TRUE || obj->affectedByUpgrade( command->getUpgradeTemplate() ) == FALSE ) return COMMAND_CANT_AFFORD;//COMMAND_RESTRICTED; if( TheUpgradeCenter->canAffordUpgrade( player, command->getUpgradeTemplate() ) == FALSE ) return COMMAND_RESTRICTED;//COMMAND_CANT_AFFORD; break; } case GUI_COMMAND_FIRE_WEAPON: { AIUpdateInterface *ai = obj->getAIUpdateInterface(); // no ai, can't possibly fire weapon if( ai == NULL ) return COMMAND_RESTRICTED; // ask the ai if the weapon is ready to fire const Weapon* w = obj->getWeaponInWeaponSlot( command->getWeaponSlot() ); // changed this to Log rather than Crash, because this can legitimately happen now for // dozers and workers with mine-clearing stuff... (srj) //DEBUG_ASSERTLOG( w, ("Unit %s's CommandButton %s is trying to access weaponslot %d, but doesn't have a weapon there in its FactionUnit ini entry.\n", // obj->getTemplate()->getName().str(), command->getName().str(), (Int)command->getWeaponSlot() ) ); UnsignedInt now = TheGameLogic->getFrame(); /// @Kris -- We need to show the button as always available for anything with a 0 clip reload time. if( w && w->getClipReloadTime( obj ) == 0 ) { return COMMAND_AVAILABLE; } if( w == NULL // No weapon || w->getStatus() != READY_TO_FIRE // Weapon not ready || w->getPossibleNextShotFrame() == now // Weapon ready, but could fire this exact frame (handle button flicker since it may be going to fire anyway) /// @todo srj -- not sure why this next check is necessary, but the Comanche missile buttons will flicker without it. figure out someday. /// @todo ml -- and note: that the "now-1" below causes zero-clip-reload weapons to never be ready, so I added this /// If you make changes to this code, make sure that the DragonTank's firewall weapon can be retargeted while active, /// that is, while the tank is squirting out flames all over the floor, you can click the firewall button (or "F"), /// and re-target the firewall without having to stop or move in-betwen.. Thanks for reading || (w->getPossibleNextShotFrame()==now-1) ) { if ( w != NULL ) { // only draw the clock when reloading a clip, not when merely between shots, since that's usually a tiny amount of time if ( w->getStatus() == RELOADING_CLIP) { Int percent = w->getPercentReadyToFire() * 100; GadgetButtonDrawInverseClock(win, percent, m_buildUpClockColor); } return COMMAND_NOT_READY; } else { // if this is a mine-clearing button but we don't have the right weaponset, // just declare it available... we'll switch weaponsets when the time comes if ( (command->getOptions() & USES_MINE_CLEARING_WEAPONSET) != 0 && !obj->testWeaponSetFlag(WEAPONSET_MINE_CLEARING_DETAIL) ) { return COMMAND_AVAILABLE; } // no weapon in the slot means "gray me out" return COMMAND_RESTRICTED; } } break; } case GUI_COMMAND_GUARD: case GUI_COMMAND_GUARD_WITHOUT_PURSUIT: case GUI_COMMAND_GUARD_FLYING_UNITS_ONLY: // always available break; case GUI_COMMAND_COMBATDROP: { if( getRappellerCount(obj) <= 0 ) return COMMAND_RESTRICTED; break; } case GUI_COMMAND_EXIT_CONTAINER: { // // this method is really used as a per frame update to see if we should enable // disable a control ... inventory of objects shows as buttons have that enable // disable logic handled elsewhere, where if the contained count of the entire // container changes the UI is completely repopulated // break; } case GUI_COMMAND_EVACUATE: { // if we have no contained objects we can't evacuate anything if( !obj->getContain() || obj->getContain()->getContainCount() <= 0 ) return COMMAND_RESTRICTED; break; } case GUI_COMMAND_EXECUTE_RAILED_TRANSPORT: { DockUpdateInterface *dui = obj->getDockUpdateInterface(); // if the dock is closed or not present this command is invalid if( dui == NULL || dui->isDockOpen() == FALSE ) return COMMAND_RESTRICTED; break; } case GUI_COMMAND_SPECIAL_POWER_FROM_COMMAND_CENTER: case GUI_COMMAND_SPECIAL_POWER: { // sanity DEBUG_ASSERTCRASH( command->getSpecialPowerTemplate() != NULL, ("The special power in the command '%s' is NULL\n", command->getName().str()) ); // get special power module from the object to execute it SpecialPowerModuleInterface *mod = obj->getSpecialPowerModule( command->getSpecialPowerTemplate() ); if( mod == NULL ) { // sanity ... we must have a module for the special power, if we don't somebody probably // forgot to put it in the object DEBUG_CRASH(( "Object does not contain special power module (%s) to execute. Did you forget to add it to the object INI?\n", command->getSpecialPowerTemplate()->getName().str() )); } else if( mod->isReady() == FALSE ) { Int percent = mod->getPercentReady() * 100; GadgetButtonDrawInverseClock(win, percent, m_buildUpClockColor); return COMMAND_NOT_READY; } else if( SpecialAbilityUpdate *spUpdate = obj->findSpecialAbilityUpdate( command->getSpecialPowerTemplate()->getSpecialPowerType() ) ) { if( spUpdate->isPowerCurrentlyInUse( command ) ) { return COMMAND_RESTRICTED; } } else if( mod->getSpecialPowerTemplate()->getSpecialPowerType() == SPECIAL_CHANGE_BATTLE_PLANS ) { static NameKeyType key_BattlePlanUpdate = NAMEKEY( "BattlePlanUpdate" ); BattlePlanUpdate *update = (BattlePlanUpdate*)obj->findUpdateModule( key_BattlePlanUpdate ); if( update && update->getCommandOption() & command->getOptions() ) { return COMMAND_ACTIVE; } } break; } case GUI_COMMAND_TOGGLE_OVERCHARGE: { OverchargeBehaviorInterface *obi; // search object behavior mdoules for( BehaviorModule **bmi = obj->getBehaviorModules(); *bmi; ++bmi ) { // we're looking for the overcharge interface obi = (*bmi)->getOverchargeBehaviorInterface(); if( obi ) { if( obi->isOverchargeActive() ) return COMMAND_ACTIVE; } } break; } // switch weapon command case GUI_COMMAND_SWITCH_WEAPON: { // ask the ai which weapon is in the current slot const Weapon* w = obj->getWeaponInWeaponSlot( command->getWeaponSlot() ); DEBUG_ASSERTCRASH( w, ("Unit %s's CommandButton %s is trying to access weaponslot %d, but doesn't have a weapon there in its FactionUnit ini entry.", obj->getTemplate()->getName().str(), command->getName().str(), (Int)command->getWeaponSlot() ) ); if( w == NULL) return COMMAND_RESTRICTED; const DrawableList *selected = TheInGameUI->getAllSelectedDrawables(); for( DrawableListCIt it = selected->begin(); it != selected->end(); ++it ) { Drawable *draw = *it; if( draw && draw->getObject() && draw->getObject()->isLocallyControlled() && draw->getObject()->getCurrentWeapon()) { WeaponSlotType wslot = draw->getObject()->getCurrentWeapon()->getWeaponSlot(); if (wslot != command->getWeaponSlot()) return COMMAND_AVAILABLE; } } return COMMAND_ACTIVE; } case GUI_COMMAND_HACK_INTERNET: { AIUpdateInterface *ai = obj->getAI(); if( ai ) { HackInternetAIInterface *hackAI = ai->getHackInternetAIInterface(); if( hackAI && hackAI->isHackingPackingOrUnpacking() ) { return COMMAND_RESTRICTED; } } return COMMAND_AVAILABLE; } case GUI_COMMAND_STOP: { if( !BitTest( command->getOptions(), OPTION_ONE ) ) { return COMMAND_AVAILABLE; } //We're dealing with a strategy center stop button. Only show the button //if we're in bombardment mode (to stop the artillery cannon). static NameKeyType key_BattlePlanUpdate = NAMEKEY( "BattlePlanUpdate" ); BattlePlanUpdate *bpUpdate = (BattlePlanUpdate*)obj->findUpdateModule( key_BattlePlanUpdate ); if( bpUpdate && bpUpdate->getActiveBattlePlan() != PLANSTATUS_BOMBARDMENT ) { return COMMAND_RESTRICTED; } return COMMAND_AVAILABLE; } } // all is well with the command return COMMAND_AVAILABLE; } // end getCommandAvailability