mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 01:34:01 +00:00
* Makes jobconfig.toml respect values of 0 and Fixes the Generate Job Configuration verb when there's a job that wasn't in the jobconfig.toml already (#70738) About The Pull Request Basically it was in the documentation of #70199 that this verb was meant for when a new job is added, or for downstreams that have more jobs than /tg/ does. So I tried to use it, and it runtimed because it didn't have one of our own jobs in the jobconfig.toml file already, thus completely defeating the point of the verb. So I just scooted the variable declarations into the proper null-checked if block, and now it works just fine! Also, I noticed that a job we had disabled via config wasn't getting properly disabled, turns out values of 0 in the config weren't being respected, and were getting reset to the codebase default. This behavior is now fixed, and shouldn't be an issue anymore, hopefully. Why It's Good For The Game Makes a useful verb, actually work for its intended usecase :) Changelog 🆑 GoldenAlpharex config: The Generate Job Configuration verb now works as expected when there's at least one job in-game that doesn't exist in the new jobconfig.toml file already. fix: Any value that is set to 0 in jobconfig.toml will no longer default to the codebase's default, and will actually be considered as 0. /🆑 * Makes jobconfig.toml respect values of 0 and Fixes the Generate Job Configuration verb when there's a job that wasn't in the jobconfig.toml already Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com>
1142 lines
53 KiB
Plaintext
1142 lines
53 KiB
Plaintext
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
|
|
|
|
/**
|
|
* 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(
|
|
JOB_CAPTAIN = 1,
|
|
JOB_HEAD_OF_PERSONNEL = 2,
|
|
JOB_RESEARCH_DIRECTOR = 3,
|
|
JOB_CHIEF_ENGINEER = 4,
|
|
JOB_CHIEF_MEDICAL_OFFICER = 5,
|
|
JOB_HEAD_OF_SECURITY = 6,
|
|
JOB_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
|
|
|
|
/// Dictionary that maps job priorities to low/medium/high. Keys have to be number-strings as assoc lists cannot be indexed by integers. Set in setup_job_lists.
|
|
var/list/job_priorities_to_strings
|
|
|
|
/// Are we using the old job config system (txt) or the new job config system (TOML)? IF we are going to use the txt file, then we are in "legacy mode", and this will flip to TRUE.
|
|
var/legacy_mode = FALSE
|
|
|
|
/// This is just the message we prepen and put into all of the config files to ensure documentation. We use this in more than one place, so let's put it in the SS to make life a bit easier.
|
|
var/config_documentation = "## This is the configuration file for the job system.\n## This will only be enabled when the config flag LOAD_JOBS_FROM_TXT is enabled.\n\
|
|
## We use a system of keys here that directly correlate to the job, just to ensure they don't desync if we choose to change the name of a job.\n## You are able to change (as of now) four different variables in this file.\n\
|
|
## Total Positions are how many job slots you get in a shift, Spawn Positions are how many you get that load in at spawn. If you set this to -1, it is unrestricted.\n## Playtime Requirements is in minutes, and the job will unlock when a player reaches that amount of time.\n\
|
|
## However, that can be superseded by Required Account Age, which is a time in days that you need to have had an account on the server for.\n## As time goes on, more config options may be added to this file.\n\
|
|
## You can use the admin verb 'Generate Job Configuration' in-game to auto-regenerate this config as a downloadable file without having to manually edit this file if we add more jobs or more things you can edit here.\n\
|
|
## It will always respect prior-existing values in the config, but will appropriately add more fields when they generate.\n## It's strongly advised you create your own version of this file rather than use the one provisioned on the codebase.\n\n\
|
|
## The game will not read any line that is commented out with a '#', as to allow you to defer to codebase defaults.\n## If you want to override the codebase values, add the value and then uncomment that line by removing the # from the job key's name.\n\
|
|
## Ensure that the key is flush, do not introduce any whitespaces when you uncomment a key. For example:\n## \"# Total Positions\" should always be changed to \"Total Positions\", no additional spacing. \n\
|
|
## Best of luck editing!\n"
|
|
|
|
/datum/controller/subsystem/job/Initialize()
|
|
setup_job_lists()
|
|
if(!length(all_occupations))
|
|
SetupOccupations()
|
|
if(CONFIG_GET(flag/load_jobs_from_txt))
|
|
load_jobs_from_config()
|
|
set_overflow_role(CONFIG_GET(string/overflow_job))
|
|
return SS_INIT_SUCCESS
|
|
|
|
|
|
/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
|
|
log_job_debug("Removed [job.title] 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)
|
|
RETURN_TYPE(/datum/job)
|
|
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]
|
|
|
|
/**
|
|
* Assigns the given job role to the player.
|
|
*
|
|
* Arguments:
|
|
* * player - The player to assign the job to
|
|
* * job - The job to assign
|
|
* * latejoin - Set to TRUE if this is a latejoin role assignment.
|
|
* * do_eligibility_checks - Set to TRUE to conduct all job eligibility tests and reject on failure. Set to FALSE if job eligibility has been tested elsewhere and they can be safely skipped.
|
|
*/
|
|
/datum/controller/subsystem/job/proc/AssignRole(mob/dead/new_player/player, datum/job/job, latejoin = FALSE, do_eligibility_checks = TRUE)
|
|
JobDebug("Running AR, Player: [player], Job: [isnull(job) ? "null" : job], LateJoin: [latejoin]")
|
|
if(!player?.mind || !job)
|
|
JobDebug("AR has failed, player has no mind or job is null, Player: [player], Rank: [isnull(job) ? "null" : job.type]")
|
|
return FALSE
|
|
|
|
if(do_eligibility_checks && (check_job_eligibility(player, job, "AR", add_job_to_log = TRUE) != JOB_AVAILABLE))
|
|
return FALSE
|
|
|
|
JobDebug("Player: [player] is now Rank: [job.title], JCP:[job.current_positions], JPL:[latejoin ? job.total_positions : job.spawn_positions]")
|
|
player.mind.set_assigned_role(job)
|
|
unassigned -= player
|
|
job.current_positions++
|
|
return TRUE
|
|
|
|
/datum/controller/subsystem/job/proc/FindOccupationCandidates(datum/job/job, level)
|
|
JobDebug("Running FOC, Job: [job], Level: [job_priority_level_to_string(level)]")
|
|
var/list/candidates = list()
|
|
for(var/mob/dead/new_player/player in unassigned)
|
|
if(!player)
|
|
JobDebug("FOC player no longer exists.")
|
|
continue
|
|
if(!player.client)
|
|
JobDebug("FOC player client no longer exists, Player: [player]")
|
|
continue
|
|
// Initial screening check. Does the player even have the job enabled, if they do - Is it at the correct priority level?
|
|
var/player_job_level = player.client?.prefs.job_preferences[job.title]
|
|
if(isnull(player_job_level))
|
|
JobDebug("FOC player job not enabled, Player: [player]")
|
|
continue
|
|
else if(player_job_level != level)
|
|
JobDebug("FOC player job enabled at wrong level, Player: [player], TheirLevel: [job_priority_level_to_string(player_job_level)], ReqLevel: [job_priority_level_to_string(level)]")
|
|
continue
|
|
|
|
// This check handles its own output to JobDebug.
|
|
if(check_job_eligibility(player, job, "FOC", add_job_to_log = FALSE) != JOB_AVAILABLE)
|
|
continue
|
|
|
|
// They have the job enabled, at this priority level, with no restrictions applying to them.
|
|
JobDebug("FOC pass, Player: [player], Level: [job_priority_level_to_string(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(QDELETED(player))
|
|
JobDebug("GRJ player is deleted, aborting")
|
|
break
|
|
|
|
if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1)
|
|
JobDebug("GRJ job lacks spawn positions to be eligible, Player: [player], Job: [job]")
|
|
continue
|
|
|
|
if(istype(job, GetJobType(overflow_role))) // We don't want to give him assistant, that's boring!
|
|
JobDebug("GRJ skipping overflow role, Player: [player], Job: [job]")
|
|
continue
|
|
|
|
if(job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) //If you want a command position, select it!
|
|
JobDebug("GRJ skipping command role, Player: [player], Job: [job]")
|
|
continue
|
|
|
|
//SKYRAT EDIT ADDITION
|
|
if(job.departments_bitflags & DEPARTMENT_BITFLAG_CENTRAL_COMMAND) //If you want a CC position, select it!
|
|
JobDebug("GRJ skipping Central Command role, Player: [player], Job: [job]")
|
|
continue
|
|
//SKYRAT EDIT END
|
|
|
|
// This check handles its own output to JobDebug.
|
|
if(check_job_eligibility(player, job, "GRJ", add_job_to_log = TRUE) != JOB_AVAILABLE)
|
|
continue
|
|
|
|
if(AssignRole(player, job, do_eligibility_checks = FALSE))
|
|
JobDebug("GRJ Random job given, Player: [player], Job: [job]")
|
|
return TRUE
|
|
|
|
JobDebug("GRJ Player eligible but AssignRole failed, Player: [player], Job: [job]")
|
|
|
|
|
|
/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
|
|
|
|
|
|
/**
|
|
* Will try to select a head, ignoring ALL non-head preferences for every level until.
|
|
*
|
|
* Basically tries to ensure there is at least one head in every shift if anyone has that job preference enabled at all.
|
|
*/
|
|
/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)
|
|
// Eligibility checks done as part of FindOccupationCandidates.
|
|
if(AssignRole(candidate, job, do_eligibility_checks = FALSE))
|
|
return TRUE
|
|
return FALSE
|
|
|
|
|
|
/**
|
|
* Attempts to fill out all possible head positions for players with that job at a a given job priority level.
|
|
*
|
|
* Arguments:
|
|
* * level - One of the JP_LOW, JP_MEDIUM or JP_HIGH defines. Attempts to find candidates with head jobs at this priority only.
|
|
*/
|
|
/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)
|
|
// Eligibility checks done as part of FindOccupationCandidates
|
|
AssignRole(candidate, job, do_eligibility_checks = FALSE)
|
|
|
|
/// Attempts to fill out all available AI positions.
|
|
/datum/controller/subsystem/job/proc/fill_ai_positions()
|
|
var/datum/job/ai_job = GetJob(JOB_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)
|
|
// Eligibility checks done as part of FindOccupationCandidates
|
|
if(AssignRole(candidate, GetJobType(/datum/job/ai), do_eligibility_checks = FALSE))
|
|
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]")
|
|
// Eligibility checks done as part of FindOccupationCandidates
|
|
AssignRole(player, GetJobType(overflow_role), do_eligibility_checks = FALSE)
|
|
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 job assignment")
|
|
// 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)
|
|
JobDebug("FOC invalid/null job in occupations, Player: [player], Job: [job]")
|
|
shuffledoccupations -= job
|
|
continue
|
|
|
|
// Make sure the job isn't filled. If it is, remove it from the list so it doesn't get checked again.
|
|
if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1)
|
|
JobDebug("FOC job filled and not overflow, Player: [player], Job: [job], Current: [job.current_positions], Limit: [job.spawn_positions]")
|
|
shuffledoccupations -= job
|
|
continue
|
|
|
|
// Filter any job that doesn't fit the current level.
|
|
var/player_job_level = player.client?.prefs.job_preferences[job.title]
|
|
if(isnull(player_job_level))
|
|
JobDebug("FOC player job not enabled, Player: [player]")
|
|
continue
|
|
else if(player_job_level != level)
|
|
JobDebug("FOC player job enabled but at different level, Player: [player], TheirLevel: [job_priority_level_to_string(player_job_level)], ReqLevel: [job_priority_level_to_string(level)]")
|
|
continue
|
|
|
|
if(check_job_eligibility(player, job, "DO", add_job_to_log = TRUE) != JOB_AVAILABLE)
|
|
continue
|
|
|
|
JobDebug("DO pass, Player: [player], Level:[level], Job:[job.title]")
|
|
AssignRole(player, job, do_eligibility_checks = FALSE)
|
|
unassigned -= player
|
|
break
|
|
|
|
JobDebug("DO, Ending standard job assignment")
|
|
|
|
JobDebug("DO, Handle 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, Ending handle unassigned.")
|
|
|
|
JobDebug("DO, Handle 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
|
|
JobDebug("DO, Forced antagonist could not be assigned any random job or the overflow role. DivideOccupations failed.")
|
|
JobDebug("---------------------------------------------------")
|
|
return FALSE //Living on the edge, the forced antagonist couldn't be assigned to overflow role (bans, client age) - just reroll
|
|
JobDebug("DO, Ending handle unrejectable unassigned")
|
|
|
|
JobDebug("All divide occupations tasks completed.")
|
|
JobDebug("---------------------------------------------------")
|
|
|
|
return TRUE
|
|
|
|
//We couldn't find a job from prefs for this guy.
|
|
/datum/controller/subsystem/job/proc/HandleUnassigned(mob/dead/new_player/player)
|
|
var/jobless_role = player.client.prefs.read_preference(/datum/preference/choiced/jobless_role)
|
|
|
|
if(PopcapReached())
|
|
RejectPlayer(player)
|
|
return
|
|
|
|
switch (jobless_role)
|
|
if (BEOVERFLOW)
|
|
var/datum/job/overflow_role_datum = GetJobType(overflow_role)
|
|
|
|
if(check_job_eligibility(player, overflow_role_datum, debug_prefix = "HU", add_job_to_log = TRUE) != JOB_AVAILABLE)
|
|
RejectPlayer(player)
|
|
return
|
|
|
|
if(!AssignRole(player, overflow_role_datum, do_eligibility_checks = FALSE))
|
|
RejectPlayer(player)
|
|
return
|
|
if (BERANDOMJOB)
|
|
if(!GiveRandomJob(player))
|
|
RejectPlayer(player)
|
|
return
|
|
if (RETURNTOLOBBY)
|
|
RejectPlayer(player)
|
|
return
|
|
else //Something gone wrong if we got here.
|
|
var/message = "HU: [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)
|
|
// SKYRAT EDIT ADDITION BEGIN - ALTERNATIVE_JOB_TITLES
|
|
// The alt job title, if user picked one, or the default
|
|
var/chosen_title = player_client?.prefs.alt_job_titles[job.title] || job.title
|
|
var/default_title = job.title
|
|
// SKYRAT EDIT ADDITION END - job.title
|
|
|
|
equipping.job = job.title
|
|
|
|
SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job)
|
|
|
|
equipping.mind?.set_assigned_role(job)
|
|
if(player_client)
|
|
to_chat(player_client, span_infoplain("You are the [chosen_title].")) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: to_chat(player_client, span_infoplain("You are the [job.title]."))
|
|
|
|
equipping.on_job_equipping(job, player_client?.prefs) //SKYRAT EDIT CHANGE
|
|
|
|
job.announce_job(equipping, chosen_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: 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)
|
|
|
|
|
|
if(player_client)
|
|
to_chat(player_client, span_infoplain("As the [chosen_title == job.title ? chosen_title : "[chosen_title] ([job.title])"] you answer directly to [job.supervisors]. Special circumstances may change this.")) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: to_chat(player_client, span_infoplain("As the [job.title] you answer directly to [job.supervisors]. Special circumstances may change this."))
|
|
|
|
job.radio_help_message(equipping)
|
|
|
|
if(player_client)
|
|
if(job.req_admin_notify)
|
|
to_chat(player_client, "<span class='infoplain'><b>You are playing a job that is important for Game Progression. If you have to disconnect, please notify the admins via adminhelp.</b></span>")
|
|
if(CONFIG_GET(number/minimal_access_threshold))
|
|
to_chat(player_client, span_notice("<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>"))
|
|
//SKYRAT EDIT START - ALTERNATIVE_JOB_TITLES
|
|
if(chosen_title != default_title)
|
|
to_chat(player_client, span_infoplain(span_warning("Remember that alternate titles are purely for flavor and roleplay.")))
|
|
to_chat(player_client, span_infoplain(span_doyourjobidiot("Do not use your \"[chosen_title]\" alt title as an excuse to forego your duties as a [job.title].")))
|
|
//SKYRAT EDIT END
|
|
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.mind.add_memory(MEMORY_ACCOUNT, list(DETAIL_ACCOUNT_ID = wageslave.account_id), story_value = STORY_VALUE_SHIT, memory_flags = MEMORY_FLAG_NOLOCATION)
|
|
|
|
setup_alt_job_items(wageslave, job, player_client) // SKYRAT EDIT ADDITION - ALTERNATIVE_JOB_TITLES
|
|
|
|
job.after_spawn(equipping, player_client)
|
|
|
|
/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(JOB_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)
|
|
// SKYRAT EDIT - Reduced from 12 max sec to 7 max sec due to departmental security being deactivated and replaced.
|
|
var/officer_positions = min(7, 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
|
|
|
|
#define TOTAL_POSITIONS "Total Positions"
|
|
#define SPAWN_POSITIONS "Spawn Positions"
|
|
#define PLAYTIME_REQUIREMENTS "Playtime Requirements"
|
|
#define REQUIRED_ACCOUNT_AGE "Required Account Age"
|
|
|
|
/// Called in jobs subsystem initialize if LOAD_JOBS_FROM_TXT config flag is set: reads jobconfig.toml (or if in legacy mode, jobs.txt) to set all of the datum's values to what the server operator wants.
|
|
/datum/controller/subsystem/job/proc/load_jobs_from_config()
|
|
var/toml_file = "[global.config.directory]/jobconfig.toml"
|
|
|
|
if(!legacy_mode) // this flag is set during the setup of SSconfig, and all warnings were handled there.
|
|
var/job_config = rustg_read_toml_file(toml_file)
|
|
|
|
for(var/datum/job/occupation as anything in joinable_occupations)
|
|
var/job_title = occupation.title
|
|
var/job_key = occupation.config_tag
|
|
if(!job_config[job_key]) // Job isn't listed, skip it.
|
|
message_admins(span_notice("[job_title] (with config key [job_key]) is missing from jobconfig.toml! Using codebase defaults.")) // List both job_title and job_key in case they de-sync over time.
|
|
continue
|
|
|
|
// If the value is commented out, we assume that the server operate did not want to override the codebase default values, so we skip it.
|
|
var/default_positions = job_config[job_key][TOTAL_POSITIONS]
|
|
var/starting_positions = job_config[job_key][SPAWN_POSITIONS]
|
|
var/playtime_requirements = job_config[job_key][PLAYTIME_REQUIREMENTS]
|
|
var/required_account_age = job_config[job_key][REQUIRED_ACCOUNT_AGE]
|
|
|
|
if(default_positions || default_positions == 0) // We need to account for jobs that were intentionally turned off via config too.
|
|
occupation.total_positions = default_positions
|
|
if(starting_positions || starting_positions == 0)
|
|
occupation.spawn_positions = starting_positions
|
|
if(playtime_requirements || playtime_requirements == 0)
|
|
occupation.exp_requirements = playtime_requirements
|
|
if(required_account_age || required_account_age == 0)
|
|
occupation.minimal_player_age = required_account_age
|
|
|
|
return
|
|
|
|
else // legacy mode, so just run the old parser.
|
|
var/jobsfile = file("[global.config.directory]/jobs.txt")
|
|
if(!fexists(jobsfile)) // sanity with a trace
|
|
stack_trace("Despite SSconfig setting SSjob.legacy_mode to TRUE, jobs.txt was not found in the config directory! Something has gone terribly wrong!")
|
|
return
|
|
var/jobstext = file2text(jobsfile)
|
|
for(var/datum/job/occupation as anything in joinable_occupations)
|
|
var/regex/parser = new("[occupation.title]=(-1|\\d+),(-1|\\d+)")
|
|
parser.Find(jobstext)
|
|
occupation.total_positions = text2num(parser.group[1])
|
|
occupation.spawn_positions = text2num(parser.group[2])
|
|
|
|
/// Called from an admin debug verb that generates the jobconfig.toml file and then allows the end user to download it to their machine. Returns TRUE if a file is successfully generated, FALSE otherwise.
|
|
/datum/controller/subsystem/job/proc/generate_config(mob/user)
|
|
var/toml_file = "[global.config.directory]/jobconfig.toml"
|
|
var/jobstext = "[global.config.directory]/jobs.txt"
|
|
var/list/file_data = list()
|
|
config_documentation = initial(config_documentation) // Reset to default juuuuust in case.
|
|
|
|
if(fexists(file(toml_file)))
|
|
to_chat(src, span_notice("Generating new jobconfig.toml, pulling from the old config settings."))
|
|
if(!regenerate_job_config(user))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
if(fexists(file(jobstext))) // Generate the new TOML format, migrating from the text format.
|
|
to_chat(user, span_notice("Found jobs.txt in config directory! Generating jobconfig.toml from it."))
|
|
jobstext = file2text(file(jobstext)) // walter i'm dying (get the file from the string, then parse it into a larger text string)
|
|
config_documentation += "\n\n## This TOML was migrated from jobs.txt. All variables are COMMENTED and will not load by default! Please verify to ensure that they are correct, and uncomment the key as you want, comparing it to the old config.\n\n" // small warning
|
|
for(var/datum/job/occupation as anything in joinable_occupations)
|
|
var/job_key = occupation.config_tag
|
|
var/regex/parser = new("[occupation.title]=(-1|\\d+),(-1|\\d+)") // TXT system used the occupation's name, we convert it to the new config_key system here.
|
|
parser.Find(jobstext)
|
|
|
|
var/default_positions = text2num(parser.group[1])
|
|
var/starting_positions = text2num(parser.group[2])
|
|
|
|
// Playtime Requirements and Required Account Age are new and we want to see it migrated, so we will just pull codebase defaults for them.
|
|
// Remember, every time we write the TOML from scratch, we want to have it commented out by default to ensure that the server operator is knows that they codebase defaults when they remove the comment.
|
|
file_data["[job_key]"] = list(
|
|
"# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements,
|
|
"# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age,
|
|
"# [TOTAL_POSITIONS]" = default_positions,
|
|
"# [SPAWN_POSITIONS]" = starting_positions,
|
|
)
|
|
|
|
if(!export_toml(user, file_data))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
else // Generate the new TOML format, using codebase defaults.
|
|
to_chat(user, span_notice("Generating new jobconfig.toml, using codebase defaults."))
|
|
for(var/datum/job/occupation as anything in joinable_occupations)
|
|
var/job_key = occupation.config_tag
|
|
// Remember, every time we write the TOML from scratch, we want to have it commented out by default to ensure that the server operator is knows that they override codebase defaults when they remove the comment.
|
|
// Having comments mean that we allow server operators to defer to codebase standards when they deem acceptable. They must uncomment to override the codebase default.
|
|
if(is_assistant_job(occupation)) // there's a concession made in jobs.txt that we should just rapidly account for here I KNOW I KNOW.
|
|
file_data["[job_key]"] = list(
|
|
"# [TOTAL_POSITIONS]" = -1,
|
|
"# [SPAWN_POSITIONS]" = -1,
|
|
"# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements,
|
|
"# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age,
|
|
)
|
|
continue
|
|
// Generate new config from codebase defaults.
|
|
file_data["[job_key]"] = list(
|
|
"# [TOTAL_POSITIONS]" = occupation.total_positions,
|
|
"# [SPAWN_POSITIONS]" = occupation.spawn_positions,
|
|
"# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements,
|
|
"# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age,
|
|
)
|
|
if(!export_toml(user, file_data))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// If we add a new job or more fields to config a job with, quickly spin up a brand new config that inherits all of your old settings, but adds the new job with codebase defaults. Returns TRUE if a file is successfully generated, FALSE otherwise.
|
|
/datum/controller/subsystem/job/proc/regenerate_job_config(mob/user)
|
|
var/toml_file = "[global.config.directory]/jobconfig.toml"
|
|
var/list/file_data = list()
|
|
|
|
if(!fexists(file(toml_file))) // You need an existing (valid) TOML for this to work. Sanity check if someone calls this directly instead of through 'Generate Job Configuration' verb.
|
|
to_chat(user, span_notice("No jobconfig.toml found in the config folder! If this is not expected, please notify a server operator or coders. You may need to generate a new config file by running 'Generate Job Configuration' from the Server tab."))
|
|
return FALSE
|
|
|
|
var/job_config = rustg_read_toml_file(toml_file)
|
|
for(var/datum/job/occupation as anything in joinable_occupations)
|
|
var/job_name = occupation.title
|
|
var/job_key = occupation.config_tag
|
|
|
|
// When we regenerate, we want to make sure commented stuff stays commented, but we also want to migrate information that remains uncommented. So, let's make sure we keep that pattern.
|
|
if(job_config["[job_key]"]) // Let's see if any data for this job exists.
|
|
var/default_positions = job_config[job_key][TOTAL_POSITIONS]
|
|
var/starting_positions = job_config[job_key][SPAWN_POSITIONS]
|
|
var/playtime_requirements = job_config[job_key][PLAYTIME_REQUIREMENTS]
|
|
var/required_account_age = job_config[job_key][REQUIRED_ACCOUNT_AGE]
|
|
|
|
if(file_data["[job_key]"]) // Sanity, let's just make sure we don't overwrite anything or add any dupe keys. We also unit test for this, but eh, you never know sometimes.
|
|
stack_trace("We were about to over-write a job key that already exists in file_data while generating a new jobconfig.toml! This should not happen! Verify you do not have any duplicate job keys in your codebase!")
|
|
continue
|
|
if(default_positions) // If the variable exists, we want to ensure it migrated into the new TOML uncommented, to allow for flush migration.
|
|
file_data["[job_key]"] += list(
|
|
TOTAL_POSITIONS = default_positions,
|
|
)
|
|
else // If we can't find anything for this variable, then we just throw in the codebase default with it commented out.
|
|
file_data["[job_key]"] += list(
|
|
"# [TOTAL_POSITIONS]" = occupation.total_positions,
|
|
)
|
|
|
|
if(starting_positions) // Same pattern as above.
|
|
file_data["[job_key]"] += list(
|
|
SPAWN_POSITIONS = starting_positions,
|
|
)
|
|
else
|
|
file_data["[job_key]"] += list(
|
|
"# [SPAWN_POSITIONS]" = occupation.spawn_positions,
|
|
)
|
|
|
|
if(playtime_requirements) // Same pattern as above.
|
|
file_data["[job_key]"] += list(
|
|
PLAYTIME_REQUIREMENTS = playtime_requirements,
|
|
)
|
|
else
|
|
file_data["[job_key]"] += list(
|
|
"# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements,
|
|
)
|
|
|
|
if(required_account_age) // Same pattern as above.
|
|
file_data["[job_key]"] += list(
|
|
REQUIRED_ACCOUNT_AGE = required_account_age,
|
|
)
|
|
else
|
|
file_data["[job_key]"] += list(
|
|
"# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age,
|
|
)
|
|
continue
|
|
else
|
|
to_chat(user, span_notice("New job [job_name] (using key [job_key]) detected! Adding to jobconfig.toml using default codebase values..."))
|
|
// Commented out keys here in case server operators wish to defer to codebase defaults.
|
|
file_data["[job_key]"] = list(
|
|
"# [TOTAL_POSITIONS]" = occupation.total_positions,
|
|
"# [SPAWN_POSITIONS]" = occupation.spawn_positions,
|
|
"# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements,
|
|
"# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age,
|
|
)
|
|
|
|
if(!export_toml(user, file_data))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// Proc that we call to generate a new jobconfig.toml file and send it to the requesting client. Returns TRUE if a file is successfully generated.
|
|
/datum/controller/subsystem/job/proc/export_toml(mob/user, data)
|
|
var/file_location = "data/jobconfig.toml" // store it in the data folder server-side so we can FTP it to the client.
|
|
var/payload = "[config_documentation]\n[rustg_toml_encode(data)]"
|
|
rustg_file_write(payload, file_location)
|
|
DIRECT_OUTPUT(user, ftp(file(file_location), "jobconfig.toml"))
|
|
return TRUE
|
|
|
|
#undef TOTAL_POSITIONS
|
|
#undef SPAWN_POSITIONS
|
|
#undef PLAYTIME_REQUIREMENTS
|
|
#undef REQUIRED_ACCOUNT_AGE
|
|
|
|
/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, "<span class='infoplain'><b>You have failed to qualify for any job you desired.</b></span>")
|
|
unassigned -= player
|
|
player.ready = PLAYER_NOT_READY
|
|
player.client << output(player.ready, "lobby_browser:imgsrc") //SKYRAT EDIT ADDITION
|
|
|
|
|
|
/datum/controller/subsystem/job/Recover()
|
|
set waitfor = FALSE
|
|
var/oldjobs = SSjob.all_occupations
|
|
sleep(2 SECONDS)
|
|
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/joining_mob, buckle)
|
|
// By default, just place the mob on the same turf as the marker or whatever.
|
|
joining_mob.forceMove(get_turf(src))
|
|
|
|
/obj/structure/chair/JoinPlayerHere(mob/joining_mob, buckle)
|
|
. = ..()
|
|
// Placing a mob in a chair will attempt to buckle it, or else fall back to default.
|
|
if(buckle && isliving(joining_mob))
|
|
buckle_mob(joining_mob, FALSE, FALSE)
|
|
|
|
/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
|
|
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/station/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()
|
|
job_priorities_to_strings = list(
|
|
"[JP_LOW]" = "Low Priority",
|
|
"[JP_MEDIUM]" = "Medium Priority",
|
|
"[JP_HIGH]" = "High Priority",
|
|
)
|
|
|
|
/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(mapload)
|
|
var/safe_code = SSid_access.spare_id_safe_code
|
|
default_raw_text = "Captain's Spare ID safe code combination: [safe_code ? safe_code : "\[REDACTED\]"]<br><br>The spare ID can be found in its dedicated safe on the bridge.<br><br>If your job would not ordinarily have Head of Staff access, your ID card has been specially modified to possess it."
|
|
return ..()
|
|
|
|
/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(mapload)
|
|
var/safe_code = SSid_access.spare_id_safe_code
|
|
default_raw_text = "Captain's Spare ID safe code combination: [safe_code ? safe_code : "\[REDACTED\]"]<br><br>The spare ID can be found in its dedicated safe on the bridge."
|
|
return ..()
|
|
|
|
/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_COMMAND in id_card.access))
|
|
id_card.add_wildcards(list(ACCESS_COMMAND), 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)
|
|
// Eligibility checks already carried out as part of the dynamic ruleset trim_candidates proc.area
|
|
// However no guarantee of game state between then and now, so don't skip eligibility checks on AssignRole.
|
|
AssignRole(new_player, GetJob(dynamic_forced_occupations[new_player]))
|
|
|
|
/// Takes a job priority #define such as JP_LOW and gets its string representation for logging.
|
|
/datum/controller/subsystem/job/proc/job_priority_level_to_string(priority)
|
|
return job_priorities_to_strings["[priority]"] || "Undefined Priority \[[priority]\]"
|
|
|
|
/**
|
|
* Runs a standard suite of eligibility checks to make sure the player can take the reqeusted job.
|
|
*
|
|
* Checks:
|
|
* * Role bans
|
|
* * How many days old the player account is
|
|
* * Whether the player has the required hours in other jobs to take that role
|
|
* * If the job is in the mind's restricted roles, for example if they have an antag datum that's incompatible with certain roles.
|
|
*
|
|
* Arguments:
|
|
* * player - The player to check for job eligibility.
|
|
* * possible_job - The job to check for eligibility against.
|
|
* * debug_prefix - Logging prefix for the JobDebug log entries. For example, GRJ during GiveRandomJob or DO during DivideOccupations.
|
|
* * add_job_to_log - If TRUE, appends the job type to the log entry. If FALSE, does not. Set to FALSE when check is part of iterating over players for a specific job, set to TRUE when check is part of iterating over jobs for a specific player and you don't want extra log entry spam.
|
|
*/
|
|
/datum/controller/subsystem/job/proc/check_job_eligibility(mob/dead/new_player/player, datum/job/possible_job, debug_prefix = "", add_job_to_log = FALSE)
|
|
if(!player.mind)
|
|
JobDebug("[debug_prefix] player has no mind, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_GENERIC
|
|
|
|
if(possible_job.title in player.mind.restricted_roles)
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_ANTAG_INCOMPAT, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_ANTAG_INCOMPAT
|
|
|
|
if(!possible_job.player_old_enough(player.client))
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_ACCOUNTAGE, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_ACCOUNTAGE
|
|
|
|
var/required_playtime_remaining = possible_job.required_playtime_remaining(player.client)
|
|
if(required_playtime_remaining)
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_PLAYTIME, possible_job.title)], Player: [player], MissingTime: [required_playtime_remaining][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_PLAYTIME
|
|
|
|
// Run the banned check last since it should be the rarest check to fail and can access the database.
|
|
if(is_banned_from(player.ckey, possible_job.title))
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_BANNED, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_BANNED
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
|
|
if(possible_job.veteran_only && !is_veteran_player(player.client))
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_NOT_VETERAN)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_NOT_VETERAN
|
|
|
|
if(possible_job.has_banned_quirk(player.client.prefs))
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_QUIRK)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_QUIRK
|
|
|
|
if(!possible_job.has_required_languages(player.client.prefs))
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_LANGUAGE)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_LANGUAGE
|
|
|
|
if(possible_job.has_banned_species(player.client.prefs))
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_SPECIES)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_SPECIES
|
|
|
|
if(length_char(player.client.prefs.read_preference(/datum/preference/text/flavor_text)) <= FLAVOR_TEXT_CHAR_REQUIREMENT)
|
|
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_FLAVOUR)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_FLAVOUR
|
|
|
|
|
|
//SKYRAT EDIT END
|
|
|
|
// Run this check after is_banned_from since it can query the DB which may sleep.
|
|
// Need to recheck the player exists after is_banned_from since it can query the DB which may sleep.
|
|
if(QDELETED(player))
|
|
JobDebug("[debug_prefix] player is qdeleted, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_GENERIC
|
|
|
|
return JOB_AVAILABLE
|