From 69a8333563ec174cab16d811d8c8483c3977ac0e Mon Sep 17 00:00:00 2001 From: torradmin Date: Fri, 4 Sep 2026 03:07:41 +0300 Subject: [PATCH 1/3] * Added support for continuous scrolling using keyboard key binds for "Scroll East/West/North/South" instead of them basically being useless keybinds. Scroll speed with keyboard is similar to edge scrolling and takes into account the "Scroll speed" slider in options. (cherry picked from commit 6956eff5aec6044bcd3f93383e182d13f73527ac) (cherry picked from commit e5f16564fc17b1ffcdab08e89309dceb51358b86) --- code/command.h | 4 ++++ code/init.cpp | 20 ++++++++++++++++---- code/mainloop.cpp | 15 ++++++++++++++- code/scroll.cpp | 48 +++++++++++++++++++++++++++++++++-------------- code/scroll.h | 2 ++ 5 files changed, 70 insertions(+), 19 deletions(-) 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..d6737bb2a 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); + } }; diff --git a/code/mainloop.cpp b/code/mainloop.cpp index 16f292b44..87ad5b652 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" @@ -476,6 +477,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() && Keyboard->Down(KeyNumType(HotkeyCommands.Fetch_ID_By_Position(index)))) { + repeatcmd->Execute(); + } + } + /* ** Don't do anything if there is not keyboard event. */ @@ -498,7 +508,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; From 9cbf4d902631a8ecb6a2f0d480ea36dcf773de5d Mon Sep 17 00:00:00 2001 From: torradmin Date: Sun, 6 Sep 2026 11:38:38 +0300 Subject: [PATCH 2/3] * Added change docs. --- manual/changes/keyboard-scroll-continuous.md | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 manual/changes/keyboard-scroll-continuous.md 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. From eeb0dbf3fb16514cd7fb197397a31d6d8094a72b Mon Sep 17 00:00:00 2001 From: torradmin Date: Sun, 6 Sep 2026 11:37:22 +0300 Subject: [PATCH 3/3] * Fix for modifier keys. --- code/init.cpp | 20 ++++++++++--- code/mainloop.cpp | 20 ++++++++++++- ...keyboard-scroll-diagonals-and-modifiers.md | 30 +++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 manual/changes/keyboard-scroll-diagonals-and-modifiers.md diff --git a/code/init.cpp b/code/init.cpp index d6737bb2a..17392785d 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -4372,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); + } }; @@ -4395,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); + } }; @@ -4418,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); + } }; @@ -4441,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 87ad5b652..fd96811ab 100644 --- a/code/mainloop.cpp +++ b/code/mainloop.cpp @@ -458,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. * * * @@ -481,7 +499,7 @@ void Keyboard_Process(KeyNumType & input) // (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() && Keyboard->Down(KeyNumType(HotkeyCommands.Fetch_ID_By_Position(index)))) { + if (repeatcmd->Is_Repeatable() && Is_Repeat_Key_Held(HotkeyCommands.Fetch_ID_By_Position(index))) { repeatcmd->Execute(); } } 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.