diff --git a/code/__DEFINES/antag_defines.dm b/code/__DEFINES/antag_defines.dm
index 9213dc724bc..b5489f6948e 100644
--- a/code/__DEFINES/antag_defines.dm
+++ b/code/__DEFINES/antag_defines.dm
@@ -106,3 +106,8 @@ GLOBAL_LIST(contractors)
// Chance that a traitor will receive a 'You are being targeted by another syndicate agent' notification regardless of being an actual target
#define ORG_PROB_PARANOIA 5
+
+/// How often a biohazard's population is recorded after the event fires.
+#define BIOHAZARD_POP_INTERVAL 5 MINUTES
+/// The string version of the interval for use in blackbox key names.
+#define BIOHAZARD_POP_INTERVAL_STR "5min"
diff --git a/code/controllers/subsystem/SSticker.dm b/code/controllers/subsystem/SSticker.dm
index 1618ad3800f..b965a597faf 100644
--- a/code/controllers/subsystem/SSticker.dm
+++ b/code/controllers/subsystem/SSticker.dm
@@ -67,6 +67,8 @@ SUBSYSTEM_DEF(ticker)
var/datum/scoreboard/score = null
/// List of ckeys who had antag rolling issues flagged
var/list/flagged_antag_rollers = list()
+ /// List of biohazards keyed to the last time their population was sampled.
+ var/list/biohazard_pop_times = list()
/datum/controller/subsystem/ticker/Initialize()
login_music = pick(\
@@ -112,6 +114,10 @@ SUBSYSTEM_DEF(ticker)
delay_end = FALSE // reset this in case round start was delayed
mode.process()
+ for(var/biohazard in biohazard_pop_times)
+ if(world.time - biohazard_pop_times[biohazard] > BIOHAZARD_POP_INTERVAL)
+ sample_biohazard_population(biohazard)
+
if(world.time > next_autotransfer)
SSvote.start_vote(new /datum/vote/crew_transfer)
next_autotransfer = world.time + GLOB.configuration.vote.autotransfer_interval_time
@@ -829,22 +835,45 @@ SUBSYSTEM_DEF(ticker)
continue
.++
-/// Return whether or not a given biohazard is an active threat.
-/// For blobs, this is simply if there are any overminds left. For terrors and
-/// xenomorphs, this is whether they have overwhelming numbers.
-/datum/controller/subsystem/ticker/proc/biohazard_active_threat(biohazard)
+/datum/controller/subsystem/ticker/proc/sample_biohazard_population(biohazard)
+ SSblackbox.record_feedback("ledger", "biohazard_pop_[BIOHAZARD_POP_INTERVAL_STR]_interval", biohazard_count(biohazard), biohazard)
+ biohazard_pop_times[biohazard] = world.time
+
+/// Record the initial time that a biohazard spawned.
+/datum/controller/subsystem/ticker/proc/record_biohazard_start(biohazard)
+ SSblackbox.record_feedback("associative", "biohazard_starts", 1, list("type" = biohazard, "time_ds" = world.time - time_game_started))
+ sample_biohazard_population(biohazard)
+
+/datum/controller/subsystem/ticker/proc/biohazard_count(biohazard)
switch(biohazard)
if(TS_INFESTATION_GREEN_SPIDER, TS_INFESTATION_WHITE_SPIDER, TS_INFESTATION_PRINCESS_SPIDER, TS_INFESTATION_QUEEN_SPIDER)
var/spiders = 0
for(var/mob/living/simple_animal/hostile/poison/terror_spider/S in GLOB.ts_spiderlist)
if(S.ckey)
spiders++
- return spiders >= 5
+ return spiders
if(TS_INFESTATION_PRINCE_SPIDER)
return length(GLOB.ts_spiderlist)
if(BIOHAZARD_XENO)
- return count_xenomorps() > 5
+ return count_xenomorps()
if(BIOHAZARD_BLOB)
return length(SSticker.mode.blob_overminds)
+ CRASH("biohazard_count got unexpected [biohazard]")
+
+/// Return whether or not a given biohazard is an active threat.
+/// For blobs, this is simply if there are any overminds left. For terrors and
+/// xenomorphs, this is whether they have overwhelming numbers.
+/datum/controller/subsystem/ticker/proc/biohazard_active_threat(biohazard)
+ var/count = biohazard_count(biohazard)
+ switch(biohazard)
+ if(TS_INFESTATION_GREEN_SPIDER, TS_INFESTATION_WHITE_SPIDER, TS_INFESTATION_PRINCESS_SPIDER, TS_INFESTATION_QUEEN_SPIDER)
+ return count >= 5
+ if(TS_INFESTATION_PRINCE_SPIDER)
+ return count > 0
+ if(BIOHAZARD_XENO)
+ return count > 5
+ if(BIOHAZARD_BLOB)
+ return count > 0
+
return FALSE
diff --git a/code/modules/events/alien_infestation.dm b/code/modules/events/alien_infestation.dm
index fb79cb5c35b..dabe6333156 100644
--- a/code/modules/events/alien_infestation.dm
+++ b/code/modules/events/alien_infestation.dm
@@ -41,4 +41,5 @@
spawncount--
successSpawn = TRUE
- SSevents.biohazards_this_round += "Xenomorphs"
+ SSticker.record_biohazard_start(BIOHAZARD_XENO)
+ SSevents.biohazards_this_round += BIOHAZARD_XENO
diff --git a/code/modules/events/blob_spawn.dm b/code/modules/events/blob_spawn.dm
index 1fbc9a4a0b9..ec180703ab7 100644
--- a/code/modules/events/blob_spawn.dm
+++ b/code/modules/events/blob_spawn.dm
@@ -39,4 +39,4 @@
to_chat(B, "For more information, check the wiki page: ([GLOB.configuration.url.wiki_url]/index.php/Blob)")
notify_ghosts("Infected Mouse has appeared in [get_area(B)].", source = B, action = NOTIFY_FOLLOW)
successSpawn = TRUE
- SSevents.biohazards_this_round += "Blob"
+ SSevents.biohazards_this_round += BIOHAZARD_BLOB
diff --git a/code/modules/events/spider_terror.dm b/code/modules/events/spider_terror.dm
index 2b2c85cb5c1..ead04dc76e9 100644
--- a/code/modules/events/spider_terror.dm
+++ b/code/modules/events/spider_terror.dm
@@ -66,6 +66,7 @@
S.give_intro_text()
spawncount--
successSpawn = TRUE
+ SSticker.record_biohazard_start(infestation_type)
SSevents.biohazards_this_round += infestation_type
#undef TS_HIGHPOP_TRIGGER
diff --git a/code/modules/mob/living/simple_animal/friendly/mouse.dm b/code/modules/mob/living/simple_animal/friendly/mouse.dm
index ae48d73f78b..b07a72482e6 100644
--- a/code/modules/mob/living/simple_animal/friendly/mouse.dm
+++ b/code/modules/mob/living/simple_animal/friendly/mouse.dm
@@ -217,6 +217,8 @@
if(!gibbed)
gib()
+ SSticker.record_biohazard_start(BIOHAZARD_BLOB)
+
/mob/living/simple_animal/mouse/blobinfected/get_scooped(mob/living/carbon/grabber)
to_chat(grabber, "You try to pick up [src], but they slip out of your grasp!")
to_chat(src, "[src] tries to pick you up, but you wriggle free of their grasp!")