SUBSYSTEM_DEF(job) name = "Jobs" init_order = INIT_ORDER_JOBS flags = SS_NO_FIRE var/list/occupations = list() //List of all jobs var/list/datum/job/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" var/list/level_order = list(JP_HIGH,JP_MEDIUM,JP_LOW) /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, "Error setting up jobs, no job datums found") 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 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 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/i in GLOB.new_player_list) var/mob/dead/new_player/player = i 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 in level_order) 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 in level_order) 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(list/required_jobs) //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/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 && !player.mind.assigned_role) unassigned += player initial_players_to_assign = unassigned.len JobDebug("DO, Len: [unassigned.len]") if(unassigned.len == 0) return validate_required_jobs(required_jobs) //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, 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, 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 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 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.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)) if(!AssignRole(player, SSjob.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 validate_required_jobs(required_jobs) /datum/controller/subsystem/job/proc/validate_required_jobs(list/required_jobs) if(!required_jobs.len) return TRUE for(var/required_group in required_jobs) var/group_ok = TRUE for(var/rank in required_group) var/datum/job/J = GetJob(rank) if(!J) SSticker.mode.setup_error = "Invalid job [rank] in gamemode required jobs." return FALSE if(J.current_positions < required_group[rank]) group_ok = FALSE break if(group_ok) return TRUE SSticker.mode.setup_error = "Required jobs not present." return FALSE //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/newplayer var/mob/living/living_mob if(!joined_late) newplayer = M living_mob = newplayer.new_character else living_mob = M var/datum/job/job = GetJob(rank) living_mob.job = rank SEND_SIGNAL(living_mob, COMSIG_JOB_RECEIVED, living_mob.job) //If we joined at roundstart we should be positioned at our workstation if(!joined_late) var/obj/S = null if(length(GLOB.jobspawn_overrides[rank])) S = pick(GLOB.jobspawn_overrides[rank]) else for(var/_sloc in GLOB.start_landmarks_list) var/obj/effect/landmark/start/sloc = _sloc if(sloc.name != rank) continue S = sloc if(locate(/mob/living) in sloc.loc) //so we can revert to spawning them on top of eachother if something goes wrong continue sloc.used = TRUE break if(S) S.JoinPlayerHere(living_mob, 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]") if(!SendToLateJoin(living_mob)) living_mob.move_to_error_room() if(living_mob.mind) living_mob.mind.assigned_role = rank to_chat(M, "You are the [rank].") if(job) var/new_mob = job.equip(living_mob, null, null, joined_late , null, M.client)//silicons override this proc to return a mob if(ismob(new_mob)) living_mob = new_mob if(!joined_late) newplayer.new_character = living_mob else M = living_mob SSpersistence.antag_rep_change[M.client.ckey] += job.GetAntagRep() if(M.client.holder) if(CONFIG_GET(flag/auto_deadmin_players) || (M.client.prefs?.toggles & DEADMIN_ALWAYS)) M.client.holder.auto_deadmin() else handle_auto_deadmin_roles(M.client, rank) to_chat(M, "As the [rank] you answer directly to [job.supervisors]. Special circumstances may change this.") job.radio_help_message(M) if(job.req_admin_notify) to_chat(M, "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(M, "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(rank) if(related_policy) to_chat(M,related_policy) if(ishuman(living_mob)) var/mob/living/carbon/human/wageslave = living_mob living_mob.add_memory("Your account ID is [wageslave.account_id].") if(job && living_mob) job.after_spawn(living_mob, M, joined_late) // note: this happens before the mob has a key! M will always have a client, H might not. return living_mob /datum/controller/subsystem/job/proc/handle_auto_deadmin_roles(client/C, rank) if(!C?.holder) return TRUE var/datum/job/job = GetJob(rank) if(!job) return if((job.auto_deadmin_role_flags & DEADMIN_POSITION_HEAD) && (CONFIG_GET(flag/auto_deadmin_heads) || (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) || (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) || (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/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/i in GLOB.new_player_list) var/mob/dead/new_player/player = i 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 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 /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 /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 && M.mind.assigned_role && length(GLOB.jobspawn_overrides[M.mind.assigned_role])) //We're doing something special today. destination = pick(GLOB.jobspawn_overrides[M.mind.assigned_role]) destination.JoinPlayerHere(M, FALSE) return TRUE if(latejoin_trackers.len) destination = pick(latejoin_trackers) destination.JoinPlayerHere(M, buckle) return TRUE //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) C.JoinPlayerHere(M, buckle) return TRUE //last hurrah var/list/avail = list() for(var/turf/T in A) if(!is_blocked_turf(T, TRUE)) avail += T if(avail.len) destination = pick(avail) destination.JoinPlayerHere(M, FALSE) return TRUE //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)) T.JoinPlayerHere(M, FALSE) return TRUE //last chance, pick ANY spot on arrivals and dump em destination = arrivals_turfs[1] destination.JoinPlayerHere(M, FALSE) return TRUE 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/i in GLOB.human_list) var/mob/living/carbon/human/player = i 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/i in GLOB.human_list) var/mob/living/carbon/human/player = i 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/i in GLOB.human_list) var/mob/living/carbon/human/player = i if(player.mind && (player.mind.assigned_role in GLOB.security_positions)) . |= player.mind /datum/controller/subsystem/job/proc/JobDebug(message) log_job_debug(message)