diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 85aba4228be..2311862b272 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -76,6 +76,7 @@ class OptionPreferences : public UserPreferences Real getScrollFactor(); Bool getDrawScrollAnchor(); Bool getMoveScrollAnchor(); + Bool getEasyMilitaryDragEnabled() const; Bool getCursorCaptureEnabledInWindowedGame() const; Bool getCursorCaptureEnabledInWindowedMenu() const; Bool getCursorCaptureEnabledInFullscreenGame() const; diff --git a/Core/GameEngine/Include/GameClient/SelectionInfo.h b/Core/GameEngine/Include/GameClient/SelectionInfo.h index 509c5923571..c21928f3622 100644 --- a/Core/GameEngine/Include/GameClient/SelectionInfo.h +++ b/Core/GameEngine/Include/GameClient/SelectionInfo.h @@ -68,12 +68,29 @@ struct PickDrawableStruct Bool isPointSelection; Bool forceAttackMode; + // TheSuperHackers @feature EasyMilitaryDrag leaves builders out of a drag selection. Cached here + // rather than read per drawable, because the constructor runs once per selection while the + // callback runs for every drawable in the region. + Bool easyMilitaryDrag; + // Set instead of easyMilitaryDrag when Ctrl is held: select only the builders the option + // normally skips, so they can still be boxed deliberately. + Bool easyMilitaryDragInverted; + // Set for a second pass when the first found nothing, which suspends the filter entirely so a + // box holding only builders still selects them rather than coming back empty. + Bool easyMilitaryDragDisabled; + // Note, this is OR'd with the things we are attempting to select. KindOfMaskType kindofsToMatch; PickDrawableStruct(); }; +//------------------------------------------------------------------------------------------------- +// TheSuperHackers @feature TRUE while a Ctrl held drag should be treated as an inverted +// EasyMilitaryDrag selection rather than as force attack targeting. Ctrl drives both, so every +// place that asks "are we force attacking?" during a drag has to agree on the answer. +extern Bool isEasyMilitaryDragInvertedActive( Bool selectionIsPoint ); + //------------------------------------------------------------------------------------------------- extern Bool contextCommandForNewSelection(const DrawableList *currentlySelectedDrawables, const DrawableList *newlySelectedDrawables, diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index e681ef8b192..e4a94c5bcc5 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -216,6 +216,21 @@ Bool OptionPreferences::getRightMouseScrollWithAlternateMouseEnabled() const return FALSE; } +// TheSuperHackers @feature Options.ini: EasyMilitaryDrag = Yes leaves builders out of a drag +// selection, so boxing over a base picks up the army without dragging them along. Covers +// KINDOF_DOZER and KINDOF_IGNORES_SELECT_ALL, the same kinds Select All already disqualifies. +Bool OptionPreferences::getEasyMilitaryDragEnabled() const +{ + OptionPreferences::const_iterator it = find("EasyMilitaryDrag"); + if (it == end()) + return FALSE; + + if (stricmp(it->second.str(), "yes") == 0) { + return TRUE; + } + return FALSE; +} + Bool OptionPreferences::getRetaliationModeEnabled() { OptionPreferences::const_iterator it = find("Retaliation"); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp index 321423df5b1..4fe34f07ce0 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp @@ -740,6 +740,49 @@ GameMessageDisposition SelectionTranslator::onMouseLeftClick(MAYBE_UNUSED const pds.isPointSelection = isPoint; TheTacticalView->iterateDrawablesInRegion(&selectionRegion, addDrawableToList, &pds); + // TheSuperHackers @feature EasyMilitaryDrag drops builders from a drag, but if that leaves + // no actual units then the box held only builders, and refusing to select them is just an + // unresponsive click. Run the region again with the filter suspended so they are selected + // after all. + // + // The test is "no units survived", not "the list is empty", because structures are never + // filtered and so stay in the list. Dragging over workers standing next to a building left + // the building as the only survivor, and the selection logic below has a branch that hands + // you a building when it is the only selectable thing in the box -- so the drag silently + // grabbed the building instead of the workers. + // + // Deliberately not done for the Ctrl inverted drag: there the player has explicitly asked + // for the builders, so grabbing the army instead would be the opposite of the request. + if (pds.easyMilitaryDrag) + { + Bool anyNonStructure = FALSE; + for (DrawableListIt it = drawablesThatWillSelect.begin(); + it != drawablesThatWillSelect.end(); ++it) + { + // Only units the final selection loop will actually keep count: a foreign drawable + // is rejected by the ownership filtering downstream, and a rider inside a container + // such as the Overlord's bunker is discarded as contained - letting either satisfy + // this test would suppress the builders-only pass and leave the drag selecting + // nothing. + const Drawable *d = *it; + if (d && !d->isKindOf(KINDOF_STRUCTURE) && + d->getObject() && d->getObject()->isLocallyControlled() && + d->getObject()->getContainedBy() == nullptr) + { + anyNonStructure = TRUE; + break; + } + } + + if (!anyNonStructure) + { + // start clean, or the structures already gathered would be listed twice + drawablesThatWillSelect.clear(); + pds.easyMilitaryDragDisabled = TRUE; + TheTacticalView->iterateDrawablesInRegion(&selectionRegion, addDrawableToList, &pds); + } + } + if (drawablesThatWillSelect.empty()) { return KEEP_MESSAGE; diff --git a/Core/GameEngine/Source/GameClient/SelectionInfo.cpp b/Core/GameEngine/Source/GameClient/SelectionInfo.cpp index e5e273f4722..b75a4c7591a 100644 --- a/Core/GameEngine/Source/GameClient/SelectionInfo.cpp +++ b/Core/GameEngine/Source/GameClient/SelectionInfo.cpp @@ -28,6 +28,8 @@ #include "GameLogic/Module/ContainModule.h" #include "Common/ActionManager.h" +// TheSuperHackers @feature for the EasyMilitaryDrag option +#include "Common/GlobalData.h" #include "Common/ThingTemplate.h" #include "Common/PlayerList.h" #include "Common/Player.h" @@ -38,6 +40,8 @@ #include "GameClient/Drawable.h" #include "GameClient/GameClient.h" #include "GameClient/KeyDefs.h" +// TheSuperHackers @feature for the EasyMilitaryDrag ctrl override +#include "GameClient/Keyboard.h" //------------------------------------------------------------------------------------------------- @@ -62,10 +66,52 @@ SelectionInfo::SelectionInfo() : selectFriends(FALSE) { } +//------------------------------------------------------------------------------------------------- +// TheSuperHackers @feature See SelectionInfo.h. A point selection is never an inverted drag, so +// plain Ctrl clicking still force attacks exactly as it always did. +Bool isEasyMilitaryDragInvertedActive( Bool selectionIsPoint ) +{ + if (selectionIsPoint) + return FALSE; + + if (!TheKeyboard || !TheKeyboard->isCtrl()) + return FALSE; + + // Read from the cached GlobalData copy: constructing OptionPreferences reparses + // Options.ini from disk, far too heavy for the input path. + return TheGlobalData && TheGlobalData->m_easyMilitaryDrag; +} + //------------------------------------------------------------------------------------------------- PickDrawableStruct::PickDrawableStruct() : drawableListToFill(nullptr), isPointSelection(FALSE) { forceAttackMode = TheInGameUI->isInForceAttackMode(); + + // TheSuperHackers @feature Read once per selection, not once per drawable. Holding Ctrl + // inverts the filter for this one drag, so the builders it normally skips are exactly what + // gets boxed -- the escape hatch for grabbing them deliberately. + // + // Ctrl is also what puts the game into force attack mode, so an inverted drag has to cancel + // that for this selection. Otherwise the click is treated as attack targeting and nothing is + // selected at all. Force attack has nothing to act on here anyway: this only engages while + // dragging a box, and what it selects is your own builders. + easyMilitaryDrag = FALSE; + easyMilitaryDragInverted = FALSE; + easyMilitaryDragDisabled = FALSE; + if (TheGlobalData && TheGlobalData->m_easyMilitaryDrag) + { + if (TheKeyboard && TheKeyboard->isCtrl()) + easyMilitaryDragInverted = TRUE; + else + easyMilitaryDrag = TRUE; + } + + // isPointSelection is set by the caller after construction, so this uses the flag resolved + // above rather than isEasyMilitaryDragInvertedActive. The two agree for drags, which is the + // only case that reaches the filter. + if (easyMilitaryDragInverted) + forceAttackMode = FALSE; + UnsignedInt pickType = getPickTypesForContext(forceAttackMode); translatePickTypesToKindof(pickType, kindofsToMatch); if (!forceAttackMode) @@ -90,6 +136,11 @@ extern Bool contextCommandForNewSelection(const DrawableList *currentlySelectedD Bool forceFire = TheInGameUI->isInForceAttackMode(); Bool forceMove = TheInGameUI->isInForceMoveToMode(); + // TheSuperHackers @feature An inverted EasyMilitaryDrag is a selection, not force attack + // targeting, even though Ctrl is what triggers both. + if (isEasyMilitaryDragInvertedActive(selectionIsPoint)) + forceFire = FALSE; + if (forceFire || forceMove) { return FALSE; } @@ -360,6 +411,27 @@ Bool addDrawableToList( Drawable *draw, void *userData ) if (!draw->getTemplate()->isAnyKindOf(pds->kindofsToMatch)) return FALSE; + // TheSuperHackers @feature EasyMilitaryDrag leaves builders out of a drag selection, so boxing + // over your own base picks up the army without dragging them off their work. Only drags are + // affected -- a point selection still picks them normally, as do double click, control groups + // and the select-matching hotkeys. + // + // The two kinds mirror what Select All already disqualifies: KINDOF_DOZER, and the explicit + // KINDOF_IGNORES_SELECT_ALL marker a mod can put on anything else it wants left alone. Note + // KINDOF_DOZER also catches GLA Workers, which carry it alongside KINDOF_INFANTRY and + // KINDOF_HARVESTER. + if (!pds->easyMilitaryDragDisabled && + !pds->isPointSelection && (pds->easyMilitaryDrag || pds->easyMilitaryDragInverted)) + { + const Bool isBuilder = + draw->isKindOf(KINDOF_DOZER) || draw->isKindOf(KINDOF_IGNORES_SELECT_ALL); + + // normally drop the builders; with Ctrl held, drop everything else instead + const Bool wantBuilders = pds->easyMilitaryDragInverted; + if (isBuilder != wantBuilders) + return FALSE; + } + if (!draw->isSelectable()) { const Object *obj = draw->getObject(); diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 0c8e8820660..a7d71d210f9 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -141,6 +141,8 @@ class GlobalData : public SubsystemInterface Bool m_useAlternateMouse; Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode. Bool m_clientRetaliationModeEnabled; + // TheSuperHackers @feature Leave builders out of drag selections. + Bool m_easyMilitaryDrag; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index f7720c351a2..cc6b9580f76 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1047,6 +1047,7 @@ GlobalData::GlobalData() m_useRightMouseScrollWithAlternateMouse = TRUE; #endif m_clientRetaliationModeEnabled = TRUE; //On by default. + m_easyMilitaryDrag = FALSE; m_doubleClickAttackMove = FALSE; } @@ -1199,6 +1200,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_easyMilitaryDrag = optionPref.getEasyMilitaryDragEnabled(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 89a5fa08f9d..30bab02ef2f 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -142,6 +142,8 @@ class GlobalData : public SubsystemInterface Bool m_useAlternateMouse; Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode. Bool m_clientRetaliationModeEnabled; + // TheSuperHackers @feature Leave builders out of drag selections. + Bool m_easyMilitaryDrag; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e862cd149d5..6294ac511b8 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1062,6 +1062,7 @@ GlobalData::GlobalData() m_useRightMouseScrollWithAlternateMouse = TRUE; #endif m_clientRetaliationModeEnabled = TRUE; //On by default. + m_easyMilitaryDrag = FALSE; m_doubleClickAttackMove = FALSE; } @@ -1206,6 +1207,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_easyMilitaryDrag = optionPref.getEasyMilitaryDragEnabled(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor();