diff --git a/code/command.h b/code/command.h index 9f72cdd69..74c221128 100644 --- a/code/command.h +++ b/code/command.h @@ -19,4 +19,8 @@ class CommandClass virtual char const * Get_Category(void) const = 0; virtual char const * Get_Description(void) const = 0; virtual void Execute(void) const = 0; + + // Repeatable commands re-execute every frame their bound key is held down, since + // Windows key-repeat events never reach the game's keyboard buffer. + virtual bool Is_Repeatable(void) const { return(false); } }; diff --git a/code/init.cpp b/code/init.cpp index c6f727f9e..17392785d 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -4268,9 +4268,12 @@ class ScrollNCommandClass : public CommandClass } virtual void Execute(void) const { - int distance = 34; + int distance = Map.Get_Keyboard_Scroll_Distance(); Map.Scroll_Map(FACING_N, distance, true); } + virtual bool Is_Repeatable(void) const { + return(true); + } }; @@ -4291,9 +4294,12 @@ class ScrollSCommandClass : public CommandClass } virtual void Execute(void) const { - int distance = 34; + int distance = Map.Get_Keyboard_Scroll_Distance(); Map.Scroll_Map(FACING_S, distance, true); } + virtual bool Is_Repeatable(void) const { + return(true); + } }; @@ -4314,9 +4320,12 @@ class ScrollECommandClass : public CommandClass } virtual void Execute(void) const { - int distance = 34; + int distance = Map.Get_Keyboard_Scroll_Distance(); Map.Scroll_Map(FACING_E, distance, true); } + virtual bool Is_Repeatable(void) const { + return(true); + } }; @@ -4337,9 +4346,12 @@ class ScrollWCommandClass : public CommandClass } virtual void Execute(void) const { - int distance = 34; + int distance = Map.Get_Keyboard_Scroll_Distance(); Map.Scroll_Map(FACING_W, distance, true); } + virtual bool Is_Repeatable(void) const { + return(true); + } }; @@ -4360,9 +4372,12 @@ class ScrollNECommandClass : public CommandClass } virtual void Execute(void) const { - int distance = 34; + int distance = Map.Get_Keyboard_Scroll_Distance(); Map.Scroll_Map(FACING_NE, distance, true); } + virtual bool Is_Repeatable(void) const { + return(true); + } }; @@ -4383,9 +4398,12 @@ class ScrollSECommandClass : public CommandClass } virtual void Execute(void) const { - int distance = 34; + int distance = Map.Get_Keyboard_Scroll_Distance(); Map.Scroll_Map(FACING_SE, distance, true); } + virtual bool Is_Repeatable(void) const { + return(true); + } }; @@ -4406,9 +4424,12 @@ class ScrollSWCommandClass : public CommandClass } virtual void Execute(void) const { - int distance = 34; + int distance = Map.Get_Keyboard_Scroll_Distance(); Map.Scroll_Map(FACING_SW, distance, true); } + virtual bool Is_Repeatable(void) const { + return(true); + } }; @@ -4429,9 +4450,12 @@ class ScrollNWCommandClass : public CommandClass } virtual void Execute(void) const { - int distance = 34; + int distance = Map.Get_Keyboard_Scroll_Distance(); Map.Scroll_Map(FACING_NW, distance, true); } + virtual bool Is_Repeatable(void) const { + return(true); + } }; diff --git a/code/mainloop.cpp b/code/mainloop.cpp index 16f292b44..fd96811ab 100644 --- a/code/mainloop.cpp +++ b/code/mainloop.cpp @@ -17,6 +17,7 @@ #include "_bench.h" #include "_command.h" +#include "_keyboar.h" #include "_logic.h" #include "_map.h" #include "_palette.h" @@ -457,6 +458,24 @@ bool Main_Loop(void) void Ingame_Menu_Dialog(void); +// Keyboard->Down() only checks a key's virtual-key code, so a repeatable command bound with +// Shift/Ctrl/Alt needs those modifiers checked separately against live key state. +static bool Is_Repeat_Key_Held(int key) +{ + if (!Keyboard->Down(KeyNumType(key))) { + return(false); + } + + bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0; + bool ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0; + bool alt = (GetKeyState(VK_MENU) & 0x8000) != 0; + + return shift == ((key & WWKEY_SHIFT_BIT) != 0) + && ctrl == ((key & WWKEY_CTRL_BIT) != 0) + && alt == ((key & WWKEY_ALT_BIT) != 0); +} + + /*********************************************************************************************** * Keyboard_Process -- Processes the tactical map input codes. * * * @@ -476,6 +495,15 @@ void Ingame_Menu_Dialog(void); *=============================================================================================*/ void Keyboard_Process(KeyNumType & input) { + // Windows key-repeat events never reach the keyboard buffer, so a repeatable command + // (e.g. map scrolling) has to be re-triggered here by polling its bound key every frame. + for (int index = 0; index < HotkeyCommands.Count(); index++) { + CommandClass const * repeatcmd = HotkeyCommands.Fetch_By_Position(index); + if (repeatcmd->Is_Repeatable() && Is_Repeat_Key_Held(HotkeyCommands.Fetch_ID_By_Position(index))) { + repeatcmd->Execute(); + } + } + /* ** Don't do anything if there is not keyboard event. */ @@ -498,7 +526,10 @@ void Keyboard_Process(KeyNumType & input) if (cmd != NULL) { - cmd->Execute(); + // Repeatable commands are driven by the per-frame poll above instead. + if (!cmd->Is_Repeatable()) { + cmd->Execute(); + } } else { diff --git a/code/scroll.cpp b/code/scroll.cpp index 3b0c2f298..777a26801 100644 --- a/code/scroll.cpp +++ b/code/scroll.cpp @@ -88,6 +88,21 @@ static double _ScrollFraction = 0.0; static double _EdgeScrollRemainder = 0.0; static double _CoastRemainderX = 0.0; static double _CoastRemainderY = 0.0; +static double _KeyScrollRemainder = 0.0; + +// Indexed by an inertia-derived rate; a lower index is a faster step. Shared by edge scroll and +// keyboard scroll so both settle at the same top speed for a given Options.ScrollRate. +static int const _ScrollRateTable[9] = { + 0x00E0*2, + 0x00C0*2, + 0x00A0*2, + 0x0080*2, + 0x0060*2, + 0x0040*2, + 0x0020*2, + 0x0010*2, + 0x0008*2 +}; /*********************************************************************************************** @@ -472,18 +487,6 @@ void ScrollClass::Scroll_Edge(Point2D const & point) ** The mouse is over a scroll region so set the mouse shape accordingly if the map ** can be scrolled in the direction indicated. */ - static int _rate[9] = { - 0x00E0*2, - 0x00C0*2, - 0x00A0*2, - 0x0080*2, - 0x0060*2, - 0x0040*2, - 0x0020*2, - 0x0010*2, - 0x0008*2 - }; - int rate = 8-Inertia; if (rate < Options.ScrollRate+1) { @@ -495,7 +498,7 @@ void ScrollClass::Scroll_Edge(Point2D const & point) ** Increase the scroll rate if the mouse button is held down. */ if (Keyboard->Down(KN_RMOUSE)) { - rate = std::clamp(rate+1, 4, (int)(sizeof(_rate)/sizeof(_rate[0]))-1); + rate = std::clamp(rate+1, 4, (int)(sizeof(_ScrollRateTable)/sizeof(_ScrollRateTable[0]))-1); } /* @@ -512,7 +515,7 @@ void ScrollClass::Scroll_Edge(Point2D const & point) } else { Override_Mouse_Shape((MouseType)(MOUSE_N+control), false); - int step = int(_rate[rate] * Rule->ScrollMultiplier); + int step = int(_ScrollRateTable[rate] * Rule->ScrollMultiplier); double scaled = step * _ScrollFraction + _EdgeScrollRemainder; distance = int(scaled); _EdgeScrollRemainder = scaled - distance; @@ -581,6 +584,23 @@ void ScrollClass::Scroll_AI(void) } +/// +/// Gives the distance a keyboard scroll command should move the map this poll, at the sustained +/// top speed edge scrolling reaches once its inertia ramp is fully wound up for the current +/// Options.ScrollRate. Call once per poll; it paces itself off Scroll_AI's timing. +/// +/// int; The distance to scroll this poll, in leptons. +int ScrollClass::Get_Keyboard_Scroll_Distance(void) +{ + int rate = Options.ScrollRate+1; + int step = int(_ScrollRateTable[rate] * Rule->ScrollMultiplier); + double scaled = step * _ScrollFraction + _KeyScrollRemainder; + int distance = int(scaled); + _KeyScrollRemainder = scaled - distance; + return(distance); +} + + /// /// Is the tactical map currently in motion? /// This routine is used to hold off work that would fight with a scroll already under way. A diff --git a/code/scroll.h b/code/scroll.h index 548aaf09d..601bbd7ea 100644 --- a/code/scroll.h +++ b/code/scroll.h @@ -99,6 +99,8 @@ class ScrollClass: public TabClass void Set_Scroll_Coasting_Allowed(bool coasting) { IsCoastScrollAllowed = coasting; } + int Get_Keyboard_Scroll_Distance(void); + virtual void AI(KeyNumType &input, Point2D const & xy) override; virtual void Init_IO(void) override {/*Counter = 0;*/BASECLASS::Init_IO();}; virtual bool Is_Scrolling(void) const override; diff --git a/manual/changes/keyboard-scroll-continuous.md b/manual/changes/keyboard-scroll-continuous.md new file mode 100644 index 000000000..4d7a05a31 --- /dev/null +++ b/manual/changes/keyboard-scroll-continuous.md @@ -0,0 +1,26 @@ +--- +title: Continuous keyboard map scrolling +category: feature +release: 0.2.0 +targets: +- type: command + id: ScrollNorth + effect: changed +- type: command + id: ScrollSouth + effect: changed +- type: command + id: ScrollEast + effect: changed +- type: command + id: ScrollWest + effect: changed +credit: +- torradmin +--- + +Binding "Scroll East/West/North/South" to a key now scrolls the map continuously while the key +is held, the way edge scrolling and mouse-drag scrolling already do; before, each press moved +the map a single fixed step and repeating it required releasing and pressing the key again, +because Windows key-repeat events never reach the game's keyboard buffer. Held keyboard scroll +ramps up to the same top speed as edge scrolling and honors the "Scroll speed" option. diff --git a/manual/changes/keyboard-scroll-diagonals-and-modifiers.md b/manual/changes/keyboard-scroll-diagonals-and-modifiers.md new file mode 100644 index 000000000..fe97cf7fd --- /dev/null +++ b/manual/changes/keyboard-scroll-diagonals-and-modifiers.md @@ -0,0 +1,30 @@ +--- +title: Fix diagonal keyboard scrolling and modifier-bound scroll keys +category: fix +release: 0.2.0 +targets: +- type: command + id: ScrollNorthEast + effect: changed +- type: command + id: ScrollSouthEast + effect: changed +- type: command + id: ScrollSouthWest + effect: changed +- type: command + id: ScrollNorthWest + effect: changed +credit: +- torradmin +--- + +Binding "Scroll Northeast/Southeast/Southwest/Northwest" to a key now scrolls the map +continuously while the key is held and honors the "Scroll speed" option, matching the cardinal +scroll commands and edge scrolling; before, each diagonal command moved the map a single fixed +step regardless of how long the key was held. + +A repeatable scroll command bound with a Shift, Ctrl, or Alt modifier (for example Shift+Up) no +longer keeps scrolling once the modifier is released, and no longer fires alongside an unrelated +command bound to the same base key with a different modifier. The per-frame poll that re-triggers +held scroll keys was checking only the base key, ignoring any modifier the binding required.