diff --git a/CREDITS.md b/CREDITS.md
index 4120d2999a..2fd83d4fbe 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -924,4 +924,6 @@ 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
-- **obsidianus** - Automatic conversion based on health
+- **Igor Kolchinskii (leosnake2208)**:
+ - Right-click to command
+ - Type selection by double/triple-click
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index 781debeb8a..5b67a2cd7b 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -270,8 +270,10 @@
+
+
diff --git a/docs/User-Interface.md b/docs/User-Interface.md
index 31fed8798a..03fcfd8927 100644
--- a/docs/User-Interface.md
+++ b/docs/User-Interface.md
@@ -320,6 +320,24 @@ RealTimeTimers=false ; boolean
RealTimeTimers.Adaptive=false ; boolean
```
+### Right-click to command
+
+- An optional control scheme matching modern RTS games: the **right mouse button** issues orders to the current selection (move, attack, enter, capture, etc.), while the **left mouse button** only selects, box-selects and self-deploys, and deselects when clicking empty ground. It reuses the game's own command dispatch, so all order and network logic is unchanged.
+ - Special left-click modes are preserved on both buttons: while placing a building, repairing, selling, toggling power, planting a beacon, planning a path or targeting a superweapon, the left button performs that action and the right button cancels it, exactly as in vanilla.
+- The minimap follows the same scheme: the right button orders the current selection to the clicked spot, the left button moves the view there. In vanilla it is the other way around, the left button commands and only moves the view when there is nothing to command.
+- Enable it with `RightClickCommand=true`.
+- Because the left button no longer commands, double and triple clicks are free for [type selection](#type-selection-by-multi-click).
+
+```{note}
+This only changes mouse behaviour; keyboard hotkeys are unaffected. It does not rebind any keys.
+```
+
+In `RA2MD.INI`:
+```ini
+[Phobos]
+RightClickCommand=false ; boolean
+```
+
### Select Box

@@ -465,6 +483,26 @@ 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 near it; triple-clicking selects every unit of that group across the whole map. This adds the double/triple-click gesture common to modern RTS games next to the vanilla type-select hotkey (hold `T` and click). Only your own selectable units are affected, and the selection group is the same one the hotkey uses (the `[TechnoType] -> GroupAs` tag, falling back to the type's ID).
+- Enable it with `TypeSelectByMultiClick=true`. It requires [right-click to command](#right-click-to-command) and is ignored without it, see the note below.
+- `TypeSelectByMultiClick.Range` is how far a double-click reaches, in cells around the clicked unit. A negative value means everything currently drawn on screen.
+- `TypeSelectByMultiClick.DeployDelay` is how long, in milliseconds, the left button refuses to deploy a unit after a click selected it. Without this delay the second click of a double-click would unpack an MCV instead of selecting its group. Set it to `0` to turn the delay off.
+ - The delay starts when a click selects something, so it does not cover a unit that is already selected on its own. Double-clicking such a unit still deploys it on the first click, because at that moment nothing tells the game that a second click is coming.
+
+```{note}
+This needs the left mouse button to be select-only, so it only works together with `RightClickCommand=true`. With vanilla controls a click on an already selected unit is a command, so a double-click would deploy an MCV or an Allied GI rather than select the group. If you enable it anyway, it is turned off and a line is written to the debug log.
+```
+
+In `RA2MD.ini`:
+```ini
+[Phobos]
+TypeSelectByMultiClick=false ; boolean
+TypeSelectByMultiClick.Range=-1 ; integer, cells
+TypeSelectByMultiClick.DeployDelay=500 ; integer, milliseconds
+```
+
### 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 929fbfad66..6ffe82a92f 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -154,6 +154,10 @@ ShowBriefing=true ; boolean
DigitalDisplay.Enable=false ; boolean
ShowDesignatorRange=false ; boolean
PrioritySelectionFiltering=true ; boolean
+RightClickCommand=false ; boolean
+TypeSelectByMultiClick=false ; boolean
+TypeSelectByMultiClick.Range=-1 ; integer, cells
+TypeSelectByMultiClick.DeployDelay=500 ; integer, milliseconds
PriorityDeployFiltering=true ; boolean
ShowPlacementPreview=yes ; boolean
RealTimeTimers=false ; boolean
@@ -402,6 +406,8 @@ HideShakeEffects=false ; boolean
:open:
#### New:
+- [Right-click to command](User-Interface.md#right-click-to-command) (by leosnake2208)
+- [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..5aeb4d7dcd
--- /dev/null
+++ b/src/Misc/MultiClickTypeSelect.cpp
@@ -0,0 +1,142 @@
+#include
+
+#include
+#include
+#include
+#include
+
+// Multi-click type selection:
+// * double-click a unit -> select every unit of its selection group nearby;
+// * triple-click a unit -> select that group across the whole map.
+//
+// Vanilla only has the type-select hotkey (hold T, then click or drag); there is no
+// double-click equivalent, a double click in the tactical area falls through to the message
+// handler's default case. Grouping reuses TechnoTypeExt::GetSelectionGroupID /
+// HasSelectionGroupID (the GroupAs tag, with the type ID as fallback), so it stays consistent
+// with the hotkey type select in Selection.cpp.
+//
+// This needs RightClickCommand. With the vanilla left button a click on an already selected
+// unit is a command, so a double click would deploy an MCV or an Allied GI instead of
+// selecting. Phobos.INI.cpp turns the setting off if RightClickCommand is not enabled.
+
+namespace RightClickCommand
+{
+ // Defined in RightClickCommand.cpp. Set by the RMB command redirect, cleared here.
+ extern bool RmbCommandInProgress;
+}
+
+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 clicked
+ // unit's selection group. wholeMap takes the whole map; otherwise the reach is
+ // TypeSelectByMultiClick.Range cells around the clicked unit, or, if that is negative,
+ // everything drawn in the tactical viewport.
+ static void SelectSameGroup(FootClass* pClicked, bool wholeMap)
+ {
+ const char* groupID = TechnoTypeExt::GetSelectionGroupID(pClicked->GetTechnoType());
+ const int rangeCells = Phobos::Config::TypeSelectByMultiClick_Range;
+
+ 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 (!wholeMap)
+ {
+ if (rangeCells >= 0)
+ {
+ if (pClicked->DistanceFrom(pFoot) > rangeCells * Unsorted::LeptonsPerCell)
+ continue;
+ }
+ else if (!TacticalClass::Instance->CoordsToClient(pFoot->GetCoords()).second)
+ {
+ continue;
+ }
+ }
+
+ pFoot->Select();
+ }
+ }
+
+ // Update the click streak from the current cursor time/position and act on it. pClicked is
+ // the object the click landed on, or null for empty ground.
+ static void HandleClick(FootClass* pClicked)
+ {
+ 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 (!pClicked) // only mobile units drive type select
+ return;
+
+ if (ClickStreak == 2)
+ SelectSameGroup(pClicked, false);
+ else if (ClickStreak == 3)
+ SelectSameGroup(pClicked, true);
+ }
+}
+
+// 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. Stolen bytes:
+// mov byte ptr [esi+0x555A], bl (absolute operand, safe to relocate).
+DEFINE_HOOK(0x693290, TacticalMsgHandler_LButtonUp_MultiClickTypeSelect, 0x6)
+{
+ // The RMB command redirect (RightClickCommand.cpp) ends here too. Consume the flag and
+ // skip, so an RMB order is not counted as a left click for the streak.
+ if (RightClickCommand::RmbCommandInProgress)
+ {
+ RightClickCommand::RmbCommandInProgress = false;
+ return 0;
+ }
+
+ if (!Phobos::Config::TypeSelectByMultiClick)
+ return 0;
+
+ // The object this click landed on, as ProcessClickCoords resolved it at 0x69325E and
+ // DecideAction and the applier were given it. ESP here equals ESP at the start of the
+ // command dispatch (0x69323E), which is where that output slot is addressed from.
+ // Not CurrentObjects[0]: with several units selected that is not the clicked one.
+ // Null when the click landed on empty ground, so this cast must keep its null check.
+ const auto pClicked = abstract_cast(R->Stack(0x2C));
+
+ MultiClickTypeSelect::HandleClick(pClicked);
+
+ return 0;
+}
diff --git a/src/Misc/RightClickCommand.cpp b/src/Misc/RightClickCommand.cpp
new file mode 100644
index 0000000000..344820c38f
--- /dev/null
+++ b/src/Misc/RightClickCommand.cpp
@@ -0,0 +1,256 @@
+#include
+
+#include
+#include
+#include
+#include
+#include // pulls in the complete FootClass/TechnoClass definitions that
+ // MapClass/DisplayClass inline abstract_casts require
+
+// Right-click to command: the right mouse button issues orders, the left mouse button only
+// selects / deploys (and deselects when clicking empty ground), matching the control style
+// of modern RTS games. Off by default; enable with [Phobos] -> RightClickCommand.
+//
+// The tactical mouse message handler is MouseClass method 0x6930A0, which dispatches Windows
+// mouse messages through a jump table (0x693410). Relevant cases (reverse-engineered):
+// LBUTTONDOWN 0x201 -> 0x693126 (begin select/action; sets the drag flag [this+0x555A]=1)
+// LBUTTONUP 0x202 -> 0x6931F9 (command dispatch: ProcessClickCoords -> DecideAction
+// -> apply 0x4AB9B0, from 0x69323E onward)
+// RBUTTONUP 0x205 -> 0x693366 (vanilla: cancel current mode, then deselect)
+//
+// RBUTTONUP and LBUTTONUP are two cases of the SAME function, sharing one stack frame and
+// the same `this`; both prologues turn [esp+0x34] from a message-point pointer into an inline
+// Point2D. So the RMB-up handler can jump straight into the LMB-up command dispatch at
+// 0x69323E, reusing 100% of the game's command (and network) logic - no argument
+// reconstruction is needed.
+
+namespace RightClickCommand
+{
+ // A special LEFT-click mode is active - RMB must keep its vanilla "cancel" behaviour
+ // (cancel building placement, leave repair/sell/power/beacon/superweapon targeting) and
+ // LMB must keep its vanilla behaviour (repair/sell/place/target).
+ static bool InSpecialLeftClickMode()
+ {
+ auto& d = DisplayClass::Instance;
+ return d.RepairMode
+ || d.SellMode
+ || d.PowerToggleMode
+ || d.PlaceBeaconMode
+ || d.PlanningMode
+ || d.CurrentSWTypeIndex >= 0 // superweapon targeting
+ || d.CurrentBuilding != nullptr; // building placement
+ }
+
+ // Set by the RBUTTONUP command hook right before it jumps into the shared LMB-up command
+ // dispatch (0x69323E), which flows through the LBUTTONUP neutralise hook at 0x693276.
+ // Without this flag that hook would downgrade the RMB-issued order to None. Not static:
+ // MultiClickTypeSelect.cpp reads and clears it at 0x693290, the last point the redirect
+ // passes through.
+ bool RmbCommandInProgress = false;
+
+ // Tick of the click that last selected something, used by HoldsOffDeploy below.
+ static DWORD LastSelectTick = 0;
+
+ // Actions the LEFT button may still perform: selection and self-deploy only. Everything
+ // else (Move/Attack/Enter/Harvest/Capture/Guard/...) is a command and belongs to the
+ // RIGHT button now.
+ static bool IsLeftClickAllowed(Action action)
+ {
+ switch (action)
+ {
+ case Action::None:
+ case Action::Select:
+ case Action::ToggleSelect:
+ case Action::Self_Deploy:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // If right-click-to-command is on and no special left-click mode is active, downgrade the
+ // command action (in EAX, freshly returned by DecideAction) to None so the left button
+ // only selects/deploys. Returns true if a command was actually neutralised (the click
+ // landed on empty ground / an enemy - a command target, not a selectable own unit).
+ static bool NeutraliseLeftCommand(REGISTERS* R)
+ {
+ if (Phobos::Config::RightClickCommand && !InSpecialLeftClickMode())
+ {
+ if (!IsLeftClickAllowed(static_cast(R->EAX())))
+ {
+ R->EAX(static_cast(Action::None));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // A deployable unit is deployed by clicking it again once it is selected, which collides
+ // with the double-click type select: the second click would unpack the MCV instead. So
+ // for a short while after a click selected something, the left button does not deploy.
+ // Same trick Emperor: Battle for Dune uses. Only active with TypeSelectByMultiClick on.
+ //
+ // Known limit: the delay is armed by a click that selects, so a unit that is already
+ // selected on its own is not covered - it deploys on the first click of a double click.
+ // Covering that means holding the deploy back for the delay and cancelling it on the
+ // second click, which needs a per-frame hook to fire the deploy afterwards.
+ static bool DeployHoldOffEnabled()
+ {
+ return Phobos::Config::TypeSelectByMultiClick
+ && Phobos::Config::TypeSelectByMultiClick_DeployDelay > 0;
+ }
+
+ static bool HoldsOffDeploy()
+ {
+ const auto delay = static_cast(Phobos::Config::TypeSelectByMultiClick_DeployDelay);
+
+ return GetTickCount() - LastSelectTick <= delay;
+ }
+
+ // Mouse flags RadarClass::GetMouseAction (0x6539D0) is called with, in its first stack
+ // argument. The "up" bits mean the button is not down, i.e. the cursor is just hovering.
+ enum RadarInput : BYTE
+ {
+ LeftPress = 0x01, LeftHeld = 0x02, LeftRelease = 0x04, LeftUp = 0x08,
+ RightPress = 0x10, RightHeld = 0x20, RightRelease = 0x40, RightUp = 0x80,
+ };
+
+ // Swap the two buttons for the minimap, keeping the hover bits as they are so the cursor
+ // still updates while moving over the radar.
+ static BYTE SwapRadarButtons(BYTE flags)
+ {
+ const BYTE left = flags & (LeftPress | LeftHeld | LeftRelease);
+ const BYTE right = flags & (RightPress | RightHeld | RightRelease);
+
+ return static_cast((flags & (LeftUp | RightUp)) | (left << 4) | (right >> 4));
+ }
+
+ // Runs after DecideAction on both left button hooks. Returns true if the action was
+ // downgraded to a command-less None because of the deploy hold-off, which unlike a
+ // neutralised command must NOT deselect - the unit stays selected and waits.
+ static bool ApplyDeployHoldOff(REGISTERS* R)
+ {
+ const auto action = static_cast(R->EAX());
+
+ // The object the click landed on, as ProcessClickCoords resolved it - the same slot the
+ // game itself reads at 0x693276 to hand it to the applier. Both left button hooks sit at
+ // the same stack depth, so the offset holds for either. Null on empty ground.
+ const auto pClicked = R->Stack(0x30);
+
+ // Whether this click (re)selects the clicked unit: either it was not selected yet, or it
+ // was part of a bigger selection which now narrows down to it. DecideAction reports that
+ // as Select, but also as NoMove or Self_Deploy depending on what sits under the cursor,
+ // so the action alone is not a usable signal - go by the clicked object. Getting this
+ // wrong leaves the hold-off unarmed and the next click unpacks the unit.
+ const bool reselects = pClicked
+ && (!pClicked->IsSelected || ObjectClass::CurrentObjects.Count > 1);
+
+ if (reselects)
+ LastSelectTick = GetTickCount();
+
+ if (action != Action::Self_Deploy || !DeployHoldOffEnabled())
+ return false;
+
+ // A click that reselects is the first click of a possible double click, never a deploy.
+ if (reselects)
+ {
+ R->EAX(static_cast(Action::Select));
+ return true;
+ }
+
+ if (!HoldsOffDeploy())
+ return false;
+
+ R->EAX(static_cast(Action::None));
+ return true;
+ }
+}
+
+// RBUTTONUP handler, just past its "press/drag in progress" gate (cmp [this+0x555A],bl /
+// je 0x693408 at 0x69338F), so it inherits the same gate the vanilla deselect uses. Stolen
+// bytes: cmp byte ptr [0x884D40], bl (absolute operand, safe to relocate).
+//
+// The LMB-up dispatch we jump into (0x69323E) reads the click point as &[esp+0x10], i.e. the
+// view-relative coords (raw window xy minus the tactical view origin at 0x886FA0/0x886FA4).
+// The LMB prologue stores those at [esp+0x10]/[esp+0x14]; the RMB prologue computes the same
+// values but only passes them to 0x63AB00 without storing them, so we populate the slots
+// ourselves before jumping (otherwise ProcessClickCoords reads stale stack and the order
+// lands on the wrong cell).
+DEFINE_HOOK(0x693397, TacticalMsgHandler_RButtonUp_RightClickCommand, 0x6)
+{
+ if (Phobos::Config::RightClickCommand
+ && ObjectClass::CurrentObjects.Count > 0
+ && !RightClickCommand::InSpecialLeftClickMode())
+ {
+ // Raw packed window xy stashed by the RMB prologue at [esp+0x34].
+ const int packed = R->Stack(0x34);
+ const int rawX = static_cast(packed & 0xFFFF);
+ const int rawY = static_cast((packed >> 16) & 0xFFFF);
+
+ const int originX = *reinterpret_cast(0x886FA0);
+ const int originY = *reinterpret_cast(0x886FA4);
+
+ // Feed the LMB-up command dispatch the view-relative click point.
+ R->Stack(0x10, rawX - originX);
+ R->Stack(0x14, rawY - originY);
+
+ // Tell the LBUTTONUP neutralise hook to leave this (RMB-issued) command alone.
+ RightClickCommand::RmbCommandInProgress = true;
+
+ // Issue the order to the current selection instead of deselecting.
+ return 0x69323E;
+ }
+
+ // Vanilla: run the stolen compare and fall through to the cancel/deselect path.
+ return 0;
+}
+
+// The minimap needs the same treatment. RadarClass::GetMouseAction (0x6539D0) decides what a
+// click on the radar does, and vanilla already commands from there: the LEFT button runs the
+// same applier the tactical view uses (0x4AB9B0, called at 0x653D58) and only moves the view
+// when there is nothing to command, while the RIGHT button just moves the view. The whole
+// function reads the buttons out of its flags argument, so swapping the button bits there
+// once turns it around: right commands, left moves the view.
+//
+// Hooked right at the top, before the first read of the argument. Stolen bytes: mov dl,
+// byte ptr [esp+0x48] (the flags we just rewrote) + push ebx.
+DEFINE_HOOK(0x6539D3, RadarClass_GetMouseAction_RightClickCommand, 0x5)
+{
+ if (Phobos::Config::RightClickCommand && !RightClickCommand::InSpecialLeftClickMode())
+ R->Stack8(0x48, RightClickCommand::SwapRadarButtons(R->Stack8(0x48)));
+
+ return 0;
+}
+
+// LBUTTONDOWN: neutralise a command action right after DecideAction so button-down does not
+// preview/issue a command. Stolen bytes: mov reg,[esp+..] + push eax (the possibly-modified
+// EAX is what the following push forwards to the applier).
+DEFINE_HOOK(0x6931B4, TacticalMsgHandler_LButtonDown_RightClickSelectOnly, 0x5)
+{
+ if (!RightClickCommand::ApplyDeployHoldOff(R))
+ RightClickCommand::NeutraliseLeftCommand(R);
+
+ return 0;
+}
+
+// LBUTTONUP: same neutralise, and turn a would-be command on empty ground / an enemy into a
+// deselect (MapClass::UnselectAll). Clicking own units (Select/ToggleSelect) or deploying
+// (Self_Deploy) is not neutralised, so it selects/deploys as normal - no deselect there.
+// Completed band-selects never reach here (0x63A8E0 consumes them and early-exits at
+// 0x693408), so deselecting on a neutralised command is always correct.
+DEFINE_HOOK(0x693276, TacticalMsgHandler_LButtonUp_RightClickSelectOnly, 0x5)
+{
+ // If we arrived here via the RMB command redirect (0x69323E), let the order stand. The
+ // flag is cleared further along the same path, at 0x693290 in MultiClickTypeSelect.cpp.
+ if (RightClickCommand::RmbCommandInProgress)
+ return 0;
+
+ // A held-off deploy leaves the unit selected, so no deselect here.
+ if (RightClickCommand::ApplyDeployHoldOff(R))
+ return 0;
+
+ if (RightClickCommand::NeutraliseLeftCommand(R))
+ MapClass::UnselectAll();
+
+ return 0;
+}
diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp
index e980a9e09e..e771cf5d46 100644
--- a/src/Phobos.INI.cpp
+++ b/src/Phobos.INI.cpp
@@ -2,6 +2,7 @@
#include
+#include
#include
#include
#include
@@ -44,6 +45,10 @@ bool Phobos::Config::ToolTipDescriptions = true;
bool Phobos::Config::ToolTipBlur = false;
bool Phobos::Config::PrioritySelectionFiltering = true;
bool Phobos::Config::PriorityDeployFiltering = true;
+bool Phobos::Config::RightClickCommand = false;
+bool Phobos::Config::TypeSelectByMultiClick = false;
+int Phobos::Config::TypeSelectByMultiClick_Range = -1;
+int Phobos::Config::TypeSelectByMultiClick_DeployDelay = 500;
bool Phobos::Config::TypeSelectUseIFVMode = true;
bool Phobos::Config::DevelopmentCommands = true;
bool Phobos::Config::SuperWeaponSidebarCommands = false;
@@ -92,6 +97,20 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5)
Phobos::Config::ToolTipBlur = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ToolTipBlur", false);
Phobos::Config::PrioritySelectionFiltering = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "PrioritySelectionFiltering", true);
Phobos::Config::PriorityDeployFiltering = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "PriorityDeployFiltering", true);
+ Phobos::Config::RightClickCommand = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "RightClickCommand", false);
+ Phobos::Config::TypeSelectByMultiClick = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "TypeSelectByMultiClick", false);
+ Phobos::Config::TypeSelectByMultiClick_Range = CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "TypeSelectByMultiClick.Range", -1);
+ Phobos::Config::TypeSelectByMultiClick_DeployDelay = CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "TypeSelectByMultiClick.DeployDelay", 500);
+
+ // Multi-click type select only works when the left button is select-only. With vanilla
+ // controls the second click already commands the unit, so it would deploy an MCV or an
+ // Allied GI instead of selecting the group.
+ if (Phobos::Config::TypeSelectByMultiClick && !Phobos::Config::RightClickCommand)
+ {
+ Debug::Log("[Phobos] TypeSelectByMultiClick requires RightClickCommand=true, disabling it.\n");
+ Phobos::Config::TypeSelectByMultiClick = false;
+ }
+
Phobos::Config::TypeSelectUseIFVMode = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "TypeSelectUseIFVMode", true);
Phobos::Config::ShowPlacementPreview = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowPlacementPreview", true);
Phobos::Config::MessageApplyHoverState = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "MessageApplyHoverState", false);
diff --git a/src/Phobos.h b/src/Phobos.h
index 8947fb02ed..f448fff203 100644
--- a/src/Phobos.h
+++ b/src/Phobos.h
@@ -84,6 +84,10 @@ class Phobos
static bool ToolTipBlur;
static bool PrioritySelectionFiltering;
static bool PriorityDeployFiltering;
+ static bool RightClickCommand;
+ static bool TypeSelectByMultiClick;
+ static int TypeSelectByMultiClick_Range;
+ static int TypeSelectByMultiClick_DeployDelay;
static bool TypeSelectUseIFVMode;
static bool DevelopmentCommands;
static bool SuperWeaponSidebarCommands;