diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm index 107393b935bb..48954c1f0ed1 100644 --- a/code/__DEFINES/flags.dm +++ b/code/__DEFINES/flags.dm @@ -291,8 +291,10 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204 #define IGNORE_INCAPACITATED (1<<3) /// Used to prevent important slowdowns from being abused by drugs like kronkaine #define IGNORE_SLOWDOWNS (1<<4) +/// If the user has their next_move value changed (usually by clicking), fail. +#define DO_RESTRICT_CLICKING (1<<5) /// Shown to all mobs not just the user -#define DO_PUBLIC (1<<5) +#define DO_PUBLIC (1<<6) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 71e0ad5ad67b..4b2d6b3b5d4e 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -192,6 +192,7 @@ #define FIRE_PRIORITY_CHAT 400 #define FIRE_PRIORITY_SPEECH_CONTROLLER 300 #define FIRE_PRIORITY_RUNECHAT 250 +#define FIRE_PRIORITY_DO_AFTERS 100 #define FIRE_PRIORITY_THROWING 20 /* DEFAULT WOULD BE HERE */ #define FIRE_PRIORITY_SPACEDRIFT 15 diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index e52ee128ead0..9929a910194f 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -245,197 +245,6 @@ GLOBAL_LIST_EMPTY(species_list) else return "unknown" -//some additional checks as a callback for for do_afters that want to break on losing health or on the mob taking action -/mob/proc/break_do_after_checks(list/checked_health, check_clicks) - if(check_clicks && next_move > world.time) - return FALSE - return TRUE - -//pass a list in the format list("health" = mob's health var) to check health during this -/mob/living/break_do_after_checks(list/checked_health, check_clicks) - if(islist(checked_health)) - if(health < checked_health["health"]) - return FALSE - checked_health["health"] = health - return ..() - - -/** - * Timed action involving one mob user. Target is optional, defaulting to user. - * - * Checks that `user` does not move, change hands, get stunned, etc. for the - * given `time`. Returns `TRUE` on success or `FALSE` on failure. - * Interaction_key is the assoc key under which the do_after is capped, with max_interact_count being the cap. Interaction key will default to target if not set. - */ -/proc/do_after(atom/movable/user, atom/target, time, timed_action_flags = NONE, progress = TRUE, datum/callback/extra_checks, interaction_key, max_interact_count = 1, image/display) - if(!user) - return FALSE - - if(!target) - target = user - if(isnum(target)) - CRASH("a do_after created by [user] had a target set as [target]- probably intended to be the time instead.") - if(isatom(time)) - CRASH("a do_after created by [user] had a timer of [time]- probably intended to be the target instead.") - - if(!interaction_key) - interaction_key = target //Use the direct ref to the target - if(interaction_key) //Do we have a interaction_key now? - var/current_interaction_count = LAZYACCESS(user.do_afters, interaction_key) || 0 - if(current_interaction_count >= max_interact_count) //We are at our peak - return - LAZYSET(user.do_afters, interaction_key, current_interaction_count + 1) - - var/atom/user_loc = user.loc - var/atom/target_loc = target.loc - - var/drifting = FALSE - if(SSmove_manager.processing_on(user, SSspacedrift)) - drifting = TRUE - - var/holding - if(ismob(user)) - var/mob/mobuser = user - holding = mobuser.get_active_held_item() - if(!(timed_action_flags & IGNORE_SLOWDOWNS)) - time *= mobuser.cached_multiplicative_actions_slowdown - else - timed_action_flags |= IGNORE_HELD_ITEM|IGNORE_INCAPACITATED|IGNORE_SLOWDOWNS|DO_PUBLIC - - var/datum/progressbar/progbar - if(progress) - if(timed_action_flags & DO_PUBLIC) - progbar = new /datum/world_progressbar(user, time, display) - else - progbar = new(user, time, target || user) - - var/endtime = world.time + time - var/starttime = world.time - . = TRUE - while (world.time < endtime) - stoplag(1) - - if(!QDELETED(progbar)) - progbar.update(world.time - starttime) - - if(drifting && !SSmove_manager.processing_on(user, SSspacedrift)) - drifting = FALSE - user_loc = user.loc - - if( - QDELETED(user) \ - || (!(timed_action_flags & IGNORE_USER_LOC_CHANGE) && !drifting && user.loc != user_loc) \ - || (!(timed_action_flags & IGNORE_HELD_ITEM) && user:get_active_held_item() != holding) \ - || (!(timed_action_flags & IGNORE_INCAPACITATED) && HAS_TRAIT(user, TRAIT_INCAPACITATED)) \ - || (extra_checks && !extra_checks.Invoke()) \ - ) - . = FALSE - break - - if(user != target && \ - (QDELETED(target) || (!(timed_action_flags & IGNORE_TARGET_LOC_CHANGE) && target.loc != target_loc))) - . = FALSE - break - - if(!QDELETED(progbar)) - progbar.end_progress() - - if(interaction_key) - var/reduced_interaction_count = (LAZYACCESS(user.do_afters, interaction_key)) - 1 - if(reduced_interaction_count > 0) // Not done yet! - LAZYSET(user.do_afters, interaction_key, reduced_interaction_count) - return - LAZYREMOVE(user.do_afters, interaction_key) - - -///Timed action involving at least one mob user and a list of targets. interaction_key is the assoc key under which the do_after is capped under, and the max interaction count is how many of this interaction you can do at once. -/proc/do_after_mob(mob/user, list/targets, time = 3 SECONDS, timed_action_flags = NONE, progress = TRUE, datum/callback/extra_checks, interaction_key, max_interact_count = 1) - if(!user) - return FALSE - if(!islist(targets)) - targets = list(targets) - if(!length(targets)) - return FALSE - var/user_loc = user.loc - - if(!(timed_action_flags & IGNORE_SLOWDOWNS)) - time *= user.cached_multiplicative_actions_slowdown - - var/drifting = FALSE - if(SSmove_manager.processing_on(user, SSspacedrift)) - drifting = TRUE - - var/list/originalloc = list() - - for(var/atom/target in targets) - originalloc[target] = target.loc - - if(interaction_key) - var/current_interaction_count = LAZYACCESS(user.do_afters, interaction_key) || 0 - if(current_interaction_count >= max_interact_count) //We are at our peak - to_chat(user, span_warning("You can't do this at the moment!")) - return - LAZYSET(user.do_afters, interaction_key, current_interaction_count + 1) - - - var/holding = user.get_active_held_item() - var/datum/progressbar/progbar - if(progress) - progbar = new(user, time, targets[1]) - - var/endtime = world.time + time - var/starttime = world.time - . = TRUE - while(world.time < endtime) - stoplag(1) - - if(!QDELETED(progbar)) - progbar.update(world.time - starttime) - if(QDELETED(user) || !length(targets)) - . = FALSE - break - - if(drifting && !SSmove_manager.processing_on(user, SSspacedrift)) - drifting = FALSE - user_loc = user.loc - - if( - (!(timed_action_flags & IGNORE_USER_LOC_CHANGE) && !drifting && user_loc != user.loc) \ - || (!(timed_action_flags & IGNORE_HELD_ITEM) && user.get_active_held_item() != holding) \ - || (!(timed_action_flags & IGNORE_INCAPACITATED) && HAS_TRAIT(user, TRAIT_INCAPACITATED)) \ - || (extra_checks && !extra_checks.Invoke()) \ - ) - . = FALSE - break - - for(var/atom/target as anything in targets) - if( - (QDELETED(target)) \ - || (!(timed_action_flags & IGNORE_TARGET_LOC_CHANGE) && originalloc[target] != target.loc) \ - ) - . = FALSE - break - - if(!.) // In case the for-loop found a reason to break out of the while. - break - - if(!QDELETED(progbar)) - progbar.end_progress() - - if(interaction_key) - var/reduced_interaction_count = (LAZYACCESS(user.do_afters, interaction_key)) - 1 - if(reduced_interaction_count > 0) // Not done yet! - LAZYSET(user.do_afters, interaction_key, reduced_interaction_count) - return - LAZYREMOVE(user.do_afters, interaction_key) - -/// Returns the total amount of do_afters this mob is taking part in -/mob/proc/do_after_count() - var/count = 0 - for(var/key in do_afters) - count += do_afters[key] - return count - /proc/is_species(A, species_datum) . = FALSE if(ishuman(A)) diff --git a/code/controllers/subsystem/processing/do_afters.dm b/code/controllers/subsystem/processing/do_afters.dm new file mode 100644 index 000000000000..2148492c2f51 --- /dev/null +++ b/code/controllers/subsystem/processing/do_afters.dm @@ -0,0 +1,5 @@ +PROCESSING_SUBSYSTEM_DEF(timed_action) + name = "Timed Actions" + priority = FIRE_PRIORITY_DO_AFTERS + flags = SS_TICKER|SS_NO_INIT|SS_HIBERNATE + wait = 1 diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index 73bc78d9f927..91c89d045920 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -512,7 +512,7 @@ DEFINE_INTERACTABLE(/obj/structure/closet) span_warning("You [actuallyismob ? "try to ":""]stuff [O] into [src]."), \ span_hear("You hear clanging.")) if(actuallyismob) - if(do_after_mob(user, targets, 40)) + if(do_after(user, targets, 40)) user.visible_message(span_notice("[user] stuffs [O] into [src]."), \ span_notice("You stuff [O] into [src]."), \ span_hear("You hear a loud metal bang.")) diff --git a/code/modules/antagonists/cult/rune_spawn_action.dm b/code/modules/antagonists/cult/rune_spawn_action.dm index 3e61d6294f98..e2b6b7a6f100 100644 --- a/code/modules/antagonists/cult/rune_spawn_action.dm +++ b/code/modules/antagonists/cult/rune_spawn_action.dm @@ -58,15 +58,22 @@ cooldown = base_cooldown + world.time owner?.update_mob_action_buttons() addtimer(CALLBACK(owner, TYPE_PROC_REF(/mob, update_mob_action_buttons)), base_cooldown) - var/list/health + + var/mob_health = null if(damage_interrupt && isliving(owner)) var/mob/living/L = owner - health = list("health" = L.health) + mob_health = L.health + var/scribe_mod = scribe_time if(istype(T, /turf/open/floor/engine/cult)) scribe_mod *= 0.5 + playsound(T, 'sound/magic/enter_blood.ogg', 100, FALSE) - if(do_after(owner, owner, scribe_mod, extra_checks = CALLBACK(owner, TYPE_PROC_REF(/mob, break_do_after_checks), health, action_interrupt))) + + var/flags = action_interrupt ? DO_RESTRICT_CLICKING : NONE + var/datum/callback/cb = mob_health ? CALLBACK(src, PROC_REF(check_health_changed), owner, list(mob_health)) : null + + if(do_after(owner, owner, scribe_mod, flags, extra_checks = cb)) new rune_type(owner.loc, chosen_keyword) else qdel(R1) @@ -79,6 +86,13 @@ cooldown = 0 owner?.update_mob_action_buttons() +/datum/action/innate/cult/create_rune/proc/check_health_changed(mob/living/user, list/old_health) + if(user.health < old_health[1]) + return FALSE + + old_health[1] = user.health + return TRUE + //teleport rune /datum/action/innate/cult/create_rune/tele name = "Summon Teleport Rune" diff --git a/code/modules/do_after/do_after.dm b/code/modules/do_after/do_after.dm new file mode 100644 index 000000000000..e956ec5733d3 --- /dev/null +++ b/code/modules/do_after/do_after.dm @@ -0,0 +1,64 @@ +/** + * Timed action involving one mob user. Target is optional, defaulting to user. + * Returns TRUE if the action succeeded and was not interrupted. + * + * Args: + * * user: The one performing the action. + * * target: An atom or list of atoms to perform the action on. If null, defaults to user. + * * timed_action_flags: A bitfield defining the behavior of the action. + * * progress: Boolean value, if TRUE, show a progress bar over the target's head. + * * extra_checks: An optional callback to check in addition to the default checks. + * * interaction_key: An optional non-numeric value to disamibiguate the action, to be used with DOING_INTERACTION() macros. Defaults to target. + * * max_interact_count: The action will automatically fail if they are already performing this many or more actions with the given interaction_key. + * * display: An atom or image to display over the user's head. Only works with DO_PUBLIC flag. + */ +/proc/do_after(atom/movable/user, atom/target, time, timed_action_flags = NONE, progress = TRUE, datum/callback/extra_checks, interaction_key, max_interact_count = 1, image/display) + if(!user) + return FALSE + + if(time == 0) + return TRUE + + if(!target) + target = user + + if(isnum(target)) + CRASH("a do_after created by [user] had a target set as [target]- probably intended to be the time instead.") + if(isatom(time)) + CRASH("a do_after created by [user] had a timer of [time]- probably intended to be the target instead.") + + if(!interaction_key) + if(!islist(target)) + interaction_key = target + else + var/list/temp = list() + for(var/atom/atom as anything in target) + temp += ref(atom) + + sortTim(temp, GLOBAL_PROC_REF(cmp_text_asc)) + interaction_key = jointext(temp, "-") + + if(interaction_key) //Do we have a interaction_key now? + var/current_interaction_count = LAZYACCESS(user.do_afters, interaction_key) || 0 + if(current_interaction_count >= max_interact_count) //We are at our peak + return + LAZYSET(user.do_afters, interaction_key, current_interaction_count + 1) + + var/datum/timed_action/action = new(user, target, time, progress, timed_action_flags, extra_checks, display) + + . = action.wait() + + if(interaction_key) + var/reduced_interaction_count = (LAZYACCESS(user.do_afters, interaction_key)) - 1 + if(reduced_interaction_count > 0) // Not done yet! + LAZYSET(user.do_afters, interaction_key, reduced_interaction_count) + return + LAZYREMOVE(user.do_afters, interaction_key) + + +/// Returns the total amount of do_afters this mob is taking part in +/mob/proc/do_after_count() + var/count = 0 + for(var/key in do_afters) + count += do_afters[key] + return count diff --git a/code/modules/do_after/timed_action.dm b/code/modules/do_after/timed_action.dm new file mode 100644 index 000000000000..b4e760a2338d --- /dev/null +++ b/code/modules/do_after/timed_action.dm @@ -0,0 +1,168 @@ +#define WORKING 0 +#define CANCELLED 1 +#define SUCCEEDED 2 + +/datum/timed_action + var/atom/movable/user + var/list/targets + + var/atom/user_loc + var/obj/item/user_held_item + + var/datum/progressbar/progbar + + var/datum/callback/extra_checks + + var/start_time + var/end_time + + var/timed_action_flags = NONE + + var/drifting = FALSE + + var/status = WORKING + +/datum/timed_action/New(_user, _targets, _time, _progress, _timed_action_flags, _extra_checks, image/_display) + user = _user + timed_action_flags = _timed_action_flags + extra_checks = _extra_checks + + if(isnull(_targets)) + _targets = list(user) + + else if(!islist(_targets)) + _targets = list(_targets) + + for(var/atom/target as anything in _targets) + _targets[target] = target.loc + + targets = _targets + + if(_progress) + if(timed_action_flags & DO_PUBLIC) + progbar = new /datum/world_progressbar(user, _time, _display) + else + progbar = new /datum/progressbar(user, _time, targets[1] || user) + + if(ismob(user)) + var/mob/mobuser = user + user_held_item = mobuser.get_active_held_item() + if(!(timed_action_flags & IGNORE_SLOWDOWNS)) + _time *= mobuser.cached_multiplicative_actions_slowdown + else + timed_action_flags |= IGNORE_HELD_ITEM|IGNORE_INCAPACITATED|IGNORE_SLOWDOWNS|DO_PUBLIC + + start_time = world.time + end_time = world.time + _time + + if(SSmove_manager.processing_on(user, SSspacedrift)) + drifting = TRUE + + register_signals() + +/datum/timed_action/Destroy(force, ...) + user = null + targets = null + user_loc = null + user_held_item = null + extra_checks = null + progbar = null + STOP_PROCESSING(SStimed_action, src) + return ..() + +/datum/timed_action/proc/register_signals() + RegisterSignal(user, COMSIG_PARENT_QDELETING, PROC_REF(user_gone)) + + if(!(timed_action_flags & IGNORE_USER_LOC_CHANGE)) + RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(user_moved)) + + if(!(timed_action_flags & IGNORE_INCAPACITATED)) + RegisterSignal(user, SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), PROC_REF(user_incap)) + + if(!(timed_action_flags & DO_RESTRICT_CLICKING)) + RegisterSignal(user, COMSIG_LIVING_CHANGENEXT_MOVE, PROC_REF(on_changenext_move)) + + for(var/atom/target as anything in targets) + if(target == user) + continue + + RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(target_gone)) + + if(!(timed_action_flags & IGNORE_TARGET_LOC_CHANGE)) + RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(target_moved)) + +/datum/timed_action/proc/cancel() + status = CANCELLED + STOP_PROCESSING(SStimed_action, src) + +/datum/timed_action/proc/wait() + START_PROCESSING(SStimed_action, src) + + while(status == WORKING) + sleep(world.tick_lag) + + . = (status == CANCELLED) ? FALSE : TRUE + + if(!QDELETED(progbar)) + progbar.end_progress() + + qdel(src) + +/datum/timed_action/process(delta_time) + if(world.time >= end_time) + status = SUCCEEDED + return PROCESS_KILL + + // Anything requiring a mob cannot happen if it isn't a mob in New() so this is safe. + var/mob/mob_user = user + + if(!(timed_action_flags & IGNORE_HELD_ITEM) && mob_user.get_active_held_item() != user_held_item) + cancel() + return + + if((extra_checks && !extra_checks.InvokeAsync())) + cancel() + return + + if(!QDELETED(progbar)) + progbar.update(world.time - start_time) + +/datum/timed_action/proc/user_gone(datum/source) + SIGNAL_HANDLER + + cancel() + +/datum/timed_action/proc/user_moved(datum/source) + SIGNAL_HANDLER + + if(drifting && !SSmove_manager.processing_on(user, SSspacedrift)) + drifting = FALSE + user_loc = user.loc + + if(!drifting && user.loc != user_loc) + cancel() + +/datum/timed_action/proc/user_incap(datum/source) + SIGNAL_HANDLER + cancel() + +/datum/timed_action/proc/on_changenext_move(datum/source, next_move) + SIGNAL_HANDLER + + if(next_move > world.time) + cancel() + +/datum/timed_action/proc/target_gone(datum/source) + SIGNAL_HANDLER + + cancel() + +/datum/timed_action/proc/target_moved(atom/movable/source) + SIGNAL_HANDLER + + if(source.loc != targets[source]) + cancel() + +#undef WORKING +#undef CANCELLED +#undef SUCCEEDED diff --git a/daedalus.dme b/daedalus.dme index 9f2b4e48b659..1a5f1bd383fa 100644 --- a/daedalus.dme +++ b/daedalus.dme @@ -622,6 +622,7 @@ #include "code\controllers\subsystem\processing\aura_healing.dm" #include "code\controllers\subsystem\processing\clock_component.dm" #include "code\controllers\subsystem\processing\conveyors.dm" +#include "code\controllers\subsystem\processing\do_afters.dm" #include "code\controllers\subsystem\processing\embeds.dm" #include "code\controllers\subsystem\processing\fastprocess.dm" #include "code\controllers\subsystem\processing\greyscale.dm" @@ -2878,6 +2879,8 @@ #include "code\modules\discord\discord_helpers.dm" #include "code\modules\discord\discord_link_record.dm" #include "code\modules\discord\discord_sql_functions.dm" +#include "code\modules\do_after\do_after.dm" +#include "code\modules\do_after\timed_action.dm" #include "code\modules\dreams\_dream.dm" #include "code\modules\dreams\detective_nightmare.dm" #include "code\modules\dreams\mob_dream_procs.dm"