From 204ce466e1b480bdb4c133be66b2d45a7c7aa4d3 Mon Sep 17 00:00:00 2001 From: Phireh Date: Wed, 1 Jul 2026 08:30:21 -0500 Subject: [PATCH 1/5] Basic skeleton for KeyBindingsChanged callin Co-authored-by: Amadeus Folego --- cont/LuaUI/callins.lua | 1 + rts/Game/UI/KeyBindings.cpp | 14 +++++++++---- rts/Game/UI/KeyBindings.h | 2 +- rts/Lua/LuaHandle.cpp | 41 +++++++++++++++++++++++++++++++++++++ rts/Lua/LuaHandle.h | 1 + rts/System/EventClient.h | 1 + rts/System/EventHandler.cpp | 6 ++++++ rts/System/EventHandler.h | 1 + rts/System/Events.def | 1 + 9 files changed, 63 insertions(+), 5 deletions(-) diff --git a/cont/LuaUI/callins.lua b/cont/LuaUI/callins.lua index db7d4380c99..2279b135c37 100644 --- a/cont/LuaUI/callins.lua +++ b/cont/LuaUI/callins.lua @@ -23,6 +23,7 @@ CallInsList = { "MiniMapGeometryChanged", "CommandNotify", + "KeyBindingsChanged", "KeyMapChanged", "KeyPress", "KeyRelease", diff --git a/rts/Game/UI/KeyBindings.cpp b/rts/Game/UI/KeyBindings.cpp index 9bafd6ca542..cc6a623c103 100644 --- a/rts/Game/UI/KeyBindings.cpp +++ b/rts/Game/UI/KeyBindings.cpp @@ -9,6 +9,7 @@ #include "KeySet.h" #include "Sim/Units/UnitDef.h" #include "Sim/Units/UnitDefHandler.h" +#include "System/EventHandler.h" #include "System/FileSystem/FileHandler.h" #include "System/FileSystem/SimpleParser.h" #include "System/Log/ILog.h" @@ -858,7 +859,7 @@ void CKeyBindings::PushAction(const Action& action) } } -bool CKeyBindings::ExecuteCommand(const std::string& line) +bool CKeyBindings::ExecuteCommand(const std::string& line, bool sendEvents) { RECOIL_DETAILED_TRACY_ZONE; const std::vector words = CSimpleParser::Tokenize(line, 2); @@ -867,6 +868,7 @@ bool CKeyBindings::ExecuteCommand(const std::string& line) return false; const std::string command = StringToLower(words[0]); + bool changedKeys = true; if (command == "keydebug") { if (words.size() == 1) { @@ -876,6 +878,7 @@ bool CKeyBindings::ExecuteCommand(const std::string& line) // set debugEnabled = atoi(words[1].c_str()); } + changedKeys = false; // only 'keydebug' leaves keybinds unchanged } else if (command == "keyload") { const std::string& filename = words.size() > 1 ? words[1] : DEFAULT_FILENAME; @@ -895,8 +898,8 @@ bool CKeyBindings::ExecuteCommand(const std::string& line) if (debugEnabled) LOG("[CKeyBindings::%s] line=%s", __func__, line.c_str()); - ExecuteCommand("unbindall"); - ExecuteCommand("unbind enter chat"); + ExecuteCommand("unbindall", false); + ExecuteCommand("unbind enter chat", false); if (loadStack.empty() && words.size() == 1) LoadDefaults(); @@ -942,6 +945,9 @@ bool CKeyBindings::ExecuteCommand(const std::string& line) if (buildHotkeyMap) BuildHotkeyMap(); + if (changedKeys && sendEvents) + eventHandler.KeyBindingsChanged(); + return false; } @@ -973,7 +979,7 @@ bool CKeyBindings::Load(const std::string& filename) CSimpleParser parser(ifs); while (!parser.Eof()) { - ExecuteCommand(parser.GetCleanLine()); + ExecuteCommand(parser.GetCleanLine(), false); } loadStack.pop_back(); diff --git a/rts/Game/UI/KeyBindings.h b/rts/Game/UI/KeyBindings.h index 04260a7993e..f51cf1bc1ba 100644 --- a/rts/Game/UI/KeyBindings.h +++ b/rts/Game/UI/KeyBindings.h @@ -48,7 +48,7 @@ class CKeyBindings : public CommandReceiver const HotkeyList& GetHotkeys(const std::string& action) const; virtual void PushAction(const Action&); - bool ExecuteCommand(const std::string& line); + bool ExecuteCommand(const std::string& line, bool sendEvents = true); // Receive configuration notifications (for KeyChainTimeout) void ConfigNotify(const std::string& key, const std::string& value); diff --git a/rts/Lua/LuaHandle.cpp b/rts/Lua/LuaHandle.cpp index 650a810499d..2f15c62d829 100644 --- a/rts/Lua/LuaHandle.cpp +++ b/rts/Lua/LuaHandle.cpp @@ -25,6 +25,7 @@ #include "Game/Players/PlayerHandler.h" #include "Sim/Misc/LosHandler.h" #include "Net/Protocol/NetProtocol.h" +#include "Game/UI/KeyBindings.h" #include "Game/UI/KeySet.h" #include "Game/UI/MiniMap.h" #include "Rendering/GlobalRendering.h" @@ -3103,6 +3104,46 @@ void CLuaHandle::Pong(uint8_t pingTag, const spring_time pktSendTime, const spri RunCallIn(L, cmdStr, 3, 0); } +/*** Called when keybindings change. + * + * Called when: + * + * - An operation that changed current keybindings occurred, e.g. `bind k action`. If the operation operated on multiple keybindings, just a single event is called, at the end of it, e.g. `keyreload`. + * - Any operation that changes how actions are retrieved from input triggers happened, e.g. `fakemeta space`. + * + * @function Callins:KeyBindingsChanged + * @return KeyBinding[] currentKeybindings list of all actions and their bound keys + */ + +void CLuaHandle::KeyBindingsChanged() +{ + RECOIL_DETAILED_TRACY_ZONE; + LUA_CALL_IN_CHECK(L); + static const LuaHashString cmdStr(__ + luaL_checkstack(L, 1, __func__); + + if (!cmdStr.GetGlobalFunc(L)) + return; + + // This list imitates the format of LuaUnsyncedRead::GetKeyBindings + ActionList actions = keyBindings.GetActionList(); + + int i = 1; + lua_createtable(L, actions.size(), 0); + for (const Action& action: actions) { + lua_createtable(L, 0, 4); + lua_pushsstring(L, action.command); + lua_pushsstring(L, action.extra); + lua_rawset(L, -3); + LuaPushNamedString(L, "command", action.command); + LuaPushNamedString(L, "extra", action. + LuaPushNamedString(L, "boundWith", action.boundWith); + lua_rawseti(L, -2, i++); + } + + RunCallIn(L, cmdStr, 1, 0); +} + /*** Called when the keymap changes * diff --git a/rts/Lua/LuaHandle.h b/rts/Lua/LuaHandle.h index 5256825a2b6..ab3ba6815e5 100644 --- a/rts/Lua/LuaHandle.h +++ b/rts/Lua/LuaHandle.h @@ -200,6 +200,7 @@ class CLuaHandle : public CEventClient void UnsyncedHeightMapUpdate(const SRectangle& rect) override; void Update() override; + void KeyBindingsChanged() override; bool KeyMapChanged() override; bool KeyPress(int keyCode, int scanCode, bool isRepeat) override; bool KeyRelease(int keyCode, int scanCode) override; diff --git a/rts/System/EventClient.h b/rts/System/EventClient.h index 3e17d1171c2..5f5e4251d71 100644 --- a/rts/System/EventClient.h +++ b/rts/System/EventClient.h @@ -283,6 +283,7 @@ class CEventClient virtual void Update(); virtual void UnsyncedHeightMapUpdate(const SRectangle& rect); + virtual void KeyBindingsChanged() {}; virtual bool KeyMapChanged(); virtual bool KeyPress(int keyCode, int scanCode, bool isRepeat); virtual bool KeyRelease(int keyCode, int scanCode); diff --git a/rts/System/EventHandler.cpp b/rts/System/EventHandler.cpp index d7969be7aa7..c529af67ce2 100644 --- a/rts/System/EventHandler.cpp +++ b/rts/System/EventHandler.cpp @@ -805,6 +805,12 @@ bool CEventHandler::CommandNotify(const Command& cmd) return ControlReverseIterateDefTrue(listCommandNotify, &CEventClient::CommandNotify, cmd); } +void CEventHandler::KeyBindingsChanged() +{ + ZoneScoped; + ITERATE_EVENTCLIENTLIST_NA(KeyBindingsChanged); +} + bool CEventHandler::KeyMapChanged() { ZoneScoped; diff --git a/rts/System/EventHandler.h b/rts/System/EventHandler.h index e7432aaaf50..7c9596883f1 100644 --- a/rts/System/EventHandler.h +++ b/rts/System/EventHandler.h @@ -221,6 +221,7 @@ class CEventHandler void UnsyncedHeightMapUpdate(const SRectangle& rect); void Update(); + void KeyBindingsChanged(); bool KeyMapChanged(); bool KeyPress(int keyCode, int scanCode, bool isRepeat); bool KeyRelease(int keyCode, int scanCode); diff --git a/rts/System/Events.def b/rts/System/Events.def index a0e069c27d3..3eaba68a8c2 100644 --- a/rts/System/Events.def +++ b/rts/System/Events.def @@ -97,6 +97,7 @@ SETUP_EVENT(Update, MANAGED_BIT | UNSYNCED_BIT) + SETUP_EVENT(KeyBindingsChanged, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(KeyMapChanged, MANAGED_BIT | UNSYNCED_BIT | CONTROL_BIT) SETUP_EVENT(KeyPress, MANAGED_BIT | UNSYNCED_BIT | CONTROL_BIT) SETUP_EVENT(KeyRelease, MANAGED_BIT | UNSYNCED_BIT | CONTROL_BIT) From 1c23ffacbdedb807c1fa5f7b4759e75ecd3b756f Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Wed, 1 Jul 2026 08:30:21 -0500 Subject: [PATCH 2/5] Drop the payload and dispatch KeyBindingsChanged via the widget handler --- cont/LuaUI/widgets.lua | 7 +++++++ rts/Game/UI/KeyBindings.cpp | 2 +- rts/Lua/LuaHandle.cpp | 28 ++++++---------------------- rts/System/EventClient.cpp | 1 + rts/System/EventClient.h | 2 +- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/cont/LuaUI/widgets.lua b/cont/LuaUI/widgets.lua index 4808aa9a436..bbed83d538f 100644 --- a/cont/LuaUI/widgets.lua +++ b/cont/LuaUI/widgets.lua @@ -195,6 +195,7 @@ local callInLists = { 'AddConsoleLine', 'ViewResize', 'DrawScreen', + 'KeyBindingsChanged', 'KeyMapChanged', 'KeyPress', 'KeyRelease', @@ -1416,6 +1417,12 @@ end -- Keyboard call-ins -- +function widgetHandler:KeyBindingsChanged() + for _,w in ipairs(self.KeyBindingsChangedList) do + w:KeyBindingsChanged() + end +end + function widgetHandler:KeyMapChanged() for _,w in ipairs(self.KeyMapChangedList) do w:KeyMapChanged() diff --git a/rts/Game/UI/KeyBindings.cpp b/rts/Game/UI/KeyBindings.cpp index cc6a623c103..741584da127 100644 --- a/rts/Game/UI/KeyBindings.cpp +++ b/rts/Game/UI/KeyBindings.cpp @@ -878,7 +878,7 @@ bool CKeyBindings::ExecuteCommand(const std::string& line, bool sendEvents) // set debugEnabled = atoi(words[1].c_str()); } - changedKeys = false; // only 'keydebug' leaves keybinds unchanged + changedKeys = false; // keydebug only toggles debug logging; it never changes bindings } else if (command == "keyload") { const std::string& filename = words.size() > 1 ? words[1] : DEFAULT_FILENAME; diff --git a/rts/Lua/LuaHandle.cpp b/rts/Lua/LuaHandle.cpp index 2f15c62d829..6043ec4b38a 100644 --- a/rts/Lua/LuaHandle.cpp +++ b/rts/Lua/LuaHandle.cpp @@ -25,7 +25,6 @@ #include "Game/Players/PlayerHandler.h" #include "Sim/Misc/LosHandler.h" #include "Net/Protocol/NetProtocol.h" -#include "Game/UI/KeyBindings.h" #include "Game/UI/KeySet.h" #include "Game/UI/MiniMap.h" #include "Rendering/GlobalRendering.h" @@ -3111,37 +3110,22 @@ void CLuaHandle::Pong(uint8_t pingTag, const spring_time pktSendTime, const spri * - An operation that changed current keybindings occurred, e.g. `bind k action`. If the operation operated on multiple keybindings, just a single event is called, at the end of it, e.g. `keyreload`. * - Any operation that changes how actions are retrieved from input triggers happened, e.g. `fakemeta space`. * + * Nothing is passed; call `Spring.GetKeyBindings` to read the current state. + * * @function Callins:KeyBindingsChanged - * @return KeyBinding[] currentKeybindings list of all actions and their bound keys */ - void CLuaHandle::KeyBindingsChanged() { RECOIL_DETAILED_TRACY_ZONE; LUA_CALL_IN_CHECK(L); - static const LuaHashString cmdStr(__ - luaL_checkstack(L, 1, __func__); + luaL_checkstack(L, 2, __func__); + + static const LuaHashString cmdStr(__func__); if (!cmdStr.GetGlobalFunc(L)) return; - // This list imitates the format of LuaUnsyncedRead::GetKeyBindings - ActionList actions = keyBindings.GetActionList(); - - int i = 1; - lua_createtable(L, actions.size(), 0); - for (const Action& action: actions) { - lua_createtable(L, 0, 4); - lua_pushsstring(L, action.command); - lua_pushsstring(L, action.extra); - lua_rawset(L, -3); - LuaPushNamedString(L, "command", action.command); - LuaPushNamedString(L, "extra", action. - LuaPushNamedString(L, "boundWith", action.boundWith); - lua_rawseti(L, -2, i++); - } - - RunCallIn(L, cmdStr, 1, 0); + RunCallIn(L, cmdStr, 0, 0); } diff --git a/rts/System/EventClient.cpp b/rts/System/EventClient.cpp index de94b03c58d..849ad9d0fce 100644 --- a/rts/System/EventClient.cpp +++ b/rts/System/EventClient.cpp @@ -61,6 +61,7 @@ void CEventClient::DrawLoadScreen() {} void CEventClient::LoadProgress(const std::string& msg, const bool replace_lastline) {} // from LuaUI +void CEventClient::KeyBindingsChanged() {} bool CEventClient::KeyMapChanged() { return false; } bool CEventClient::KeyPress(int keyCode, int scanCode, bool isRepeat) { return false; } bool CEventClient::KeyRelease(int keyCode, int scanCode) { return false; } diff --git a/rts/System/EventClient.h b/rts/System/EventClient.h index 5f5e4251d71..8f9acc6e772 100644 --- a/rts/System/EventClient.h +++ b/rts/System/EventClient.h @@ -283,7 +283,7 @@ class CEventClient virtual void Update(); virtual void UnsyncedHeightMapUpdate(const SRectangle& rect); - virtual void KeyBindingsChanged() {}; + virtual void KeyBindingsChanged(); virtual bool KeyMapChanged(); virtual bool KeyPress(int keyCode, int scanCode, bool isRepeat); virtual bool KeyRelease(int keyCode, int scanCode); From 15cd27e5e0d8e7fd5aef158b2e521aa6deaa9933 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Sun, 26 Jul 2026 22:38:20 -0500 Subject: [PATCH 3/5] Emit KeyBindingsChanged on well-formed commands and on load --- rts/Game/UI/KeyBindings.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/rts/Game/UI/KeyBindings.cpp b/rts/Game/UI/KeyBindings.cpp index 94abaf70f0b..716cd53c58d 100644 --- a/rts/Game/UI/KeyBindings.cpp +++ b/rts/Game/UI/KeyBindings.cpp @@ -881,6 +881,9 @@ bool CKeyBindings::ExecuteCommandInternal(const std::string& line) const std::string command = StringToLower(words[0]); + // emit even on a no-op command so clients can tell it ran; keydebug is logging-only, so skip it + bool emitEvent = true; + if (command == "keydebug") { if (words.size() == 1) { // toggle @@ -889,6 +892,7 @@ bool CKeyBindings::ExecuteCommandInternal(const std::string& line) // set debugEnabled = atoi(words[1].c_str()); } + emitEvent = false; } else if (command == "keyload") { const std::string& filename = words.size() > 1 ? words[1] : DEFAULT_FILENAME; @@ -953,20 +957,19 @@ bool CKeyBindings::ExecuteCommandInternal(const std::string& line) return false; } - return false; + return emitEvent; } bool CKeyBindings::ExecuteCommand(const std::string& line) { - const bool ret = ExecuteCommandInternal(line); - const bool changed = buildHotkeyMap; + const bool emitEvent = ExecuteCommandInternal(line); MaybeBuildHotkeyMap(); - if (changed) + if (emitEvent) eventHandler.KeyBindingsChanged(); - return ret; + return emitEvent; } @@ -1007,6 +1010,10 @@ bool CKeyBindings::Load(const std::string& filename) { const bool ret = LoadInternal(filename); MaybeBuildHotkeyMap(); + + if (ret) + eventHandler.KeyBindingsChanged(); + return ret; } From c48a57736a055ccc57998de72b9522bb4a17f9cb Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Mon, 27 Jul 2026 12:55:12 -0500 Subject: [PATCH 4/5] Emit KeyBindingsChanged for well-formed unbind commands --- rts/Game/UI/KeyBindings.cpp | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/rts/Game/UI/KeyBindings.cpp b/rts/Game/UI/KeyBindings.cpp index 716cd53c58d..32c55dcfb6a 100644 --- a/rts/Game/UI/KeyBindings.cpp +++ b/rts/Game/UI/KeyBindings.cpp @@ -678,19 +678,17 @@ bool CKeyBindings::UnBind(const std::string& keystr, const std::string& command) KeyMap& bindings = ks.IsKeyCode() ? codeBindings : scanBindings; const auto it = bindings.find(ks); - if (it == bindings.end()) - return false; - - ActionList& al = it->second; - const bool success = RemoveCommandFromList(al, command); + if (it != bindings.end()) { + ActionList& al = it->second; - if (al.empty()) - bindings.erase(it); + if (RemoveCommandFromList(al, command)) + buildHotkeyMap = true; - if (success) - buildHotkeyMap = true; + if (al.empty()) + bindings.erase(it); + } - return success; + return true; } @@ -710,11 +708,11 @@ bool CKeyBindings::UnBindKeyset(const std::string& keystr) const auto it = bindings.find(ks); - if (it == bindings.end()) - return false; + if (it != bindings.end()) { + bindings.erase(it); + buildHotkeyMap = true; + } - bindings.erase(it); - buildHotkeyMap = true; return true; } @@ -757,7 +755,7 @@ bool CKeyBindings::UnBindAction(const std::string& command) if (changed) buildHotkeyMap = true; - return changed; + return true; } From 3b291ea0aa63916506259f337d7e13ec733ec4f8 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Mon, 27 Jul 2026 12:55:12 -0500 Subject: [PATCH 5/5] Document that KeyBindingsChanged does not imply bindings differ --- rts/Lua/LuaHandle.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rts/Lua/LuaHandle.cpp b/rts/Lua/LuaHandle.cpp index 7e070c846c1..7851c69b2e9 100644 --- a/rts/Lua/LuaHandle.cpp +++ b/rts/Lua/LuaHandle.cpp @@ -3129,11 +3129,11 @@ void CLuaHandle::Pong(uint8_t pingTag, const spring_time pktSendTime, const spri RunCallIn(L, cmdStr, 3, 0); } -/*** Called when keybindings change. +/*** Called after a keybinding command runs. * * Called when: * - * - An operation that changed current keybindings occurred, e.g. `bind k action`. If the operation operated on multiple keybindings, just a single event is called, at the end of it, e.g. `keyreload`. + * - A keybinding command runs, e.g. `bind k action`. This may fire even when nothing actually changed (e.g. a redundant bind), so treat it as "a binding command ran", not a guarantee that bindings differ. An operation covering multiple keybindings fires a single event at the end, e.g. `keyreload`. * - Any operation that changes how actions are retrieved from input triggers happened, e.g. `fakemeta space`. * * Nothing is passed; call `Spring.GetKeyBindings` to read the current state.