Skip to content
Open
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
14 changes: 14 additions & 0 deletions Core/GameEngine/Include/Common/OptionPreferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@
typedef UnsignedInt CursorCaptureMode;
typedef UnsignedInt ScreenEdgeScrollMode;

// TheSuperHackers @feature How targeted commands (guard, attack move, abilities) are triggered.
// Only affects hotkey activation -- a mouse click on a cameo leaves the cursor over the control
// bar, where there is no world position to cast at, so it always uses the normal two step flow.
enum CastMode CPP_11(: Int)
{
CastMode_Normal = 0, ///< click the button, then click the world (retail behavior)
CastMode_QuickCast, ///< hotkey fires immediately at the cursor
CastMode_QuickCastWithIndicator, ///< as above, but flash the targeting decal where it fired

CastMode_Count,
CastMode_Default = CastMode_Normal
};

//-----------------------------------------------------------------------------
// OptionsPreferences options menu class
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -76,6 +89,7 @@ class OptionPreferences : public UserPreferences
Real getScrollFactor();
Bool getDrawScrollAnchor();
Bool getMoveScrollAnchor();
CastMode getCastMode() const;
Bool getCursorCaptureEnabledInWindowedGame() const;
Bool getCursorCaptureEnabledInWindowedMenu() const;
Bool getCursorCaptureEnabledInFullscreenGame() const;
Expand Down
13 changes: 13 additions & 0 deletions Core/GameEngine/Include/GameClient/HotKey.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ class HotKeyManager : public SubsystemInterface
AsciiString searchHotKey( const AsciiString& label);
AsciiString searchHotKey( const UnicodeString& uStr );

// TheSuperHackers @feature True while a button press is being synthesized from a keyboard
// hotkey rather than an actual mouse click. Quick cast needs to tell the two apart.
static void setExecutingHotKey( Bool executing ) { s_executingHotKey = executing; }
static Bool isExecutingHotKey( void ) { return s_executingHotKey; }

// TheSuperHackers @feature True on the key down half of a hold to aim quick cast, when the
// command should arm and show its decal rather than fire.
static void setQuickCastAiming( Bool aiming ) { s_quickCastAiming = aiming; }
static Bool isQuickCastAiming( void ) { return s_quickCastAiming; }

static Bool s_executingHotKey;
static Bool s_quickCastAiming;

private:
typedef std::map<AsciiString, HotKey> HotKeyMap;
HotKeyMap m_hotKeyMap;
Expand Down
21 changes: 21 additions & 0 deletions Core/GameEngine/Source/Common/OptionPreferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,27 @@ Bool OptionPreferences::getRightMouseScrollWithAlternateMouseEnabled() const
return FALSE;
}

// TheSuperHackers @feature Options.ini: CastMode = Normal | QuickCast | QuickCastWithIndicator
CastMode OptionPreferences::getCastMode() const
{
OptionPreferences::const_iterator it = find("CastMode");
if (it == end())
return CastMode_Default;

if (stricmp(it->second.str(), "QuickCastWithIndicator") == 0)
return CastMode_QuickCastWithIndicator;
if (stricmp(it->second.str(), "QuickCast") == 0)
return CastMode_QuickCast;
if (stricmp(it->second.str(), "Normal") == 0)
return CastMode_Normal;

Int mode = atoi(it->second.str());
if (mode >= 0 && mode < CastMode_Count)
return (CastMode)mode;

return CastMode_Default;
}

