diff --git a/CREDITS.md b/CREDITS.md index 731ccc454c..435efb019c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -888,3 +888,5 @@ This page lists all the individual contributions to the project by their author. - **Chang_zhi**: - Interop export interface for accessing scenario local/global variables - Add `ClampToScreen` tag for `BannerType` to control whether banner position is clamped to the visible area +- **Igor Kolchinskii (leosnake2208)**: + - Type selection by double/triple-click diff --git a/Phobos.vcxproj b/Phobos.vcxproj index cea5e31f02..815b17d939 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -263,6 +263,7 @@ + diff --git a/docs/User-Interface.md b/docs/User-Interface.md index 26fc2b8418..52b81006dd 100644 --- a/docs/User-Interface.md +++ b/docs/User-Interface.md @@ -450,6 +450,17 @@ BuildingTypeSelectable=false ; boolean Due to technical limitations, this feature is forcibly disabled without Ares. ``` +### Type selection by multi-click + +- Double-clicking a unit selects every unit of the same selection group currently on screen; triple-clicking selects every unit of that group across the whole map. This complements the vanilla type-select hotkey (hold `T` and click) with the double/triple-click gesture common to modern RTS games. Only your own selectable units are affected, and the selection group is the same one used by the type-select hotkey (the `[TechnoType] -> GroupAs` tag, falling back to the type's ID). +- Enable it with `TypeSelectByMultiClick=true`. + +In `RA2MD.ini`: +```ini +[Phobos] +TypeSelectByMultiClick=false ; boolean +``` + ### Visual effects toggling - It is possible to toggle certain light flash effects off. These light flash effects include: diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 1befcdb9ae..5df6aa0381 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -150,6 +150,7 @@ ShowWeedsCounter=true ; boolean ToolTipDescriptions=true ; boolean ToolTipBlur=false ; boolean SaveGameOnScenarioStart=true ; boolean +TypeSelectByMultiClick=false ; boolean HideLightFlashEffects=false ; boolean HideLaserTrailEffects=false ; boolean HideShakeEffects=false ; boolean @@ -386,6 +387,7 @@ HideShakeEffects=false ; boolean :open: #### New: +- [Type selection by double/triple-click](User-Interface.md#type-selection-by-multi-click) (by leosnake2208) - [Allow using waypoints, area guard and attack move with aircraft](Fixed-or-Improved-Logics.md#extended-aircraft-missions) (by CrimRecya) - [Enhanced Straight trajectory](New-or-Enhanced-Logics.md#straight-trajectory) (by CrimRecya) - [Enable building production queue](User-Interface.md#building-production-queue) (by CrimRecya) diff --git a/src/Misc/MultiClickTypeSelect.cpp b/src/Misc/MultiClickTypeSelect.cpp new file mode 100644 index 0000000000..1bd5ed001d --- /dev/null +++ b/src/Misc/MultiClickTypeSelect.cpp @@ -0,0 +1,111 @@ +#include + +#include +#include +#include +#include + +// Multi-click type selection. +// +// Vanilla Yuri's Revenge only offers "type select" through the type-select hotkey (hold T, +// then click/drag) - there is no double-click equivalent; a double-click in the tactical +// area falls through to the message handler's default case. This adds the modern-RTS gesture: +// * double-click a unit -> select every unit of the same selection group on screen; +// * triple-click a unit -> select every unit of the same selection group across the map. +// +// "Selection group" reuses Phobos' existing concept (TechnoTypeExt::GetSelectionGroupID / +// HasSelectionGroupID, i.e. the [TechnoType]GroupAs tag with the type ID as fallback), so it +// stays consistent with the hotkey-driven type select in Selection.cpp. +// +// Gated behind [Phobos]TypeSelectByMultiClick (default off). + +namespace MultiClickTypeSelect +{ + // Clicks within GetDoubleClickTime() and this many screen pixels of the previous one + // count as part of the same streak. + static constexpr int PositionTolerance = 4; + + static DWORD LastClickTick = 0; + static POINT LastClickPos = { -9999, -9999 }; + static int ClickStreak = 0; + + // Mirrors ExtSelection::ObjectClass_IsSelectable (Selection.cpp): an own, alive, currently + // selectable object. + static bool IsOwnSelectable(TechnoClass* pTechno) + { + const auto pOwner = pTechno->GetOwningHouse(); + return pOwner && pOwner->IsControlledByCurrentPlayer() + && pTechno->CanBeSelected() && pTechno->CanBeSelectedNow() + && !pTechno->InLimbo; + } + + // Add to the current selection every own, selectable mobile unit sharing the just-clicked + // unit's selection group. onScreenOnly limits it to units drawn in the tactical viewport. + static void SelectSameGroup(bool onScreenOnly) + { + if (ObjectClass::CurrentObjects.Count < 1) + return; + + // The single click that preceded this streak left the clicked unit as the selection. + const auto pClicked = abstract_cast(ObjectClass::CurrentObjects.GetItem(0)); + if (!pClicked) + return; // only mobile units drive type select + + const char* groupID = TechnoTypeExt::GetSelectionGroupID(pClicked->GetTechnoType()); + + for (auto const pTechno : TechnoClass::Array) + { + const auto pFoot = abstract_cast(pTechno); + + if (!pFoot || pFoot->IsSelected || !IsOwnSelectable(pFoot)) + continue; + + if (!TechnoTypeExt::HasSelectionGroupID(pFoot->GetTechnoType(), groupID)) + continue; + + if (onScreenOnly && !TacticalClass::Instance->CoordsToClient(pFoot->GetCoords()).second) + continue; + + pFoot->Select(); + } + } + + // Update the click streak from the current cursor time/position and act on it. + static void HandleClick() + { + POINT pos { 0, 0 }; + GetCursorPos(&pos); + const DWORD now = GetTickCount(); + + const int dx = pos.x - LastClickPos.x; + const int dy = pos.y - LastClickPos.y; + const bool sameSpot = dx >= -PositionTolerance && dx <= PositionTolerance + && dy >= -PositionTolerance && dy <= PositionTolerance; + + if (now - LastClickTick <= GetDoubleClickTime() && sameSpot) + ClickStreak = ClickStreak < 3 ? ClickStreak + 1 : 3; + else + ClickStreak = 1; + + LastClickTick = now; + LastClickPos = pos; + + if (ClickStreak == 2) + SelectSameGroup(true); // same group, on screen + else if (ClickStreak == 3) + SelectSameGroup(false); // same group, whole map + } +} + +// Tactical LBUTTONUP handler, just after the click's own selection has been applied (call to +// 0x4AB9B0) and before the drag flag is cleared. This point is only reached by a genuine +// single click - a completed rubber-band selection returns earlier - so the preceding click +// has already made the clicked unit the current selection. Stolen bytes: +// mov byte ptr [esi+0x555A], bl (absolute operand, safe to relocate). +DEFINE_HOOK(0x693290, TacticalClass_LButtonUp_MultiClickTypeSelect, 0x6) +{ + if (Phobos::Config::TypeSelectByMultiClick) + MultiClickTypeSelect::HandleClick(); + + return 0; +} diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index ac9f7649b2..04dd08a16e 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -50,6 +50,7 @@ bool Phobos::Config::ToolTipBlur = false; bool Phobos::Config::PrioritySelectionFiltering = true; bool Phobos::Config::PriorityDeployFiltering = true; bool Phobos::Config::TypeSelectUseIFVMode = true; +bool Phobos::Config::TypeSelectByMultiClick = false; bool Phobos::Config::DevelopmentCommands = true; bool Phobos::Config::SuperWeaponSidebarCommands = false; bool Phobos::Config::ShowPlanningPath = false; @@ -95,6 +96,7 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::Config::PrioritySelectionFiltering = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "PrioritySelectionFiltering", true); Phobos::Config::PriorityDeployFiltering = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "PriorityDeployFiltering", true); Phobos::Config::TypeSelectUseIFVMode = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "TypeSelectUseIFVMode", true); + Phobos::Config::TypeSelectByMultiClick = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "TypeSelectByMultiClick", false); Phobos::Config::ShowPlacementPreview = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowPlacementPreview", true); Phobos::Config::MessageApplyHoverState = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "MessageApplyHoverState", false); Phobos::Config::MessageDisplayInCenter = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "MessageDisplayInCenter", false); diff --git a/src/Phobos.h b/src/Phobos.h index 6bdb4073ec..b4b50d23b4 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -85,6 +85,7 @@ class Phobos static bool PrioritySelectionFiltering; static bool PriorityDeployFiltering; static bool TypeSelectUseIFVMode; + static bool TypeSelectByMultiClick; static bool DevelopmentCommands; static bool SuperWeaponSidebarCommands; static bool ArtImageSwap;