diff --git a/code/__DEFINES/__game.dm b/code/__DEFINES/__game.dm
index d2777c99cacb..2f960e28546c 100644
--- a/code/__DEFINES/__game.dm
+++ b/code/__DEFINES/__game.dm
@@ -148,6 +148,10 @@
#define PLAY_SYNTH (1<<5)
#define PLAY_MISC (1<<6)
+//toggles_survivor
+#define PLAY_SURVIVOR_HOSTILE (1<<0)
+#define PLAY_SURVIVOR_NON_HOSTILE (1<<1)
+
//toggles_admin
/// Splits admin tabs in Statpanel
#define SPLIT_ADMIN_TABS (1<<0)
@@ -167,6 +171,8 @@
#define TOGGLES_ERT_DEFAULT (PLAY_LEADER|PLAY_MEDIC|PLAY_ENGINEER|PLAY_HEAVY|PLAY_SMARTGUNNER|PLAY_SYNTH|PLAY_MISC)
+#define TOGGLES_SURVIVOR_DEFAULT (PLAY_SURVIVOR_HOSTILE|PLAY_SURVIVOR_NON_HOSTILE)
+
#define TOGGLES_ADMIN_DEFAULT (NONE)
// Game Intents
diff --git a/code/__DEFINES/job.dm b/code/__DEFINES/job.dm
index 79f40c89bb53..c85082127c6c 100644
--- a/code/__DEFINES/job.dm
+++ b/code/__DEFINES/job.dm
@@ -40,11 +40,10 @@ GLOBAL_LIST_INIT(job_squad_roles, JOB_SQUAD_ROLES_LIST)
#define MEDICAL_SURVIVOR "Medical Survivor"
#define ENGINEERING_SURVIVOR "Engineering Survivor"
#define CORPORATE_SURVIVOR "Corporate Survivor"
-#define HOSTILE_SURVIVOR "Hostile Survivor" //AKA Marine Killers assuming they survive. Will do cultist survivor at some point.
-#define SURVIVOR_VARIANT_LIST list(ANY_SURVIVOR = "Any", CIVILIAN_SURVIVOR = "Civ", SECURITY_SURVIVOR = "Sec", SCIENTIST_SURVIVOR = "Sci", MEDICAL_SURVIVOR = "Med", ENGINEERING_SURVIVOR = "Eng", CORPORATE_SURVIVOR = "W-Y", HOSTILE_SURVIVOR = "CLF")
+#define SURVIVOR_VARIANT_LIST list(ANY_SURVIVOR = "Any", CIVILIAN_SURVIVOR = "Civ", SECURITY_SURVIVOR = "Sec", SCIENTIST_SURVIVOR = "Sci", MEDICAL_SURVIVOR = "Med", ENGINEERING_SURVIVOR = "Eng", CORPORATE_SURVIVOR = "W-Y")
//-1 is infinite amount, these are soft caps and can be bypassed by randomization
-#define MAX_SURVIVOR_PER_TYPE list(ANY_SURVIVOR = -1, CIVILIAN_SURVIVOR = -1, SECURITY_SURVIVOR = 2, SCIENTIST_SURVIVOR = 2, MEDICAL_SURVIVOR = 3, ENGINEERING_SURVIVOR = 4, CORPORATE_SURVIVOR = 2, HOSTILE_SURVIVOR = 1)
+#define MAX_SURVIVOR_PER_TYPE list(ANY_SURVIVOR = -1, CIVILIAN_SURVIVOR = -1, SECURITY_SURVIVOR = 2, SCIENTIST_SURVIVOR = 2, MEDICAL_SURVIVOR = 3, ENGINEERING_SURVIVOR = 4, CORPORATE_SURVIVOR = 2)
#define SPAWN_PRIORITY_VERY_HIGH 1
#define SPAWN_PRIORITY_HIGH 2
diff --git a/code/__DEFINES/nightmare.dm b/code/__DEFINES/nightmare.dm
index 3395f365d07c..c518d631ee94 100644
--- a/code/__DEFINES/nightmare.dm
+++ b/code/__DEFINES/nightmare.dm
@@ -6,6 +6,8 @@
#define NIGHTMARE_CTX_GROUND "ground"
/// Ship Map Context: Performs actions relevant to the ship map
#define NIGHTMARE_CTX_SHIP "ship"
+/// Hostile Survivor Scenarios
+#define NIGHTMARE_SCENARIO_HOSTILE_SURVIVOR list("lvevent" = list("fallen_ship", "clfship"), "riot_in_progress" = list("clfship"), "panic_room" = list("clfship"))
// File names for use in context configs
#define NIGHTMARE_FILE_SCENARIO "scenario.json"
diff --git a/code/controllers/subsystem/nightmare.dm b/code/controllers/subsystem/nightmare.dm
index e963653b54a0..75b146a079d5 100644
--- a/code/controllers/subsystem/nightmare.dm
+++ b/code/controllers/subsystem/nightmare.dm
@@ -12,6 +12,8 @@ SUBSYSTEM_DEF(nightmare)
var/list/contexts = list()
/// List of parsed file nodes
var/list/roots = list()
+ /// Associated list of scenarios that indicate hostile survivor spawning
+ var/list/hostile_survivor_scenarios = NIGHTMARE_SCENARIO_HOSTILE_SURVIVOR
/datum/controller/subsystem/nightmare/Initialize(start_timeofday)
var/global_nightmare_path = CONFIG_GET(string/nightmare_path)
@@ -139,3 +141,13 @@ SUBSYSTEM_DEF(nightmare)
else
CRASH("Tried to instanciate an invalid node type")
+/// Returns whether the ground context indicates a hostile survivor scenario
+/datum/controller/subsystem/nightmare/proc/get_scenario_is_hostile_survivor()
+ // Assumption: Only ground context is relevant
+ var/datum/nmcontext/ground_context = contexts[NIGHTMARE_CTX_GROUND]
+ for(var/key in hostile_survivor_scenarios)
+ var/scenario = ground_context.get_scenario_value(key)
+ for(var/value in hostile_survivor_scenarios[key])
+ if(scenario == value)
+ return TRUE
+ return FALSE
diff --git a/code/game/jobs/job/civilians/other/survivors.dm b/code/game/jobs/job/civilians/other/survivors.dm
index 87b7fcb2b18f..22eaeee2ca56 100644
--- a/code/game/jobs/job/civilians/other/survivors.dm
+++ b/code/game/jobs/job/civilians/other/survivors.dm
@@ -32,6 +32,16 @@
"
to_chat_spaced(survivor, html = entrydisplay)
+/datum/job/civilian/survivor/can_play_role_in_scenario(client/client)
+ . = ..()
+ if(!.)
+ return .
+
+ if(SSnightmare.get_scenario_is_hostile_survivor())
+ return HAS_FLAG(client.prefs?.toggles_survivor, PLAY_SURVIVOR_HOSTILE)
+ else
+ return HAS_FLAG(client.prefs?.toggles_survivor, PLAY_SURVIVOR_NON_HOSTILE)
+
/datum/job/civilian/survivor/spawn_in_player(mob/new_player/NP)
. = ..()
var/mob/living/carbon/human/H = .
@@ -44,6 +54,12 @@
potential_spawners += spawner
if(length(potential_spawners))
break
+ if(!length(potential_spawners))
+ // Generally this shouldn't happen since role authority shouldn't be rolling us for a survivor in a hostile scenario
+ message_admins("Failed to spawn_in_player [key_name_admin(H)] as a survivor! This likely means NIGHTMARE_SCENARIO_HOSTILE_SURVIVOR is incorrect for this map!")
+ H.send_to_lobby()
+ qdel(H)
+ return null
var/obj/effect/landmark/survivor_spawner/picked_spawner = pick(potential_spawners)
H.forceMove(get_turf(picked_spawner))
diff --git a/code/game/jobs/job/job.dm b/code/game/jobs/job/job.dm
index 0af315fc3b9d..f332d9a85648 100644
--- a/code/game/jobs/job/job.dm
+++ b/code/game/jobs/job/job.dm
@@ -127,6 +127,10 @@
return TRUE
+/// Whether the client passes requirements for the scenario
+/datum/job/proc/can_play_role_in_scenario(client/client)
+ return TRUE
+
/datum/job/proc/get_role_requirements(client/C)
var/list/return_requirements = list()
for(var/prereq in minimum_playtimes)
diff --git a/code/game/jobs/role_authority.dm b/code/game/jobs/role_authority.dm
index f32860c06d2c..7d71f086bca6 100644
--- a/code/game/jobs/role_authority.dm
+++ b/code/game/jobs/role_authority.dm
@@ -362,6 +362,8 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou
return FALSE
if(!J.can_play_role(M.client))
return FALSE
+ if(!J.can_play_role_in_scenario(M.client))
+ return FALSE
if(!J.check_whitelist_status(M))
return FALSE
if(J.total_positions != -1 && J.get_total_positions(latejoin) <= J.current_positions)
diff --git a/code/game/objects/effects/landmarks/survivor_spawner.dm b/code/game/objects/effects/landmarks/survivor_spawner.dm
index 4a6e5272ed05..0a9c6f7ce39f 100644
--- a/code/game/objects/effects/landmarks/survivor_spawner.dm
+++ b/code/game/objects/effects/landmarks/survivor_spawner.dm
@@ -26,6 +26,12 @@
// prevents stacking survivors on top of eachother
if(locate(/mob/living/carbon/human) in loc)
return FALSE
+ if(!survivor)
+ return FALSE
+ if(hostile && !HAS_FLAG(survivor.client?.prefs?.toggles_survivor, PLAY_SURVIVOR_HOSTILE))
+ return FALSE
+ if(!hostile && !HAS_FLAG(survivor.client?.prefs?.toggles_survivor, PLAY_SURVIVOR_NON_HOSTILE))
+ return FALSE
return TRUE
/obj/effect/landmark/survivor_spawner/lv624_crashed_clf
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index b4ee572d8d2d..f7e1856c2d92 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -62,6 +62,7 @@ GLOBAL_LIST_INIT(bgstate_options, list(
var/toggles_sound = TOGGLES_SOUND_DEFAULT
var/toggles_flashing = TOGGLES_FLASHING_DEFAULT
var/toggles_ert = TOGGLES_ERT_DEFAULT
+ var/toggles_survivor = TOGGLES_SURVIVOR_DEFAULT
var/chat_display_preferences = CHAT_TYPE_ALL
var/item_animation_pref_level = SHOW_ITEM_ANIMATIONS_ALL
var/pain_overlay_pref_level = PAIN_OVERLAY_BLURRY
@@ -652,6 +653,12 @@ GLOBAL_LIST_INIT(bgstate_options, list(
dat += "Spawn as Miscellaneous: [toggles_ert & PLAY_MISC ? "Yes" : "No"]
"
dat += ""
+ dat += "