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")