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.