diff --git a/aurorastation.dme b/aurorastation.dme
index 7293025217a..9ca8ee4cff0 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -153,6 +153,7 @@
#include "code\__DEFINES\traits.dm"
#include "code\__DEFINES\turfs.dm"
#include "code\__DEFINES\typeids.dm"
+#include "code\__DEFINES\verb_manager.dm"
#include "code\__DEFINES\verbs.dm"
#include "code\__DEFINES\vv.dm"
#include "code\__DEFINES\webhook.dm"
@@ -375,6 +376,7 @@
#include "code\controllers\subsystems\sound_loops.dm"
#include "code\controllers\subsystems\sounds.dm"
#include "code\controllers\subsystems\spatial_gridmap.dm"
+#include "code\controllers\subsystems\speech_controller.dm"
#include "code\controllers\subsystems\statistics.dm"
#include "code\controllers\subsystems\statpanel.dm"
#include "code\controllers\subsystems\stickyban.dm"
@@ -384,6 +386,7 @@
#include "code\controllers\subsystems\ticker.dm"
#include "code\controllers\subsystems\timer.dm"
#include "code\controllers\subsystems\trade.dm"
+#include "code\controllers\subsystems\verb_manager.dm"
#include "code\controllers\subsystems\virtual_reality.dm"
#include "code\controllers\subsystems\vis_contents.dm"
#include "code\controllers\subsystems\vote.dm"
@@ -453,6 +456,7 @@
#include "code\datums\statistic.dm"
#include "code\datums\tgs_event_handler.dm"
#include "code\datums\tgui_module.dm"
+#include "code\datums\verb_callbacks.dm"
#include "code\datums\weakrefs.dm"
#include "code\datums\changelog\changelog.dm"
#include "code\datums\components\_component.dm"
diff --git a/code/__DEFINES/callbacks.dm b/code/__DEFINES/callbacks.dm
index 951de34653f..7e07c3d0541 100644
--- a/code/__DEFINES/callbacks.dm
+++ b/code/__DEFINES/callbacks.dm
@@ -19,3 +19,6 @@
call(0 || proc_owner, proc_path)(##proc_arguments); \
}; \
}
+
+/// like CALLBACK but specifically for verb callbacks
+#define VERB_CALLBACK new /datum/callback/verb_callback
diff --git a/code/__DEFINES/cooldowns.dm b/code/__DEFINES/cooldowns.dm
index 2b15ee53782..0ff525dac5a 100644
--- a/code/__DEFINES/cooldowns.dm
+++ b/code/__DEFINES/cooldowns.dm
@@ -1,3 +1,102 @@
+//// COOLDOWN SYSTEMS
+/*
+ * We have 2 cooldown systems: timer cooldowns (divided between stoppable and regular) and world.time cooldowns.
+ *
+ * When to use each?
+ *
+ * * Adding a commonly-checked cooldown, like on a subsystem to check for processing
+ * * * Use the world.time ones, as they are cheaper.
+ *
+ * * Adding a rarely-used one for special situations, such as giving an uncommon item a cooldown on a target.
+ * * * Timer cooldown, as adding a new variable on each mob to track the cooldown of said uncommon item is going too far.
+ *
+ * * Triggering events at the end of a cooldown.
+ * * * Timer cooldown, registering to its signal.
+ *
+ * * Being able to check how long left for the cooldown to end.
+ * * * Either world.time or stoppable timer cooldowns, depending on the other factors. Regular timer cooldowns do not support this.
+ *
+ * * Being able to stop the timer before it ends.
+ * * * Either world.time or stoppable timer cooldowns, depending on the other factors. Regular timer cooldowns do not support this.
+*/
+
+
+/*
+ * Cooldown system based on an datum-level associative lazylist using timers.
+*/
+
+//INDEXES
+#define COOLDOWN_BORG_SELF_REPAIR "borg_self_repair"
+#define COOLDOWN_EXPRESSPOD_CONSOLE "expresspod_console"
+
+//Mecha cooldowns
+#define COOLDOWN_MECHA_MESSAGE "mecha_message"
+#define COOLDOWN_MECHA_EQUIPMENT(type) ("mecha_equip_[type]")
+#define COOLDOWN_MECHA_MELEE_ATTACK "mecha_melee"
+#define COOLDOWN_MECHA_SMOKE "mecha_smoke"
+#define COOLDOWN_MECHA_SKYFALL "mecha_skyfall"
+#define COOLDOWN_MECHA_MISSILE_STRIKE "mecha_missile_strike"
+#define COOLDOWN_MECHA_CABIN_SEAL "mecha_cabin_seal"
+
+//car cooldowns
+#define COOLDOWN_CAR_HONK "car_honk"
+
+//clown car cooldowns
+#define COOLDOWN_CLOWNCAR_RANDOMNESS "clown_car_randomness"
+
+// item cooldowns
+#define COOLDOWN_SIGNALLER_SEND "cooldown_signaller_send"
+#define COOLDOWN_TOOL_SOUND "cooldown_tool_sound"
+
+//circuit cooldowns
+#define COOLDOWN_CIRCUIT_SOUNDEMITTER "circuit_soundemitter"
+#define COOLDOWN_CIRCUIT_SPEECH "circuit_speech"
+#define COOLDOWN_CIRCUIT_PATHFIND_SAME "circuit_pathfind_same"
+#define COOLDOWN_CIRCUIT_PATHFIND_DIF "circuit_pathfind_different"
+#define COOLDOWN_CIRCUIT_TARGET_INTERCEPT "circuit_target_intercept"
+#define COOLDOWN_CIRCUIT_VIEW_SENSOR "circuit_view_sensor"
+
+// mob cooldowns
+#define COOLDOWN_YAWN_PROPAGATION "yawn_propagation_cooldown"
+
+// admin verb cooldowns
+#define COOLDOWN_INTERNET_SOUND "internet_sound"
+
+//Shared cooldowns for actions
+#define MOB_SHARED_COOLDOWN_1 (1<<0)
+#define MOB_SHARED_COOLDOWN_2 (1<<1)
+#define MOB_SHARED_COOLDOWN_3 (1<<2)
+#define MOB_SHARED_COOLDOWN_BOT_ANNOUNCMENT (1<<3)
+
+//TIMER COOLDOWN MACROS
+
+#define COMSIG_CD_STOP(cd_index) "cooldown_[cd_index]"
+#define COMSIG_CD_RESET(cd_index) "cd_reset_[cd_index]"
+
+#define TIMER_COOLDOWN_START(cd_source, cd_index, cd_time) LAZYSET(cd_source.cooldowns, cd_index, addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(end_cooldown), cd_source, cd_index), cd_time))
+
+/// Checks if a timer based cooldown is NOT finished.
+#define TIMER_COOLDOWN_RUNNING(cd_source, cd_index) LAZYACCESS(cd_source.cooldowns, cd_index)
+
+/// Checks if a timer based cooldown is finished.
+#define TIMER_COOLDOWN_FINISHED(cd_source, cd_index) (!TIMER_COOLDOWN_RUNNING(cd_source, cd_index))
+
+#define TIMER_COOLDOWN_END(cd_source, cd_index) LAZYREMOVE(cd_source.cooldowns, cd_index)
+
+/*
+ * Stoppable timer cooldowns.
+ * Use indexes the same as the regular tiemr cooldowns.
+ * They make use of the TIMER_COOLDOWN_RUNNING() and TIMER_COOLDOWN_END() macros the same, just not the TIMER_COOLDOWN_START() one.
+ * A bit more expensive than the regular timers, but can be reset before they end and the time left can be checked.
+*/
+
+#define S_TIMER_COOLDOWN_START(cd_source, cd_index, cd_time) LAZYSET(cd_source.cooldowns, cd_index, addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(end_cooldown), cd_source, cd_index), cd_time, TIMER_STOPPABLE))
+
+#define S_TIMER_COOLDOWN_RESET(cd_source, cd_index) reset_cooldown(cd_source, cd_index)
+
+#define S_TIMER_COOLDOWN_TIMELEFT(cd_source, cd_index) (timeleft(TIMER_COOLDOWN_RUNNING(cd_source, cd_index)))
+
+
/*
* Cooldown system based on storing world.time on a variable, plus the cooldown time.
* Better performance over timer cooldowns, lower control. Same functionality.
@@ -10,8 +109,10 @@
#define COOLDOWN_START(cd_source, cd_index, cd_time) (cd_source.cd_index = world.time + (cd_time))
//Returns true if the cooldown has run its course, false otherwise
-#define COOLDOWN_FINISHED(cd_source, cd_index) (cd_source.cd_index < world.time)
+#define COOLDOWN_FINISHED(cd_source, cd_index) (cd_source.cd_index <= world.time)
#define COOLDOWN_RESET(cd_source, cd_index) cd_source.cd_index = 0
+#define COOLDOWN_STARTED(cd_source, cd_index) (cd_source.cd_index != 0)
+
#define COOLDOWN_TIMELEFT(cd_source, cd_index) (max(0, cd_source.cd_index - world.time))
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index da7b08a0447..ea0c2e0cd9e 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -268,6 +268,8 @@
#define FIRE_PRIORITY_RUNECHAT 410
#define FIRE_PRIORITY_TIMER 700
#define FIRE_PRIORITY_SOUND_LOOPS 800
+#define FIRE_PRIORITY_SPEECH_CONTROLLER 900
+#define FIRE_PRIORITY_DELAYED_VERBS 950
/**
Create a new timer and add it to the queue.
diff --git a/code/__DEFINES/verb_manager.dm b/code/__DEFINES/verb_manager.dm
new file mode 100644
index 00000000000..11ea6ada4d8
--- /dev/null
+++ b/code/__DEFINES/verb_manager.dm
@@ -0,0 +1,36 @@
+/**
+ * verb queuing thresholds. remember that since verbs execute after SendMaps the player wont see the effects of the verbs on the game world
+ * until SendMaps executes next tick, and then when that later update reaches them. thus most player input has a minimum latency of world.tick_lag + player ping.
+ * however thats only for the visual effect of player input, when a verb processes the actual latency of game state changes or semantic latency is effectively 1/2 player ping,
+ * unless that verb is queued for the next tick in which case its some number probably smaller than world.tick_lag.
+ * so some verbs that represent player input are important enough that we only introduce semantic latency if we absolutely need to.
+ * its for this reason why player clicks are handled in SSinput before even movement - semantic latency could cause someone to move out of range
+ * when the verb finally processes but it was in range if the verb had processed immediately and overtimed.
+ */
+
+///queuing tick_usage threshold for verbs that are high enough priority that they only queue if the server is overtiming.
+///ONLY use for critical verbs
+#define VERB_OVERTIME_QUEUE_THRESHOLD 100
+///queuing tick_usage threshold for verbs that need lower latency more than most verbs.
+#define VERB_HIGH_PRIORITY_QUEUE_THRESHOLD 95
+///default queuing tick_usage threshold for most verbs which can allow a small amount of latency to be processed in the next tick
+#define VERB_DEFAULT_QUEUE_THRESHOLD 85
+
+///attempt to queue this verb process if the server is overloaded. evaluates to FALSE if queuing isnt necessary or if it failed.
+///_verification_args... are only necessary if the verb_manager subsystem youre using checks them in can_queue_verb()
+///if you put anything in _verification_args that ISNT explicitely put in the can_queue_verb() override of the subsystem youre using,
+///it will runtime.
+#define TRY_QUEUE_VERB(_verb_callback, _tick_check, _subsystem_to_use, _verification_args...) (_queue_verb(_verb_callback, _tick_check, _subsystem_to_use, _verification_args))
+///queue wrapper for TRY_QUEUE_VERB() when you want to call the proc if the server isnt overloaded enough to queue
+#define QUEUE_OR_CALL_VERB(_verb_callback, _tick_check, _subsystem_to_use, _verification_args...) \
+ if(!TRY_QUEUE_VERB(_verb_callback, _tick_check, _subsystem_to_use, _verification_args)) {\
+ _verb_callback:InvokeAsync() \
+ };
+
+//goes straight to SSverb_manager with default tick threshold
+#define DEFAULT_TRY_QUEUE_VERB(_verb_callback, _verification_args...) (TRY_QUEUE_VERB(_verb_callback, VERB_DEFAULT_QUEUE_THRESHOLD, null, _verification_args))
+#define DEFAULT_QUEUE_OR_CALL_VERB(_verb_callback, _verification_args...) QUEUE_OR_CALL_VERB(_verb_callback, VERB_DEFAULT_QUEUE_THRESHOLD, null, _verification_args)
+
+//default tick threshold but nondefault subsystem
+#define TRY_QUEUE_VERB_FOR(_verb_callback, _subsystem_to_use, _verification_args...) (TRY_QUEUE_VERB(_verb_callback, VERB_DEFAULT_QUEUE_THRESHOLD, _subsystem_to_use, _verification_args))
+#define QUEUE_OR_CALL_VERB_FOR(_verb_callback, _subsystem_to_use, _verification_args...) QUEUE_OR_CALL_VERB(_verb_callback, VERB_DEFAULT_QUEUE_THRESHOLD, _subsystem_to_use, _verification_args)
diff --git a/code/controllers/subsystems/speech_controller.dm b/code/controllers/subsystems/speech_controller.dm
new file mode 100644
index 00000000000..e293c89a9bb
--- /dev/null
+++ b/code/controllers/subsystems/speech_controller.dm
@@ -0,0 +1,5 @@
+/// verb_manager subsystem just for handling say's
+VERB_MANAGER_SUBSYSTEM_DEF(speech_controller)
+ name = "Speech Controller"
+ wait = 1
+ priority = FIRE_PRIORITY_SPEECH_CONTROLLER//has to be high priority, second in priority ONLY to SSinput
diff --git a/code/controllers/subsystems/verb_manager.dm b/code/controllers/subsystems/verb_manager.dm
new file mode 100644
index 00000000000..f09c0509641
--- /dev/null
+++ b/code/controllers/subsystems/verb_manager.dm
@@ -0,0 +1,167 @@
+/**
+ * SSverb_manager, a subsystem that runs every tick and runs through its entire queue without yielding like SSinput.
+ * this exists because of how the byond tick works and where user inputted verbs are put within it.
+ *
+ * see TICK_ORDER.md for more info on how the byond tick is structured.
+ *
+ * The way the MC allots its time is via TICK_LIMIT_RUNNING, it simply subtracts the cost of SendMaps (MAPTICK_LAST_INTERNAL_TICK_USAGE)
+ * plus TICK_BYOND_RESERVE from the tick and uses up to that amount of time (minus the percentage of the tick used by the time it executes subsystems)
+ * on subsystems running cool things like atmospherics or Life or SSInput or whatever.
+ *
+ * Without this subsystem, verbs are likely to cause overtime if the MC uses all of the time it has allotted for itself in the tick, and SendMaps
+ * uses as much as its expected to, and an expensive verb ends up executing that tick. This is because the MC is completely blind to the cost of
+ * verbs, it can't account for it at all. The only chance for verbs to not cause overtime in a tick where the MC used as much of the tick
+ * as it allotted itself and where SendMaps costed as much as it was expected to is if the verb(s) take less than TICK_BYOND_RESERVE percent of
+ * the tick, which isn't much. Not to mention if SendMaps takes more than 30% of the tick and the MC forces itself to take at least 70% of the
+ * normal tick duration which causes ticks to naturally overrun even in the absence of verbs.
+ *
+ * With this subsystem, the MC can account for the cost of verbs and thus stop major overruns of ticks. This means that the most important subsystems
+ * like SSinput can start at the same time they were supposed to, leading to a smoother experience for the player since ticks aren't riddled with
+ * minor hangs over and over again.
+ */
+SUBSYSTEM_DEF(verb_manager)
+ name = "Verb Manager"
+ wait = 1
+ flags = SS_TICKER | SS_NO_INIT
+ priority = FIRE_PRIORITY_DELAYED_VERBS
+ runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
+
+ ///list of callbacks to procs called from verbs or verblike procs that were executed when the server was overloaded and had to delay to the next tick.
+ ///this list is ran through every tick, and the subsystem does not yield until this queue is finished.
+ var/list/datum/callback/verb_callback/verb_queue = list()
+
+ ///running average of how many verb callbacks are executed every second. used for the stat entry
+ var/verbs_executed_per_second = 0
+
+ ///if TRUE we treat usr's with holders just like usr's without holders. otherwise they always execute immediately
+ var/can_queue_admin_verbs = FALSE
+
+ ///if this is true all verbs immediately execute and don't queue. in case the mc is fucked or something
+ var/FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs = FALSE
+
+ ///used for subtypes to determine if they use their own stats for the stat entry
+ var/use_default_stats = TRUE
+
+ ///if TRUE this will... message admins every time a verb is queued to this subsystem for the next tick with stats.
+ ///for obvious reasons don't make this be TRUE on the code level this is for admins to turn on
+ var/message_admins_on_queue = FALSE
+
+ ///always queue if possible. overrides can_queue_admin_verbs but not FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs
+ var/always_queue = FALSE
+
+/**
+ * queue a callback for the given verb/verblike proc and any given arguments to the specified verb subsystem, so that they process in the next tick.
+ * intended to only work with verbs or verblike procs called directly from client input, use as part of TRY_QUEUE_VERB() and co.
+ *
+ * returns TRUE if the queuing was successful, FALSE otherwise.
+ */
+/proc/_queue_verb(datum/callback/verb_callback/incoming_callback, tick_check, datum/controller/subsystem/verb_manager/subsystem_to_use = SSverb_manager, ...)
+ if(QDELETED(incoming_callback))
+ var/destroyed_string
+ if(!incoming_callback)
+ destroyed_string = "callback is null."
+ else
+ destroyed_string = "callback was deleted [DS2TICKS(world.time - incoming_callback.gc_destroyed)] ticks ago. callback was created [DS2TICKS(world.time) - incoming_callback.creation_time] ticks ago."
+
+ stack_trace("_queue_verb() returned false because it was given a deleted callback! [destroyed_string]")
+ return FALSE
+
+ if(!istext(incoming_callback.object) && QDELETED(incoming_callback.object)) //just in case the object is GLOBAL_PROC
+ var/destroyed_string
+ if(!incoming_callback.object)
+ destroyed_string = "callback.object is null."
+ else
+ destroyed_string = "callback.object was deleted [DS2TICKS(world.time - incoming_callback.object.gc_destroyed)] ticks ago. callback was created [DS2TICKS(world.time) - incoming_callback.creation_time] ticks ago."
+
+ stack_trace("_queue_verb() returned false because it was given a callback acting on a qdeleted object! [destroyed_string]")
+ return FALSE
+
+ //we want unit tests to be able to directly call verbs that attempt to queue, and since unit tests should test internal behavior, we want the queue
+ //to happen as if it was actually from player input if its called on a mob.
+#ifdef UNIT_TESTS
+ if(QDELETED(usr) && ismob(incoming_callback.object))
+ incoming_callback.user = WEAKREF(incoming_callback.object)
+ var/datum/callback/new_us = CALLBACK(arglist(list(GLOBAL_PROC, GLOBAL_PROC_REF(_queue_verb)) + args.Copy()))
+ return world.push_usr(incoming_callback.object, new_us)
+
+#else
+
+ if(QDELETED(usr) || isnull(usr.client))
+ stack_trace("_queue_verb() returned false because it wasn't called from player input!")
+ return FALSE
+
+#endif
+
+ if(!istype(subsystem_to_use))
+ stack_trace("_queue_verb() returned false because it was given an invalid subsystem to queue for!")
+ return FALSE
+
+ if((TICK_USAGE < tick_check) && !subsystem_to_use.always_queue)
+ return FALSE
+
+ var/list/args_to_check = args.Copy()
+ args_to_check.Cut(2, 4)//cut out tick_check and subsystem_to_use
+
+ //any subsystem can use the additional arguments to refuse queuing
+ if(!subsystem_to_use.can_queue_verb(arglist(args_to_check)))
+ return FALSE
+
+ return subsystem_to_use.queue_verb(incoming_callback)
+
+/**
+ * subsystem-specific check for whether a callback can be queued.
+ * intended so that subsystem subtypes can verify whether
+ *
+ * subtypes may include additional arguments here if they need them! you just need to include them properly
+ * in TRY_QUEUE_VERB() and co.
+ */
+/datum/controller/subsystem/verb_manager/proc/can_queue_verb(datum/callback/verb_callback/incoming_callback)
+ if(always_queue && !FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs)
+ return TRUE
+
+ if((usr.client?.holder && !can_queue_admin_verbs) \
+ || (!initialized && !(flags & SS_NO_INIT)) \
+ || FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs \
+ || !(runlevels & Master.current_runlevel))
+ return FALSE
+
+ return TRUE
+
+/**
+ * queue a callback for the given proc, so that it is invoked in the next tick.
+ * intended to only work with verbs or verblike procs called directly from client input, use as part of TRY_QUEUE_VERB()
+ *
+ * returns TRUE if the queuing was successful, FALSE otherwise.
+ */
+/datum/controller/subsystem/verb_manager/proc/queue_verb(datum/callback/verb_callback/incoming_callback)
+ . = FALSE //errored
+ if(message_admins_on_queue)
+ message_admins("[name] verb queuing: tick usage: [TICK_USAGE]%, proc: [incoming_callback.delegate], object: [incoming_callback.object], usr: [usr]")
+ verb_queue += incoming_callback
+ return TRUE
+
+/datum/controller/subsystem/verb_manager/fire(resumed)
+ run_verb_queue()
+
+/// runs through all of this subsystems queue of verb callbacks.
+/// goes through the entire verb queue without yielding.
+/// used so you can flush the queue outside of fire() without interfering with anything else subtype subsystems might do in fire().
+/datum/controller/subsystem/verb_manager/proc/run_verb_queue()
+ var/executed_verbs = 0
+
+ for(var/datum/callback/verb_callback/verb_callback as anything in verb_queue)
+ if(!istype(verb_callback))
+ stack_trace("non /datum/callback/verb_callback inside [name]'s verb_queue!")
+ continue
+
+ verb_callback.InvokeAsync()
+ executed_verbs++
+
+ verb_queue.Cut()
+ verbs_executed_per_second = MC_AVG_SECONDS(verbs_executed_per_second, executed_verbs, wait SECONDS)
+ //note that wait SECONDS is incorrect if this is called outside of fire() but because byond is garbage i need to add a timer to rustg to find a valid solution
+
+/datum/controller/subsystem/verb_manager/stat_entry(msg)
+ . = ..()
+ if(use_default_stats)
+ . += "V/S: [round(verbs_executed_per_second, 0.01)]"
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index 5e7a4b38d51..cf0d7816d5b 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -46,6 +46,14 @@
/// A weak reference to another datum
var/datum/weakref/weak_reference
+ /*
+ * Lazy associative list of currently active cooldowns.
+ *
+ * cooldowns [ COOLDOWN_INDEX ] = add_timer()
+ * add_timer() returns the truthy value of -1 when not stoppable, and else a truthy numeric index
+ */
+ var/list/cooldowns
+
/// Used to avoid unnecessary refstring creation in Destroy().
var/tmp/has_state_machine = FALSE
@@ -147,6 +155,37 @@
for(var/target in _signal_procs)
UnregisterSignal(target, _signal_procs[target])
+/**
+ * Callback called by a timer to end an associative-list-indexed cooldown.
+ *
+ * Arguments:
+ * * source - datum storing the cooldown
+ * * index - string index storing the cooldown on the cooldowns associative list
+ *
+ * This sends a signal reporting the cooldown end.
+ */
+/proc/end_cooldown(datum/source, index)
+ if(QDELETED(source))
+ return
+ SEND_SIGNAL(source, COMSIG_CD_STOP(index))
+ TIMER_COOLDOWN_END(source, index)
+
+
+/**
+ * Proc used by stoppable timers to end a cooldown before the time has ran out.
+ *
+ * Arguments:
+ * * source - datum storing the cooldown
+ * * index - string index storing the cooldown on the cooldowns associative list
+ *
+ * This sends a signal reporting the cooldown end, passing the time left as an argument.
+ */
+/proc/reset_cooldown(datum/source, index)
+ if(QDELETED(source))
+ return
+ SEND_SIGNAL(source, COMSIG_CD_RESET(index), S_TIMER_COOLDOWN_TIMELEFT(source, index))
+ TIMER_COOLDOWN_END(source, index)
+
///Generate a tag for this /datum, if it implements one
///Should be called as early as possible, best would be in New, to avoid weakref mistargets
///Really just don't use this, you don't need it, global lists will do just fine MOST of the time
diff --git a/code/datums/verb_callbacks.dm b/code/datums/verb_callbacks.dm
new file mode 100644
index 00000000000..563ac3ac4d4
--- /dev/null
+++ b/code/datums/verb_callbacks.dm
@@ -0,0 +1,29 @@
+///like normal callbacks but they also record their creation time for measurement purposes
+///they also require the same usr/user that made the callback to both still exist and to still have a client in order to execute
+/datum/callback/verb_callback
+ ///the tick this callback datum was created in. used for testing latency
+ var/creation_time = 0
+
+/datum/callback/verb_callback/New(thingtocall, proctocall, ...)
+ creation_time = DS2TICKS(world.time)
+ . = ..()
+
+#ifndef UNIT_TESTS
+/datum/callback/verb_callback/Invoke(...)
+ var/mob/our_user = user?.resolve()
+ if(QDELETED(our_user) || isnull(our_user.client))
+ return
+ var/mob/temp = usr
+ . = ..()
+ usr = temp
+
+/datum/callback/verb_callback/InvokeAsync(...)
+ var/mob/our_user = user?.resolve()
+ if(QDELETED(our_user) || isnull(our_user.client))
+ return
+ var/mob/temp = usr
+ . = ..()
+ usr = temp
+#endif
+
+
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 4c7d8f54d1c..9f8698d003e 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -25,7 +25,8 @@ var/list/localhost_addresses = list(
If you have any questions about this stuff feel free to ask. ~Carn
*/
-/client/Topic(href, href_list, hsrc)
+//the undocumented 4th argument is for ?[0x\ref] style topic links. hsrc is set to the reference and anything after the ] gets put into hsrc_command
+/client/Topic(href, href_list, hsrc, hsrc_command)
if(!usr || usr != mob) //stops us calling Topic for somebody else's client. Also helps prevent usr=null
return
@@ -257,8 +258,17 @@ var/list/localhost_addresses = list(
if(QDELETED(real_src))
return
+ //fun fact: Topic() acts like a verb and is executed at the end of the tick like other verbs. So we have to queue it if the server is
+ //overloaded
+ if(hsrc && hsrc != holder && DEFAULT_TRY_QUEUE_VERB(VERB_CALLBACK(src, PROC_REF(_Topic), hsrc, href, href_list)))
+ return
..() //redirect to hsrc.Topic()
+///dumb workaround because byond doesnt seem to recognize the Topic() typepath for /datum/proc/Topic() from the client Topic,
+///so we cant queue it without this
+/client/proc/_Topic(datum/hsrc, href, list/href_list)
+ return hsrc.Topic(href, href_list)
+
/proc/client_by_ckey(ckey)
return GLOB.directory[ckey]
diff --git a/code/modules/mob/abstract/freelook/eye.dm b/code/modules/mob/abstract/freelook/eye.dm
index ca15b4a7887..9fd10d7563c 100644
--- a/code/modules/mob/abstract/freelook/eye.dm
+++ b/code/modules/mob/abstract/freelook/eye.dm
@@ -67,7 +67,7 @@
/mob/abstract/eye/pointed()
set popup_menu = 0
set src = usr.contents
- return 0
+ return FALSE
/mob/abstract/eye/examine(mob/user, distance, is_adjacent, infix, suffix, show_extended)
SHOULD_CALL_PARENT(FALSE)
diff --git a/code/modules/mob/abstract/ghost/observer/observer.dm b/code/modules/mob/abstract/ghost/observer/observer.dm
index 0b0dec58d61..ccf88d7b9e7 100644
--- a/code/modules/mob/abstract/ghost/observer/observer.dm
+++ b/code/modules/mob/abstract/ghost/observer/observer.dm
@@ -430,11 +430,12 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
W.add_hiddenprint(src)
W.visible_message(SPAN_WARNING("Invisible fingers crudely paint something in blood on [T]..."))
-/mob/abstract/ghost/observer/pointed(atom/A as mob|obj|turf in view())
- if(!..())
- return 0
- src.visible_message("[src] points to [A]")
- return 1
+/mob/abstract/ghost/observer/pointed(atom/pointing_at)
+ . = ..()
+ if(!.)
+ return
+
+ src.visible_message("[src] points to [pointing_at]")
/mob/abstract/ghost/observer/proc/manifest(mob/user)
is_manifest = 0
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index b43b9af96a7..42662ae733b 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -8,9 +8,7 @@
return
-//mob verbs are faster than object verbs. See above.
-var/mob/living/next_point_time = 0
-/mob/living/pointed(atom/A as mob|obj|turf in view())
+/mob/living/_pointed(atom/pointing_at)
if(src.stat || src.restrained())
return FALSE
if(src.status_flags & FAKEDEATH)
@@ -19,7 +17,7 @@ var/mob/living/next_point_time = 0
. = ..()
if(.)
- visible_message("\The [src] points to \the [A].")
+ visible_message("\The [src] points to \the [pointing_at].")
/mob/living/drop_from_inventory(var/obj/item/W, var/atom/target)
. = ..(W, target)
@@ -673,6 +671,11 @@ default behaviour is:
set name = "Resist"
set category = "IC"
+ DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(execute_resist)))
+
+///proc extender of [/mob/living/verb/resist] meant to make the process queable if the server is overloaded when the verb is called
+/mob/living/proc/execute_resist()
+
if(!incapacitated(INCAPACITATION_KNOCKOUT) && canClick())
resist_grab()
if(!weakened)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 219718d8f0b..3a99eeadbe6 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -382,7 +382,8 @@
set name = "Examine"
set category = "IC"
- examinate(usr, A)
+ //examinate(usr, A)
+ DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, GLOBAL_PROC_REF(examinate), src, A))
/mob/proc/can_examine()
if(client?.eye == src)
@@ -407,25 +408,32 @@
set name = "Point To"
set category = "Object"
- if(!isturf(src.loc) || !(A in range(world.view, get_turf(src))))
+ DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(_pointed), A))
+
+/// possibly delayed verb that finishes the pointing process starting in [/mob/verb/pointed()].
+/// either called immediately or in the tick after pointed() was called, as per the [DEFAULT_QUEUE_OR_CALL_VERB()] macro
+/mob/proc/_pointed(atom/pointing_at)
+
+ if(!isturf(src.loc) || !(pointing_at in range(world.view, get_turf(src))))
return FALSE
- if(next_point_time >= world.time)
+ if(TIMER_COOLDOWN_RUNNING(src, "point_verb_emote_cooldown"))
return FALSE
+ else
+ TIMER_COOLDOWN_START(src, "point_verb_emote_cooldown", 2.5 SECONDS)
- next_point_time = world.time + 25
- face_atom(A)
- if(isturf(A))
+ face_atom(pointing_at)
+ if(isturf(pointing_at))
if(pointing_effect)
end_pointing_effect()
- pointing_effect = new /obj/effect/decal/point(A)
+ pointing_effect = new /obj/effect/decal/point(pointing_at)
pointing_effect.set_invisibility(invisibility)
addtimer(CALLBACK(src, PROC_REF(end_pointing_effect), pointing_effect), 2 SECONDS)
else if(!invisibility)
- var/atom/movable/M = A
+ var/atom/movable/M = pointing_at
M.add_filter("pointglow", 1, list(type = "drop_shadow", x = 0, y = -1, offset = 1, size = 1, color = "#F00"))
addtimer(CALLBACK(M, TYPE_PROC_REF(/atom/movable, remove_filter), "pointglow"), 2 SECONDS)
- A.handle_pointed_at(src)
- SEND_SIGNAL(src, COMSIG_MOB_POINT, A)
+ pointing_at.handle_pointed_at(src)
+ SEND_SIGNAL(src, COMSIG_MOB_POINT, pointing_at)
return TRUE
/mob/proc/end_pointing_effect()
@@ -436,6 +444,10 @@
set category = "Object"
set src = usr
+ DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(execute_mode)))
+
+///proc version to finish /mob/verb/mode() execution. used in case the proc needs to be queued for the tick after its first called
+/mob/proc/execute_mode()
if(hand)
var/obj/item/W = l_hand
if (W)
diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm
index 60e7833f951..7885b6c2d66 100644
--- a/code/modules/mob/say.dm
+++ b/code/modules/mob/say.dm
@@ -1,6 +1,7 @@
/mob/proc/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE)
return
+///what clients use to speak. when you type a message into the chat bar in say mode, this is the first thing that goes off serverside.
/mob/verb/say_verb(message as text)
set name = "Say"
set category = "IC"
@@ -14,7 +15,10 @@
if (src.client.handle_spam_prevention(message, MUTE_IC))
return
- usr.say(message)
+ //queue this message because verbs are scheduled to process after SendMaps in the tick and speech is pretty expensive when it happens.
+ //by queuing this for next tick the mc can compensate for its cost instead of having speech delay the start of the next tick
+ if(message)
+ QUEUE_OR_CALL_VERB_FOR(VERB_CALLBACK(src, PROC_REF(say), message), SSspeech_controller)
/mob/verb/me_verb(message as text)
set name = "Me"
@@ -30,9 +34,9 @@
return
if(use_me)
- usr.client_emote("me",usr.emote_type,message)
+ QUEUE_OR_CALL_VERB_FOR(VERB_CALLBACK(src, PROC_REF(client_emote), "me", usr.emote_type, message), SSspeech_controller)
else
- usr.emote(message)
+ QUEUE_OR_CALL_VERB_FOR(VERB_CALLBACK(src, PROC_REF(emote), message), SSspeech_controller)
/mob/proc/say_dead(var/message)
if(say_disabled) //This is here to try to identify lag problems
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index a1d234cce5a..c140d560949 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -329,8 +329,7 @@
window = window,
src_object = src_object)
process_status()
- on_act_message(act_type, payload, state)
- //DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(on_act_message), act_type, payload, state))
+ DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(on_act_message), act_type, payload, state))
return FALSE
switch(type)
if("ready")