diff --git a/code/__DEFINES/cooldowns.dm b/code/__DEFINES/cooldowns.dm
new file mode 100644
index 00000000000..88520028f4d
--- /dev/null
+++ b/code/__DEFINES/cooldowns.dm
@@ -0,0 +1,70 @@
+//// 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"
+
+
+//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, /proc/end_cooldown, cd_source, cd_index), cd_time))
+
+#define TIMER_COOLDOWN_CHECK(cd_source, cd_index) LAZYACCESS(cd_source.cooldowns, 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_CHECK() 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, /proc/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_CHECK(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.
+*/
+
+#define COOLDOWN_DECLARE(cd_index) var/##cd_index = 0
+
+#define COOLDOWN_START(cd_source, cd_index, cd_time) (cd_source.cd_index = world.time + cd_time)
+
+#define COOLDOWN_CHECK(cd_source, cd_index) (cd_source.cd_index < world.time)
+
+#define COOLDOWN_RESET(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/datums/action.dm b/code/datums/action.dm
index 52eee38f51e..591b8667f77 100644
--- a/code/datums/action.dm
+++ b/code/datums/action.dm
@@ -519,10 +519,9 @@
background_icon_state = "bg_agent"
icon_icon = 'icons/mob/actions/actions_items.dmi'
button_icon_state = "deploy_box"
- ///Cooldown between deploys. Uses world.time
- var/cooldown = 0
///The type of closet this action spawns.
var/boxtype = /obj/structure/closet/cardboard/agent
+ COOLDOWN_DECLARE(box_cooldown)
///Handles opening and closing the box.
/datum/action/item_action/agent_box/Trigger()
@@ -538,11 +537,12 @@
if(!isturf(owner.loc)) //Don't let the player use this to escape mechs/welded closets.
to_chat(owner, "You need more space to activate this implant!")
return
- if(cooldown < world.time - 100)
- var/box = new boxtype(owner.drop_location())
- owner.forceMove(box)
- cooldown = world.time
- owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE)
+ if(COOLDOWN_CHECK(src, box_cooldown))
+ return
+ COOLDOWN_START(src, box_cooldown, 10 SECONDS)
+ var/box = new boxtype(owner.drop_location())
+ owner.forceMove(box)
+ owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE)
//Preset for spells
/datum/action/spell_action
diff --git a/code/datums/components/caltrop.dm b/code/datums/components/caltrop.dm
index 1e91d771685..73fd0e16ef8 100644
--- a/code/datums/components/caltrop.dm
+++ b/code/datums/components/caltrop.dm
@@ -3,8 +3,8 @@
var/max_damage
var/probability
var/flags
+ COOLDOWN_DECLARE(caltrop_cooldown)
- var/cooldown = 0
/datum/component/caltrop/Initialize(_min_damage = 0, _max_damage = 0, _probability = 100, _flags = NONE)
min_damage = _min_damage
@@ -50,7 +50,9 @@
if(HAS_TRAIT(H, TRAIT_LIGHT_STEP))
damage *= 0.75
- if(cooldown < world.time - 10) //cooldown to avoid message spam.
+
+ if(!COOLDOWN_CHECK(src, caltrop_cooldown))
+ COOLDOWN_START(src, caltrop_cooldown, 1 SECONDS) //cooldown to avoid message spam.
var/atom/A = parent
if(!H.incapacitated(ignore_restraints = TRUE))
H.visible_message("[H] steps on [A].", \
@@ -59,6 +61,5 @@
H.visible_message("[H] slides on [A]!", \
"You slide on [A]!")
- cooldown = world.time
H.apply_damage(damage, BRUTE, picked_def_zone, wound_bonus = CANT_WOUND)
H.Paralyze(60)
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index 4608b0ceb68..bf0f877ed01 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -49,6 +49,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
+
#ifdef TESTING
var/running_find_references
var/last_find_references = 0
@@ -221,3 +229,34 @@
qdel(D)
else
return returned
+
+/**
+ * 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)
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index d6ea1077e2c..e18db2f9ab8 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -67,7 +67,7 @@
var/obj/machinery/door/airlock/closeOther
var/justzap = FALSE
var/obj/item/electronics/airlock/electronics
- var/shockCooldown = FALSE //Prevents multiple shocks from happening
+ COOLDOWN_DECLARE(shockCooldown)
var/obj/item/note //Any papers pinned to the airlock
var/detonated = FALSE
var/abandoned = FALSE
@@ -404,14 +404,14 @@
/obj/machinery/door/airlock/proc/shock(mob/living/user, prb)
if(!istype(user) || !hasPower()) // unpowered, no shock
return FALSE
- if(shockCooldown > world.time)
+ if(COOLDOWN_CHECK(src, shockCooldown))
return FALSE //Already shocked someone recently?
if(!prob(prb))
return FALSE //you lucked out, no shock for you
do_sparks(5, TRUE, src)
var/check_range = TRUE
if(electrocute_mob(user, get_area(src), src, 1, check_range))
- shockCooldown = world.time + 10
+ COOLDOWN_START(src, shockCooldown, 1 SECONDS)
return TRUE
else
return FALSE
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index 8ee7bac787c..1ed4cc23af1 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -273,7 +273,6 @@
require_module = 1
var/repair_amount = -1
var/repair_tick = 1
- var/msg_cooldown = 0
var/on = FALSE
var/powercost = 10
var/datum/action/toggle_action
@@ -355,14 +354,14 @@
cyborg.cell.use(5)
repair_tick = 0
- if((world.time - 2000) > msg_cooldown )
+ if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_BORG_SELF_REPAIR))
+ TIMER_COOLDOWN_START(src, COOLDOWN_BORG_SELF_REPAIR, 200 SECONDS)
var/msgmode = "standby"
if(cyborg.health < 0)
msgmode = "critical"
else if(cyborg.health < cyborg.maxHealth)
msgmode = "normal"
to_chat(cyborg, "Self-repair is active in [msgmode] mode.")
- msg_cooldown = world.time
else
deactivate_sr()
diff --git a/tgstation.dme b/tgstation.dme
index c9d58366bfe..e2cec0e435c 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -37,6 +37,7 @@
#include "code\__DEFINES\configuration.dm"
#include "code\__DEFINES\construction.dm"
#include "code\__DEFINES\contracts.dm"
+#include "code\__DEFINES\cooldowns.dm"
#include "code\__DEFINES\cult.dm"
#include "code\__DEFINES\diseases.dm"
#include "code\__DEFINES\DNA.dm"