From a94ffeca5fc2a984a7329234058ad859094da457 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:33:51 -0400 Subject: [PATCH 01/17] SSinput legacy/focus chat handling improvements A port of Daedalus' handling of input keys. --- code/controllers/subsystems/input.dm | 114 +++++++++++++++++- code/modules/client/client_defines.dm | 2 + code/modules/client/client_procs.dm | 10 +- .../controls/01_keybindings.dm | 30 ++++- code/modules/keybindings/binds/client.dm | 14 --- code/modules/keybindings/setup.dm | 68 ++++++++++- code/world.dm | 67 ++++++++++ interface/skin.dmf | 10 +- 8 files changed, 282 insertions(+), 33 deletions(-) diff --git a/code/controllers/subsystems/input.dm b/code/controllers/subsystems/input.dm index 833eec99b36..7a03eb5c94e 100644 --- a/code/controllers/subsystems/input.dm +++ b/code/controllers/subsystems/input.dm @@ -6,7 +6,17 @@ SUBSYSTEM_DEF(input) priority = SS_PRIORITY_INPUT runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY + /// Standard macroset *ALL* players get var/list/macro_set + /// Macros applied only to hotkey users + var/list/hotkey_only_set + /// Macros applied onlt to classic users + var/list/classic_only_set + /// Typecache of all unprintable keys that are safe for classic to bind + var/list/unprintables_cache + /// Macro IDs we shouldn't clear during client.clear_macros() + var/list/protected_macro_ids + /datum/controller/subsystem/input/Initialize() setup_default_macro_sets() @@ -16,9 +26,107 @@ SUBSYSTEM_DEF(input) // This is for when macro sets are eventualy datumized /datum/controller/subsystem/input/proc/setup_default_macro_sets() macro_set = list( - "Any" = "\"KeyDown \[\[*\]\]\"", - "Any+UP" = "\"KeyUp \[\[*\]\]\"", - "Back" = "\".winset \\\"outputwindow.input.text=\\\"\\\"\\\"\"" + // These could probably just be put in the skin. I actually don't understand WHY they aren't just in the skin. Besides the use of defines for Tab. + "Back" = "\".winset \\\"input.text=\\\"\\\"\\\"\"", + "Tab" = "\".winset \\\"input.focus=true?map.focus=true input.background-color=[COLOR_INPUT_DISABLED]:input.focus=true input.background-color=[COLOR_INPUT_ENABLED]\\\"\"", + "Escape" = "Reset-Held-Keys", + ) + hotkey_only_set = list( + // We don't need to protect printables with hotkey mode, We can save time and just use the magic key. + "Any" = "\"KeyDown \[\[*\]\]\"", + "Any+UP" = "\"KeyUp \[\[*\]\]\"", + ) + classic_only_set = list( + //We need to force these to capture them for macro modifiers. + //Did I mention I fucking despise the way this system works at a base, almost reptilian-barely-understands-consciousness level? + //Because I do. + "Alt" = "\"KeyDown Alt\"", + "Alt+UP" = "\"KeyUp Alt\"", + "Ctrl" = "\"KeyDown Ctrl\"", + "Ctrl+UP" = "\"KeyUp Ctrl\"", + ) + // This list may be out of date, and may include keys not actually legal to bind? + // The only full list is from 2008. http://www.byond.com/docs/notes/macro.html + unprintables_cache = list( + // Arrow Keys + "North" = TRUE, + "West" = TRUE, + "East" = TRUE, + "South" = TRUE, + // Numpad-Lock Disabled + "Northwest" = TRUE, // KP_Home + "Northeast" = TRUE, // KP_PgUp + "Center" = TRUE, + "Southwest" = TRUE, // KP_End + "Southeast" = TRUE, // KP_PgDn + // Keys you really shouldn't touch, but are technically unprintable + "Return" = TRUE, + "Escape" = TRUE, + "Delete" = TRUE, + // Things I'm not sure BYOND actually supports anymore. + "Select" = TRUE, + "Execute" = TRUE, + "Snapshot" = TRUE, + "Attn" = TRUE, + "CrSel" = TRUE, + "ExSel" = TRUE, + "ErEOF" = TRUE, + "Zoom" = TRUE, + "PA1" = TRUE, + "OEMClear" = TRUE, + // Things the modern ref says is okay + "Pause" = TRUE, + "Play" = TRUE, + "Insert" = TRUE, + "Help" = TRUE, + "LWin" = TRUE, + "RWin" = TRUE, + "Apps" = TRUE, + "Numpad0" = TRUE, + "Numpad1" = TRUE, + "Numpad2" = TRUE, + "Numpad3" = TRUE, + "Numpad4" = TRUE, + "Numpad5" = TRUE, + "Numpad6" = TRUE, + "Numpad7" = TRUE, + "Numpad8" = TRUE, + "Numpad9" = TRUE, + "Multiply" = TRUE, + "Add" = TRUE, + "Separator" = TRUE, + "Subtract" = TRUE, + "Decimal" = TRUE, + "Divide" = TRUE, + "F1" = TRUE, + "F2" = TRUE, + "F3" = TRUE, + "F4" = TRUE, + "F5" = TRUE, + "F6" = TRUE, + "F7" = TRUE, + "F8" = TRUE, + "F9" = TRUE, + "F10" = TRUE, + "F11" = TRUE, + "F12" = TRUE, + "F13" = TRUE, + "F14" = TRUE, + "F15" = TRUE, + "F16" = TRUE, + "F17" = TRUE, + "F18" = TRUE, + "F19" = TRUE, + "F20" = TRUE, + "F21" = TRUE, + "F22" = TRUE, + "F23" = TRUE, + "F24" = TRUE, + ) + // Macro IDs we don't delete on wipe, Usually stuff baked into the skin, or that we have to be more careful with. + protected_macro_ids = list( + "PROTECTED-Shift", + "PROTECTED-ShiftUp" ) // Badmins just wanna have fun ♪ diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 15d3d676b99..a2ec7f01509 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -71,3 +71,5 @@ var/next_move_dir_sub /// Movement dir of the most recently pressed movement key. Used in cardinal-only movement mode. var/last_move_dir_pressed + /// Semaphore for macro updates, so that they all complete and don't stomp over each other. + var/updating_macros = 0 \ No newline at end of file diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 5631eb2ae6b..dd411f598c9 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -637,6 +637,8 @@ var/global/const/MAX_VIEW = 41 var/list/communication_hotkeys = list() for(var/key in D.key_bindings) for(var/kb_name in D.key_bindings[key]) + if(!prefs.hotkeys && !SSinput.unprintables_cache[key]) + continue switch(kb_name) if("north") movement_keys[key] = NORTH @@ -674,12 +676,12 @@ var/global/const/MAX_VIEW = 41 /client/proc/set_right_click_menu_mode(shift_only) if(shift_only) winset(src, "mapwindow.map", "right-click=true") - winset(src, "ShiftUp", "is-disabled=false") - winset(src, "Shift", "is-disabled=false") + winset(src, "default.PROTECTED-Shift", "command=\".winset :map.right-click=false\nKeyDown Shift\"") + winset(src, "default.PROTECTED-ShiftUp", "command=\".winset :map.right-click=true\nKeyUp Shift\"") else winset(src, "mapwindow.map", "right-click=false") - winset(src, "default.Shift", "is-disabled=true") - winset(src, "default.ShiftUp", "is-disabled=true") + winset(src, "default.PROTECTED-Shift", "command=\"KeyDown Shift\"") + winset(src, "default.PROTECTED-ShiftUp", "command=\"KeyUp Shift\"") /client/verb/drop_item() set hidden = 1 diff --git a/code/modules/client/preference_setup/controls/01_keybindings.dm b/code/modules/client/preference_setup/controls/01_keybindings.dm index 5400207e650..faa4e152ad1 100644 --- a/code/modules/client/preference_setup/controls/01_keybindings.dm +++ b/code/modules/client/preference_setup/controls/01_keybindings.dm @@ -50,13 +50,41 @@ /datum/category_item/player_setup_item/controls abstract_type = /datum/category_item/player_setup_item/controls +/datum/category_group/player_setup_category/controls/content(mob/user) + . = "" + for(var/datum/category_item/player_setup_item/PI in items) + . += "[PI.content(user)]
" + +/datum/category_item/player_setup_item/controls/hotkey_mode + name = "Hotkey Mode" + sort_order = 1 + +/datum/category_item/player_setup_item/controls/hotkey_mode/load_preferences(datum/pref_record_reader/R) + pref.hotkeys = R.read("hotkey_mode") + +/datum/category_item/player_setup_item/controls/hotkey_mode/save_preferences(datum/pref_record_writer/writer) + writer.write("hotkey_mode", pref.hotkeys) + +/datum/category_item/player_setup_item/controls/hotkey_mode/sanitize_preferences() + pref.hotkeys = sanitize_bool(pref.hotkeys, TRUE) + +/datum/category_item/player_setup_item/controls/hotkey_mode/content(mob/user) + return "
Hotkey Mode:[pref.hotkeys ? "Hotkey" : "Focus Chat"]
" + +/datum/category_item/player_setup_item/controls/hotkey_mode/OnTopic(href, list/href_list, mob/user) + pref.hotkeys = !pref.hotkeys + user.client.set_macros() + return TOPIC_REFRESH + + /datum/category_item/player_setup_item/controls/keybindings name = "Keybindings" - sort_order = 1 + sort_order = 2 /datum/category_item/player_setup_item/controls/keybindings/load_preferences(datum/pref_record_reader/R) pref.key_bindings = R.read("key_bindings") + /datum/category_item/player_setup_item/controls/keybindings/sanitize_preferences() pref.key_bindings = sanitize_keybindings(pref.key_bindings) pref.check_keybindings() diff --git a/code/modules/keybindings/binds/client.dm b/code/modules/keybindings/binds/client.dm index 8e1cc489b6e..d8afd54d97e 100644 --- a/code/modules/keybindings/binds/client.dm +++ b/code/modules/keybindings/binds/client.dm @@ -2,20 +2,6 @@ abstract_type = /datum/keybinding/client category = CATEGORY_CLIENT -/datum/keybinding/client/hotkey_mode - hotkey_keys = list("Tab") - name = "hotkey_mode" - full_name = "Toggle Hotkeys" - -/datum/keybinding/client/hotkey_mode/down(client/user) - if(user.prefs) - user.prefs.hotkeys = !user.prefs.hotkeys - if(user.prefs.hotkeys) - winset(user, null, "outputwindow.input.background-color=[COLOR_INPUT_DISABLED];mapwindow.map.focus=true") - else - winset(user, null, "outputwindow.input.background-color=[COLOR_INPUT_ENABLED];outputwindow.input.focus=true") - return TRUE - /datum/keybinding/client/admin_help hotkey_keys = list("F1") name = "admin_help" diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 846dd32d3ad..2c8baa6d690 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -18,26 +18,84 @@ erase_output = "[erase_output];[macro_name].parent=null" winset(src, null, erase_output) +/// Apply client macros. Has a system to prevent infighting overcalls. /client/proc/set_macros() - set waitfor = FALSE + set waitfor = FALSE //We're going to sleep here even more than TG. + + /* Queue States: + * 0 - No running updates + * 1 - Running update + * 2 - Update requested while already updating. Will rerun update next tick. + * 3 - Update requested while already in state 2. Will immediately return. + */ + updating_macros++ + if(updating_macros > 2) //Are we the only one in line? + updating_macros-- //No, dequeue and let them handle it. + return + //This isn't an UNTIL because we would rather this lag than deadlock. + while(!(updating_macros == 1)) + sleep(1) + + //Get their personal macro set, This may be null if we're loading too early + var/list/personal_macro_set = prefs?.key_bindings + if(!personal_macro_set) + //We're too early, Just return, Someone'll follow us up. + updating_macros-- + return //Reset the buffer reset_held_keys() erase_all_macros() + //Set up the stuff we don't let them override. var/list/macro_set = SSinput.macro_set for(var/k in 1 to length(macro_set)) var/key = macro_set[k] var/command = macro_set[key] - winset(src, "default-\ref[key]", "parent=default;name=[key];command=[command]") + winset(src, "shared-\ref[key]", "parent=default;name=[key];command=[command]") - if(prefs?.hotkeys) - winset(src, null, "outputwindow.input.focus=true input.background-color=[COLOR_INPUT_ENABLED]") + var/list/printables + //If they use hotkeys, we can safely use ANY + if(prefs.hotkeys) + var/list/hk_macro_set = SSinput.hotkey_only_set + for(var/k in 1 to length(hk_macro_set)) + var/key = hk_macro_set[k] + var/command = hk_macro_set[key] + winset(src, "hotkey_only-\ref[key]", "parent=default;name=[key];command=[command]") + else //Otherwise, we can't. + /// Install the shared set, so that we force capture all modifier keys + var/list/c_macro_set = SSinput.classic_only_set + for(var/k in 1 to length(c_macro_set)) + var/key = c_macro_set[k] + var/command = c_macro_set[key] + winset(src, "classic_only-\ref[key]", "parent=default;name=[key];command=[command]") + printables = list() + //This is to save time muching down this massive list, it might result in holes, it may be better to simply hardcode all these into the skin. + //I might try that one day, but that day is not today. + for(var/key in personal_macro_set) //We don't care about the bound key, just the key itself + if(!prefs.hotkeys && !SSinput.unprintables_cache[key]) //Track printable hotkeys and skip them. + printables += key + continue + winset(src, "personal-\ref[key]", "parent=default;name=[key];command=\"KeyDown [key]\"") + winset(src, "personal-\ref[key]]-UP", "parent=default;name=[key]+UP;command=\"KeyUp [key]\"") + + + if(prefs.hotkeys) + winset(src, null, "input.background-color=[COLOR_INPUT_ENABLED]") else - winset(src, null, "outputwindow.input.focus=true input.background-color=[COLOR_INPUT_DISABLED]") + winset(src, null, "input.background-color=[COLOR_INPUT_DISABLED]") + + //Do we have bad bindings at all, and if so, do we actually care? + if(printables?.len && !prefs.hotkeys) + to_chat(src, "Hey, you might have some bad keybinds!\n\ + The following keys are bound despite Classic Hotkeys being enabled. These binds are not applied.\n\ + The code used to generate this list is imperfect, You can silence this warning in your Game Preferences.\n\ + Keys: [jointext(printables, ", ")]\ + ") //Pref NYI, FIXME, beat me with a stick before margetime. update_special_keybinds() + updating_macros-- //Decrement, Let the next thread through. // byond bug ID:2694120 /client/verb/reset_macros_wrapper() diff --git a/code/world.dm b/code/world.dm index 057199cfb0a..16767ead239 100644 --- a/code/world.dm +++ b/code/world.dm @@ -18,3 +18,70 @@ loop_checks = FALSE #pragma pop #endif + +#warn MACRO DEBUG CODE DO NOT PUSH + +/client + var/x_mt_watchingfocus = FALSE + var/x_mt_winmon_enabled = FALSE + var/list/x_mt_winmon_packet //Lazylist + +/// Dumps the list of all macros. This should almost always be just default +/client/verb/dump_macroset_ids() + set name = "mt Dump Macroset IDs" + set category = "_MACRO_TEST" + to_chat(usr, (jointext(splittext(winget(src, "", "macros"), ";"), "\n") || "NULL (Bad. Incredibly. Incredibly bad.)")) + +/// List all children of default. Name for macros is their bound key. +/client/verb/dump_set() + set name = "mt Dump default bindings" + set category = "_MACRO_TEST" + to_chat(usr, (jointext(splittext(winget(src, "default.*" , "name"), ";"), "\n")|| "NULL (Bad. Real bad.)")) + +/// A slightly more pleasant way to execute free wingets. +/client/verb/arbitrary_winget(cmd as text) + set name = "awing" + set desc = "Run an arbitrary Winset call, Space-separated arguments." + set category = "_MACRO_TEST" + var/list/parts = splittext(cmd, " ") + to_chat(usr, (winget(src, parts[1], parts[2]) || "NULL (Bad Call?)")) + +/// A slightly more pleasant way to execute free winsets. +/client/verb/arbitrary_winset(cmd as text) + set name = "aswin" + set desc = "Run an arbitrary Winset call, Space-separated arguments." + set category = "_MACRO_TEST" + var/list/parts = splittext(cmd, " ") + winset(src, parts[1], parts[2]) + to_chat(usr, ("CALLED: winset({client:[src.ckey]}, \"[parts[1]]\",\"[parts[2]]\")")) + +/// Will dump the currently focused skin element to chat. Used for tracking down focus juggling issues. +/client/verb/focuswatch() + set name = "mt toggle focus watch" + set category = "_MACRO_TEST" + if(x_mt_watchingfocus) + x_mt_watchingfocus = FALSE + return + else + x_mt_watchingfocus = TRUE + while(x_mt_watchingfocus) + // Live-report the element with focus. + to_chat(usr, (winget(src, "", "focus") || "NULL (Entire game defocused?)")) + sleep(0.5) //Every half second + +/client/verb/winmon(cmd as text|null) + set name = "winmon" + set desc = "Repeatedly run a winget to monitor it's value" + set category = "_MACRO_TEST" + if(x_mt_winmon_enabled || isnull(cmd)) + x_mt_winmon_enabled = FALSE + return + else + x_mt_winmon_enabled = TRUE + var/list/parts = splittext(cmd, " ") + x_mt_winmon_packet = parts + while(x_mt_winmon_enabled) + // Repeatedly rerun the same winget to watch the value + var/winout = winget(src, x_mt_winmon_packet[1], x_mt_winmon_packet[2]) + to_chat(usr, ( winout ? "WINMON:[winout]": "WINMON: NULL (Bad Call?)")) + sleep(0.5) \ No newline at end of file diff --git a/interface/skin.dmf b/interface/skin.dmf index c02a82497db..f2512e4e0bf 100644 --- a/interface/skin.dmf +++ b/interface/skin.dmf @@ -1,12 +1,10 @@ macro "default" - elem ".winset :map.right-click=false" - name = "SHIFT+Shift" - elem "Shift" + elem "PROTECTED-Shift" name = "SHIFT" - command = ".winset :map.right-click=false" - elem "ShiftUp" + command = ".winset :map.right-click=false\nKeyDown Shift" + elem "PROTECTED-ShiftUp" name = "SHIFT+UP" - command = ".winset :map.right-click=true" + command = ".winset :map.right-click=true\nKeyUp Shift" menu "menu" elem From d866404f16a56003ad9dc832e74e17651f762e3f Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:40:50 -0400 Subject: [PATCH 02/17] Clarify why not UNTIL comment --- code/modules/keybindings/setup.dm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 2c8baa6d690..44a899d8750 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -32,7 +32,8 @@ if(updating_macros > 2) //Are we the only one in line? updating_macros-- //No, dequeue and let them handle it. return - //This isn't an UNTIL because we would rather this lag than deadlock. + //This isn't an UNTIL because the lock time should be relatively short, + //and we want this resolved as fast as possible (instead of waiting for stoplag()'s cycle time) while(!(updating_macros == 1)) sleep(1) From 0629e01338ddd4ed3de429644455351fc667e135 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:04:10 -0400 Subject: [PATCH 03/17] Less naiive keycode parsing --- code/modules/keybindings/setup.dm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 44a899d8750..1b96a63bcc7 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -75,11 +75,14 @@ //This is to save time muching down this massive list, it might result in holes, it may be better to simply hardcode all these into the skin. //I might try that one day, but that day is not today. for(var/key in personal_macro_set) //We don't care about the bound key, just the key itself - if(!prefs.hotkeys && !SSinput.unprintables_cache[key]) //Track printable hotkeys and skip them. + var/keycode = replacetext(key, regex("(Alt|Shift|Ctrl)", "g"), "") + if(!length(keycode)) + continue //Modifier-only keybind entry. We always those. + if(!prefs.hotkeys && !SSinput.unprintables_cache[keycode]) //Track printable hotkeys and skip them. printables += key continue - winset(src, "personal-\ref[key]", "parent=default;name=[key];command=\"KeyDown [key]\"") - winset(src, "personal-\ref[key]]-UP", "parent=default;name=[key]+UP;command=\"KeyUp [key]\"") + winset(src, "personal-\ref[keycode]", "parent=default;name=[keycode];command=\"KeyDown [keycode]\"") + winset(src, "personal-\ref[keycode]]-UP", "parent=default;name=[keycode]+UP;command=\"KeyUp [keycode]\"") if(prefs.hotkeys) From ec89570d7bd4889e271b9a7015248548a0f18635 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:03:42 -0400 Subject: [PATCH 04/17] Always bind per-key, Clarify macroset names, Warn for buggy bind attempts. Properly protect hardcoded macros. Prevent overbinding special macros. --- code/controllers/subsystems/input.dm | 40 ++++++++------- .../controls/01_keybindings.dm | 15 +++++- code/modules/keybindings/setup.dm | 49 ++++++++----------- 3 files changed, 58 insertions(+), 46 deletions(-) diff --git a/code/controllers/subsystems/input.dm b/code/controllers/subsystems/input.dm index 7a03eb5c94e..fe01c2ff48d 100644 --- a/code/controllers/subsystems/input.dm +++ b/code/controllers/subsystems/input.dm @@ -6,40 +6,46 @@ SUBSYSTEM_DEF(input) priority = SS_PRIORITY_INPUT runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY - /// Standard macroset *ALL* players get - var/list/macro_set - /// Macros applied only to hotkey users - var/list/hotkey_only_set - /// Macros applied onlt to classic users - var/list/classic_only_set + /// 'Hard-wired' macros that have special behaviour. + var/list/core_macro_set + /// Macros for capturing modifier keys. These are always applied. + var/list/modifier_set /// Typecache of all unprintable keys that are safe for classic to bind var/list/unprintables_cache /// Macro IDs we shouldn't clear during client.clear_macros() var/list/protected_macro_ids - + /// Keys with global warnings associated with them. + var/list/warn_keys + /// Fully reserved blacklisted keys + var/list/blacklisted_keys /datum/controller/subsystem/input/Initialize() setup_default_macro_sets() refresh_client_macro_sets() + + warn_keys = list( + "C" = "Interferes with the ability to copy text from the chatbox.", + "V" = "Interferes with the ability to paste text into the chatbox." + ) + // This should include everything in core_macro_set + blacklisted_keys = list( + "Back" = "Hardwired to Clear Input", + "Tab" = "Hardwired to Focus Chat" + //Escape can't be bound as it's the 'unbind' key during setup. + ) + return ..() // This is for when macro sets are eventualy datumized /datum/controller/subsystem/input/proc/setup_default_macro_sets() - macro_set = list( - // These could probably just be put in the skin. I actually don't understand WHY they aren't just in the skin. Besides the use of defines for Tab. + core_macro_set = list( + // These could probably just be put in the skin. I actually don't understand WHY they aren't just in the skin. Besides the use of defines for Tab. "Back" = "\".winset \\\"input.text=\\\"\\\"\\\"\"", "Tab" = "\".winset \\\"input.focus=true?map.focus=true input.background-color=[COLOR_INPUT_DISABLED]:input.focus=true input.background-color=[COLOR_INPUT_ENABLED]\\\"\"", "Escape" = "Reset-Held-Keys", ) - hotkey_only_set = list( - // We don't need to protect printables with hotkey mode, We can save time and just use the magic key. - "Any" = "\"KeyDown \[\[*\]\]\"", - "Any+UP" = "\"KeyUp \[\[*\]\]\"", - ) - classic_only_set = list( + modifier_set = list( //We need to force these to capture them for macro modifiers. - //Did I mention I fucking despise the way this system works at a base, almost reptilian-barely-understands-consciousness level? - //Because I do. "Alt" = "\"KeyDown Alt\"", "Alt+UP" = "\"KeyUp Alt\"", "Ctrl" = "\"KeyDown Ctrl\"", diff --git a/code/modules/client/preference_setup/controls/01_keybindings.dm b/code/modules/client/preference_setup/controls/01_keybindings.dm index faa4e152ad1..ef6e816641b 100644 --- a/code/modules/client/preference_setup/controls/01_keybindings.dm +++ b/code/modules/client/preference_setup/controls/01_keybindings.dm @@ -226,7 +226,20 @@ if(global._kbMap[new_key]) new_key = global._kbMap[new_key] - + if(SSinput.blacklisted_keys[new_key]) + alert(user, "Warning: \[[new_key]\] can't be rebound:\n[SSinput.blacklisted_keys[new_key]]","Bind Error", "Cancel") + show_browser(user, null, "window=capturekeypress") + return TOPIC_REFRESH + if(SSinput.warn_keys[new_key]) + var/response = alert(user, "Warning: Binding the key \[[new_key]\] can cause issues:\n[SSinput.warn_keys[new_key]]","Bind Warning", "Cancel", "Bind Anyways") + if(response != "Bind Anyways") + show_browser(user, null, "window=capturekeypress") + return TOPIC_REFRESH + if(!pref.hotkeys && !SSinput.unprintables_cache[new_key]) + var/response = alert(user, "Notice: Binding the key \[[new_key]\] will have no effect, as you are in Focus Chat mode.","Bind Warning", "Cancel", "Bind Anyways") + if(response != "Bind Anyways") + show_browser(user, null, "window=capturekeypress") + return TOPIC_REFRESH var/full_key switch(new_key) if("Alt") diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 1b96a63bcc7..670226444db 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -14,6 +14,9 @@ var/list/macro_set = params2list(winget(src, "default.*", "command")) // The third arg doesnt matter here as we're just removing them all for(var/k in 1 to length(macro_set)) var/list/split_name = splittext(macro_set[k], ".") + if(split_name[2] in SSinput.protected_macro_ids) + testing("Skipping Protected Macro [split_name[2]]") + continue //Skip protected macros var/macro_name = "[split_name[1]].[split_name[2]]" // [3] is "command" erase_output = "[erase_output];[macro_name].parent=null" winset(src, null, erase_output) @@ -50,39 +53,29 @@ erase_all_macros() //Set up the stuff we don't let them override. - var/list/macro_set = SSinput.macro_set + var/list/macro_set = SSinput.core_macro_set for(var/k in 1 to length(macro_set)) var/key = macro_set[k] var/command = macro_set[key] winset(src, "shared-\ref[key]", "parent=default;name=[key];command=[command]") - var/list/printables - //If they use hotkeys, we can safely use ANY - if(prefs.hotkeys) - var/list/hk_macro_set = SSinput.hotkey_only_set - for(var/k in 1 to length(hk_macro_set)) - var/key = hk_macro_set[k] - var/command = hk_macro_set[key] - winset(src, "hotkey_only-\ref[key]", "parent=default;name=[key];command=[command]") - else //Otherwise, we can't. - /// Install the shared set, so that we force capture all modifier keys - var/list/c_macro_set = SSinput.classic_only_set - for(var/k in 1 to length(c_macro_set)) - var/key = c_macro_set[k] - var/command = c_macro_set[key] - winset(src, "classic_only-\ref[key]", "parent=default;name=[key];command=[command]") - printables = list() - //This is to save time muching down this massive list, it might result in holes, it may be better to simply hardcode all these into the skin. - //I might try that one day, but that day is not today. - for(var/key in personal_macro_set) //We don't care about the bound key, just the key itself - var/keycode = replacetext(key, regex("(Alt|Shift|Ctrl)", "g"), "") - if(!length(keycode)) - continue //Modifier-only keybind entry. We always those. - if(!prefs.hotkeys && !SSinput.unprintables_cache[keycode]) //Track printable hotkeys and skip them. - printables += key - continue - winset(src, "personal-\ref[keycode]", "parent=default;name=[keycode];command=\"KeyDown [keycode]\"") - winset(src, "personal-\ref[keycode]]-UP", "parent=default;name=[keycode]+UP;command=\"KeyUp [keycode]\"") + /// Install the shared set, so that we force capture all modifier keys + var/list/m_macro_set = SSinput.modifier_set + for(var/k in 1 to length(m_macro_set)) + var/key = m_macro_set[k] + var/command = m_macro_set[key] + winset(src, "modifier-\ref[key]", "parent=default;name=[key];command=[command]") + var/list/printables = list() + + for(var/key in personal_macro_set) //We don't care about the bound key, just the key itself + var/keycode = replacetext(key, regex("(Alt|Shift|Ctrl)", "g"), "") + if(!length(keycode) || keycode == "Unbound" || keycode[SSinput.forced_macro_set]) + continue //Modifier-only keybind entry or empty keybind. We've already bound those. + if(!prefs.hotkeys && !SSinput.unprintables_cache[keycode]) //Track printable hotkeys and skip them. + printables += key + continue + winset(src, "personal-\ref[keycode]", "parent=default;name=[keycode];command=\"KeyDown [keycode]\"") + winset(src, "personal-\ref[keycode]]-UP", "parent=default;name=[keycode]+UP;command=\"KeyUp [keycode]\"") if(prefs.hotkeys) From a4b8311951521183b0ac01873bf9fa5f861d6655 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:23:05 -0400 Subject: [PATCH 05/17] Keybind unit test --- code/unit_tests/keybinds.dm | 23 +++++++++++++++++++++++ nebula.dme | 1 + 2 files changed, 24 insertions(+) create mode 100644 code/unit_tests/keybinds.dm diff --git a/code/unit_tests/keybinds.dm b/code/unit_tests/keybinds.dm new file mode 100644 index 00000000000..0a31ebd14ee --- /dev/null +++ b/code/unit_tests/keybinds.dm @@ -0,0 +1,23 @@ +/datum/unit_test/default_fc_keybinds_shall_be_unprintable + name = "INPUT: Focus Chat default keybinds shall be unprintable." + +/datum/unit_test/default_fc_keybinds_shall_be_unprintable/start_test() + var/list/failures = list() + for (var/name in global.keybindings_by_name) + var/datum/keybinding/binding = global.keybindings_by_name[name] + /// If a classic keylist is provided, test that one instead. + var/list/keys_to_check = binding.classic_keys || binding.hotkey_keys + for(var/fc_key in keys_to_check) + /// Strip off default modifiers. + var/stripped_key = replacetext(fc_key, regex("(Alt|Shift|Ctrl)", "g"), "") + + if(!SSinput.unprintables_cache[stripped_key]) + failures.Add(binding.type) + + if(failures) + fail("Printable keys bound by default in Focus Chat keybind set.") + log_bad("Bad Types:") + for(var/bad_type in failures) + log_bad("[bad_type]") + else + pass("All Focus Chat keys are sane.") \ No newline at end of file diff --git a/nebula.dme b/nebula.dme index a4f4fa071a9..cf4d359eb3d 100644 --- a/nebula.dme +++ b/nebula.dme @@ -4066,6 +4066,7 @@ #include "code\unit_tests\items.dm" #include "code\unit_tests\job_tests.dm" #include "code\unit_tests\json.dm" +#include "code\unit_tests\keybinds.dm" #include "code\unit_tests\machine_tests.dm" #include "code\unit_tests\map_tests.dm" #include "code\unit_tests\materials.dm" From 6f2821a4311678552ff175dfca371c181aa1046f Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:33:02 -0400 Subject: [PATCH 06/17] Bad macroset reference in setup I'm great at what I do. --- code/modules/keybindings/setup.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 670226444db..7560b28294b 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -69,8 +69,8 @@ for(var/key in personal_macro_set) //We don't care about the bound key, just the key itself var/keycode = replacetext(key, regex("(Alt|Shift|Ctrl)", "g"), "") - if(!length(keycode) || keycode == "Unbound" || keycode[SSinput.forced_macro_set]) - continue //Modifier-only keybind entry or empty keybind. We've already bound those. + if(!length(keycode) || keycode == "Unbound" || keycode[SSinput.core_macro_set]) + continue //Modifier-only, empty, or special keybind entry. if(!prefs.hotkeys && !SSinput.unprintables_cache[keycode]) //Track printable hotkeys and skip them. printables += key continue From 05c18ae36cf890412e7d5abe61635bde74d4ffba Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:37:44 -0400 Subject: [PATCH 07/17] Snip skin debugging code --- code/world.dm | 67 --------------------------------------------------- 1 file changed, 67 deletions(-) diff --git a/code/world.dm b/code/world.dm index 16767ead239..057199cfb0a 100644 --- a/code/world.dm +++ b/code/world.dm @@ -18,70 +18,3 @@ loop_checks = FALSE #pragma pop #endif - -#warn MACRO DEBUG CODE DO NOT PUSH - -/client - var/x_mt_watchingfocus = FALSE - var/x_mt_winmon_enabled = FALSE - var/list/x_mt_winmon_packet //Lazylist - -/// Dumps the list of all macros. This should almost always be just default -/client/verb/dump_macroset_ids() - set name = "mt Dump Macroset IDs" - set category = "_MACRO_TEST" - to_chat(usr, (jointext(splittext(winget(src, "", "macros"), ";"), "\n") || "NULL (Bad. Incredibly. Incredibly bad.)")) - -/// List all children of default. Name for macros is their bound key. -/client/verb/dump_set() - set name = "mt Dump default bindings" - set category = "_MACRO_TEST" - to_chat(usr, (jointext(splittext(winget(src, "default.*" , "name"), ";"), "\n")|| "NULL (Bad. Real bad.)")) - -/// A slightly more pleasant way to execute free wingets. -/client/verb/arbitrary_winget(cmd as text) - set name = "awing" - set desc = "Run an arbitrary Winset call, Space-separated arguments." - set category = "_MACRO_TEST" - var/list/parts = splittext(cmd, " ") - to_chat(usr, (winget(src, parts[1], parts[2]) || "NULL (Bad Call?)")) - -/// A slightly more pleasant way to execute free winsets. -/client/verb/arbitrary_winset(cmd as text) - set name = "aswin" - set desc = "Run an arbitrary Winset call, Space-separated arguments." - set category = "_MACRO_TEST" - var/list/parts = splittext(cmd, " ") - winset(src, parts[1], parts[2]) - to_chat(usr, ("CALLED: winset({client:[src.ckey]}, \"[parts[1]]\",\"[parts[2]]\")")) - -/// Will dump the currently focused skin element to chat. Used for tracking down focus juggling issues. -/client/verb/focuswatch() - set name = "mt toggle focus watch" - set category = "_MACRO_TEST" - if(x_mt_watchingfocus) - x_mt_watchingfocus = FALSE - return - else - x_mt_watchingfocus = TRUE - while(x_mt_watchingfocus) - // Live-report the element with focus. - to_chat(usr, (winget(src, "", "focus") || "NULL (Entire game defocused?)")) - sleep(0.5) //Every half second - -/client/verb/winmon(cmd as text|null) - set name = "winmon" - set desc = "Repeatedly run a winget to monitor it's value" - set category = "_MACRO_TEST" - if(x_mt_winmon_enabled || isnull(cmd)) - x_mt_winmon_enabled = FALSE - return - else - x_mt_winmon_enabled = TRUE - var/list/parts = splittext(cmd, " ") - x_mt_winmon_packet = parts - while(x_mt_winmon_enabled) - // Repeatedly rerun the same winget to watch the value - var/winout = winget(src, x_mt_winmon_packet[1], x_mt_winmon_packet[2]) - to_chat(usr, ( winout ? "WINMON:[winout]": "WINMON: NULL (Bad Call?)")) - sleep(0.5) \ No newline at end of file From 7582796f17c6b3ef06ced3e60ee178524da4865f Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:48:21 -0400 Subject: [PATCH 08/17] Unit test fixes Unit tests should return something. Addresses keybinds. --- code/modules/keybindings/binds/communication.dm | 4 ++++ code/modules/keybindings/binds/human.dm | 3 +++ code/modules/keybindings/binds/living.dm | 2 ++ code/modules/keybindings/binds/mob.dm | 10 ++++++++++ code/unit_tests/keybinds.dm | 7 +++++-- code/world.dm | 2 +- 6 files changed, 25 insertions(+), 3 deletions(-) diff --git a/code/modules/keybindings/binds/communication.dm b/code/modules/keybindings/binds/communication.dm index 7f1b6f76ac7..ffce7fa14db 100644 --- a/code/modules/keybindings/binds/communication.dm +++ b/code/modules/keybindings/binds/communication.dm @@ -4,20 +4,24 @@ /datum/keybinding/client/communication/ooc hotkey_keys = list("O", "F2") + classic_keys = list("F2") name = "ooc" full_name = "Out Of Character Say (OOC)" /datum/keybinding/client/communication/looc hotkey_keys = list("L") + classic_keys = list("Unbound") name = "looc" full_name = "Local Out Of Character Say (LOOC)" /datum/keybinding/client/communication/say hotkey_keys = list("T", "F3") + classic_keys = list("F3") name = "say" full_name = "IC Say" /datum/keybinding/client/communication/me hotkey_keys = list("M", "F4") + classic_keys = list("F4") name = "me" full_name = "Custom Emote (/Me)" diff --git a/code/modules/keybindings/binds/human.dm b/code/modules/keybindings/binds/human.dm index cc283c2f158..827bace3104 100644 --- a/code/modules/keybindings/binds/human.dm +++ b/code/modules/keybindings/binds/human.dm @@ -7,6 +7,7 @@ /datum/keybinding/human/quick_equip hotkey_keys = list("E") + classic_keys = list("Unbound") name = "quick_equip" full_name = "Quick Equip" description = "Quickly puts an item in the best slot available" @@ -18,6 +19,7 @@ /datum/keybinding/human/holster hotkey_keys = list("H") + classic_keys = list("Unbound") name = "holster" full_name = "Holster" description = "Draw or holster weapon" @@ -57,6 +59,7 @@ /datum/keybinding/human/give hotkey_keys = list("G") + classic_keys = list("Unbound") name = "give_item" full_name = "Give Item" description = "Give the item you're currently holding" diff --git a/code/modules/keybindings/binds/living.dm b/code/modules/keybindings/binds/living.dm index 7f9ba3530a2..3d8025e92de 100644 --- a/code/modules/keybindings/binds/living.dm +++ b/code/modules/keybindings/binds/living.dm @@ -7,6 +7,7 @@ /datum/keybinding/living/rest hotkey_keys = list("ShiftB") + classic_keys = list("Unbound") name = "rest" full_name = "Rest" description = "You lay down/get up" @@ -18,6 +19,7 @@ /datum/keybinding/living/resist hotkey_keys = list("B") + classic_keys = list("Unbound") name = "resist" full_name = "Resist" description = "Break free of your current state. Handcuffed? On fire? Resist!" diff --git a/code/modules/keybindings/binds/mob.dm b/code/modules/keybindings/binds/mob.dm index b77720b6997..1d4e01ba333 100644 --- a/code/modules/keybindings/binds/mob.dm +++ b/code/modules/keybindings/binds/mob.dm @@ -7,6 +7,7 @@ /datum/keybinding/mob/toggle_throw_mode hotkey_keys = list("R", "Southwest") + classic_keys = list("Southwest") name = "toggle_throw_mode" full_name = "Toggle Throw mode" description = "Toggle throwing the current item or not." @@ -17,6 +18,7 @@ /datum/keybinding/mob/hold_throw_mode hotkey_keys = list("Space") + classic_keys = list("Unbound") name = "hold_throw_mode" full_name = "Hold throw mode" description = "Hold this to turn on throw mode, and release it to turn off throw mode" @@ -31,6 +33,7 @@ /datum/keybinding/mob/swap_hands hotkey_keys = list("X", "Northeast") + classic_keys = list("Northeast") name = "swap_hands" full_name = "Swap Hands" @@ -40,6 +43,7 @@ /datum/keybinding/mob/drop_item hotkey_keys = list("Q", "Northwest") + classic_keys = list("Northwest") name = "drop_item" full_name = "Drop Item" @@ -49,6 +53,7 @@ /datum/keybinding/mob/select_help_intent hotkey_keys = list("1") + classic_keys = list("Unbound") name = "select_help_intent" full_name = "Select Help Intent" @@ -58,6 +63,7 @@ /datum/keybinding/mob/select_disarm_intent hotkey_keys = list("2") + classic_keys = list("Unbound") name = "select_disarm_intent" full_name = "Select Disarm Intent" @@ -67,6 +73,7 @@ /datum/keybinding/mob/select_grab_intent hotkey_keys = list("3") + classic_keys = list("Unbound") name = "select_grab_intent" full_name = "Select Grab Intent" @@ -76,6 +83,7 @@ /datum/keybinding/mob/select_harm_intent hotkey_keys = list("4") + classic_keys = list("Unbound") name = "select_harm_intent" full_name = "Select Harm Intent" @@ -85,6 +93,7 @@ /datum/keybinding/mob/cycle_intent_right hotkey_keys = list("G", "Insert") + classic_keys = list("Insert") name = "cycle_intent_right" full_name = "Сycle Intent: Right" @@ -103,6 +112,7 @@ /datum/keybinding/mob/activate_inhand hotkey_keys = list("Z", "Y","Southeast") // Southeast = PAGEDOWN + classic_keys = list("Southeast") name = "activate_inhand" full_name = "Activate In-Hand" description = "Uses whatever item you have inhand" diff --git a/code/unit_tests/keybinds.dm b/code/unit_tests/keybinds.dm index 0a31ebd14ee..b4c65d2952c 100644 --- a/code/unit_tests/keybinds.dm +++ b/code/unit_tests/keybinds.dm @@ -11,7 +11,9 @@ /// Strip off default modifiers. var/stripped_key = replacetext(fc_key, regex("(Alt|Shift|Ctrl)", "g"), "") - if(!SSinput.unprintables_cache[stripped_key]) + if(length(stripped_key)) + continue //Pure Modifier key (Alt, Ctrl, Shift) + if(!SSinput.unprintables_cache[stripped_key] && stripped_key != "Unbound") failures.Add(binding.type) if(failures) @@ -20,4 +22,5 @@ for(var/bad_type in failures) log_bad("[bad_type]") else - pass("All Focus Chat keys are sane.") \ No newline at end of file + pass("All Focus Chat keys are sane.") + return 1 \ No newline at end of file diff --git a/code/world.dm b/code/world.dm index 057199cfb0a..0c64f48fce7 100644 --- a/code/world.dm +++ b/code/world.dm @@ -17,4 +17,4 @@ #pragma ignore loop_checks loop_checks = FALSE #pragma pop -#endif +#endif \ No newline at end of file From fc84c33984c202d05ea3843836b8911a6e8a863b Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 04:55:37 -0400 Subject: [PATCH 09/17] Keybind reset fixes Removes keyboard language nag Maps ENTER, TAB, and BACKSPACE to BYOND key mappings. --- code/_global_vars/client.dm | 5 ++++- code/_helpers/global_lists.dm | 10 ++++++++++ code/controllers/subsystems/input.dm | 4 ++++ code/modules/client/client_procs.dm | 6 ------ .../controls/01_keybindings.dm | 19 ++++++++++++++++--- code/modules/keybindings/binds/mob.dm | 3 ++- code/modules/keybindings/binds/movement.dm | 2 ++ code/modules/keybindings/setup.dm | 2 +- 8 files changed, 39 insertions(+), 12 deletions(-) diff --git a/code/_global_vars/client.dm b/code/_global_vars/client.dm index 5a8b7f1b1c5..baf263d480d 100644 --- a/code/_global_vars/client.dm +++ b/code/_global_vars/client.dm @@ -13,7 +13,10 @@ var/global/list/_kbMap = list( "SPACEBAR" = "Space", "ALT" = "Alt", "SHIFT" = "Shift", - "CONTROL" = "Ctrl" + "CONTROL" = "Ctrl", + "ENTER" = "Return", + "TAB" = "Tab", + "BACKSPACE" = "Back" ) // Without alt, shift, ctrl and etc because its not necessary diff --git a/code/_helpers/global_lists.dm b/code/_helpers/global_lists.dm index daeb6381141..1c48d90a0bd 100644 --- a/code/_helpers/global_lists.dm +++ b/code/_helpers/global_lists.dm @@ -50,6 +50,7 @@ var/global/list/child_stance_limbs = list( // TODO: Replace keybinding datums with keybinding decls to make this unnecessary. var/global/list/hotkey_keybinding_list_by_key = list() // Replace this with just looping over all keybinding decls (as below) in a 'reset hotkeys' proc. +var/global/list/hotkey_keybinding_list_by_key_fc = list() var/global/list/keybindings_by_name = list() // Replace this with just decl lookups. /proc/makeDatumRefLists() // Keybindings @@ -60,9 +61,18 @@ var/global/list/keybindings_by_name = list() // Replace this with just decl look ASSERT(keybinding.name) var/datum/keybinding/instance = new keybinding global.keybindings_by_name[instance.name] = instance + var/classic_stored = FALSE + if(length(instance.classic_keys)) + classic_stored = TRUE + for(var/bound_key in instance.classic_keys) + global.hotkey_keybinding_list_by_key_fc[bound_key] += list(instance.name) if(length(instance.hotkey_keys)) for(var/bound_key in instance.hotkey_keys) global.hotkey_keybinding_list_by_key[bound_key] += list(instance.name) + if(!classic_stored) + global.hotkey_keybinding_list_by_key_fc[bound_key] += list(instance.name) + + /proc/get_playable_species() var/static/list/_playable_species // A list of ALL playable species, whitelisted, latejoin or otherwise. (read: non-restricted) diff --git a/code/controllers/subsystems/input.dm b/code/controllers/subsystems/input.dm index fe01c2ff48d..c3056b258c1 100644 --- a/code/controllers/subsystems/input.dm +++ b/code/controllers/subsystems/input.dm @@ -54,6 +54,10 @@ SUBSYSTEM_DEF(input) // This list may be out of date, and may include keys not actually legal to bind? // The only full list is from 2008. http://www.byond.com/docs/notes/macro.html unprintables_cache = list( + // Modifiers. Not actually ON the list, but still safe as they're. Special. + "Shift" = TRUE, + "Ctrl" = TRUE, + "Alt" = TRUE, // Arrow Keys "North" = TRUE, "West" = TRUE, diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index dd411f598c9..85450de6882 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -664,12 +664,6 @@ var/global/const/MAX_VIEW = 41 winset(src, "default-\ref[key]", "parent=default;name=[key];command=.me") communication_hotkeys += key - // winget() does not work for F1 and F2 - for(var/key in communication_hotkeys) - if(!(key in list("F1","F2")) && !winget(src, "default-\ref[key]", "command")) - to_chat(src, SPAN_WARNING("You probably entered the game with a different keyboard layout.\nPlease switch to the English layout and click here to fix the communication hotkeys.")) - break - /client/proc/get_byond_membership() return prefs?.is_byond_member || IsByondMember() diff --git a/code/modules/client/preference_setup/controls/01_keybindings.dm b/code/modules/client/preference_setup/controls/01_keybindings.dm index ef6e816641b..6ef7a744c8f 100644 --- a/code/modules/client/preference_setup/controls/01_keybindings.dm +++ b/code/modules/client/preference_setup/controls/01_keybindings.dm @@ -121,7 +121,12 @@ var/datum/keybinding/kb = i if(!length(user_binds[kb.name]) || (user_binds[kb.name][1] == "Unbound" && length(user_binds[kb.name]) == 1)) . += "[kb.full_name]Unbound" - var/list/default_keys = pref.hotkeys ? kb.hotkey_keys : kb.classic_keys + var/list/default_keys + if(pref.hotkeys) + default_keys = kb.hotkey_keys + else + default_keys = kb.classic_keys || kb.hotkey_keys + var/class if(user_binds[kb.name] ~= default_keys) class = "class='linkOff fluid'" @@ -264,7 +269,10 @@ return TOPIC_REFRESH if("keybindings_reset") - pref.key_bindings = deepCopyList(global.hotkey_keybinding_list_by_key) + if(pref.hotkeys) + pref.key_bindings = deepCopyList(global.hotkey_keybinding_list_by_key) + else + pref.key_bindings = deepCopyList(global.hotkey_keybinding_list_by_key_fc) user.client.set_macros() return TOPIC_REFRESH @@ -280,7 +288,12 @@ pref.key_bindings -= old_key var/datum/keybinding/kb = global.keybindings_by_name[kb_name] - for(var/key in kb.hotkey_keys) + var/list/default_keys + if(pref.hotkeys) + default_keys = kb.hotkey_keys + else + default_keys = kb.classic_keys || kb.hotkey_keys + for(var/key in default_keys) pref.key_bindings[key] += list(kb_name) pref.key_bindings[key] = sortTim(pref.key_bindings[key], /proc/cmp_text_asc) user.client.set_macros() diff --git a/code/modules/keybindings/binds/mob.dm b/code/modules/keybindings/binds/mob.dm index 1d4e01ba333..0bdcb614ccb 100644 --- a/code/modules/keybindings/binds/mob.dm +++ b/code/modules/keybindings/binds/mob.dm @@ -103,6 +103,7 @@ /datum/keybinding/mob/cycle_intent_left hotkey_keys = list("F") + classic_keys = list("Unbound") name = "cycle_intent_left" full_name = "Сycle Intent: Left" @@ -195,7 +196,7 @@ return TRUE /datum/keybinding/mob/interact - hotkey_keys = list("Enter") + hotkey_keys = list("Return") name = "interact" full_name = "Interact" description = "Interact with the turf directly in front of you." diff --git a/code/modules/keybindings/binds/movement.dm b/code/modules/keybindings/binds/movement.dm index 78e1b4417c8..274b85394de 100644 --- a/code/modules/keybindings/binds/movement.dm +++ b/code/modules/keybindings/binds/movement.dm @@ -46,6 +46,7 @@ /datum/keybinding/movement/move_up hotkey_keys = list(",") + classic_keys = list("Unbound") name = "move_up" full_name = "Move Up" description = "Makes you go up" @@ -56,6 +57,7 @@ /datum/keybinding/movement/move_down hotkey_keys = list(".") + classic_keys = list("Unbound") name = "move_down" full_name = "Move Down" description = "Makes you go down" diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 7560b28294b..3a9382f143a 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -69,7 +69,7 @@ for(var/key in personal_macro_set) //We don't care about the bound key, just the key itself var/keycode = replacetext(key, regex("(Alt|Shift|Ctrl)", "g"), "") - if(!length(keycode) || keycode == "Unbound" || keycode[SSinput.core_macro_set]) + if(!length(keycode) || keycode == "Unbound" || SSinput.core_macro_set[keycode]) continue //Modifier-only, empty, or special keybind entry. if(!prefs.hotkeys && !SSinput.unprintables_cache[keycode]) //Track printable hotkeys and skip them. printables += key From ae4165f79eea499bbc1a241b066db7b6e45bd2f4 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:06:12 -0400 Subject: [PATCH 10/17] Nag preference --- .../controls/01_keybindings.dm | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/code/modules/client/preference_setup/controls/01_keybindings.dm b/code/modules/client/preference_setup/controls/01_keybindings.dm index 6ef7a744c8f..38c475aea81 100644 --- a/code/modules/client/preference_setup/controls/01_keybindings.dm +++ b/code/modules/client/preference_setup/controls/01_keybindings.dm @@ -1,6 +1,8 @@ /datum/preferences /// Whether or not this client has standard hotkeys enabled var/hotkeys = TRUE + /// Focus Chat masked-out hotkey nag. + var/fc_hotkey_nag = TRUE /// Custom Keybindings var/list/key_bindings = list() @@ -76,6 +78,25 @@ user.client.set_macros() return TOPIC_REFRESH +/datum/category_item/player_setup_item/controls/fc_hotkey_nag + name = "Focus Chat Masked Hotkey Nag" + sort_order = 1 + +/datum/category_item/player_setup_item/controls/fc_hotkey_nag/load_preferences(datum/pref_record_reader/R) + pref.fc_hotkey_nag = R.read("fc_hotkey_nag") + +/datum/category_item/player_setup_item/controls/fc_hotkey_nag/save_preferences(datum/pref_record_writer/writer) + writer.write("fc_hotkey_nag", pref.fc_hotkey_nag) + +/datum/category_item/player_setup_item/controls/fc_hotkey_nag/sanitize_preferences() + pref.fc_hotkey_nag = sanitize_bool(pref.fc_hotkey_nag, TRUE) + +/datum/category_item/player_setup_item/controls/fc_hotkey_nag/content(mob/user) + return "
Masked Hotkey Warning:[pref.fc_hotkey_nag ? "Enabled" : "Disabled"]
" + +/datum/category_item/player_setup_item/controls/fc_hotkey_nag/OnTopic(href, list/href_list, mob/user) + pref.fc_hotkey_nag = !pref.fc_hotkey_nag + return TOPIC_REFRESH /datum/category_item/player_setup_item/controls/keybindings name = "Keybindings" From f225a7312799008787ecf6e7b9a667f4b8f899a3 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:08:24 -0400 Subject: [PATCH 11/17] actually check the pref --- code/modules/keybindings/setup.dm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 3a9382f143a..78aa4400383 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -84,12 +84,12 @@ winset(src, null, "input.background-color=[COLOR_INPUT_DISABLED]") //Do we have bad bindings at all, and if so, do we actually care? - if(printables?.len && !prefs.hotkeys) + if(printables?.len && !prefs.hotkeys && prefs.fc_hotkey_nag) to_chat(src, "Hey, you might have some bad keybinds!\n\ - The following keys are bound despite Classic Hotkeys being enabled. These binds are not applied.\n\ - The code used to generate this list is imperfect, You can silence this warning in your Game Preferences.\n\ + The following keys are bound despite Focus Chat being enabled. These binds are not applied.\n\ + The code used to generate this list is imperfect, You can silence this warning in your Control preferences.\n\ Keys: [jointext(printables, ", ")]\ - ") //Pref NYI, FIXME, beat me with a stick before margetime. + ") update_special_keybinds() updating_macros-- //Decrement, Let the next thread through. From 06725565454857b7f0d83a9997988500401cac46 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:09:40 -0400 Subject: [PATCH 12/17] Sort order. --- .../client/preference_setup/controls/01_keybindings.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/client/preference_setup/controls/01_keybindings.dm b/code/modules/client/preference_setup/controls/01_keybindings.dm index 38c475aea81..4b52511dc64 100644 --- a/code/modules/client/preference_setup/controls/01_keybindings.dm +++ b/code/modules/client/preference_setup/controls/01_keybindings.dm @@ -80,7 +80,7 @@ /datum/category_item/player_setup_item/controls/fc_hotkey_nag name = "Focus Chat Masked Hotkey Nag" - sort_order = 1 + sort_order = 2 /datum/category_item/player_setup_item/controls/fc_hotkey_nag/load_preferences(datum/pref_record_reader/R) pref.fc_hotkey_nag = R.read("fc_hotkey_nag") @@ -100,7 +100,7 @@ /datum/category_item/player_setup_item/controls/keybindings name = "Keybindings" - sort_order = 2 + sort_order = 3 /datum/category_item/player_setup_item/controls/keybindings/load_preferences(datum/pref_record_reader/R) pref.key_bindings = R.read("key_bindings") From e8ba318ba9a528764ef3418a524fbddb48f2d7fe Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:12:30 -0400 Subject: [PATCH 13/17] Span choice up to comment. --- code/modules/keybindings/setup.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 78aa4400383..35fc02dcd8f 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -85,7 +85,7 @@ //Do we have bad bindings at all, and if so, do we actually care? if(printables?.len && !prefs.hotkeys && prefs.fc_hotkey_nag) - to_chat(src, "Hey, you might have some bad keybinds!\n\ + to_chat(src, "Hey, you might have some bad keybinds!\n\ The following keys are bound despite Focus Chat being enabled. These binds are not applied.\n\ The code used to generate this list is imperfect, You can silence this warning in your Control preferences.\n\ Keys: [jointext(printables, ", ")]\ From 1934ca794ff39230d2ced19e572b1a64fd182b6d Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:25:42 -0400 Subject: [PATCH 14/17] check for empty string, this broke the entire logic. --- code/unit_tests/keybinds.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/unit_tests/keybinds.dm b/code/unit_tests/keybinds.dm index b4c65d2952c..aeec0610a79 100644 --- a/code/unit_tests/keybinds.dm +++ b/code/unit_tests/keybinds.dm @@ -11,7 +11,7 @@ /// Strip off default modifiers. var/stripped_key = replacetext(fc_key, regex("(Alt|Shift|Ctrl)", "g"), "") - if(length(stripped_key)) + if(!length(stripped_key)) continue //Pure Modifier key (Alt, Ctrl, Shift) if(!SSinput.unprintables_cache[stripped_key] && stripped_key != "Unbound") failures.Add(binding.type) From f2988011f127d855c0cca9d6982bf7ae092da554 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:28:14 -0400 Subject: [PATCH 15/17] Pull out regex --- code/modules/keybindings/setup.dm | 3 ++- code/unit_tests/keybinds.dm | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 35fc02dcd8f..a3805b7fdce 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -67,8 +67,9 @@ winset(src, "modifier-\ref[key]", "parent=default;name=[key];command=[command]") var/list/printables = list() + var/regex/rgx_strip_modifiers = regex("(Alt|Shift|Ctrl)", "g") for(var/key in personal_macro_set) //We don't care about the bound key, just the key itself - var/keycode = replacetext(key, regex("(Alt|Shift|Ctrl)", "g"), "") + var/keycode = replacetext(key, rgx_strip_modifiers, "") if(!length(keycode) || keycode == "Unbound" || SSinput.core_macro_set[keycode]) continue //Modifier-only, empty, or special keybind entry. if(!prefs.hotkeys && !SSinput.unprintables_cache[keycode]) //Track printable hotkeys and skip them. diff --git a/code/unit_tests/keybinds.dm b/code/unit_tests/keybinds.dm index aeec0610a79..6a4e43a0dd4 100644 --- a/code/unit_tests/keybinds.dm +++ b/code/unit_tests/keybinds.dm @@ -3,13 +3,14 @@ /datum/unit_test/default_fc_keybinds_shall_be_unprintable/start_test() var/list/failures = list() + var/regex/rgx_strip_modifiers = regex("(Alt|Shift|Ctrl)", "g") for (var/name in global.keybindings_by_name) var/datum/keybinding/binding = global.keybindings_by_name[name] /// If a classic keylist is provided, test that one instead. var/list/keys_to_check = binding.classic_keys || binding.hotkey_keys for(var/fc_key in keys_to_check) /// Strip off default modifiers. - var/stripped_key = replacetext(fc_key, regex("(Alt|Shift|Ctrl)", "g"), "") + var/stripped_key = replacetext(fc_key, rgx_strip_modifiers, "") if(!length(stripped_key)) continue //Pure Modifier key (Alt, Ctrl, Shift) From dfe1d1f21a9351ddf97ae3ec5a75385a5f038d42 Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:34:15 -0400 Subject: [PATCH 16/17] empty lists are truthy. --- code/unit_tests/keybinds.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/unit_tests/keybinds.dm b/code/unit_tests/keybinds.dm index 6a4e43a0dd4..6267ec05faf 100644 --- a/code/unit_tests/keybinds.dm +++ b/code/unit_tests/keybinds.dm @@ -17,7 +17,7 @@ if(!SSinput.unprintables_cache[stripped_key] && stripped_key != "Unbound") failures.Add(binding.type) - if(failures) + if(length(failures)) fail("Printable keys bound by default in Focus Chat keybind set.") log_bad("Bad Types:") for(var/bad_type in failures) From 438e334b278d0a64221d08151c3e290e7c94c28d Mon Sep 17 00:00:00 2001 From: Francinum <5572280+francinum@users.noreply.github.com> Date: Sat, 19 Sep 2026 00:29:29 -0400 Subject: [PATCH 17/17] Address both review comments renames keybinding map list renames focus chat nag var and savefile key --- code/_helpers/global_lists.dm | 6 ++--- .../controls/01_keybindings.dm | 26 +++++++++---------- code/modules/keybindings/setup.dm | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/code/_helpers/global_lists.dm b/code/_helpers/global_lists.dm index 1c48d90a0bd..0fcbe2720b1 100644 --- a/code/_helpers/global_lists.dm +++ b/code/_helpers/global_lists.dm @@ -50,7 +50,7 @@ var/global/list/child_stance_limbs = list( // TODO: Replace keybinding datums with keybinding decls to make this unnecessary. var/global/list/hotkey_keybinding_list_by_key = list() // Replace this with just looping over all keybinding decls (as below) in a 'reset hotkeys' proc. -var/global/list/hotkey_keybinding_list_by_key_fc = list() +var/global/list/hotkey_keybinding_list_by_key_focused = list() var/global/list/keybindings_by_name = list() // Replace this with just decl lookups. /proc/makeDatumRefLists() // Keybindings @@ -65,12 +65,12 @@ var/global/list/keybindings_by_name = list() // Replace this with just decl look if(length(instance.classic_keys)) classic_stored = TRUE for(var/bound_key in instance.classic_keys) - global.hotkey_keybinding_list_by_key_fc[bound_key] += list(instance.name) + global.hotkey_keybinding_list_by_key_focused[bound_key] += list(instance.name) if(length(instance.hotkey_keys)) for(var/bound_key in instance.hotkey_keys) global.hotkey_keybinding_list_by_key[bound_key] += list(instance.name) if(!classic_stored) - global.hotkey_keybinding_list_by_key_fc[bound_key] += list(instance.name) + global.hotkey_keybinding_list_by_key_focused[bound_key] += list(instance.name) diff --git a/code/modules/client/preference_setup/controls/01_keybindings.dm b/code/modules/client/preference_setup/controls/01_keybindings.dm index 4b52511dc64..185cf667a69 100644 --- a/code/modules/client/preference_setup/controls/01_keybindings.dm +++ b/code/modules/client/preference_setup/controls/01_keybindings.dm @@ -2,7 +2,7 @@ /// Whether or not this client has standard hotkeys enabled var/hotkeys = TRUE /// Focus Chat masked-out hotkey nag. - var/fc_hotkey_nag = TRUE + var/focus_chat_hotkey_nag = TRUE /// Custom Keybindings var/list/key_bindings = list() @@ -78,24 +78,24 @@ user.client.set_macros() return TOPIC_REFRESH -/datum/category_item/player_setup_item/controls/fc_hotkey_nag +/datum/category_item/player_setup_item/controls/focus_chat_hotkey_nag name = "Focus Chat Masked Hotkey Nag" sort_order = 2 -/datum/category_item/player_setup_item/controls/fc_hotkey_nag/load_preferences(datum/pref_record_reader/R) - pref.fc_hotkey_nag = R.read("fc_hotkey_nag") +/datum/category_item/player_setup_item/controls/focus_chat_hotkey_nag/load_preferences(datum/pref_record_reader/R) + pref.focus_chat_hotkey_nag = R.read("focus_chat_hotkey_nag") -/datum/category_item/player_setup_item/controls/fc_hotkey_nag/save_preferences(datum/pref_record_writer/writer) - writer.write("fc_hotkey_nag", pref.fc_hotkey_nag) +/datum/category_item/player_setup_item/controls/focus_chat_hotkey_nag/save_preferences(datum/pref_record_writer/writer) + writer.write("focus_chat_hotkey_nag", pref.focus_chat_hotkey_nag) -/datum/category_item/player_setup_item/controls/fc_hotkey_nag/sanitize_preferences() - pref.fc_hotkey_nag = sanitize_bool(pref.fc_hotkey_nag, TRUE) +/datum/category_item/player_setup_item/controls/focus_chat_hotkey_nag/sanitize_preferences() + pref.focus_chat_hotkey_nag = sanitize_bool(pref.focus_chat_hotkey_nag, TRUE) -/datum/category_item/player_setup_item/controls/fc_hotkey_nag/content(mob/user) - return "
Masked Hotkey Warning:[pref.fc_hotkey_nag ? "Enabled" : "Disabled"]
" +/datum/category_item/player_setup_item/controls/focus_chat_hotkey_nag/content(mob/user) + return "
Masked Hotkey Warning:[pref.focus_chat_hotkey_nag ? "Enabled" : "Disabled"]
" -/datum/category_item/player_setup_item/controls/fc_hotkey_nag/OnTopic(href, list/href_list, mob/user) - pref.fc_hotkey_nag = !pref.fc_hotkey_nag +/datum/category_item/player_setup_item/controls/focus_chat_hotkey_nag/OnTopic(href, list/href_list, mob/user) + pref.focus_chat_hotkey_nag = !pref.focus_chat_hotkey_nag return TOPIC_REFRESH /datum/category_item/player_setup_item/controls/keybindings @@ -293,7 +293,7 @@ if(pref.hotkeys) pref.key_bindings = deepCopyList(global.hotkey_keybinding_list_by_key) else - pref.key_bindings = deepCopyList(global.hotkey_keybinding_list_by_key_fc) + pref.key_bindings = deepCopyList(global.hotkey_keybinding_list_by_key_focused) user.client.set_macros() return TOPIC_REFRESH diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index a3805b7fdce..e378fff2264 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -85,7 +85,7 @@ winset(src, null, "input.background-color=[COLOR_INPUT_DISABLED]") //Do we have bad bindings at all, and if so, do we actually care? - if(printables?.len && !prefs.hotkeys && prefs.fc_hotkey_nag) + if(printables?.len && !prefs.hotkeys && prefs.focus_chat_hotkey_nag) to_chat(src, "Hey, you might have some bad keybinds!\n\ The following keys are bound despite Focus Chat being enabled. These binds are not applied.\n\ The code used to generate this list is imperfect, You can silence this warning in your Control preferences.\n\