diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm
index 1bf5a6d8189..86c326ad9e7 100644
--- a/_maps/map_files/Deltastation/DeltaStation2.dmm
+++ b/_maps/map_files/Deltastation/DeltaStation2.dmm
@@ -8936,6 +8936,7 @@
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4{
dir = 4
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aHO" = (
@@ -8946,6 +8947,7 @@
/obj/machinery/atmospherics/pipe/simple/supply/hidden/layer4{
dir = 4
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aHP" = (
@@ -9310,6 +9312,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aJo" = (
@@ -9318,6 +9321,7 @@
/obj/effect/turf_decal/tile/red{
dir = 4
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aJy" = (
@@ -9638,6 +9642,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aKM" = (
@@ -9645,6 +9650,7 @@
/obj/effect/turf_decal/tile/red{
dir = 4
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aKN" = (
@@ -20336,6 +20342,7 @@
/obj/effect/turf_decal/tile/red{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bQe" = (
@@ -20343,6 +20350,7 @@
/obj/effect/turf_decal/tile/red{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bQf" = (
@@ -20354,6 +20362,7 @@
/obj/effect/turf_decal/tile/red{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bQg" = (
@@ -21426,6 +21435,7 @@
dir = 4
},
/obj/machinery/power/apc/auto_name/north,
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bUg" = (
@@ -21438,6 +21448,7 @@
/obj/machinery/atmospherics/pipe/simple/supply/hidden/layer4{
dir = 4
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bUh" = (
@@ -21451,6 +21462,7 @@
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bUi" = (
@@ -22342,6 +22354,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bWw" = (
@@ -22357,6 +22370,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bWx" = (
@@ -22371,6 +22385,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bWy" = (
@@ -34582,6 +34597,7 @@
dir = 8
},
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"cSn" = (
@@ -34595,6 +34611,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"cSo" = (
@@ -35424,6 +35441,7 @@
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{
dir = 10
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"cUb" = (
@@ -35441,6 +35459,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"cUc" = (
@@ -35842,6 +35861,7 @@
dir = 8
},
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2,
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"cUM" = (
@@ -36396,6 +36416,7 @@
dir = 8
},
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"cVY" = (
@@ -36411,6 +36432,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"cVZ" = (
@@ -36572,6 +36594,7 @@
dir = 8
},
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2,
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"cWr" = (
@@ -36580,6 +36603,7 @@
/obj/effect/turf_decal/tile/red{
dir = 4
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"cWu" = (
@@ -37030,6 +37054,7 @@
dir = 1
},
/obj/machinery/power/apc/auto_name/west,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"cXp" = (
@@ -37038,6 +37063,7 @@
/obj/effect/turf_decal/tile/red{
dir = 8
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"cXq" = (
@@ -37293,6 +37319,7 @@
dir = 8
},
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2,
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"cXU" = (
@@ -37300,6 +37327,7 @@
/obj/effect/turf_decal/tile/red{
dir = 4
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"cXY" = (
@@ -38014,6 +38042,7 @@
/obj/machinery/atmospherics/pipe/manifold/scrubbers/hidden/layer2{
dir = 8
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"cZE" = (
@@ -38030,6 +38059,7 @@
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{
dir = 8
},
+/obj/effect/landmark/start/depsec/medical,
/obj/structure/extinguisher_cabinet{
pixel_x = 26;
pixel_y = -32
@@ -38450,6 +38480,7 @@
/obj/effect/turf_decal/tile/red{
dir = 4
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"daI" = (
@@ -39291,6 +39322,7 @@
/obj/effect/turf_decal/tile/red{
dir = 8
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science/research)
"dcx" = (
diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
index c522cf71ae3..6d3853719be 100644
--- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm
+++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
@@ -13020,6 +13020,7 @@
/area/medical/morgue)
"bhs" = (
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"bht" = (
@@ -13680,6 +13681,7 @@
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{
dir = 10
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"bjN" = (
@@ -14197,6 +14199,7 @@
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2{
dir = 4
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"blq" = (
@@ -16190,6 +16193,7 @@
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2{
dir = 1
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"bsW" = (
@@ -17227,6 +17231,7 @@
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4{
dir = 4
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"bwO" = (
@@ -18138,6 +18143,7 @@
/area/icemoon/surface/outdoors)
"bAG" = (
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"bAH" = (
@@ -26518,6 +26524,7 @@
"cBI" = (
/obj/effect/landmark/event_spawn,
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2,
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"cBK" = (
@@ -39520,6 +39527,7 @@
},
/obj/structure/cable,
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4,
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"lNw" = (
@@ -47720,6 +47728,7 @@
dir = 8
},
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"rAo" = (
@@ -54642,6 +54651,7 @@
dir = 4
},
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"wlA" = (
diff --git a/_maps/map_files/KiloStation/KiloStation.dmm b/_maps/map_files/KiloStation/KiloStation.dmm
index d084ea0608f..31b9405bfc0 100644
--- a/_maps/map_files/KiloStation/KiloStation.dmm
+++ b/_maps/map_files/KiloStation/KiloStation.dmm
@@ -18429,6 +18429,7 @@
/obj/effect/turf_decal/siding/red/corner{
dir = 1
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"aSD" = (
@@ -21908,6 +21909,7 @@
dir = 4
},
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron/showroomfloor,
/area/security/checkpoint/medical)
"aZv" = (
@@ -21972,6 +21974,7 @@
dir = 8
},
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron/showroomfloor,
/area/security/checkpoint/medical)
"aZB" = (
@@ -24359,6 +24362,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 4
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron/dark,
/area/security/checkpoint/science/research)
"bea" = (
@@ -24388,6 +24392,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 4
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron/dark,
/area/security/checkpoint/science/research)
"bec" = (
@@ -27238,6 +27243,7 @@
/obj/effect/turf_decal/siding/red{
dir = 9
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"blo" = (
@@ -27308,6 +27314,7 @@
/obj/effect/turf_decal/siding/red{
dir = 8
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"blw" = (
@@ -31419,6 +31426,7 @@
/obj/machinery/atmospherics/pipe/simple/supply/hidden/layer4,
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2,
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron/dark,
/area/security/checkpoint/supply)
"bFa" = (
@@ -41130,6 +41138,7 @@
/obj/effect/turf_decal/stripes/line{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"ccu" = (
@@ -49317,6 +49326,7 @@
/obj/effect/turf_decal/siding/red{
dir = 1
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"cyk" = (
diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm
index 9cf18a9780f..f2b4be6fdb4 100644
--- a/_maps/map_files/MetaStation/MetaStation.dmm
+++ b/_maps/map_files/MetaStation/MetaStation.dmm
@@ -8131,6 +8131,7 @@
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2{
dir = 4
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aHg" = (
@@ -8139,6 +8140,7 @@
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{
dir = 8
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aHh" = (
@@ -8149,6 +8151,7 @@
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{
dir = 10
},
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aHj" = (
@@ -8356,6 +8359,7 @@
/obj/structure/disposalpipe/segment,
/obj/machinery/atmospherics/pipe/simple/supply/hidden/layer4,
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2,
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"aIs" = (
@@ -15380,6 +15384,7 @@
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"btK" = (
@@ -15648,6 +15653,7 @@
/obj/machinery/newscaster/security_unit{
pixel_y = -30
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"bvm" = (
@@ -20516,6 +20522,7 @@
},
/obj/effect/turf_decal/tile/red,
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2,
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron/white,
/area/security/checkpoint/medical)
"cag" = (
@@ -20535,6 +20542,7 @@
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{
dir = 10
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron/white,
/area/security/checkpoint/medical)
"can" = (
@@ -31674,6 +31682,7 @@
/obj/effect/turf_decal/tile/red{
dir = 1
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"dHX" = (
@@ -32096,6 +32105,7 @@
/obj/effect/turf_decal/siding/red{
dir = 6
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"dSW" = (
@@ -32139,6 +32149,7 @@
dir = 1
},
/obj/structure/chair,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"dVp" = (
@@ -36612,6 +36623,7 @@
dir = 10
},
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"fHg" = (
@@ -37900,6 +37912,7 @@
dir = 1
},
/obj/machinery/power/apc/auto_name/south,
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"gkw" = (
@@ -37977,6 +37990,7 @@
dir = 1
},
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"glu" = (
@@ -43552,6 +43566,7 @@
dir = 1
},
/obj/effect/turf_decal/siding/red,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"izY" = (
@@ -50116,6 +50131,7 @@
/obj/structure/chair,
/obj/structure/cable,
/obj/effect/turf_decal/siding/red/corner,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"liD" = (
@@ -51231,6 +51247,7 @@
pixel_x = -22;
pixel_y = 24
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"lJB" = (
@@ -59734,6 +59751,7 @@
/obj/effect/turf_decal/siding/red{
dir = 4
},
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"oXP" = (
@@ -65964,6 +65982,7 @@
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2{
dir = 1
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron/white,
/area/security/checkpoint/medical)
"ruM" = (
@@ -66472,6 +66491,7 @@
/obj/structure/chair{
dir = 4
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"rEC" = (
@@ -67397,6 +67417,7 @@
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{
dir = 8
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron/white,
/area/security/checkpoint/medical)
"rVj" = (
@@ -68999,7 +69020,7 @@
/obj/effect/turf_decal/siding/red{
dir = 4
},
-/obj/effect/landmark/start/depsec/supply,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"sBj" = (
@@ -73191,6 +73212,7 @@
"uow" = (
/obj/structure/cable,
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2,
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"uoE" = (
@@ -75695,6 +75717,7 @@
},
/obj/structure/cable,
/obj/effect/turf_decal/siding/red,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"voD" = (
diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm
index c4ca074fee3..04c22a682e1 100644
--- a/_maps/map_files/tramstation/tramstation.dmm
+++ b/_maps/map_files/tramstation/tramstation.dmm
@@ -4833,6 +4833,7 @@
dir = 4
},
/obj/machinery/airalarm/directional/east,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"akH" = (
@@ -8266,6 +8267,7 @@
},
/obj/structure/cable,
/obj/machinery/power/apc/auto_name/east,
+/obj/effect/landmark/start/depsec/science,
/turf/open/floor/iron,
/area/security/checkpoint/science)
"arM" = (
@@ -16964,6 +16966,7 @@
network = list("medbay");
pixel_y = -32
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"aKM" = (
@@ -25855,6 +25858,7 @@
"dDP" = (
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4,
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"dDR" = (
@@ -33028,6 +33032,7 @@
dir = 4
},
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"gFR" = (
@@ -35241,6 +35246,7 @@
},
/obj/structure/cable,
/obj/effect/landmark/event_spawn,
+/obj/effect/landmark/start/depsec/supply,
/turf/open/floor/iron,
/area/security/checkpoint/supply)
"hEP" = (
@@ -37143,6 +37149,7 @@
/obj/structure/chair{
dir = 1
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"iAL" = (
@@ -46632,6 +46639,7 @@
/obj/structure/chair{
dir = 1
},
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"mVw" = (
@@ -48224,6 +48232,7 @@
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2{
dir = 8
},
+/obj/effect/landmark/start/depsec/engineering,
/turf/open/floor/iron,
/area/security/checkpoint/engineering)
"nHG" = (
@@ -52059,6 +52068,7 @@
/obj/effect/turf_decal/trimline/red/filled/line,
/obj/machinery/power/apc/auto_name/south,
/obj/structure/cable,
+/obj/effect/landmark/start/depsec/medical,
/turf/open/floor/iron,
/area/security/checkpoint/medical)
"pre" = (
diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm
index 2c389806567..03cd9a361ba 100644
--- a/code/__DEFINES/preferences.dm
+++ b/code/__DEFINES/preferences.dm
@@ -64,7 +64,6 @@
#define PARALLAX_DELAY_LOW 2
#define SEC_DEPT_NONE "None"
-#define SEC_DEPT_RANDOM "Random"
#define SEC_DEPT_ENGINEERING "Engineering"
#define SEC_DEPT_MEDICAL "Medical"
#define SEC_DEPT_SCIENCE "Science"
diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm
index 76e224f235d..cdfd5f2dde2 100644
--- a/code/_globalvars/lists/flavor_misc.dm
+++ b/code/_globalvars/lists/flavor_misc.dm
@@ -146,7 +146,13 @@ GLOBAL_LIST_INIT(ai_core_display_screens, sortList(list(
return "ai-portrait" //just take this until they decide
return "ai-[lowertext(input)]"
-GLOBAL_LIST_INIT(security_depts_prefs, sortList(list(SEC_DEPT_RANDOM, SEC_DEPT_NONE, SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, SEC_DEPT_SCIENCE, SEC_DEPT_SUPPLY)))
+GLOBAL_LIST_INIT(security_depts_prefs, sortList(list(
+ SEC_DEPT_NONE,
+ SEC_DEPT_ENGINEERING,
+ SEC_DEPT_MEDICAL,
+ SEC_DEPT_SCIENCE,
+ SEC_DEPT_SUPPLY,
+)))
//Backpacks
#define GBACKPACK "Grey Backpack"
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 02b49e090e8..2297b574bc2 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -388,6 +388,11 @@ SUBSYSTEM_DEF(ticker)
/datum/controller/subsystem/ticker/proc/equip_characters()
+ GLOB.security_officer_distribution = decide_security_officer_departments(
+ shuffle(GLOB.new_player_list),
+ shuffle(GLOB.available_depts),
+ )
+
var/captainless = TRUE
var/highest_rank = length(SSjob.chain_of_command) + 1
@@ -437,6 +442,31 @@ SUBSYSTEM_DEF(ticker)
to_chat(new_player_mob, "Captainship not forced on anyone.")
CHECK_TICK
+/datum/controller/subsystem/ticker/proc/decide_security_officer_departments(
+ list/new_players,
+ list/departments,
+)
+ var/list/officer_mobs = list()
+ var/list/officer_preferences = list()
+
+ for (var/mob/dead/new_player/new_player_mob as anything in new_players)
+ var/mob/living/carbon/human/character = new_player_mob.new_character
+ if (istype(character) && character.mind?.assigned_role == "Security Officer")
+ officer_mobs += character
+
+ var/datum/client_interface/client = GET_CLIENT(new_player_mob)
+ var/preference = client?.prefs?.prefered_security_department || SEC_DEPT_NONE
+ officer_preferences += preference
+
+ var/distribution = get_officer_departments(officer_preferences, departments)
+
+ var/list/output = list()
+
+ for (var/index in 1 to officer_mobs.len)
+ output[REF(officer_mobs[index])] = distribution[index]
+
+ return output
+
/datum/controller/subsystem/ticker/proc/transfer_characters()
var/list/livings = list()
for(var/i in GLOB.new_player_list)
diff --git a/code/game/machinery/announcement_system.dm b/code/game/machinery/announcement_system.dm
index 9e1df3bd087..1748c60beb1 100644
--- a/code/game/machinery/announcement_system.dm
+++ b/code/game/machinery/announcement_system.dm
@@ -86,6 +86,17 @@ GLOBAL_LIST_EMPTY(announcement_systems)
else if(message_type == "ARRIVALS_BROKEN")
message = "The arrivals shuttle has been damaged. Docking for repairs..."
+ broadcast(message, channels)
+
+/// Announces a new security officer joining over the radio
+/obj/machinery/announcement_system/proc/announce_officer(mob/officer, department)
+ if (!is_operational)
+ return
+
+ broadcast("Officer [officer.real_name] has been assigned to [department].", list(RADIO_CHANNEL_SECURITY))
+
+/// Sends a message to the appropriate channels.
+/obj/machinery/announcement_system/proc/broadcast(message, list/channels)
if(channels.len == 0)
radio.talk_into(src, message, null)
else
diff --git a/code/game/objects/effects/landmarks.dm b/code/game/objects/effects/landmarks.dm
index ad5e8addc6f..0220e34b306 100644
--- a/code/game/objects/effects/landmarks.dm
+++ b/code/game/objects/effects/landmarks.dm
@@ -216,26 +216,32 @@ INITIALIZE_IMMEDIATE(/obj/effect/landmark)
/obj/effect/landmark/start/depsec
name = "department_sec"
icon_state = "Security Officer"
+ /// What department this spawner is for
+ var/department
/obj/effect/landmark/start/depsec/New()
..()
- GLOB.department_security_spawns += src
+ LAZYADDASSOCLIST(GLOB.department_security_spawns, department, src)
/obj/effect/landmark/start/depsec/Destroy()
- GLOB.department_security_spawns -= src
+ LAZYREMOVEASSOC(GLOB.department_security_spawns, department, src)
return ..()
/obj/effect/landmark/start/depsec/supply
name = "supply_sec"
+ department = SEC_DEPT_SUPPLY
/obj/effect/landmark/start/depsec/medical
name = "medical_sec"
+ department = SEC_DEPT_MEDICAL
/obj/effect/landmark/start/depsec/engineering
name = "engineering_sec"
+ department = SEC_DEPT_ENGINEERING
/obj/effect/landmark/start/depsec/science
name = "science_sec"
+ department = SEC_DEPT_SCIENCE
//Antagonist spawns
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index 405e3bb0cff..2549005c645 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -86,7 +86,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
var/list/custom_names = list()
var/preferred_ai_core_display = "Blue"
- var/prefered_security_department = SEC_DEPT_RANDOM
+ var/prefered_security_department = SEC_DEPT_NONE
//Quirk list
var/list/all_quirks = list()
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 3734b3b7586..7949c475fd9 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -431,6 +431,15 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
READ_FILE(S["preferred_ai_core_display"], preferred_ai_core_display)
READ_FILE(S["prefered_security_department"], prefered_security_department)
+ // This is the version when the random security department was removed.
+ // When the minimum is higher than that version, it's impossible for someone to have the "Random" department.
+ #if SAVEFILE_VERSION_MIN > 40
+ #warn The prefered_security_department check in preferences_savefile.dm is no longer necessary.
+ #endif
+
+ if (!(prefered_security_department in GLOB.security_depts_prefs))
+ prefered_security_department = SEC_DEPT_NONE
+
//Jobs
READ_FILE(S["joblessrole"], joblessrole)
//Load prefs
diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm
index 2aacf66d148..63f96e12e9f 100644
--- a/code/modules/jobs/job_types/security_officer.dm
+++ b/code/modules/jobs/job_types/security_officer.dm
@@ -28,47 +28,57 @@
GLOBAL_LIST_INIT(available_depts, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, SEC_DEPT_SCIENCE, SEC_DEPT_SUPPLY))
-/datum/job/security_officer/after_spawn(mob/living/carbon/human/H, mob/M)
+/**
+ * The department distribution of the security officers.
+ *
+ * Keys are refs of the security officer mobs. This is to preserve the list's structure even if the
+ * mob gets deleted. This is also safe, as mobs are guaranteed to have a unique ref, as per /mob/GenerateTag().
+ */
+GLOBAL_LIST_EMPTY(security_officer_distribution)
+
+/datum/job/security_officer/after_spawn(mob/living/carbon/human/H, mob/M, latejoin = FALSE)
. = ..()
- // Assign department security
+
var/department
- if(M && M.client && M.client.prefs)
- department = M.client.prefs.prefered_security_department
- if(!LAZYLEN(GLOB.available_depts) || department == "None")
- return
- else if(department in GLOB.available_depts)
- LAZYREMOVE(GLOB.available_depts, department)
- else
- department = pick_n_take(GLOB.available_depts)
+
+ var/prefered_department = M.client?.prefs?.prefered_security_department
+ if (!isnull(prefered_department))
+ department = get_my_department(H, prefered_department)
+
+ if (latejoin)
+ announce_latejoin(H, department, GLOB.security_officer_distribution)
+
+ // In the event we're a latejoin, or otherwise aren't in the round-start distributions.
+ // This is outside the latejoin check because this should theoretically still run if
+ // a player isn't in the distributions, but isn't a late join.
+ GLOB.security_officer_distribution[REF(H)] = department
+
var/ears = null
var/accessory = null
var/list/dep_trim = null
var/destination = null
- var/spawn_point = null
+ var/spawn_point = pick(LAZYACCESS(GLOB.department_security_spawns, department))
+
switch(department)
if(SEC_DEPT_SUPPLY)
ears = /obj/item/radio/headset/headset_sec/alt/department/supply
dep_trim = /datum/id_trim/job/security_officer/supply
destination = /area/security/checkpoint/supply
- spawn_point = locate(/obj/effect/landmark/start/depsec/supply) in GLOB.department_security_spawns
accessory = /obj/item/clothing/accessory/armband/cargo
if(SEC_DEPT_ENGINEERING)
ears = /obj/item/radio/headset/headset_sec/alt/department/engi
dep_trim = /datum/id_trim/job/security_officer/engineering
destination = /area/security/checkpoint/engineering
- spawn_point = locate(/obj/effect/landmark/start/depsec/engineering) in GLOB.department_security_spawns
accessory = /obj/item/clothing/accessory/armband/engine
if(SEC_DEPT_MEDICAL)
ears = /obj/item/radio/headset/headset_sec/alt/department/med
dep_trim = /datum/id_trim/job/security_officer/medical
destination = /area/security/checkpoint/medical
- spawn_point = locate(/obj/effect/landmark/start/depsec/medical) in GLOB.department_security_spawns
accessory = /obj/item/clothing/accessory/armband/medblue
if(SEC_DEPT_SCIENCE)
ears = /obj/item/radio/headset/headset_sec/alt/department/sci
dep_trim = /datum/id_trim/job/security_officer/science
destination = /area/security/checkpoint/science
- spawn_point = locate(/obj/effect/landmark/start/depsec/science) in GLOB.department_security_spawns
accessory = /obj/item/clothing/accessory/armband/science
if(accessory)
@@ -107,7 +117,53 @@ GLOBAL_LIST_INIT(available_depts, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, S
else
to_chat(M, "You have not been assigned to any department. Patrol the halls and help where needed.")
+/datum/job/security_officer/proc/announce_latejoin(
+ mob/officer,
+ department,
+ distribution,
+)
+ var/obj/machinery/announcement_system/announcement_system = pick(GLOB.announcement_systems)
+ if (isnull(announcement_system))
+ return
+ announcement_system.announce_officer(officer, department)
+
+ var/list/targets = list()
+
+ var/list/partners = list()
+ for (var/officer_ref in distribution)
+ var/mob/partner = locate(officer_ref)
+ if (!istype(partner))
+ continue
+ partners += partner.real_name
+
+ if (partners.len)
+ for (var/obj/item/pda/pda as anything in GLOB.PDAs)
+ if (pda.owner in partners)
+ targets += "[pda.owner] ([pda.ownjob])"
+
+ var/datum/signal/subspace/messaging/pda/signal = new(announcement_system, list(
+ "name" = "Security Department Update",
+ "job" = "Automated Announcement System",
+ "message" = "Officer [officer.real_name] has been assigned to your department, [department].",
+ "targets" = targets,
+ "automated" = TRUE,
+ ))
+
+ signal.send_to_receivers()
+
+/datum/job/security_officer/proc/get_my_department(mob/character, preferred_department)
+ var/department = GLOB.security_officer_distribution[REF(character)]
+
+ // This passes when they are a round start security officer.
+ if (department)
+ return department
+
+ return get_new_officer_distribution_from_late_join(
+ preferred_department,
+ shuffle(GLOB.available_depts),
+ GLOB.security_officer_distribution,
+ )
/datum/outfit/job/security
name = "Security Officer"
@@ -150,7 +206,6 @@ GLOBAL_LIST_INIT(available_depts, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, S
/obj/item/radio/headset/headset_sec/alt/department/supply
keyslot = new /obj/item/encryptionkey/headset_sec
keyslot2 = new /obj/item/encryptionkey/headset_cargo
-
/obj/item/radio/headset/headset_sec/alt/department/med
keyslot = new /obj/item/encryptionkey/headset_sec
keyslot2 = new /obj/item/encryptionkey/headset_med
@@ -158,3 +213,221 @@ GLOBAL_LIST_INIT(available_depts, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, S
/obj/item/radio/headset/headset_sec/alt/department/sci
keyslot = new /obj/item/encryptionkey/headset_sec
keyslot2 = new /obj/item/encryptionkey/headset_sci
+
+/// Returns the distribution of splitting the given security officers into departments.
+/// Return value is an assoc list of candidate => SEC_DEPT_*.
+/proc/get_officer_departments(list/preferences, list/departments)
+ if (!preferences.len)
+ return list()
+
+ /**
+ * This is a pretty complicated algorithm, but it's one I'm rather proud of.
+ *
+ * This is the function that is responsible for taking the list of preferences,
+ * and spitting out what to put them in.
+ *
+ * However, it should, wherever possible, prevent solo departments.
+ * That means that if there's one medical officer, and one engineering officer,
+ * that they should be put onto the same department (either medical or engineering).
+ *
+ * The first step is to get the "distribution". This describes how many officers
+ * should be in each department, no matter what they are.
+ * This is handled in `get_distribution`. Examples of inputs/outputs are:
+ * get_distribution(1, 4) => [1]
+ * get_distribution(2, 4) => [2]
+ * get_distribution(3, 4) => [3] # If this returned [2, 1], then we'd get a loner.
+ * get_distribution(4, 4) => [2, 2] # We have enough to put into a separate group
+ *
+ * Once this distribution is received, the next step is to figure out where to put everyone.
+ *
+ * If all members have no preference, just make one an unused department (from the departments argument).
+ * Then, call ourselves again.
+ *
+ * Order the groups from most populated to least.
+ *
+ * If the top group has enough officers who actually *want* that department, then we give it to them.
+ * If there are any leftovers (for example, if 3 officers want medical, but we only want 2), then we
+ * update those to have no preference instead.
+ *
+ * If the top group does NOT have enough officers, then we kill the least popular group by setting
+ * them all to have no preference.
+ *
+ * Anyone in the most popular group will be removed from the list, and the final tally will be updated.
+ * In the case of not having enough officers, this is a no-op, as there won't be any in the most popular group yet.
+ *
+ * If there are any candidates left, then we call the algorithm again, but for everyone who hasn't been selected yet.
+ * We take the results from that run, and put them in the correct order.
+ *
+ * As an example, let's assume we have the following preferences:
+ * [engineer, medical, medical, medical, medical, cargo]
+ *
+ * The distribution of this is [2, 2, 2], meaning there will be 3 departments chosen and they will have 2 each.
+ * We order from most popular to least popular and get:
+ * - medical: 4
+ * - engineer: 1
+ * - cargo: 1
+ *
+ * We need 2 to fill the first group. There are enough medical staff to do it. Thus, we take the first 2 medical staff
+ * and update the output, making it now: [engineer, medical, medical, ?, ?, cargo].
+ *
+ * The remaining two want-to-be-medical officers are now updated to act as no preference. We run the algorithm again.
+ * This time, are candidates are [engineer, none, none, cargo].
+ * The distribution of this is [2, 2]. The frequency is:
+ * - engineer: 1
+ * - cargo: 1
+ * - no preference: 2
+ *
+ * We need 2 to fill the engineering group, but only have one who wants to do it.
+ * We have enough no preferences for it, making our result: [engineer, engineer, none, cargo].
+ * We run the algorithm again, but this time with: [none, cargo].
+ * Frequency is:
+ * - cargo: 1
+ * - no preference: 1
+ * Enough to fill cargo, etc, and we get [cargo, cargo].
+ *
+ * These are all then compounded into one list.
+ *
+ * In the case that all are no preference, it will pop the last department, and use that.
+ * For example, if `departments` is [engi, medical, cargo], and we have the preferences:
+ * [none, none, none]...
+ * Then we will just give them all cargo.
+ *
+ * One of the most important parts of this algorithm is IT IS DETERMINISTIC.
+ * That means that this proc is 100% testable.
+ * Instead, to get random results, the preferences and departments are shuffled
+ * before the proc is ever called.
+ */
+
+ preferences = preferences.Copy()
+ departments = departments.Copy()
+
+ var/distribution = get_distribution(preferences.len, departments.len)
+ var/selection[preferences.len]
+
+ var/list/grouped = list()
+ var/list/biggest_group
+ var/biggest_preference
+ var/list/indices = list()
+
+ for (var/index in 1 to preferences.len)
+ indices += index
+
+ var/preference = preferences[index]
+ if (!(preference in grouped))
+ grouped[preference] = list()
+ grouped[preference] += index
+
+ var/list/preferred_group = grouped[preference]
+
+ if (preference != SEC_DEPT_NONE && (isnull(biggest_group) || biggest_group.len < preferred_group.len))
+ biggest_group = grouped[preference]
+ biggest_preference = preference
+
+ if (isnull(biggest_group))
+ preferences[1] = pop(departments)
+ return get_officer_departments(preferences, departments)
+
+ if (biggest_group.len >= distribution[1])
+ for (var/index in 1 to distribution[1])
+ selection[biggest_group[index]] = biggest_preference
+
+ if (biggest_group.len > distribution[1])
+ for (var/leftover in (distribution[1] + 1) to biggest_group.len)
+ preferences[leftover] = SEC_DEPT_NONE
+ else
+ var/needed = distribution[1] - biggest_group.len
+ if (LAZYLEN(LAZYACCESS(grouped, SEC_DEPT_NONE)) >= needed)
+ for (var/candidate_index in biggest_group)
+ selection[candidate_index] = biggest_preference
+
+ for (var/index in 1 to needed)
+ selection[grouped[SEC_DEPT_NONE][index]] = biggest_preference
+ else
+ var/least_popular_index = grouped[grouped.len]
+ if (least_popular_index == SEC_DEPT_NONE)
+ least_popular_index = grouped[grouped.len - 1]
+ var/least_popular = grouped[least_popular_index]
+ for (var/candidate_index in least_popular)
+ preferences[candidate_index] = SEC_DEPT_NONE
+
+ // Remove all members of the most popular candidate from the list
+ for (var/chosen in 1 to selection.len)
+ if (selection[chosen] == biggest_preference)
+ indices -= chosen
+ preferences[chosen] = null
+
+ listclearnulls(preferences)
+
+ departments -= biggest_preference
+
+ if (grouped.len != 1)
+ var/list/next_step = get_officer_departments(preferences, departments)
+ for (var/index in 1 to indices.len)
+ var/place = indices[index]
+ selection[place] = next_step[index]
+
+ return selection
+
+/proc/get_distribution(candidates, departments)
+ var/number_of_twos = min(departments, round(candidates / 2))
+ var/redistribute = candidates - (2 * number_of_twos)
+
+ var/distribution[max(1, number_of_twos)]
+
+ for (var/index in 1 to number_of_twos)
+ distribution[index] = 2
+
+ for (var/index in 0 to redistribute - 1)
+ distribution[(index % departments) + 1] += 1
+
+ return distribution
+
+/proc/get_new_officer_distribution_from_late_join(
+ preference,
+ list/departments,
+ list/distribution,
+)
+ /**
+ * For late joiners, we're forced to put them in an alone department at some point.
+ *
+ * This is because reusing the round-start algorithm would force existing officers into
+ * a different department in order to preserve having partners at all times.
+ *
+ * This would mean retroactively updating their access as well, which is too much
+ * of a headache for me to want to bother.
+ *
+ * So, here's the method. If any department currently has 1 officer, they are forced into
+ * that.
+ *
+ * Otherwise, the department with the least officers in it is chosen.
+ * Preference takes priority, meaning that if both medical and engineering have zero officers,
+ * and the preference is medical, then medical is what will be chosen.
+ *
+ * Just like `get_officer_departments`, this function is deterministic.
+ * Randomness should instead be handled in the shuffling of the `departments` argument.
+ */
+ var/list/amount_in_departments = list()
+
+ for (var/department in departments)
+ amount_in_departments[department] = 0
+
+ for (var/officer in distribution)
+ var/department = distribution[officer]
+ if (!isnull(department))
+ amount_in_departments[department] += 1
+
+ var/list/lowest_departments = list(departments[1])
+ var/lowest_amount = INFINITY
+
+ for (var/department in amount_in_departments)
+ var/amount = amount_in_departments[department]
+
+ if (amount == 1)
+ return department
+ else if (lowest_amount > amount)
+ lowest_departments = list(department)
+ lowest_amount = amount
+ else if (lowest_amount == amount)
+ lowest_departments += department
+
+ return (preference in lowest_departments) ? preference : lowest_departments[1]
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 040f9f0bceb..d0618a6d9f0 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -72,6 +72,7 @@
#include "reagent_recipe_collisions.dm"
#include "resist.dm"
#include "say.dm"
+#include "security_officer_distribution.dm"
#include "serving_tray.dm"
#include "siunit.dm"
#include "spawn_humans.dm"
diff --git a/code/modules/unit_tests/security_officer_distribution.dm b/code/modules/unit_tests/security_officer_distribution.dm
new file mode 100644
index 00000000000..0b2ad98b878
--- /dev/null
+++ b/code/modules/unit_tests/security_officer_distribution.dm
@@ -0,0 +1,101 @@
+#define SECURITY_OFFICER_DEPARTMENTS list("a", "b", "c", "d")
+
+/// Test that security officers with specific distributions get their departments.
+/datum/unit_test/security_officer_roundstart_distribution
+
+/datum/unit_test/security_officer_roundstart_distribution/proc/test(
+ list/preferences,
+ list/expected,
+)
+ var/list/outcome = get_officer_departments(preferences, SECURITY_OFFICER_DEPARTMENTS)
+ var/failure_message = "Tested with [json_encode(preferences)] and expected [json_encode(expected)], got [json_encode(outcome)]"
+
+ if (outcome.len == expected.len)
+ for (var/index in 1 to outcome.len)
+ if (outcome[index] != expected[index])
+ Fail(failure_message)
+ return
+ else
+ Fail(failure_message)
+
+/datum/unit_test/security_officer_roundstart_distribution/Run()
+ test_distributions()
+ test_with_mock_players()
+
+/datum/unit_test/security_officer_roundstart_distribution/proc/test_distributions()
+ test(list("a"), list("a"))
+ test(list("a", "b"), list("a", "a"))
+ test(list("a", "b", "c"), list("a", "a", "a"))
+ test(list("a", "a", "b"), list("a", "a", "a"))
+ test(list("a", "a", "b", "b"), list("a", "a", "b", "b"))
+ test(list("a", "a", "a", "b"), list("a", "a", "b", "b"))
+ test(list("a", "b", "c", "d"), list("a", "b", "b", "a"))
+ test(list(SEC_DEPT_NONE), list("d"))
+ test(list("a", SEC_DEPT_NONE), list("a", "a"))
+ test(list(SEC_DEPT_NONE, SEC_DEPT_NONE, SEC_DEPT_NONE, SEC_DEPT_NONE), list("d", "d", "c", "c"))
+
+/datum/unit_test/security_officer_roundstart_distribution/proc/test_with_mock_players()
+ var/mob/dead/new_player/officer_a = create_officer("a")
+ var/mob/dead/new_player/officer_b = create_officer("b")
+ var/mob/dead/new_player/officer_c = create_officer("c")
+ var/mob/dead/new_player/officer_d = create_officer("d")
+
+ var/list/outcome = SSticker.decide_security_officer_departments(
+ list(officer_a, officer_b, officer_c, officer_d),
+ SECURITY_OFFICER_DEPARTMENTS,
+ )
+
+ TEST_ASSERT_EQUAL(outcome[REF(officer_a.new_character)], "a", "Officer A's department outcome was incorrect.")
+ TEST_ASSERT_EQUAL(outcome[REF(officer_b.new_character)], "b", "Officer B's department outcome was incorrect.")
+ TEST_ASSERT_EQUAL(outcome[REF(officer_c.new_character)], "b", "Officer C's department outcome was incorrect.")
+ TEST_ASSERT_EQUAL(outcome[REF(officer_d.new_character)], "a", "Officer D's department outcome was incorrect.")
+
+/datum/unit_test/security_officer_roundstart_distribution/proc/create_officer(preference)
+ var/mob/dead/new_player/new_player = allocate(/mob/dead/new_player)
+ var/datum/client_interface/mock_client = new
+
+ mock_client.prefs = new
+ mock_client.prefs.prefered_security_department = preference
+
+ var/mob/living/carbon/human/new_character = allocate(/mob/living/carbon/human)
+ new_character.mind_initialize()
+ new_character.mind.assigned_role = "Security Officer"
+
+ new_player.new_character = new_character
+ new_player.mock_client = mock_client
+ return new_player
+
+/// Test that latejoin security officers are put into the correct department
+/datum/unit_test/security_officer_latejoin_distribution
+
+/datum/unit_test/security_officer_latejoin_distribution/proc/test(
+ preference,
+ list/preferences_of_others,
+ expected,
+)
+ var/list/distribution = list()
+
+ for (var/officer_preference in preferences_of_others)
+ var/mob/officer = allocate(/mob/living/carbon/human)
+ distribution[officer] = officer_preference
+
+ var/result = get_new_officer_distribution_from_late_join(
+ preference,
+ SECURITY_OFFICER_DEPARTMENTS,
+ distribution,
+ )
+
+ var/failure_message = "Latejoin distribution was incorrect (preference = [preference], preferences_of_others = [json_encode(preferences_of_others)])."
+
+ TEST_ASSERT_EQUAL(result, expected, failure_message)
+
+/datum/unit_test/security_officer_latejoin_distribution/Run()
+ test("a", list(), "a")
+ test("b", list(), "b")
+ test("a", list("b"), "b")
+ test("a", list("a", "a"), "b")
+ test("a", list("a", "a", "b"), "b")
+ test("a", list("a", "a", "b", "b"), "c")
+ test("a", list("a", "a", "b", "b", "c", "c", "d", "d"), "a")
+
+#undef SECURITY_OFFICER_DEPARTMENTS
diff --git a/config/game_options.txt b/config/game_options.txt
index 501749c3bc3..f16ccbcb565 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -312,7 +312,7 @@ MINIMAL_ACCESS_THRESHOLD 20
#EVERYONE_HAS_MAINT_ACCESS
## Comment this out this to make security officers spawn in departmental security posts
-SEC_START_BRIG
+#SEC_START_BRIG
## GHOST INTERACTION ###