Bool OptionPreferences::getRetaliationModeEnabled()
{
OptionPreferences::const_iterator it = find("Retaliation");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@
#include "GameClient/GameWindowManager.h"
#include "GameClient/InGameUI.h"
#include "GameClient/AnimateWindowManager.h"
// TheSuperHackers @feature for quick cast
#if RTS_ZEROHOUR
#include "Common/OptionPreferences.h"
#include "Common/Recorder.h"
#include "GameClient/HotKey.h"
#include "GameClient/Mouse.h"
#include "GameClient/View.h"
#include "GameLogic/Module/SpecialPowerModule.h"
#endif

#include "GameLogic/GameLogic.h"
#include "GameLogic/Object.h"
Expand Down Expand Up @@ -117,6 +126,123 @@ CBCommandStatus ControlBar::processCommandTransitionUI( GameWindow *control, Gad
/** Process a button selected message from the window system that should be for one of
* our GUI commands */
//-------------------------------------------------------------------------------------------------
#if RTS_ZEROHOUR
// TheSuperHackers @feature Quick cast (Options.ini: CastMode).
/**
* Fire a targeted command at the cursor instead of waiting for a second click.
*
* Returns TRUE only if the command was actually dispatched. Every rejection path returns
* FALSE so the caller arms the command normally -- an input is never silently eaten.
*
* Deliberately limited to keyboard activation. Clicking a cameo with the mouse leaves the
* cursor over the control bar, where there is no world position worth targeting.
*/
static Bool tryQuickCast( const CommandButton *commandButton )
{
if( commandButton == nullptr || TheGlobalData == nullptr )
return FALSE;

if( TheGlobalData->m_castMode == CastMode_Normal )
return FALSE;

// only from a hotkey -- see the note above
if( !HotKeyManager::isExecutingHotKey() )
return FALSE;

// Hold to aim: on the key down pass we want the normal arming path, which already shows the
// targeting decal and lets the player move the cursor. The key up pass then fires.
if( HotKeyManager::isQuickCastAiming() )
return FALSE;

if( TheInGameUI == nullptr || TheMouse == nullptr || TheTacticalView == nullptr )
return FALSE;

// leave replay playback alone, matching InGameUI::setGUICommand
if( TheRecorder && TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK )
return FALSE;

const UnsignedInt options = commandButton->getOptions();

// A single use command burns the button permanently, so a misfire is unrecoverable --
// never fire one blind.
if( BitIsSet( options, SINGLE_USE_COMMAND ) )
return FALSE;

// Rally points and beacons place a marker wherever the cursor happens to be, which is
// silent and easy to miss. Structure placement needs a deliberate footprint.
switch( commandButton->getCommandType() )
{
case GUI_COMMAND_SET_RALLY_POINT:
case GUICOMMANDMODE_PLACE_BEACON:
case GUI_COMMAND_DOZER_CONSTRUCT:
case GUI_COMMAND_SPECIAL_POWER_CONSTRUCT:
case GUI_COMMAND_SPECIAL_POWER_CONSTRUCT_FROM_SHORTCUT:
return FALSE;

// Superweapons are excluded on purpose: firing one at an unintended spot cannot be
// undone, and the stray keypress that does it is easy to make.
case GUI_COMMAND_SPECIAL_POWER:
case GUI_COMMAND_SPECIAL_POWER_FROM_SHORTCUT:
return FALSE;

default:
break;
}

// The cursor has to be over the battlefield, not the command bar or another panel.
const MouseIO *mouseIO = TheMouse->getMouseStatus();
if( mouseIO == nullptr )
return FALSE;

if( TheWindowManager &&
TheWindowManager->getWindowUnderCursor( mouseIO->pos.x, mouseIO->pos.y ) != nullptr )
return FALSE;

// TheSuperHackers @feature If the ability is still recharging, remember the cast and let
// InGameUI fire it the moment the logic side says it is ready, rather than throwing the
// input away. The cooldown itself is untouched -- this only stops the press being wasted.
if( commandButton->getSpecialPowerTemplate() )
{
Drawable *draw = TheInGameUI->getFirstSelectedDrawable();
Object *source = draw ? draw->getObject() : nullptr;
if( source )
{
SpecialPowerModuleInterface *mod =
source->getSpecialPowerModule( commandButton->getSpecialPowerTemplate() );
if( mod && !mod->isReady() )
{
TheInGameUI->queueQuickCast( commandButton, mouseIO->pos );
TheInGameUI->triggerQuickCastHint( commandButton, mouseIO->pos );
return TRUE;
}
}
}

// Hand the click to the normal path. Synthesizing the message rather than calling the
// do*Command helpers directly means quick cast reuses the engine's own validation,
// voice responses and cleanup, and cannot drift away from normal behaviour.
//
// In hold to aim mode the command was already armed on key down; re-arming here is
// harmless and covers the case where something cleared it while the key was held.
TheInGameUI->setGUICommand( commandButton );

// GUICommandTranslator reads the click position from pixelRegion.hi
IRegion2D clickRegion;
clickRegion.lo = mouseIO->pos;
clickRegion.hi = mouseIO->pos;

GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_MOUSE_LEFT_CLICK );
msg->appendPixelRegionArgument( clickRegion );

// In indicator mode the decal has been visible the whole time the key was held, so let it
// linger briefly at the point it fired rather than vanishing the instant the key comes up.
if( TheGlobalData->m_castMode == CastMode_QuickCastWithIndicator )
TheInGameUI->triggerQuickCastHint( commandButton, mouseIO->pos );

return TRUE;
}
#endif // RTS_ZEROHOUR

CBCommandStatus ControlBar::processCommandUI( GameWindow *control,
GadgetGameMessage gadgetMessage )
{
Expand Down Expand Up @@ -221,8 +347,27 @@ CBCommandStatus ControlBar::processCommandUI( GameWindow *control,
//with. For example, the terrorist can jack a car and convert it into a carbomb, but he has to
//click on a valid car. In this case the doCommandOrHint code will determine if the mode is valid
//or not and the cursor modes will be set appropriately.

#if RTS_ZEROHOUR
// TheSuperHackers @feature Quick cast fires the command at the cursor instead of waiting for
// a second click. If it declines -- wrong mode, unsafe command, cursor not over the
// battlefield -- fall through and arm normally, so nothing is ever silently swallowed.
if( tryQuickCast( commandButton ) )
return CBC_COMMAND_USED;
#endif

TheInGameUI->setGUICommand( commandButton );
}
#if RTS_ZEROHOUR
// TheSuperHackers @fix In hold to aim mode the key down pass exists only to arm targeted
// commands so the decal shows while aiming. Everything else must act on the key up pass
// alone - otherwise a production hotkey queues two units per press and a toggle undoes
// itself, one per key transition.
else if( HotKeyManager::isQuickCastAiming() )
{
// swallow the key down half; the key up pass executes normally
}
#endif
else switch( commandButton->getCommandType() )
{

Expand Down
65 changes: 61 additions & 4 deletions Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
// USER INCLUDES //////////////////////////////////////////////////////////////
//-----------------------------------------------------------------------------
#include "GameClient/HotKey.h"
// TheSuperHackers @feature for hold to aim quick cast
#include "Common/GlobalData.h"
#include "Common/OptionPreferences.h"
#include "GameClient/KeyDefs.h"
#include "GameClient/MetaEvent.h"
#include "GameClient/GameWindow.h"
Expand All @@ -72,7 +75,17 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage
GameMessageDisposition disp = KEEP_MESSAGE;
GameMessage::Type t = msg->getType();

if ( t == GameMessage::MSG_RAW_KEY_UP)
// TheSuperHackers @feature In QuickCastWithIndicator mode a hotkey arms on key down so the
// player can see the targeting decal and still adjust aim, then fires on key up. Every other
// mode keeps the original behaviour of acting only on key up.
#if RTS_ZEROHOUR
const Bool holdToAim = TheGlobalData &&
TheGlobalData->m_castMode == CastMode_QuickCastWithIndicator;
#else
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const Bool holdToAim = FALSE;
#endif

if ( t == GameMessage::MSG_RAW_KEY_UP || (holdToAim && t == GameMessage::MSG_RAW_KEY_DOWN) )
{

//char key = msg->getArgument(0)->integer;
Expand Down Expand Up @@ -103,8 +116,16 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage
uKey.concat(key);
AsciiString aKey;
aKey.translate(uKey);
if(TheHotKeyManager && TheHotKeyManager->executeHotKey(aKey))
disp = DESTROY_MESSAGE;
if( TheHotKeyManager )
{
// key down arms and previews, key up commits at wherever the cursor ended up
HotKeyManager::setQuickCastAiming( holdToAim && t == GameMessage::MSG_RAW_KEY_DOWN );

if( TheHotKeyManager->executeHotKey(aKey) )
disp = DESTROY_MESSAGE;

HotKeyManager::setQuickCastAiming( FALSE );
}
}
return disp;
}
Expand Down Expand Up @@ -157,6 +178,10 @@ void HotKeyManager::addHotKey( GameWindow *win, const AsciiString& keyIn)
m_hotKeyMap[key] = newHK;
}

// TheSuperHackers @feature See HotKey.h -- set only while synthesizing a button press.
Bool HotKeyManager::s_executingHotKey = FALSE;
Bool HotKeyManager::s_quickCastAiming = FALSE;

//-----------------------------------------------------------------------------
Bool HotKeyManager::executeHotKey( const AsciiString& keyIn )
{
Expand All @@ -170,9 +195,41 @@ Bool HotKeyManager::executeHotKey( const AsciiString& keyIn )
return FALSE;
if( !BitIsSet( win->winGetStatus(), WIN_STATUS_HIDDEN ) )
{
if( BitIsSet( win->winGetStatus(), WIN_STATUS_ENABLED ) )
// TheSuperHackers @feature A button that is merely not ready yet -- a recharging ability,
// or a weapon still working through its burst -- is disabled, which normally swallows the
// hotkey outright. In quick cast let it through anyway.
//
// Without this a repeat press is dropped here, before quick cast ever runs, and the only
// way to retarget is to Stop first. A FIRE_WEAPON cameo reports COMMAND_NOT_READY for as
// long as its weapon is not READY_TO_FIRE, so any unit still shooting has its own button
// disabled underneath the player. Retargeting mid burst is expected behaviour -- see the
// DragonTank firewall note in ControlBarCommand.cpp, which describes the same case.
//
// Deliberately narrow: this only opens up buttons disabled by WIN_STATUS_NOT_READY. Ones
// that are restricted or unaffordable stay rejected, and the order still only does
// anything if the logic side accepts it, so this cannot fire something that is genuinely
// unavailable -- it just stops the keypress being thrown away before it is even looked at.
#if RTS_ZEROHOUR
Bool allowWhileNotReady = FALSE;
if( !BitIsSet( win->winGetStatus(), WIN_STATUS_ENABLED ) &&
BitIsSet( win->winGetStatus(), WIN_STATUS_NOT_READY ) &&
TheGlobalData &&
TheGlobalData->m_castMode != CastMode_Normal )
{
allowWhileNotReady = TRUE;
}
#else
const Bool allowWhileNotReady = FALSE;
#endif

if( BitIsSet( win->winGetStatus(), WIN_STATUS_ENABLED ) || allowWhileNotReady )
{
// TheSuperHackers @feature Tell the command bar this press came from the keyboard, so
// quick cast can fire at the cursor. A mouse click on the cameo leaves the cursor over
// the control bar, where there is nothing sensible to target.
HotKeyManager::setExecutingHotKey( TRUE );
TheWindowManager->winSendSystemMsg( win->winGetParent(), GBM_SELECTED, (WindowMsgData)win, win->winGetWindowId() );
HotKeyManager::setExecutingHotKey( FALSE );

// here we make the same click sound that the GUI uses when you click a button
AudioEventRTS buttonClick("GUIClick");
Expand Down
13 changes: 13 additions & 0 deletions Core/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,19 @@ GameMessageDisposition SelectionTranslator::onRawMouseRightButtonUp(MAYBE_UNUSED
{
//Added support to cancel the GUI command without deselecting the unit(s) involved
//when you right click.
// TheSuperHackers @feature Right click also cancels a queued quick cast, so a cast
// waiting on a cooldown can be called off the same way an armed command is.
#if RTS_ZEROHOUR
if( TheInGameUI->hasQueuedQuickCast() )
{
TheInGameUI->cancelQueuedQuickCast();
TheInGameUI->setScrolling( FALSE );

//With a queued cast cancel, we want no other behavior.
return DESTROY_MESSAGE;
}
#endif

if( TheInGameUI->getGUICommand() )
{
//Cancel GUI command mode... don't deselect units.
Expand Down
3 changes: 3 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ 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 Hotkey activation behaviour for targeted commands.
// Holds a CastMode; stored as Int to avoid pulling OptionPreferences.h in here.
Int m_castMode;
Bool m_doubleClickAttackMove;
Bool m_rightMouseAlwaysScrolls;
Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots.
Expand Down
Loading