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/list/job_debug = list() //Debug info 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 /datum/controller/subsystem/job/Initialize(timeofday) if(!occupations.len) SetupOccupations() if(CONFIG_GET(flag/load_jobs_from_txt)) LoadJobs() generate_selectable_species() ..() /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/Debug(text) if(!GLOB.Debug2) return 0 job_debug.Add(text) 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=0) Debug("Running AR, Player: [player], Rank: [rank], LJ: [latejoin]") if(player && player.mind && rank) var/datum/job/job = GetJob(rank) if(!job) return 0 if(jobban_isbanned(player, rank)) return 0 if(!job.player_old_enough(player.client)) return 0 if(job.required_playtime_remaining(player.client)) return 0 var/position_limit = job.total_positions if(!latejoin) position_limit = job.spawn_positions Debug("Player: [player] is now Rank: [rank], JCP:[job.current_positions], JPL:[position_limit]") player.mind.assigned_role = rank unassigned -= player job.current_positions++ return 1 Debug("AR has failed, Player: [player], Rank: [rank]") return 0 /datum/controller/subsystem/job/proc/FindOccupationCandidates(datum/job/job, level, flag) Debug("Running FOC, Job: [job], Level: [level], Flag: [flag]") var/list/candidates = list() for(var/mob/dead/new_player/player in unassigned) if(jobban_isbanned(player, job.title)) Debug("FOC isbanned failed, Player: [player]") continue if(!job.player_old_enough(player.client)) Debug("FOC player not old enough, Player: [player]") continue if(job.required_playtime_remaining(player.client)) Debug("FOC player not enough xp, Player: [player]") continue if(flag && (!(flag in player.client.prefs.be_special))) Debug("FOC flag failed, Player: [player], Flag: [flag], ") continue if(player.mind && job.title in player.mind.restricted_roles) Debug("FOC incompatible with antagonist role, Player: [player]") continue if(player.client.prefs.GetJobDepartment(job, level) & job.flag) Debug("FOC pass, Player: [player], Level:[level]") candidates += player return candidates /datum/controller/subsystem/job/proc/GiveRandomJob(mob/dead/new_player/player) Debug("GRJ Giving random job, Player: [player]") . = FALSE for(var/datum/job/job in shuffle(occupations)) if(!job) continue if(istype(job, GetJob("Assistant"))) // 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(jobban_isbanned(player, job.title)) Debug("GRJ isbanned failed, Player: [player], Job: [job.title]") continue if(!job.player_old_enough(player.client)) Debug("GRJ player not old enough, Player: [player]") continue if(job.required_playtime_remaining(player.client)) Debug("GRJ player not enough xp, Player: [player]") continue if(player.mind && job.title in player.mind.restricted_roles) Debug("GRJ incompatible with antagonist role, Player: [player], Job: [job.title]") continue if((job.current_positions < job.spawn_positions) || job.spawn_positions == -1) Debug("GRJ Random job given, Player: [player], Job: [job]") if(AssignRole(player, job.title)) return TRUE /datum/controller/subsystem/job/proc/ResetOccupations() 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 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 Debug("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 //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 Debug("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 assistants, sure, go on. Debug("DO, Running Assistant Check 1") var/datum/job/assist = new /datum/job/assistant() var/list/assistant_candidates = FindOccupationCandidates(assist, 3) Debug("AC1, Candidates: [assistant_candidates.len]") for(var/mob/dead/new_player/player in assistant_candidates) Debug("AC1 pass, Player: [player]") AssignRole(player, "Assistant") assistant_candidates -= player Debug("DO, AC1 end") //Select one head Debug("DO, Running Head Check") FillHeadPosition() Debug("DO, Head Check end") //Check for an AI Debug("DO, Running AI Check") FillAIPosition() Debug("DO, AI Check end") //Other jobs are now checked Debug("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(jobban_isbanned(player, job.title)) Debug("DO isbanned failed, Player: [player], Job:[job.title]") continue if(!job.player_old_enough(player.client)) Debug("DO player not old enough, Player: [player], Job:[job.title]") continue if(job.required_playtime_remaining(player.client)) Debug("DO player not enough xp, Player: [player], Job:[job.title]") continue if(player.mind && job.title in player.mind.restricted_roles) Debug("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) Debug("DO pass, Player: [player], Level:[level], Job:[job.title]") AssignRole(player, job.title) unassigned -= player break // 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) if(PopcapReached()) RejectPlayer(player) else if(jobban_isbanned(player, "Assistant")) GiveRandomJob(player) //you get to roll for random before everyone else just to be sure you don't get assistant. you're so speshul for(var/mob/dead/new_player/player in unassigned) if(PopcapReached()) RejectPlayer(player) else if(player.client.prefs.joblessrole == BERANDOMJOB) GiveRandomJob(player) Debug("DO, Standard Check end") Debug("DO, Running AC2") // For those who wanted to be assistant if their preferences were filled, here you go. for(var/mob/dead/new_player/player in unassigned) if(PopcapReached()) RejectPlayer(player) if(player.client.prefs.joblessrole == BEASSISTANT) Debug("AC2 Assistant located, Player: [player]") AssignRole(player, "Assistant") else // For those who don't want to play if their preference were filled, back you go. RejectPlayer(player) 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, "Assistant") //If everything is already filled, make them an assistant return 1 //Gives the player the stuff he should have with his rank /datum/controller/subsystem/job/proc/EquipRank(mob/M, rank, joined_late=0) 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 break 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) if(ismob(new_mob)) H = new_mob if(!joined_late) N.new_character = H else M = H to_chat(M, "You are the [rank].") to_chat(M, "As the [rank] you answer directly to [job.supervisors]. Special circumstances may change this.") to_chat(M, "To speak on your departments radio, use the :h button. To see others, look closely at your headset.") 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.") if(job && H) job.after_spawn(H, M) 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 Debug("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("config/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/tmp_str = "|[job.title]|" var/level1 = 0 //high var/level2 = 0 //medium var/level3 = 0 //low var/level4 = 0 //never var/level5 = 0 //banned var/level6 = 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(jobban_isbanned(player, job.title)) level5++ continue if(!job.player_old_enough(player.client)) level6++ continue if(job.required_playtime_remaining(player.client)) level6++ continue if(player.client.prefs.GetJobDepartment(job, 1) & job.flag) level1++ else if(player.client.prefs.GetJobDepartment(job, 2) & job.flag) level2++ else if(player.client.prefs.GetJobDepartment(job, 3) & job.flag) level3++ else level4++ //not selected tmp_str += "HIGH=[level1]|MEDIUM=[level2]|LOW=[level3]|NEVER=[level4]|BANNED=[level5]|YOUNG=[level6]|-" SSblackbox.add_details("job_preferences",tmp_str) /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()) Debug("Popcap overflow Check observer located, Player: [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 /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(latejoin_trackers.len) SendToAtom(M, pick(latejoin_trackers), buckle) else //bad mojo var/area/shuttle/arrival/A = locate() in GLOB.sortedAreas 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