diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm
index 3877ac213c5..65f4da2b50a 100644
--- a/code/__DEFINES/admin.dm
+++ b/code/__DEFINES/admin.dm
@@ -63,4 +63,11 @@
#define AREACOORD(src) "[src ? "[get_area_name(src, TRUE)] [COORD(src)]" : "nonexistent location" ]"
#define ADMIN_COORDJMP(src) "[src ? "[COORD(src)] [ADMIN_JMP(src)]" : "nonexistent location"]"
#define ADMIN_VERBOSEJMP(src) "[src ? "[AREACOORD(src)] [ADMIN_JMP(src)]" : "nonexistent location"]"
-#define ADMIN_SHOWDETAILS(mask, content) "[mask]"
\ No newline at end of file
+#define ADMIN_SHOWDETAILS(mask, content) "[mask]"
+
+///Max length of a keypress command before it's considered to be a forged packet/bogus command
+#define MAX_KEYPRESS_COMMANDLENGTH 16
+///Max amount of keypress messages per second over two seconds before client is autokicked
+#define MAX_KEYPRESS_AUTOKICK 50
+///Length of held key rolling buffer
+#define HELD_KEY_BUFFER_LENGTH 15
\ No newline at end of file
diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm
index 22325fed75f..0feca103c12 100644
--- a/code/modules/client/client defines.dm
+++ b/code/modules/client/client defines.dm
@@ -95,3 +95,8 @@
// Has the client been varedited by an admin? [Inherits from datum now]
// var/var_edited = FALSE
+
+ var/client_keysend_amount = 0
+ var/next_keysend_reset = 0
+ var/next_keysend_trip_reset = 0
+ var/keysend_tripped = FALSE
\ No newline at end of file
diff --git a/code/modules/keybindings/bindings_client.dm b/code/modules/keybindings/bindings_client.dm
index e3925aa5283..d59e3da4e8e 100644
--- a/code/modules/keybindings/bindings_client.dm
+++ b/code/modules/keybindings/bindings_client.dm
@@ -3,7 +3,46 @@
/client/verb/keyDown(_key as text)
set instant = TRUE
set hidden = TRUE
+
+ client_keysend_amount += 1
+
+ var/cache = client_keysend_amount
+
+ if(keysend_tripped && next_keysend_trip_reset <= world.time)
+ keysend_tripped = FALSE
+
+ if(next_keysend_reset <= world.time)
+ client_keysend_amount = 0
+ next_keysend_reset = world.time + (1 SECONDS)
+
+ //The "tripped" system is to confirm that flooding is still happening after one spike
+ //not entirely sure how byond commands interact in relation to lag
+ //don't want to kick people if a lag spike results in a huge flood of commands being sent
+ if(cache >= MAX_KEYPRESS_AUTOKICK)
+ if(!keysend_tripped)
+ keysend_tripped = TRUE
+ next_keysend_trip_reset = world.time + (2 SECONDS)
+ else
+ log_admin("Client [ckey] was just autokicked for flooding keysends; likely abuse but potentially lagspike.")
+ message_admins("Client [ckey] was just autokicked for flooding keysends; likely abuse but potentially lagspike.")
+ QDEL_IN(src, 1)
+ return
+
+ ///Check if the key is short enough to even be a real key
+ if(LAZYLEN(_key) > MAX_KEYPRESS_COMMANDLENGTH)
+ to_chat(src, "Invalid KeyDown detected! You have been disconnected from the server automatically.")
+ log_admin("Client [ckey] just attempted to send an invalid keypress. Keymessage was over [MAX_KEYPRESS_COMMANDLENGTH] characters, autokicking due to likely abuse.")
+ message_admins("Client [ckey] just attempted to send an invalid keypress. Keymessage was over [MAX_KEYPRESS_COMMANDLENGTH] characters, autokicking due to likely abuse.")
+ QDEL_IN(src, 1)
+ return
+ //offset by 1 because the buffer address is 0 indexed because the math was simpler
+ keys_held[current_key_address + 1] = _key
+ //the time a key was pressed isn't actually used anywhere (as of 2019-9-10) but this allows easier access usage/checking
+
keys_held[_key] = world.time
+
+ current_key_address = ((current_key_address + 1) % HELD_KEY_BUFFER_LENGTH)
+
var/movement = SSinput.movement_keys[_key]
if (prefs.toggles & AZERTY) movement = SSinput.alt_movement_keys[_key]
if(!(next_move_dir_sub & movement) && !keys_held["Ctrl"])
@@ -40,7 +79,11 @@
/client/verb/keyUp(_key as text)
set instant = TRUE
set hidden = TRUE
- keys_held -= _key
+ //Can't just do a remove because it would alter the length of the rolling buffer, instead search for the key then null it out if it exists
+ for(var/i in 1 to HELD_KEY_BUFFER_LENGTH)
+ if(keys_held[i] == _key)
+ keys_held[i] = null
+ break
var/movement = SSinput.movement_keys[_key]
if (prefs.toggles & AZERTY) movement = SSinput.alt_movement_keys[_key]
if(!(next_move_dir_add & movement))
diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm
index cc76256386e..b6e80452304 100644
--- a/code/modules/keybindings/setup.dm
+++ b/code/modules/keybindings/setup.dm
@@ -1,9 +1,14 @@
/client
- var/list/keys_held = list() // A list of any keys held currently
- // These next two vars are to apply movement for keypresses and releases made while move delayed.
- // Because discarding that input makes the game less responsive.
- var/next_move_dir_add // On next move, add this dir to the move that would otherwise be done
- var/next_move_dir_sub // On next move, subtract this dir from the move that would otherwise be done
+ /// A rolling buffer of any keys held currently
+ var/list/keys_held = list()
+ ///used to keep track of the current rolling buffer position
+ var/current_key_address = 0
+ /// These next two vars are to apply movement for keypresses and releases made while move delayed.
+ /// Because discarding that input makes the game less responsive.
+ /// On next move, add this dir to the move that would otherwise be done
+ var/next_move_dir_add
+ /// On next move, subtract this dir from the move that would otherwise be done
+ var/next_move_dir_sub
// Set a client's focus to an object and override these procs on that object to let it handle keypresses
@@ -31,6 +36,11 @@
/client/proc/set_macros()
set waitfor = FALSE
+ //Reset and populate the rolling buffer
+ keys_held.Cut()
+ for(var/i in 1 to HELD_KEY_BUFFER_LENGTH)
+ keys_held += null
+
erase_all_macros()
var/list/macro_sets = SSinput.macro_sets