SUBSYSTEM_DEF(job)
name = "Jobs"
init_order = INIT_ORDER_JOBS
flags = SS_NO_FIRE
/// List of all jobs.
var/list/datum/job/all_occupations = list()
/// List of jobs that can be joined through the starting menu.
var/list/datum/job/joinable_occupations = list()
/// Dictionary of all jobs, keys are titles.
var/list/name_occupations = list()
/// Dictionary of all jobs, keys are types.
var/list/datum/job/type_occupations = list()
/// Dictionary of jobs indexed by the experience type they grant.
var/list/experience_jobs_map = list()
/// List of all departments with joinable jobs.
var/list/datum/job_department/joinable_departments = list()
/// List of all joinable departments indexed by their typepath, sorted by their own display order.
var/list/datum/job_department/joinable_departments_by_type = list()
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()
var/overflow_role = /datum/job/assistant
var/list/level_order = list(JP_HIGH,JP_MEDIUM,JP_LOW)
/// Lazylist of mob:occupation_string pairs.
var/list/dynamic_forced_occupations
/// A list of all jobs associated with the station. These jobs also have various icons associated with them including sechud and card trims.
var/list/station_jobs
/// A list of all Head of Staff jobs.
var/list/head_of_staff_jobs
/// A list of additional jobs that have various icons associated with them including sechud and card trims.
var/list/additional_jobs_with_icons
/// A list of jobs associed with Centcom and should use the standard NT Centcom icons.
var/list/centcom_jobs
/**
* Keys should be assigned job roles. Values should be >= 1.
* Represents the chain of command on the station. Lower numbers mean higher priority.
* Used to give the Cap's Spare safe code to a an appropriate player.
* Assumed Captain is always the highest in the chain of command.
* See [/datum/controller/subsystem/ticker/proc/equip_characters]
*/
var/list/chain_of_command = list(
"Captain" = 1,
"Head of Personnel" = 2,
"Research Director" = 3,
"Chief Engineer" = 4,
"Chief Medical Officer" = 5,
"Head of Security" = 6,
"Quartermaster" = 7)
/// If TRUE, some player has been assigned Captaincy or Acting Captaincy at some point during the shift and has been given the spare ID safe code.
var/assigned_captain = FALSE
/// Whether the emergency safe code has been requested via a comms console on shifts with no Captain or Acting Captain.
var/safe_code_requested = FALSE
/// Timer ID for the emergency safe code request.
var/safe_code_timer_id
/// The loc to which the emergency safe code has been requested for delivery.
var/turf/safe_code_request_loc
/datum/controller/subsystem/job/Initialize(timeofday)
setup_job_lists()
if(!length(all_occupations))
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 = ispath(new_overflow_role) ? GetJobType(new_overflow_role) : GetJob(new_overflow_role)
if(!new_overflow)
JobDebug("Failed to set new overflow role: [new_overflow_role]")
CRASH("set_overflow_role failed | new_overflow_role: [isnull(new_overflow_role) ? "null" : new_overflow_role]")
var/cap = CONFIG_GET(number/overflow_cap)
new_overflow.allow_bureaucratic_error = FALSE
new_overflow.spawn_positions = cap
new_overflow.total_positions = cap
if(new_overflow.type == overflow_role)
return
var/datum/job/old_overflow = GetJobType(overflow_role)
old_overflow.allow_bureaucratic_error = initial(old_overflow.allow_bureaucratic_error)
old_overflow.spawn_positions = initial(old_overflow.spawn_positions)
old_overflow.total_positions = initial(old_overflow.total_positions)
overflow_role = new_overflow.type
JobDebug("Overflow role set to : [new_overflow.type]")
/datum/controller/subsystem/job/proc/SetupOccupations()
name_occupations = list()
type_occupations = list()
var/list/all_jobs = subtypesof(/datum/job)
if(!length(all_jobs))
all_occupations = list()
joinable_occupations = list()
joinable_departments = list()
joinable_departments_by_type = list()
experience_jobs_map = list()
to_chat(world, span_boldannounce("Error setting up jobs, no job datums found"))
return FALSE
var/list/new_all_occupations = list()
var/list/new_joinable_occupations = list()
var/list/new_joinable_departments = list()
var/list/new_joinable_departments_by_type = list()
var/list/new_experience_jobs_map = list()
for(var/job_type in all_jobs)
var/datum/job/job = new job_type()
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
new_all_occupations += job
name_occupations[job.title] = job
type_occupations[job_type] = job
if(job.job_flags & JOB_NEW_PLAYER_JOINABLE)
new_joinable_occupations += job
if(!LAZYLEN(job.departments_list))
var/datum/job_department/department = new_joinable_departments_by_type[/datum/job_department/undefined]
if(!department)
department = new /datum/job_department/undefined()
new_joinable_departments_by_type[/datum/job_department/undefined] = department
department.add_job(job)
continue
for(var/department_type in job.departments_list)
var/datum/job_department/department = new_joinable_departments_by_type[department_type]
if(!department)
department = new department_type()
new_joinable_departments_by_type[department_type] = department
department.add_job(job)
sortTim(new_all_occupations, /proc/cmp_job_display_asc)
for(var/datum/job/job as anything in new_all_occupations)
if(!job.exp_granted_type)
continue
new_experience_jobs_map[job.exp_granted_type] += list(job)
sortTim(new_joinable_departments_by_type, /proc/cmp_department_display_asc, associative = TRUE)
for(var/department_type in new_joinable_departments_by_type)
var/datum/job_department/department = new_joinable_departments_by_type[department_type]
sortTim(department.department_jobs, /proc/cmp_job_display_asc)
new_joinable_departments += department
if(department.department_experience_type)
new_experience_jobs_map[department.department_experience_type] = department.department_jobs.Copy()
all_occupations = new_all_occupations
joinable_occupations = sortTim(new_joinable_occupations, /proc/cmp_job_display_asc)
joinable_departments = new_joinable_departments
joinable_departments_by_type = new_joinable_departments_by_type
experience_jobs_map = new_experience_jobs_map
return TRUE
/datum/controller/subsystem/job/proc/GetJob(rank)
if(!length(all_occupations))
SetupOccupations()
return name_occupations[rank]
/datum/controller/subsystem/job/proc/GetJobType(jobtype)
if(!length(all_occupations))
SetupOccupations()
return type_occupations[jobtype]
/datum/controller/subsystem/job/proc/get_department_type(department_type)
if(!length(all_occupations))
SetupOccupations()
return joinable_departments_by_type[department_type]
/datum/controller/subsystem/job/proc/AssignRole(mob/dead/new_player/player, datum/job/job, latejoin = FALSE)
JobDebug("Running AR, Player: [player], Rank: [isnull(job) ? "null" : job.type], LJ: [latejoin]")
if(!player?.mind || !job)
JobDebug("AR has failed, Player: [player], Rank: [isnull(job) ? "null" : job.type]")
return FALSE
if(is_banned_from(player.ckey, job.title) || QDELETED(player))
return FALSE
if(!job.player_old_enough(player.client))
return FALSE
if(job.required_playtime_remaining(player.client))
return FALSE
//SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
if(job.has_banned_quirk(player.client.prefs))
return FALSE
if(job.has_banned_species(player.client.prefs))
return FALSE
if(!job.has_required_languages(player.client.prefs))
return FALSE
if(job.veteran_only && !is_veteran_player(player.client))
return FALSE
//SKYRAT EDIT END
var/position_limit = job.total_positions
if(!latejoin)
position_limit = job.spawn_positions
JobDebug("Player: [player] is now Rank: [job.title], JCP:[job.current_positions], JPL:[position_limit]")
player.mind.set_assigned_role(job)
unassigned -= player
job.current_positions++
return TRUE
/datum/controller/subsystem/job/proc/FreeRole(rank)
if(!rank)
return
JobDebug("Freeing role: [rank]")
var/datum/job/job = GetJob(rank)
if(!job)
return FALSE
job.current_positions = max(0, job.current_positions - 1)
/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
//SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
if(job.has_banned_quirk(player.client.prefs))
JobDebug("FOC job not compatible with quirks, Player: [player]")
continue
if(job.has_banned_species(player.client.prefs))
JobDebug("FOC job not compatible with species, Player: [player]")
continue
if(!job.has_required_languages(player.client.prefs))
JobDebug("FOC job not compatible with languages, Player: [player]")
continue
if(job.veteran_only && !is_veteran_player(player.client))
JobDebug("FOC player is not veteran, Player: [player]")
//SKYRAT EDIT END
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
if(player.client.prefs.job_preferences[job.title] == level)
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 as anything in shuffle(joinable_occupations))
if(istype(job, GetJobType(overflow_role))) // We don't want to give him assistant, that's boring!
continue
if(job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) //If you want a command position, select it!
continue
//SKYRAT EDIT ADDITION
if(job.departments_bitflags & DEPARTMENT_BITFLAG_CENTRAL_COMMAND) //If you want a CC position, select it!
continue
//SKYRAT EDIT END
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
//SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
if(job.has_banned_quirk(player.client.prefs))
JobDebug("GRJ player has incompatible quirk, Player: [player]")
continue
if(job.has_banned_species(player.client.prefs))
JobDebug("GRJ player has incompatible species, Player: [player]")
continue
if(!job.has_required_languages(player.client.prefs))
JobDebug("GRJ player has incompatible languages, Player: [player]")
continue
if(job.veteran_only && !is_veteran_player(player.client))
JobDebug("GRJ player is not veteran, Player: [player]")
//SKYRAT EDIT END
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))
return TRUE
/datum/controller/subsystem/job/proc/ResetOccupations()
JobDebug("Occupations reset.")
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
if(!player?.mind)
continue
player.mind.set_assigned_role(GetJobType(/datum/job/unassigned))
player.mind.special_role = null
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()
var/datum/job_department/command_department = get_department_type(/datum/job_department/command)
if(!command_department)
return FALSE
for(var/level in level_order)
for(var/datum/job/job as anything in command_department.department_jobs)
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, job))
return TRUE
return FALSE
//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)
var/datum/job_department/command_department = get_department_type(/datum/job_department/command)
if(!command_department)
return
for(var/datum/job/job as anything in command_department.department_jobs)
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, job)
/// Attempts to fill out all available AI positions.
/datum/controller/subsystem/job/proc/fill_ai_positions()
var/datum/job/ai_job = GetJob("AI")
if(!ai_job)
return
// In byond for(in to) loops, the iteration is inclusive so we need to stop at ai_job.total_positions - 1
for(var/i in ai_job.current_positions to ai_job.total_positions - 1)
for(var/level in level_order)
var/list/candidates = list()
candidates = FindOccupationCandidates(ai_job, level)
if(candidates.len)
var/mob/dead/new_player/candidate = pick(candidates)
if(AssignRole(candidate, GetJobType(/datum/job/ai)))
break
/** 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")
SEND_SIGNAL(src, COMSIG_OCCUPATIONS_DIVIDED)
//Get the players who are ready
for(var/i in GLOB.new_player_list)
var/mob/dead/new_player/player = i
if(player.ready == PLAYER_READY_TO_PLAY && player.check_preferences() && player.mind && is_unassigned_job(player.mind.assigned_role))
unassigned += player
initial_players_to_assign = unassigned.len
JobDebug("DO, Len: [unassigned.len]")
//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()
// Dynamic has picked a ruleset that requires enforcing some jobs before others.
JobDebug("DO, Assigning Priority Positions: [length(dynamic_forced_occupations)]")
assign_priority_positions()
//People who wants to be the overflow role, sure, go on.
JobDebug("DO, Running Overflow Check 1")
var/datum/job/overflow_datum = GetJobType(overflow_role)
var/list/overflow_candidates = FindOccupationCandidates(overflow_datum, JP_LOW)
JobDebug("AC1, Candidates: [overflow_candidates.len]")
for(var/mob/dead/new_player/player in overflow_candidates)
JobDebug("AC1 pass, Player: [player]")
AssignRole(player, GetJobType(overflow_role))
overflow_candidates -= player
JobDebug("DO, AC1 end")
//Select one head
JobDebug("DO, Running Head Check")
FillHeadPosition()
JobDebug("DO, Head Check end")
// Fill out any remaining AI positions.
JobDebug("DO, Running AI Check")
fill_ai_positions()
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(joinable_occupations)
for(var/level in level_order)
//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
//SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
if(job.has_banned_quirk(player.client.prefs))
JobDebug("DO player has incompatible quirk, Player: [player], Job:[job.title]")
continue
if(job.has_banned_species(player.client.prefs))
JobDebug("DO player has incompatible species, Player: [player], Job:[job.title]")
continue
if(!job.has_required_languages(player.client.prefs))
JobDebug("DO player has incompatible species, Player: [player], Job:[job.title]")
continue
if(job.veteran_only && !is_veteran_player(player.client))
JobDebug("DO player is not veteran, Player: [player], Job:[job.title]")
continue
//SKYRAT EDIT END
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.job_preferences[job.title] == level)
// 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)
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))
if(!AssignRole(player, GetJobType(overflow_role))) //If everything is already filled, make them an assistant
return FALSE //Living on the edge, the forced antagonist couldn't be assigned to overflow role (bans, client age) - just reroll
return TRUE
//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/datum/job/overflow_role_datum = GetJobType(overflow_role)
var/allowed_to_be_a_loser = !is_banned_from(player.ckey, overflow_role_datum.title)
if(QDELETED(player) || !allowed_to_be_a_loser)
RejectPlayer(player)
else
if(!AssignRole(player, overflow_role_datum))
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/living/equipping, datum/job/job, client/player_client)
equipping.job = job.title
SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job)
equipping.mind?.set_assigned_role(job)
//SKYRAT EDIT ADD - ALTERNATE JOB TITLES
var/display_rank = job.title
if(player_client && player_client.prefs && player_client.prefs.alt_titles_preferences[job.title])
display_rank = player_client.prefs.alt_titles_preferences[job.title]
if(player_client)
to_chat(player_client, "You are the [display_rank].")
/* SKYRAT EDIT ORIGINAL
if(player_client)
to_chat(player_client, "You are the [job.title].")
*/ // SKYRAT EDIT END
equipping.on_job_equipping(job)
job.announce_job(equipping)
if(player_client?.holder)
if(CONFIG_GET(flag/auto_deadmin_players) || (player_client.prefs?.toggles & DEADMIN_ALWAYS))
player_client.holder.auto_deadmin()
else
handle_auto_deadmin_roles(player_client, job.title)
var/list/packed_items //SKYRAT EDIT ADD - CUSTOMISATION
if(job)
if (player_client && job.no_dresscode && job.loadout)
packed_items = player_client.prefs.equip_preference_loadout(equipping,FALSE,job,blacklist=job.blacklist_dresscode_slots,initial=TRUE)
//SKYRAT EDIT ADDITION END
if(player_client)
to_chat(player_client, "As the [display_rank] you answer directly to [job.supervisors]. Special circumstances may change this. Your role is that of a [job.title]. Regardless of what your job title may be, please work to fulfil that role.") //SKYRAT EDIT -- ALT TITLES
// to_chat(player_client, "As the [job.title] you answer directly to [job.supervisors]. Special circumstances may change this." // SKYRAT EDIT ORIGINAL
job.radio_help_message(equipping)
if(player_client)
if(job.req_admin_notify)
to_chat(player_client, "You are playing a job that is important for Game Progression. If you have to disconnect, please notify the admins via adminhelp.")
if(CONFIG_GET(number/minimal_access_threshold))
to_chat(player_client, span_notice("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."))
var/related_policy = get_policy(job.title)
if(related_policy)
to_chat(player_client, related_policy)
if(ishuman(equipping))
var/mob/living/carbon/human/wageslave = equipping
wageslave.add_memory("Your account ID is [wageslave.account_id].")
job.after_spawn(equipping, player_client)
//SKYRAT CHANGE ADDITION BEGIN - CUSTOMIZATION
if(!job.no_dresscode && job.loadout)
if(player_client)
packed_items = player_client.prefs.equip_preference_loadout(equipping, FALSE, job,initial=TRUE)
if(packed_items)
player_client.prefs.add_packed_items(equipping, packed_items)
//SKYRAT CHANGE ADDITION END
/datum/controller/subsystem/job/proc/handle_auto_deadmin_roles(client/C, rank)
if(!C?.holder)
return TRUE
var/datum/job/job = GetJob(rank)
var/timegate_expired = FALSE
// allow only forcing deadminning in the first X seconds of the round if auto_deadmin_timegate is set in config
var/timegate = CONFIG_GET(number/auto_deadmin_timegate)
if(timegate && (world.time - SSticker.round_start_time > timegate))
timegate_expired = TRUE
if(!job)
return
if((job.auto_deadmin_role_flags & DEADMIN_POSITION_HEAD) && ((CONFIG_GET(flag/auto_deadmin_heads) && !timegate_expired) || (C.prefs?.toggles & DEADMIN_POSITION_HEAD)))
return C.holder.auto_deadmin()
else if((job.auto_deadmin_role_flags & DEADMIN_POSITION_SECURITY) && ((CONFIG_GET(flag/auto_deadmin_security) && !timegate_expired) || (C.prefs?.toggles & DEADMIN_POSITION_SECURITY)))
return C.holder.auto_deadmin()
else if((job.auto_deadmin_role_flags & DEADMIN_POSITION_SILICON) && ((CONFIG_GET(flag/auto_deadmin_silicons) && !timegate_expired) || (C.prefs?.toggles & DEADMIN_POSITION_SILICON))) //in the event there's ever psuedo-silicon roles added, ie synths.
return C.holder.auto_deadmin()
/datum/controller/subsystem/job/proc/setup_officer_positions()
var/datum/job/J = SSjob.GetJob("Security Officer")
if(!J)
CRASH("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/job as anything in joinable_occupations)
var/regex/jobs = new("[job.title]=(-1|\\d+),(-1|\\d+)")
jobs.Find(jobstext)
job.total_positions = text2num(jobs.group[1])
job.spawn_positions = text2num(jobs.group[2])
/datum/controller/subsystem/job/proc/HandleFeedbackGathering()
for(var/datum/job/job as anything in joinable_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/i in GLOB.new_player_list)
var/mob/dead/new_player/player = i
if(!(player.ready == PLAYER_READY_TO_PLAY && player.mind && is_unassigned_job(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
switch(player.client.prefs.job_preferences[job.title])
if(JP_HIGH)
high++
if(JP_MEDIUM)
medium++
if(JP_LOW)
low++
else
never++
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, "You have failed to qualify for any job you desired.")
unassigned -= player
player.ready = PLAYER_NOT_READY
player.client << output(player.ready, "lobbybrowser:imgsrc") //SKYRAT EDIT ADDITION
/datum/controller/subsystem/job/Recover()
set waitfor = FALSE
var/oldjobs = SSjob.all_occupations
sleep(20)
for (var/datum/job/job as anything in oldjobs)
INVOKE_ASYNC(src, .proc/RecoverJob, job)
/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
/atom/proc/JoinPlayerHere(mob/M, buckle)
// By default, just place the mob on the same turf as the marker or whatever.
M.forceMove(get_turf(src))
/obj/structure/chair/JoinPlayerHere(mob/M, buckle)
// Placing a mob in a chair will attempt to buckle it, or else fall back to default.
if (buckle && isliving(M) && buckle_mob(M, FALSE, FALSE))
return
..()
/datum/controller/subsystem/job/proc/SendToLateJoin(mob/M, buckle = TRUE)
var/atom/destination
if(M.mind && !is_unassigned_job(M.mind.assigned_role) && length(GLOB.jobspawn_overrides[M.mind.assigned_role.title])) //We're doing something special today.
destination = pick(GLOB.jobspawn_overrides[M.mind.assigned_role.title])
destination.JoinPlayerHere(M, FALSE)
return TRUE
//SKYRAT EDIT ADDITION
if(M.job)
if(M.job == "Prisoner")
destination = locate(/obj/effect/landmark/start/prisoner) in GLOB.landmarks_list
destination.JoinPlayerHere(M, buckle)
return TRUE
//SKYRAT EDIT END
if(latejoin_trackers.len)
destination = pick(latejoin_trackers)
destination.JoinPlayerHere(M, buckle)
return TRUE
destination = get_last_resort_spawn_points()
destination.JoinPlayerHere(M, buckle)
/datum/controller/subsystem/job/proc/get_last_resort_spawn_points()
//bad mojo
var/area/shuttle/arrival/arrivals_area = GLOB.areas_by_type[/area/shuttle/arrival]
if(arrivals_area)
//first check if we can find a chair
var/obj/structure/chair/shuttle_chair = locate() in arrivals_area
if(shuttle_chair)
return shuttle_chair
//last hurrah
var/list/turf/available_turfs = list()
for(var/turf/arrivals_turf in arrivals_area)
if(!arrivals_turf.is_blocked_turf(TRUE))
available_turfs += arrivals_turf
if(length(available_turfs))
return pick(available_turfs)
//pick an open spot on arrivals and dump em
var/list/arrivals_turfs = shuffle(get_area_turfs(/area/shuttle/arrival))
if(length(arrivals_turfs))
for(var/turf/arrivals_turf in arrivals_turfs)
if(!arrivals_turf.is_blocked_turf(TRUE))
return arrivals_turf
//last chance, pick ANY spot on arrivals and dump em
return pick(arrivals_turfs)
stack_trace("Unable to find last resort spawn point.")
return GET_ERROR_ROOM
///Lands specified mob at a random spot in the hallways
/datum/controller/subsystem/job/proc/DropLandAtRandomHallwayPoint(mob/living/living_mob)
var/turf/spawn_turf = get_safe_random_station_turf(typesof(/area/hallway))
if(!spawn_turf)
SendToLateJoin(living_mob)
else
var/obj/structure/closet/supplypod/centcompod/toLaunch = new()
living_mob.forceMove(toLaunch)
new /obj/effect/pod_landingzone(spawn_turf, toLaunch)
///////////////////////////////////
//Keeps track of all living heads//
///////////////////////////////////
/datum/controller/subsystem/job/proc/get_living_heads()
. = list()
for(var/mob/living/carbon/human/player as anything in GLOB.human_list)
if(player.stat != DEAD && (player.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND))
. += player.mind
////////////////////////////
//Keeps track of all heads//
////////////////////////////
/datum/controller/subsystem/job/proc/get_all_heads()
. = list()
for(var/mob/living/carbon/human/player as anything in GLOB.human_list)
if(player.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
. += 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 as anything in GLOB.human_list)
if(player.stat != DEAD && (player.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY))
. += player.mind
////////////////////////////////////////
//Keeps track of all security members//
////////////////////////////////////////
/datum/controller/subsystem/job/proc/get_all_sec()
. = list()
for(var/mob/living/carbon/human/player as anything in GLOB.human_list)
if(player.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
. += player.mind
/datum/controller/subsystem/job/proc/JobDebug(message)
log_job_debug(message)
/// Builds various lists of jobs based on station, centcom and additional jobs with icons associated with them.
/datum/controller/subsystem/job/proc/setup_job_lists()
station_jobs = list("Assistant", "Captain", "Head of Personnel", "Bartender", "Cook", "Botanist", "Quartermaster", "Cargo Technician", \
"Shaft Miner", "Clown", "Mime", "Janitor", "Curator", "Lawyer", "Chaplain", "Chief Engineer", "Station Engineer", \
"Atmospheric Technician", "Chief Medical Officer", "Medical Doctor", "Paramedic", "Chemist", "Geneticist", "Virologist", "Psychologist", \
"Research Director", "Scientist", "Roboticist", "Head of Security", "Warden", "Detective", "Security Officer", "Corrections Officer", "Prisoner")
head_of_staff_jobs = list("Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Head of Security", "Captain")
additional_jobs_with_icons = list("Emergency Response Team Commander", "Security Response Officer", "Engineering Response Officer", "Medical Response Officer", \
"Entertainment Response Officer", "Religious Response Officer", "Janitorial Response Officer", "Death Commando", "Security Officer (Engineering)", \
"Security Officer (Cargo)", "Security Officer (Medical)", "Security Officer (Science)", "Blueshield", "Nanotrasen Representative", "Shuttle Pilot",\
"Security Medic", "Security Sergeant", "Civil Disputes Officer", "Vanguard Operative")
centcom_jobs = list("Central Command","VIP Guest","Custodian","Thunderdome Overseer","CentCom Official","Medical Officer","Research Officer", \
"Special Ops Officer","Admiral","CentCom Commander","CentCom Bartender","Private Security Force")
/obj/item/paper/fluff/spare_id_safe_code
name = "Nanotrasen-Approved Spare ID Safe Code"
desc = "Proof that you have been approved for Captaincy, with all its glory and all its horror."
/obj/item/paper/fluff/spare_id_safe_code/Initialize()
. = ..()
var/safe_code = SSid_access.spare_id_safe_code
info = "Captain's Spare ID safe code combination: [safe_code ? safe_code : "\[REDACTED\]"]
The spare ID can be found in its dedicated safe on the bridge.
If your job would not ordinarily have Head of Staff access, your ID card has been specially modified to possess it."
update_appearance()
/obj/item/paper/fluff/emergency_spare_id_safe_code
name = "Emergency Spare ID Safe Code Requisition"
desc = "Proof that nobody has been approved for Captaincy. A skeleton key for a skeleton shift."
/obj/item/paper/fluff/emergency_spare_id_safe_code/Initialize()
. = ..()
var/safe_code = SSid_access.spare_id_safe_code
info = "Captain's Spare ID safe code combination: [safe_code ? safe_code : "\[REDACTED\]"]
The spare ID can be found in its dedicated safe on the bridge."
update_appearance()
/datum/controller/subsystem/job/proc/promote_to_captain(mob/living/carbon/human/new_captain, acting_captain = FALSE)
var/id_safe_code = SSid_access.spare_id_safe_code
if(!id_safe_code)
CRASH("Cannot promote [new_captain.real_name] to Captain, there is no id_safe_code.")
var/paper = new /obj/item/paper/fluff/spare_id_safe_code()
var/list/slots = list(
LOCATION_LPOCKET = ITEM_SLOT_LPOCKET,
LOCATION_RPOCKET = ITEM_SLOT_RPOCKET,
LOCATION_BACKPACK = ITEM_SLOT_BACKPACK,
LOCATION_HANDS = ITEM_SLOT_HANDS
)
var/where = new_captain.equip_in_one_of_slots(paper, slots, FALSE) || "at your feet"
if(acting_captain)
to_chat(new_captain, span_notice("Due to your position in the chain of command, you have been promoted to Acting Captain. You can find in important note about this [where]."))
else
to_chat(new_captain, span_notice("You can find the code to obtain your spare ID from the secure safe on the Bridge [where]."))
// Force-give their ID card bridge access.
var/obj/item/id_slot = new_captain.get_item_by_slot(ITEM_SLOT_ID)
if(id_slot)
var/obj/item/card/id/id_card = id_slot.GetID()
if(!(ACCESS_HEADS in id_card.access))
id_card.add_wildcards(list(ACCESS_HEADS), mode=FORCE_ADD_ALL)
assigned_captain = TRUE
/// Send a drop pod containing a piece of paper with the spare ID safe code to loc
/datum/controller/subsystem/job/proc/send_spare_id_safe_code(loc)
new /obj/effect/pod_landingzone(loc, /obj/structure/closet/supplypod/centcompod, new /obj/item/paper/fluff/emergency_spare_id_safe_code())
safe_code_timer_id = null
safe_code_request_loc = null
/// Blindly assigns the required roles to every player in the dynamic_forced_occupations list.
/datum/controller/subsystem/job/proc/assign_priority_positions()
for(var/mob/new_player in dynamic_forced_occupations)
AssignRole(new_player, GetJob(dynamic_forced_occupations[new_player]))