diff --git a/.github/TICK_ORDER.md b/.github/TICK_ORDER.md new file mode 100644 index 00000000000..5c27617db4c --- /dev/null +++ b/.github/TICK_ORDER.md @@ -0,0 +1,21 @@ +The byond tick proceeds as follows: +1. procs sleeping via walk() are resumed (i dont know why these are first) + +2. normal sleeping procs are resumed, in the order they went to sleep in the first place, this is where the MC wakes up and processes subsystems. a consequence of this is that the MC almost never resumes before other sleeping procs, because it only goes to sleep for 1 tick 99% of the time, and 99% of procs either go to sleep for less time than the MC (which guarantees that they entered the sleep queue earlier when its time to wake up) and/or were called synchronously from the MC's execution, almost all of the time the MC is the last sleeping proc to resume in any given tick. This is good because it means the MC can account for the cost of previous resuming procs in the tick, and minimizes overtime. + +3. control is passed to byond after all of our code's procs stop execution for this tick + +4. a few small things happen in byond internals + +5. SendMaps is called for this tick, which processes the game state for all clients connected to the game and handles sending them changes +in appearances within their view range. This is expensive and takes up a significant portion of our tick, about 0.45% per connected player +as of 3/20/2022. meaning that with 50 players, 22.5% of our tick is being used up by just SendMaps, after all of our code has stopped executing. Thats only the average across all rounds, for most highpop rounds it can look like 0.6% of the tick per player, which is 30% for 50 players. + +6. After SendMaps ends, client verbs sent to the server are executed, and its the last major step before the next tick begins. +During the course of the tick, a client can send a command to the server saying that they have executed any verb. The actual code defined +for that /verb/name() proc isnt executed until this point, and the way the MC is designed makes this especially likely to make verbs +"overrun" the bounds of the tick they executed in, stopping the other tick from starting and thus delaying the MC firing in that tick. + +The master controller can derive how much of the tick was used in: procs executing before it woke up (because of world.tick_usage), and SendMaps (because of world.map_cpu, since this is a running average you cant derive the tick spent on maptick on any particular tick). It cannot derive how much of the tick was used for sleeping procs resuming after the MC ran, or for verbs executing after SendMaps. + +It is for these reasons why you should heavily limit processing done in verbs, while procs resuming after the MC are rare, verbs are not, and are much more likely to cause overtime since theyre literally at the end of the tick. If you make a verb, try to offload any expensive work to the beginning of the next tick via a verb management subsystem. diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm index be82679e683..543f7f814a1 100644 --- a/code/__DEFINES/MC.dm +++ b/code/__DEFINES/MC.dm @@ -17,6 +17,15 @@ #define MC_AVG_FAST_UP_SLOW_DOWN(average, current) (average > current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current)) #define MC_AVG_SLOW_UP_FAST_DOWN(average, current) (average < current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current)) +///creates a running average of "things elapsed" per time period when you need to count via a smaller time period. +///eg you want an average number of things happening per second but you measure the event every tick (50 milliseconds). +///make sure both time intervals are in the same units. doesnt work if current_duration > total_duration or if total_duration == 0 +#define MC_AVG_OVER_TIME(average, current, total_duration, current_duration) (((total_duration - current_duration) / (total_duration)) * (average) + (current)) + +#define MC_AVG_MINUTES(average, current, current_duration) (MC_AVG_OVER_TIME(average, current, 1 MINUTES, current_duration)) + +#define MC_AVG_SECONDS(average, current, current_duration) (MC_AVG_OVER_TIME(average, current, 1 SECONDS, current_duration)) + #define NEW_SS_GLOBAL(varname) if(varname != src){if(istype(varname)){Recover();qdel(varname);}varname = src;} #define START_PROCESSING(Processor, Datum) if (!Datum.isprocessing) {Datum.isprocessing = TRUE;Processor.processing += Datum} diff --git a/code/__DEFINES/callbacks.dm b/code/__DEFINES/callbacks.dm index 18059a60839..8b828a6d017 100644 --- a/code/__DEFINES/callbacks.dm +++ b/code/__DEFINES/callbacks.dm @@ -2,3 +2,6 @@ #define CALLBACK new /datum/callback #define INVOKE_ASYNC ImmediateInvokeAsync + +/// like CALLBACK but specifically for verb callbacks +#define VERB_CALLBACK new /datum/callback/verb_callback diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 99d95963c60..56b05d60411 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -112,6 +112,7 @@ #define FIRE_PRIORITY_TICKER 200 #define FIRE_PRIORITY_RUNECHAT 410 // I hate how high the fire priority on this is -aa #define FIRE_PRIORITY_OVERLAYS 500 +#define FIRE_PRIORITY_DELAYED_VERBS 950 #define FIRE_PRIORITY_INPUT 1000 // This must always always be the max highest priority. Player input must never be lost. 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/__HELPERS/time.dm b/code/__HELPERS/time.dm index e21bc4c28d2..75e39c80280 100644 --- a/code/__HELPERS/time.dm +++ b/code/__HELPERS/time.dm @@ -1,3 +1,7 @@ +#define MILLISECONDS *0.01 + +#define DECISECONDS *1 //the base unit all of these defines are scaled by, because byond uses that as a unit of measurement for some fucking reason + // So you can be all 10 SECONDS #define SECONDS *10 diff --git a/code/controllers/subsystem/verb_manager.dm b/code/controllers/subsystem/verb_manager.dm new file mode 100644 index 00000000000..bfa780015f3 --- /dev/null +++ b/code/controllers/subsystem/verb_manager.dm @@ -0,0 +1,174 @@ +/** + * 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 alloted 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 alloted 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 isnt 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 arent riddled with + * minor hangs over and over again. + */ +SUBSYSTEM_DEF(verb_manager) + name = "Verb Queue Manager" + wait = 1 + flags = SS_TICKER | SS_NO_INIT + priority = FIRE_PRIORITY_DELAYED_VERBS + runlevels = RUNLEVEL_INIT | 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 dont queue. in case the mc is fucked or something + var/FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs = FALSE + + ///if TRUE this will... message admins every time a verb is queued to this subsystem for the next tick with stats. + ///for obvious reasons dont 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. overides 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.usr_uid = incoming_callback.object.UID() + var/datum/callback/new_us = CALLBACK(arglist(list(GLOBAL_PROC, GLOBAL_PROC_REF(_queue_verb)) + args.Copy())) + return world.invoke_callback_with_usr(incoming_callback.object, new_us) +#endif + + //debatable whether this is needed, this is just to try and ensure that you dont use this to queue stuff that isnt from player input. + if(QDELETED(usr)) + stack_trace("_queue_verb() returned false because it wasnt called from player input!") + return FALSE + + 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/get_stat_details() + return "V/S: [round(verbs_executed_per_second, 0.01)]" + +/datum/controller/subsystem/verb_manager/Recover() + verb_queue = SSverb_manager.verb_queue + +/client/proc/force_verb_bypass() + set category = "Debug" + set name = "Enable Forced Verb Execution" + + if(!check_rights(R_DEBUG)) + return + + if(alert(src,"This will make all verbs bypass the queueing system, creating more lag. Are you absolutely sure?","Verb Manager","Yes","No") == "Yes") + SSverb_manager.FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs = TRUE + message_admins("Admin [key_name_admin(usr)] has forced verbs to bypass the verb queue subsystem.") diff --git a/code/datums/callback.dm b/code/datums/callback.dm index b96d5c25eae..9f2c8d0cd5f 100644 --- a/code/datums/callback.dm +++ b/code/datums/callback.dm @@ -40,6 +40,7 @@ var/datum/object = GLOBAL_PROC var/delegate var/list/arguments + var/usr_uid /datum/callback/New(thingtocall, proctocall, ...) if(thingtocall) @@ -47,6 +48,8 @@ delegate = proctocall if(length(args) > 2) arguments = args.Copy(3) + if(usr) + usr_uid = usr.UID() /proc/ImmediateInvokeAsync(thingtocall, proctocall, ...) set waitfor = FALSE @@ -62,6 +65,12 @@ call(thingtocall, proctocall)(arglist(calling_arguments)) /datum/callback/proc/Invoke(...) + if(!usr && usr_uid) + var/mob/M = locateUID(usr_uid) + if(M) + if(length(args)) + return world.invoke_callback_with_usr(arglist(list(M, src) + args)) + return world.invoke_callback_with_usr(M, src) if(!object) return var/list/calling_arguments = arguments @@ -77,6 +86,14 @@ //copy and pasted because fuck proc overhead /datum/callback/proc/InvokeAsync(...) set waitfor = FALSE + + if(!usr && usr_uid) + var/mob/M = locateUID(usr_uid) + if(M) + if(length(args)) + return world.invoke_callback_with_usr(arglist(list(M, src) + args)) + return world.invoke_callback_with_usr(M, src) + if(!object) return var/list/calling_arguments = arguments diff --git a/code/datums/verb_callbacks.dm b/code/datums/verb_callbacks.dm new file mode 100644 index 00000000000..6468974260f --- /dev/null +++ b/code/datums/verb_callbacks.dm @@ -0,0 +1,8 @@ +///like normal callbacks but they also record their creation time for measurement purposes +/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) + . = ..() diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 0140ea75190..b2689d02dea 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -177,6 +177,7 @@ GLOBAL_LIST_INIT(admin_verbs_debug, list( /client/proc/dmapi_log, /client/proc/timer_log, /client/proc/debug_timers, + /client/proc/force_verb_bypass, )) GLOBAL_LIST_INIT(admin_verbs_possess, list( /proc/possess, diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 289f989d02a..250e1768c0e 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -173,8 +173,18 @@ if("openLink") src << link(href_list["link"]) + //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) + /client/proc/get_display_key() var/fakekey = holder?.fakekey diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 6a843c2fe57..45092d972df 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -745,7 +745,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp //this is a mob verb instead of atom for performance reasons //see /mob/verb/examinate() in mob.dm for more info //overriden here and in /mob/living for different point span classes and sanity checks -/mob/dead/observer/pointed(atom/A as mob|obj|turf in view()) +/mob/dead/observer/run_pointed(atom/A as mob|obj|turf in view()) if(!..()) return FALSE var/follow_link diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index 3a287ee3b71..d06e2343b87 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -10,6 +10,11 @@ set name = "quick-equip" set hidden = 1 + + DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(run_quick_equip))) + +///proc extender of [/mob/verb/quick_equip] used to make the verb queuable if the server is overloaded +/mob/proc/run_quick_equip() var/obj/item/I = get_active_hand() if(I) I.equip_to_best_slot(src) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 04ba1d8b13f..2eaa718ce33 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -249,6 +249,9 @@ return FALSE if(HAS_TRAIT(src, TRAIT_FAKEDEATH)) return FALSE + return ..() + +/mob/living/run_pointed(atom/A) if(!..()) return FALSE var/obj/item/hand_item = get_active_hand() @@ -721,6 +724,10 @@ set name = "Resist" set category = "IC" + DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(run_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/run_resist() if(!can_resist()) return changeNext_move(CLICK_CD_RESIST) diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm index d9db6a5772c..aa64d12aec7 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -843,8 +843,8 @@ else return null -/mob/living/simple_animal/bot/mulebot/resist() - ..() +/mob/living/simple_animal/bot/mulebot/run_resist() + . = ..() if(load) unload() diff --git a/code/modules/mob/living/simple_animal/friendly/diona.dm b/code/modules/mob/living/simple_animal/friendly/diona.dm index 1c6a8ae5c9b..0aace25123b 100644 --- a/code/modules/mob/living/simple_animal/friendly/diona.dm +++ b/code/modules/mob/living/simple_animal/friendly/diona.dm @@ -95,8 +95,7 @@ else ..() -/mob/living/simple_animal/diona/resist() - ..() +/mob/living/simple_animal/diona/run_resist() split() /mob/living/simple_animal/diona/attack_hand(mob/living/carbon/human/M) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 8ad31a6cce9..d94012b1823 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -597,6 +597,9 @@ GLOBAL_LIST_INIT(slot_equipment_priority, list( \ set name = "Examine" set category = "IC" + DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(run_examinate), A)) + +/mob/proc/run_examinate(atom/A) if(!has_vision(information_only = TRUE) && !isobserver(src)) to_chat(src, "Something is there but you can't see it.") return 1 @@ -649,6 +652,10 @@ GLOBAL_LIST_INIT(slot_equipment_priority, list( \ set category = null set src = usr + DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(run_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/run_mode() if(ismecha(loc)) return if(hand) @@ -1569,3 +1576,13 @@ GLOBAL_LIST_INIT(holy_areas, typecacheof(list( . = stat stat = new_stat SEND_SIGNAL(src, COMSIG_MOB_STATCHANGE, new_stat, .) + +///Makes a call in the context of a different usr. Use sparingly +/world/proc/invoke_callback_with_usr(mob/user_mob, datum/callback/invoked_callback, ...) + var/temp = usr + usr = user_mob + if (length(args) > 2) + . = invoked_callback.Invoke(arglist(args.Copy(3))) + else + . = invoked_callback.Invoke() + usr = temp diff --git a/code/modules/point/point.dm b/code/modules/point/point.dm index 7c1c252f78d..cf860d237e3 100644 --- a/code/modules/point/point.dm +++ b/code/modules/point/point.dm @@ -104,6 +104,14 @@ if(istype(A, /obj/effect/temp_visual/point) || istype(A, /atom/movable/emissive_blocker)) return FALSE + DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(run_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/run_pointed(atom/A) + if(client && !(A in view(client.view, src))) + return FALSE + changeNext_move(CLICK_CD_POINT) if(A.loc in src) // Object is inside a container on the mob. It's not part of the verb's list since it's not in view and requires middle clicking. point_at(A) diff --git a/paradise.dme b/paradise.dme index fe21625f385..d8b3993bed3 100644 --- a/paradise.dme +++ b/paradise.dme @@ -104,6 +104,7 @@ #include "code\__DEFINES\typeids.dm" #include "code\__DEFINES\uplinks.dm" #include "code\__DEFINES\vampire.dm" +#include "code\__DEFINES\verb_manager.dm" #include "code\__DEFINES\vv.dm" #include "code\__DEFINES\wires.dm" #include "code\__DEFINES\zlevel.dm" @@ -275,6 +276,7 @@ #include "code\controllers\subsystem\ticker.dm" #include "code\controllers\subsystem\time_track.dm" #include "code\controllers\subsystem\timer.dm" +#include "code\controllers\subsystem\verb_manager.dm" #include "code\controllers\subsystem\vote.dm" #include "code\controllers\subsystem\weather.dm" #include "code\controllers\subsystem\non_firing\assets.dm" @@ -341,6 +343,7 @@ #include "code\datums\statclick.dm" #include "code\datums\tgs_event_handler.dm" #include "code\datums\uplink_item.dm" +#include "code\datums\verb_callbacks.dm" #include "code\datums\vision_override.dm" #include "code\datums\cache\air_alarm.dm" #include "code\datums\cache\apc.dm"