From f44c20cdf4242fa6ef5285b549df3b7119b9496a Mon Sep 17 00:00:00 2001 From: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Date: Sat, 20 Mar 2021 17:11:11 -0700 Subject: [PATCH] Departmental officers are now put together, rather than separate, if possible (#57685) Security officers will now be paired up together in the same department, across departments. This means that, instead of 4 officers being split across 4 departments, there'll now be 2 groups of 2. Late-join officers will be put into any department with only 1 officer. If none exist, the least populous department will be chosen, with their preference having priority. Updates the maps to have more spawns for departmental officers. Delta previously had none, and now has some. Fixed a bug where MetaStation's security departmental officer was a supply one instead. Removes the "random" departmental preference. All security officers are now given a department. The "none" preference still exists, but just to show that you don't care which department you're put into. Updates the config to comment out SEC_START_BRIG. This is what the configuration was already on live servers. This is something that should likely be test merged, but it mucks with savefiles. Luckily, the only damage it does is changing random departments to none, so if a TM is reverted, only that will have to be changed. --- .../map_files/Deltastation/DeltaStation2.dmm | 32 ++ .../map_files/IceBoxStation/IceBoxStation.dmm | 10 + _maps/map_files/KiloStation/KiloStation.dmm | 10 + _maps/map_files/MetaStation/MetaStation.dmm | 25 +- _maps/map_files/tramstation/tramstation.dmm | 10 + code/__DEFINES/preferences.dm | 1 - code/_globalvars/lists/flavor_misc.dm | 8 +- code/controllers/subsystem/ticker.dm | 30 ++ code/game/machinery/announcement_system.dm | 11 + code/game/objects/effects/landmarks.dm | 10 +- code/modules/client/preferences.dm | 2 +- code/modules/client/preferences_savefile.dm | 9 + .../jobs/job_types/security_officer.dm | 305 +++++++++++++++++- code/modules/unit_tests/_unit_tests.dm | 1 + .../security_officer_distribution.dm | 101 ++++++ config/game_options.txt | 2 +- 16 files changed, 544 insertions(+), 23 deletions(-) create mode 100644 code/modules/unit_tests/security_officer_distribution.dm 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 ###