Files
Yogstation/code/controllers/subsystem/job.dm
Jordie a28de54405 Ban system and interface update (#41176)
Spiritual successor and extension to #17798, an almost entire rebuild of the SQL ban system backend and interface.
Bantypes are removed per #8584 and #6174. All bans are now 'role bans', server bans are when a ban's role is server. Admin bans are a column, meaning it's possible to ban admins from jobs.
Bans now have only an expiry datetime, duration is calculated from this when queried.
unbanned column is removed as it's superfluous, checking unban status is now done through checking unban_datetime. unban_round_id column added. Each ip and computerid columns rearranged so ip is always first, like in other tables. Bans now permit a null ckey, ip and computerid.

Ban checking is split into two procs now is_banned_from() does a check if a ckey is banned from one or more roles and returns true or false. This effectively replaces jobban_isbanned() used in simple if() statements. If connected a client's ban cache is checked rather than querying the DB. This makes it possible for a client connected to two or more servers to ignore any bans made on one server until their ban cache is rebuilt on the others. Could be avoided with cross-server calls to update ban caches or just the removal of the ban cache but as is I've done neither since I think it's enough of an edge case to not be worth it.
The second proc is is_banned_from_with_details(), this queries the DB for a role ban on a player's ckey, ip or CID and returns the details. This replaces direct queries in IsBanned.dm and the preferences menu.

The legacy ban system is removed.

The interfaces for banning, unbanning and editing bans have been remade to require less clicking and easier simultaneous operations. The banning and jobban panel are combined. They also store player connection details when opened so a client disconnecting no longer stops a ban being placed.

New banning panel:
Key, IP and CID can all be toggled to allow excluding them from a ban.
Checking Use IP and CID from last connection lets you enter only a ckey and have the DB fill these fields in for you, if possible.
Temporary bans have a drop-menu which lets you select between seconds, minutes, hours, days, weeks, months and years so you don't need to calculate how many minutes a long ban would be. The ban is still converted into minutes on the DB however.
Checking any of the head roles will check both of the boxes for you.
The red role box indicates there is already a ban on that role for this ckey. You can apply additional role bans to stack them.

New unbanning panel:
Unbanning panel is now separate from the banning panel but otherwise functionally the same.

Ban editing panel:
Actually just a modified banning panel, all the features from it work the same here.
You can now edit almost all parameters of a ban instead of just the reason.
You can't edit severity as it's not really part of the ban.
The panels have been tested but I've not been able to get my local server to be accessible so ban functionality isn't properly confirmed. Plenty of testing will be required as I'd rather not break bans.

cl
admin: Ban interface rework. The banning and unbanning panels have received a new design which is easier to use and allows multiple role bans to be made at once.
prefix: Ban search and unbanning moved to unbanning panel, which is now a separate panel to the old banning panel.
/cl
2018-12-04 20:48:17 +01:00

645 lines
22 KiB
Plaintext

SUBSYSTEM_DEF(job)
name = "Jobs"
init_order = INIT_ORDER_JOBS
flags = SS_NO_FIRE
var/list/occupations = list() //List of all jobs
var/list/name_occupations = list() //Dict of all jobs, keys are titles
var/list/type_occupations = list() //Dict of all jobs, keys are types
var/list/unassigned = list() //Players who need jobs
var/initial_players_to_assign = 0 //used for checking against population caps
var/list/prioritized_jobs = list()
var/list/latejoin_trackers = list() //Don't read this list, use GetLateJoinTurfs() instead
var/overflow_role = "Assistant"
/datum/controller/subsystem/job/Initialize(timeofday)
SSmapping.HACK_LoadMapConfig()
if(!occupations.len)
SetupOccupations()
if(CONFIG_GET(flag/load_jobs_from_txt))
LoadJobs()
generate_selectable_species()
set_overflow_role(CONFIG_GET(string/overflow_job))
return ..()
/datum/controller/subsystem/job/proc/set_overflow_role(new_overflow_role)
var/datum/job/new_overflow = GetJob(new_overflow_role)
var/cap = CONFIG_GET(number/overflow_cap)
new_overflow.spawn_positions = cap
new_overflow.total_positions = cap
if(new_overflow_role != overflow_role)
var/datum/job/old_overflow = GetJob(overflow_role)
old_overflow.spawn_positions = initial(old_overflow.spawn_positions)
old_overflow.total_positions = initial(old_overflow.total_positions)
overflow_role = new_overflow_role
JobDebug("Overflow role set to : [new_overflow_role]")
/datum/controller/subsystem/job/proc/SetupOccupations(faction = "Station")
occupations = list()
var/list/all_jobs = subtypesof(/datum/job)
if(!all_jobs.len)
to_chat(world, "<span class='boldannounce'>Error setting up jobs, no job datums found</span>")
return 0
for(var/J in all_jobs)
var/datum/job/job = new J()
if(!job)
continue
if(job.faction != faction)
continue
if(!job.config_check())
continue
if(!job.map_check()) //Even though we initialize before mapping, this is fine because the config is loaded at new
testing("Removed [job.type] due to map config");
continue
occupations += job
name_occupations[job.title] = job
type_occupations[J] = job
return 1
/datum/controller/subsystem/job/proc/GetJob(rank)
if(!occupations.len)
SetupOccupations()
return name_occupations[rank]
/datum/controller/subsystem/job/proc/GetJobType(jobtype)
if(!occupations.len)
SetupOccupations()
return type_occupations[jobtype]
/datum/controller/subsystem/job/proc/AssignRole(mob/dead/new_player/player, rank, latejoin = FALSE)
JobDebug("Running AR, Player: [player], Rank: [rank], LJ: [latejoin]")
if(player && player.mind && rank)
var/datum/job/job = GetJob(rank)
if(!job)
return FALSE
if(is_banned_from(player.ckey, rank) || QDELETED(player))
return FALSE
if(!job.player_old_enough(player.client))
return FALSE
if(job.required_playtime_remaining(player.client))
return FALSE
var/position_limit = job.total_positions
if(!latejoin)
position_limit = job.spawn_positions
JobDebug("Player: [player] is now Rank: [rank], JCP:[job.current_positions], JPL:[position_limit]")
player.mind.assigned_role = rank
unassigned -= player
job.current_positions++
return TRUE
JobDebug("AR has failed, Player: [player], Rank: [rank]")
return FALSE
/datum/controller/subsystem/job/proc/FindOccupationCandidates(datum/job/job, level, flag)
JobDebug("Running FOC, Job: [job], Level: [level], Flag: [flag]")
var/list/candidates = list()
for(var/mob/dead/new_player/player in unassigned)
if(is_banned_from(player.ckey, job.title) || QDELETED(player))
JobDebug("FOC isbanned failed, Player: [player]")
continue
if(!job.player_old_enough(player.client))
JobDebug("FOC player not old enough, Player: [player]")
continue
if(job.required_playtime_remaining(player.client))
JobDebug("FOC player not enough xp, Player: [player]")
continue
if(flag && (!(flag in player.client.prefs.be_special)))
JobDebug("FOC flag failed, Player: [player], Flag: [flag], ")
continue
if(player.mind && job.title in player.mind.restricted_roles)
JobDebug("FOC incompatible with antagonist role, Player: [player]")
continue
// yogs start - Donor features, quiet round
if(((job.title in GLOB.command_positions) || (job.title in GLOB.nonhuman_positions)) && (player.client.prefs.toggles & QUIET_ROUND))
JobDebug("FOC quiet check failed, Player: [player]")
continue
// yogs end
if(player.client.prefs.GetJobDepartment(job, level) & job.flag)
JobDebug("FOC pass, Player: [player], Level:[level]")
candidates += player
return candidates
/datum/controller/subsystem/job/proc/GiveRandomJob(mob/dead/new_player/player)
JobDebug("GRJ Giving random job, Player: [player]")
. = FALSE
for(var/datum/job/job in shuffle(occupations))
if(!job)
continue
if(istype(job, GetJob(SSjob.overflow_role))) // We don't want to give him assistant, that's boring!
continue
if(job.title in GLOB.command_positions) //If you want a command position, select it!
continue
if(is_banned_from(player.ckey, job.title) || QDELETED(player))
if(QDELETED(player))
JobDebug("GRJ isbanned failed, Player deleted")
break
JobDebug("GRJ isbanned failed, Player: [player], Job: [job.title]")
continue
if(!job.player_old_enough(player.client))
JobDebug("GRJ player not old enough, Player: [player]")
continue
if(job.required_playtime_remaining(player.client))
JobDebug("GRJ player not enough xp, Player: [player]")
continue
if(player.mind && job.title in player.mind.restricted_roles)
JobDebug("GRJ incompatible with antagonist role, Player: [player], Job: [job.title]")
continue
if((job.current_positions < job.spawn_positions) || job.spawn_positions == -1)
JobDebug("GRJ Random job given, Player: [player], Job: [job]")
if(AssignRole(player, job.title))
return TRUE
/datum/controller/subsystem/job/proc/ResetOccupations()
JobDebug("Occupations reset.")
for(var/mob/dead/new_player/player in GLOB.player_list)
if((player) && (player.mind))
player.mind.assigned_role = null
player.mind.special_role = null
SSpersistence.antag_rep_change[player.ckey] = 0
SetupOccupations()
unassigned = list()
return
//This proc is called before the level loop of DivideOccupations() and will try to select a head, ignoring ALL non-head preferences for every level until
//it locates a head or runs out of levels to check
//This is basically to ensure that there's atleast a few heads in the round
/datum/controller/subsystem/job/proc/FillHeadPosition()
for(var/level = 1 to 3)
for(var/command_position in GLOB.command_positions)
var/datum/job/job = GetJob(command_position)
if(!job)
continue
if((job.current_positions >= job.total_positions) && job.total_positions != -1)
continue
var/list/candidates = FindOccupationCandidates(job, level)
if(!candidates.len)
continue
var/mob/dead/new_player/candidate = pick(candidates)
if(AssignRole(candidate, command_position))
return 1
return 0
//This proc is called at the start of the level loop of DivideOccupations() and will cause head jobs to be checked before any other jobs of the same level
//This is also to ensure we get as many heads as possible
/datum/controller/subsystem/job/proc/CheckHeadPositions(level)
for(var/command_position in GLOB.command_positions)
var/datum/job/job = GetJob(command_position)
if(!job)
continue
if((job.current_positions >= job.total_positions) && job.total_positions != -1)
continue
var/list/candidates = FindOccupationCandidates(job, level)
if(!candidates.len)
continue
var/mob/dead/new_player/candidate = pick(candidates)
AssignRole(candidate, command_position)
/datum/controller/subsystem/job/proc/FillAIPosition()
var/ai_selected = 0
var/datum/job/job = GetJob("AI")
if(!job)
return 0
for(var/i = job.total_positions, i > 0, i--)
for(var/level = 1 to 3)
var/list/candidates = list()
candidates = FindOccupationCandidates(job, level)
if(candidates.len)
var/mob/dead/new_player/candidate = pick(candidates)
if(AssignRole(candidate, "AI"))
ai_selected++
break
if(ai_selected)
return 1
return 0
/** Proc DivideOccupations
* fills var "assigned_role" for all ready players.
* This proc must not have any side effect besides of modifying "assigned_role".
**/
/datum/controller/subsystem/job/proc/DivideOccupations()
//Setup new player list and get the jobs list
JobDebug("Running DO")
//Holder for Triumvirate is stored in the SSticker, this just processes it
if(SSticker.triai)
for(var/datum/job/ai/A in occupations)
A.spawn_positions = 3
for(var/obj/effect/landmark/start/ai/secondary/S in GLOB.start_landmarks_list)
S.latejoin_active = TRUE
//Get the players who are ready
for(var/mob/dead/new_player/player in GLOB.player_list)
if(player.ready == PLAYER_READY_TO_PLAY && player.mind && !player.mind.assigned_role)
unassigned += player
initial_players_to_assign = unassigned.len
JobDebug("DO, Len: [unassigned.len]")
if(unassigned.len == 0)
return 0
//Scale number of open security officer slots to population
setup_officer_positions()
//Jobs will have fewer access permissions if the number of players exceeds the threshold defined in game_options.txt
var/mat = CONFIG_GET(number/minimal_access_threshold)
if(mat)
if(mat > unassigned.len)
CONFIG_SET(flag/jobs_have_minimal_access, FALSE)
else
CONFIG_SET(flag/jobs_have_minimal_access, TRUE)
//Shuffle players and jobs
unassigned = shuffle(unassigned)
HandleFeedbackGathering()
//People who wants to be the overflow role, sure, go on.
JobDebug("DO, Running Overflow Check 1")
var/datum/job/overflow = GetJob(SSjob.overflow_role)
var/list/overflow_candidates = FindOccupationCandidates(overflow, 3)
JobDebug("AC1, Candidates: [overflow_candidates.len]")
for(var/mob/dead/new_player/player in overflow_candidates)
JobDebug("AC1 pass, Player: [player]")
AssignRole(player, SSjob.overflow_role)
overflow_candidates -= player
JobDebug("DO, AC1 end")
//Select one head
JobDebug("DO, Running Head Check")
FillHeadPosition()
JobDebug("DO, Head Check end")
//Check for an AI
JobDebug("DO, Running AI Check")
FillAIPosition()
JobDebug("DO, AI Check end")
//Other jobs are now checked
JobDebug("DO, Running Standard Check")
// New job giving system by Donkie
// This will cause lots of more loops, but since it's only done once it shouldn't really matter much at all.
// Hopefully this will add more randomness and fairness to job giving.
// Loop through all levels from high to low
var/list/shuffledoccupations = shuffle(occupations)
for(var/level = 1 to 3)
//Check the head jobs first each level
CheckHeadPositions(level)
// Loop through all unassigned players
for(var/mob/dead/new_player/player in unassigned)
if(PopcapReached())
RejectPlayer(player)
// Loop through all jobs
for(var/datum/job/job in shuffledoccupations) // SHUFFLE ME BABY
if(!job)
continue
if(is_banned_from(player.ckey, job.title))
JobDebug("DO isbanned failed, Player: [player], Job:[job.title]")
continue
if(QDELETED(player))
JobDebug("DO player deleted during job ban check")
break
if(!job.player_old_enough(player.client))
JobDebug("DO player not old enough, Player: [player], Job:[job.title]")
continue
if(job.required_playtime_remaining(player.client))
JobDebug("DO player not enough xp, Player: [player], Job:[job.title]")
continue
if(player.mind && job.title in player.mind.restricted_roles)
JobDebug("DO incompatible with antagonist role, Player: [player], Job:[job.title]")
continue
// If the player wants that job on this level, then try give it to him.
if(player.client.prefs.GetJobDepartment(job, level) & job.flag)
// If the job isn't filled
if((job.current_positions < job.spawn_positions) || job.spawn_positions == -1)
JobDebug("DO pass, Player: [player], Level:[level], Job:[job.title]")
AssignRole(player, job.title)
unassigned -= player
break
JobDebug("DO, Handling unassigned.")
// Hand out random jobs to the people who didn't get any in the last check
// Also makes sure that they got their preference correct
for(var/mob/dead/new_player/player in unassigned)
HandleUnassigned(player)
JobDebug("DO, Handling unrejectable unassigned")
//Mop up people who can't leave.
for(var/mob/dead/new_player/player in unassigned) //Players that wanted to back out but couldn't because they're antags (can you feel the edge case?)
if(!GiveRandomJob(player))
AssignRole(player, SSjob.overflow_role) //If everything is already filled, make them an assistant
return 1
//We couldn't find a job from prefs for this guy.
/datum/controller/subsystem/job/proc/HandleUnassigned(mob/dead/new_player/player)
if(PopcapReached())
RejectPlayer(player)
else if(player.client.prefs.joblessrole == BEOVERFLOW)
var/allowed_to_be_a_loser = !is_banned_from(player.ckey, SSjob.overflow_role)
if(QDELETED(player) || !allowed_to_be_a_loser)
RejectPlayer(player)
else
if(!AssignRole(player, SSjob.overflow_role))
RejectPlayer(player)
else if(player.client.prefs.joblessrole == BERANDOMJOB)
if(!GiveRandomJob(player))
RejectPlayer(player)
else if(player.client.prefs.joblessrole == RETURNTOLOBBY)
RejectPlayer(player)
else //Something gone wrong if we got here.
var/message = "DO: [player] fell through handling unassigned"
JobDebug(message)
log_game(message)
message_admins(message)
RejectPlayer(player)
//Gives the player the stuff he should have with his rank
/datum/controller/subsystem/job/proc/EquipRank(mob/M, rank, joined_late = FALSE)
var/mob/dead/new_player/N
var/mob/living/H
if(!joined_late)
N = M
H = N.new_character
else
H = M
var/datum/job/job = GetJob(rank)
H.job = rank
//If we joined at roundstart we should be positioned at our workstation
if(!joined_late)
var/obj/S = null
for(var/obj/effect/landmark/start/sloc in GLOB.start_landmarks_list)
if(sloc.name != rank)
S = sloc //so we can revert to spawning them on top of eachother if something goes wrong
continue
if(locate(/mob/living) in sloc.loc)
continue
S = sloc
sloc.used = TRUE
break
if(length(GLOB.jobspawn_overrides[rank]))
S = pick(GLOB.jobspawn_overrides[rank])
if(S)
SendToAtom(H, S, buckle = FALSE)
if(!S) //if there isn't a spawnpoint send them to latejoin, if there's no latejoin go yell at your mapper
log_world("Couldn't find a round start spawn point for [rank]")
SendToLateJoin(H)
if(H.mind)
H.mind.assigned_role = rank
if(job)
var/new_mob = job.equip(H, null, null, joined_late , null, M.client)
if(ismob(new_mob))
H = new_mob
if(!joined_late)
N.new_character = H
else
M = H
SSpersistence.antag_rep_change[M.client.ckey] += job.GetAntagRep()
to_chat(M, "<b>You are the [rank].</b>")
if(job)
to_chat(M, "<b>As the [rank] you answer directly to [job.supervisors]. Special circumstances may change this.</b>")
job.radio_help_message(M)
if(job.req_admin_notify)
to_chat(M, "<b>You are playing a job that is important for Game Progression. If you have to disconnect, please notify the admins via adminhelp.</b>")
if(CONFIG_GET(number/minimal_access_threshold))
to_chat(M, "<FONT color='blue'><B>As this station was initially staffed with a [CONFIG_GET(flag/jobs_have_minimal_access) ? "full crew, only your job's necessities" : "skeleton crew, additional access may"] have been added to your ID card.</B></font>")
if(ishuman(H))
var/mob/living/carbon/human/wageslave = H
to_chat(M, "<b>Your account ID is [wageslave.account_id].</b>")
H.add_memory("Your account ID is [wageslave.account_id].")
if(job && H)
job.after_spawn(H, M, joined_late) // note: this happens before the mob has a key! M will always have a client, H might not.
log_game("[H.real_name]/[M.client.ckey] joined the round as [H.job].") //yogs - Job logging
job.give_donor_stuff(H, M) // yogs - Donor Features
return H
/datum/controller/subsystem/job/proc/setup_officer_positions()
var/datum/job/J = SSjob.GetJob("Security Officer")
if(!J)
throw EXCEPTION("setup_officer_positions(): Security officer job is missing")
var/ssc = CONFIG_GET(number/security_scaling_coeff)
if(ssc > 0)
if(J.spawn_positions > 0)
var/officer_positions = min(12, max(J.spawn_positions, round(unassigned.len / ssc))) //Scale between configured minimum and 12 officers
JobDebug("Setting open security officer positions to [officer_positions]")
J.total_positions = officer_positions
J.spawn_positions = officer_positions
//Spawn some extra eqipment lockers if we have more than 5 officers
var/equip_needed = J.total_positions
if(equip_needed < 0) // -1: infinite available slots
equip_needed = 12
for(var/i=equip_needed-5, i>0, i--)
if(GLOB.secequipment.len)
var/spawnloc = GLOB.secequipment[1]
new /obj/structure/closet/secure_closet/security/sec(spawnloc)
GLOB.secequipment -= spawnloc
else //We ran out of spare locker spawns!
break
/datum/controller/subsystem/job/proc/LoadJobs()
var/jobstext = file2text("[global.config.directory]/jobs.txt")
for(var/datum/job/J in occupations)
var/regex/jobs = new("[J.title]=(-1|\\d+),(-1|\\d+)")
jobs.Find(jobstext)
J.total_positions = text2num(jobs.group[1])
J.spawn_positions = text2num(jobs.group[2])
/datum/controller/subsystem/job/proc/HandleFeedbackGathering()
for(var/datum/job/job in occupations)
var/high = 0 //high
var/medium = 0 //medium
var/low = 0 //low
var/never = 0 //never
var/banned = 0 //banned
var/young = 0 //account too young
for(var/mob/dead/new_player/player in GLOB.player_list)
if(!(player.ready == PLAYER_READY_TO_PLAY && player.mind && !player.mind.assigned_role))
continue //This player is not ready
if(is_banned_from(player.ckey, job.title) || QDELETED(player))
banned++
continue
if(!job.player_old_enough(player.client))
young++
continue
if(job.required_playtime_remaining(player.client))
young++
continue
if(player.client.prefs.GetJobDepartment(job, 1) & job.flag)
high++
else if(player.client.prefs.GetJobDepartment(job, 2) & job.flag)
medium++
else if(player.client.prefs.GetJobDepartment(job, 3) & job.flag)
low++
else never++ //not selected
SSblackbox.record_feedback("nested tally", "job_preferences", high, list("[job.title]", "high"))
SSblackbox.record_feedback("nested tally", "job_preferences", medium, list("[job.title]", "medium"))
SSblackbox.record_feedback("nested tally", "job_preferences", low, list("[job.title]", "low"))
SSblackbox.record_feedback("nested tally", "job_preferences", never, list("[job.title]", "never"))
SSblackbox.record_feedback("nested tally", "job_preferences", banned, list("[job.title]", "banned"))
SSblackbox.record_feedback("nested tally", "job_preferences", young, list("[job.title]", "young"))
/datum/controller/subsystem/job/proc/PopcapReached()
var/hpc = CONFIG_GET(number/hard_popcap)
var/epc = CONFIG_GET(number/extreme_popcap)
if(hpc || epc)
var/relevent_cap = max(hpc, epc)
if((initial_players_to_assign - unassigned.len) >= relevent_cap)
return 1
return 0
/datum/controller/subsystem/job/proc/RejectPlayer(mob/dead/new_player/player)
if(player.mind && player.mind.special_role)
return
if(PopcapReached())
JobDebug("Popcap overflow Check observer located, Player: [player]")
JobDebug("Player rejected :[player]")
to_chat(player, "<b>You have failed to qualify for any job you desired.</b>")
unassigned -= player
player.ready = PLAYER_NOT_READY
/datum/controller/subsystem/job/Recover()
set waitfor = FALSE
var/oldjobs = SSjob.occupations
sleep(20)
for (var/datum/job/J in oldjobs)
INVOKE_ASYNC(src, .proc/RecoverJob, J)
/datum/controller/subsystem/job/proc/RecoverJob(datum/job/J)
var/datum/job/newjob = GetJob(J.title)
if (!istype(newjob))
return
newjob.total_positions = J.total_positions
newjob.spawn_positions = J.spawn_positions
newjob.current_positions = J.current_positions
/datum/controller/subsystem/job/proc/SendToAtom(mob/M, atom/A, buckle)
if(buckle && isliving(M) && istype(A, /obj/structure/chair))
var/obj/structure/chair/C = A
if(C.buckle_mob(M, FALSE, FALSE))
return
M.forceMove(get_turf(A))
/datum/controller/subsystem/job/proc/SendToLateJoin(mob/M, buckle = TRUE)
if(M.mind && M.mind.assigned_role && length(GLOB.jobspawn_overrides[M.mind.assigned_role])) //We're doing something special today.
SendToAtom(M,pick(GLOB.jobspawn_overrides[M.mind.assigned_role]),FALSE)
return
if(latejoin_trackers.len)
SendToAtom(M, pick(latejoin_trackers), buckle)
else
//bad mojo
var/area/shuttle/arrival/A = GLOB.areas_by_type[/area/shuttle/arrival]
if(A)
//first check if we can find a chair
var/obj/structure/chair/C = locate() in A
if(C)
SendToAtom(M, C, buckle)
return
else //last hurrah
var/list/avail = list()
for(var/turf/T in A)
if(!is_blocked_turf(T, TRUE))
avail += T
if(avail.len)
SendToAtom(M, pick(avail), FALSE)
return
//pick an open spot on arrivals and dump em
var/list/arrivals_turfs = shuffle(get_area_turfs(/area/shuttle/arrival))
if(arrivals_turfs.len)
for(var/turf/T in arrivals_turfs)
if(!is_blocked_turf(T, TRUE))
SendToAtom(M, T, FALSE)
return
//last chance, pick ANY spot on arrivals and dump em
SendToAtom(M, arrivals_turfs[1], FALSE)
else
var/msg = "Unable to send mob [M] to late join!"
message_admins(msg)
CRASH(msg)
///////////////////////////////////
//Keeps track of all living heads//
///////////////////////////////////
/datum/controller/subsystem/job/proc/get_living_heads()
. = list()
for(var/mob/living/carbon/human/player in GLOB.alive_mob_list)
if(player.stat != DEAD && player.mind && (player.mind.assigned_role in GLOB.command_positions))
. |= player.mind
////////////////////////////
//Keeps track of all heads//
////////////////////////////
/datum/controller/subsystem/job/proc/get_all_heads()
. = list()
for(var/i in GLOB.mob_list)
var/mob/player = i
if(player.mind && (player.mind.assigned_role in GLOB.command_positions))
. |= player.mind
//////////////////////////////////////////////
//Keeps track of all living security members//
//////////////////////////////////////////////
/datum/controller/subsystem/job/proc/get_living_sec()
. = list()
for(var/mob/living/carbon/human/player in GLOB.carbon_list)
if(player.stat != DEAD && player.mind && (player.mind.assigned_role in GLOB.security_positions))
. |= player.mind
////////////////////////////////////////
//Keeps track of all security members//
////////////////////////////////////////
/datum/controller/subsystem/job/proc/get_all_sec()
. = list()
for(var/mob/living/carbon/human/player in GLOB.carbon_list)
if(player.mind && (player.mind.assigned_role in GLOB.security_positions))
. |= player.mind
/datum/controller/subsystem/job/proc/JobDebug(message)
log_job_debug(message)