From be923762961aafac04f5f3ae16226f6cf00a0d6a Mon Sep 17 00:00:00 2001 From: Fluffy <65877598+FluffyGhoster@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:17:37 +0100 Subject: [PATCH] Verbs subsystem (#20333) What it says on the can; now we can better compensate for processing spikes by queueing and accounting for verb times (assuming we actually use this framework to invoke the bulk of them). I have added its use to some of them, more can be ported as time goes on, eventually everything but the most trivial ones should go through this. No player facing changes. Hopefully. Praise be the omnissiah. --- aurorastation.dme | 4 + code/__DEFINES/callbacks.dm | 3 + code/__DEFINES/cooldowns.dm | 103 ++++++++++- code/__DEFINES/subsystems.dm | 2 + code/__DEFINES/verb_manager.dm | 36 ++++ .../subsystems/speech_controller.dm | 5 + code/controllers/subsystems/verb_manager.dm | 167 ++++++++++++++++++ code/datums/datum.dm | 39 ++++ code/datums/verb_callbacks.dm | 29 +++ code/modules/client/client_procs.dm | 12 +- code/modules/mob/abstract/freelook/eye.dm | 2 +- .../mob/abstract/ghost/observer/observer.dm | 11 +- code/modules/mob/living/living.dm | 11 +- code/modules/mob/mob.dm | 36 ++-- code/modules/mob/say.dm | 10 +- code/modules/tgui/tgui.dm | 3 +- 16 files changed, 444 insertions(+), 29 deletions(-) create mode 100644 code/__DEFINES/verb_manager.dm create mode 100644 code/controllers/subsystems/speech_controller.dm create mode 100644 code/controllers/subsystems/verb_manager.dm create mode 100644 code/datums/verb_callbacks.dm 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)))) - return FALSE - if(next_point_time >= world.time) - return FALSE + DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(_pointed), A)) - next_point_time = world.time + 25 - face_atom(A) - if(isturf(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(TIMER_COOLDOWN_RUNNING(src, "point_verb_emote_cooldown")) + return FALSE + else + TIMER_COOLDOWN_START(src, "point_verb_emote_cooldown", 2.5 SECONDS) + + 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")