Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**:
- Right-click to command
1 change: 1 addition & 0 deletions Phobos.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
<ClCompile Include="src\Misc\MessageColumn.cpp" />
<ClCompile Include="src\Misc\PhobosToolTip.cpp" />
<ClCompile Include="src\Misc\RetryDialog.cpp" />
<ClCompile Include="src\Misc\RightClickCommand.cpp" />
<ClCompile Include="src\Misc\Selection.cpp" />
<ClCompile Include="src\Misc\SyncLogging.cpp" />
<ClCompile Include="src\Misc\TextInput.cpp" />
Expand Down
16 changes: 16 additions & 0 deletions docs/User-Interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,22 @@ 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.
- Enable it with `RightClickCommand=true`.

```{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

![selectbox](_static/images/selectbox.png)
Expand Down
2 changes: 2 additions & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ ShowBriefing=true ; boolean
DigitalDisplay.Enable=false ; boolean
ShowDesignatorRange=false ; boolean
PrioritySelectionFiltering=true ; boolean
RightClickCommand=false ; boolean
PriorityDeployFiltering=true ; boolean
ShowPlacementPreview=yes ; boolean
RealTimeTimers=false ; boolean
Expand Down Expand Up @@ -386,6 +387,7 @@ HideShakeEffects=false ; boolean
:open:

#### New:
- [Right-click to command](User-Interface.md#right-click-to-command) (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)
Expand Down
151 changes: 151 additions & 0 deletions src/Misc/RightClickCommand.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#include <Phobos.h>

#include <MapClass.h>
#include <ObjectClass.h>
#include <DisplayClass.h>
#include <GeneralDefinitions.h>
#include <Ext/Techno/Body.h> // 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; the LBUTTONUP
// hook consumes (clears) it.
static bool RmbCommandInProgress = false;

// 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<Action>(R->EAX())))
{
R->EAX(static_cast<DWORD>(Action::None));
return true;
}
}
return false;
}
}

// 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<int>(0x34);
const int rawX = static_cast<short>(packed & 0xFFFF);
const int rawY = static_cast<short>((packed >> 16) & 0xFFFF);

const int originX = *reinterpret_cast<int*>(0x886FA0);
const int originY = *reinterpret_cast<int*>(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;
}

// 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)
{
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.
if (RightClickCommand::RmbCommandInProgress)
{
RightClickCommand::RmbCommandInProgress = false;
return 0;
}

if (RightClickCommand::NeutraliseLeftCommand(R))
MapClass::UnselectAll();

return 0;
}
2 changes: 2 additions & 0 deletions src/Phobos.INI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ 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::TypeSelectUseIFVMode = true;
bool Phobos::Config::DevelopmentCommands = true;
bool Phobos::Config::SuperWeaponSidebarCommands = false;
Expand Down Expand Up @@ -94,6 +95,7 @@ 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::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);
Expand Down
1 change: 1 addition & 0 deletions src/Phobos.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class Phobos
static bool ToolTipBlur;
static bool PrioritySelectionFiltering;
static bool PriorityDeployFiltering;
static bool RightClickCommand;
static bool TypeSelectUseIFVMode;
static bool DevelopmentCommands;
static bool SuperWeaponSidebarCommands;
Expand Down