mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 01:34:01 +00:00
Implements https://hackmd.io/@tgstation/SkeUS7lSp , rewriting Dynamic
from the ground-up
- Dynamic configuration is now vastly streamlined, making it far far far
easier to understand and edit
- Threat is gone entirely; round chaos is now determined by dynamic
tiers
- There's 5 dynamic tiers, 0 to 4.
- 0 is a pure greenshift.
- Tiers are just picked via weight - "16% chance of getting a high chaos
round".
- Tiers have min pop ranges. "Tier 4 (high chaos) requires 25 pop to be
selected".
- Tier determines how much of every ruleset is picked. "Tier 4 (High
Chaos) will pick 3-4 roundstart[1], 1-2 light, 1-2 heavy, and 2-3
latejoins".
- The number of rulesets picked depends on how many people are in the
server - this is also configurable[2]. As an example, a tier that
demands "1-3" rulesets will not spawn 3 rulesets if population <= 40 and
will not spawn 2 rulesets if population <= 25.
- Tiers also determine time before light, heavy, and latejoin rulesets
are picked, as well as the cooldown range between spawns. More chaotic
tiers may send midrounds sooner or wait less time between sending them.
- On the ruleset side of things, "requirements", "scaling", and
"enemies" is gone.
- You can configure a ruleset's min pop and weight flat, or per tier.
- For example a ruleset like Obsession is weighted higher for tiers 1-2
and lower for tiers 3-4.
- Rather than scaling up, roundstart rulesets can just be selected
multiple times.
- Rulesets also have `min_antag_cap` and `max_antag_cap`.
`min_antag_cap` determines how many candidates are needed for it to run,
and `max_antag_cap` determines how many candidates are selected.
- Rulesets attempt to run every 2.5 minutes. [3]
- Light rulesets will ALWAYS be picked before heavy rulesets. [4]
- Light injection chance is no longer 100%, heavy injection chance
formula has been simplified.
- Chance simply scales based on number of dead players / total number
off players, with a flag 50% chance if no antags exist. [5]
[1] This does not guarantee you will actually GET 3-4 roundstart
rulesets. If a roundstart ruleset is picked, and it ends up being unable
to execute (such as "not enough candidates", that slot is effectively a
wash.) This might be revisited.
[2] Currently, this is a hard limit - below X pop, you WILL get a
quarter or a half of the rulesets. This might be revisited to just be
weighted - you are just MORE LIKELY to get a quarter or a half.
[3] Little worried about accidentally frontloading everything so we'll
see about this
[4] This may be revisited but in most contexts it seems sensible.
[5] This may also be revisited, I'm not 100% sure what the best / most
simple way to tackle midround chances is.
Other implementation details
- The process of making rulesets has been streamlined as well. Many
rulesets only amount to a definition and `assign_role`.
- Dynamic.json -> Dynamic.toml
- Dynamic event hijacked was ripped out entirely.
- Most midround antag random events are now dynamic rulesets. Fugitives,
Morphs, Slaughter Demons, etc.
- The 1 weight slaughter demon event is gone. RIP in peace.
- There is now a hidden midround event that simply adds +1 latejoin, +1
light, or +1 heavy ruleset.
- `mind.special_role` is dead. Minds have a lazylist of special roles
now but it's essentially only used for traitor panel.
- Revs refactored almost entirely. Revs can now exist without a dynamic
ruleset.
- Cult refactored a tiny bit.
- Antag datums cleaned up.
- Pre round setup is less centralized on Dynamic.
- Admins have a whole panel for interfacing with dynamic. It's pretty
slapdash I'm sure someone could make a nicer looking one.


- Maybe some other things.
See readme for more info.
Will you see a massive change in how rounds play out? My hunch says
rounds will spawn less rulesets on average, but it's ultimately to how
it's configured
🆑 Melbert
refactor: Dynamic rewritten entirely, report any strange rounds
config: Dynamic config reworked, it's now a TOML file
refactor: Refactored antag roles somewhat, report any oddities
refactor: Refactored Revolution entirely, report any oddities
del: Deleted most midround events that spawn antags - they use dynamic
rulesets now
add: Dynamic rulesets can now be false alarms
add: Adds a random event that gives dynamic the ability to run another
ruleset later
admin: Adds a panel for messing around with dynamic
admin: Adds a panel for chance for every dynamic ruleset to be selected
admin: You can spawn revs without using dynamic now
fix: Nuke team leaders get their fun title back
/🆑
(cherry picked from commit 4c277dc572)
1045 lines
49 KiB
Plaintext
1045 lines
49 KiB
Plaintext
SUBSYSTEM_DEF(job)
|
|
name = "Jobs"
|
|
dependencies = list(
|
|
/datum/controller/subsystem/processing/station,
|
|
)
|
|
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
|
|
// Whether to run divide_occupations pure so that there are no side-effects from calling it other than
|
|
// a player's assigned_role being set to some value.
|
|
var/run_divide_occupation_pure = FALSE
|
|
|
|
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. Forces mobs into certain occupations with highest priority.
|
|
var/list/forced_occupations
|
|
/// Lazylist of mob:list(occupation_string) pairs. Prevents mobs from taking certain occupations at all.
|
|
var/list/prevented_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
|
|
|
|
/// List of job config datum singletons.
|
|
var/list/job_config_datum_singletons = list()
|
|
|
|
/// 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) five (six if the job is a command head) 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\
|
|
## Also there is a required character age in years. It prevents player from joining as this job, if their character's age as is lower than required. Setting it to 0 means it is turned off for this job.\n\
|
|
## Lastly there's Human Authority Whitelist Setting. You can set it to either \"HUMANS_ONLY\" or \"NON_HUMANS_ALLOWED\". Check the \"Human Authority\" setting on the game_options file to know which you should choose. Note that this entry only appears on jobs that are marked as heads of staff.\n\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()
|
|
job_config_datum_singletons = generate_config_singletons() // we set this up here regardless in case someone wants to use the verb to generate the config file.
|
|
if(!length(all_occupations))
|
|
setup_occupations()
|
|
if(CONFIG_GET(flag/load_jobs_from_txt))
|
|
load_jobs_from_config()
|
|
set_overflow_role(CONFIG_GET(string/overflow_job)) // this must always go after load_jobs_from_config() due to how the legacy systems operate, this always takes precedent.
|
|
return SS_INIT_SUCCESS
|
|
|
|
/// Returns a list of jobs that we are allowed to fuck with during random events
|
|
/datum/controller/subsystem/job/proc/get_valid_overflow_jobs()
|
|
var/static/list/overflow_jobs
|
|
if (!isnull(overflow_jobs))
|
|
return overflow_jobs
|
|
|
|
overflow_jobs = list()
|
|
for (var/datum/job/check_job in joinable_occupations)
|
|
if (!check_job.allow_bureaucratic_error)
|
|
continue
|
|
overflow_jobs += check_job
|
|
return overflow_jobs
|
|
|
|
/datum/controller/subsystem/job/proc/set_overflow_role(new_overflow_role)
|
|
var/datum/job/new_overflow = ispath(new_overflow_role) ? get_job_type(new_overflow_role) : get_job(new_overflow_role)
|
|
if(!new_overflow)
|
|
job_debug("SET_OVRFLW: 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
|
|
new_overflow.job_flags |= JOB_CANNOT_OPEN_SLOTS
|
|
|
|
if(new_overflow.type == overflow_role)
|
|
return
|
|
var/datum/job/old_overflow = get_job_type(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)
|
|
if(!(initial(old_overflow.job_flags) & JOB_CANNOT_OPEN_SLOTS))
|
|
old_overflow.job_flags &= ~JOB_CANNOT_OPEN_SLOTS
|
|
overflow_role = new_overflow.type
|
|
job_debug("SET_OVRFLW: Overflow role set to: [new_overflow.type]")
|
|
|
|
/datum/controller/subsystem/job/proc/setup_occupations()
|
|
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
|
|
for(var/alt_title in job.alternate_titles)
|
|
name_occupations[alt_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, GLOBAL_PROC_REF(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, GLOBAL_PROC_REF(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, GLOBAL_PROC_REF(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, GLOBAL_PROC_REF(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
|
|
|
|
SEND_SIGNAL(src, COMSIG_OCCUPATIONS_SETUP)
|
|
|
|
return TRUE
|
|
|
|
|
|
/datum/controller/subsystem/job/proc/get_job(rank)
|
|
if(!length(all_occupations))
|
|
setup_occupations()
|
|
return name_occupations[rank]
|
|
|
|
/datum/controller/subsystem/job/proc/get_job_type(jobtype)
|
|
RETURN_TYPE(/datum/job)
|
|
if(!length(all_occupations))
|
|
setup_occupations()
|
|
return type_occupations[jobtype]
|
|
|
|
/datum/controller/subsystem/job/proc/get_department_type(department_type)
|
|
if(!length(all_occupations))
|
|
setup_occupations()
|
|
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/assign_role(mob/dead/new_player/player, datum/job/job, latejoin = FALSE, do_eligibility_checks = TRUE)
|
|
job_debug("AR: Running, Player: [player], Job: [isnull(job) ? "null" : job], LateJoin: [latejoin]")
|
|
if(!player?.mind || !job)
|
|
job_debug("AR: 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
|
|
|
|
job_debug("AR: Role now set and assigned - [player] is [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/find_occupation_candidates(datum/job/job, level = 0)
|
|
job_debug("FOC: Now running, Job: [job], Level: [job_priority_level_to_string(level)]")
|
|
var/list/candidates = list()
|
|
for(var/mob/dead/new_player/player in unassigned)
|
|
if(!player)
|
|
job_debug("FOC: Player no longer exists.")
|
|
continue
|
|
|
|
if(!player.client)
|
|
job_debug("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))
|
|
job_debug("FOC: Player job not enabled, Player: [player]")
|
|
continue
|
|
|
|
if(level && (player_job_level != level))
|
|
job_debug("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 job_debug.
|
|
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.
|
|
job_debug("FOC: Player eligible, Player: [player], Level: [job_priority_level_to_string(level)]")
|
|
candidates += player
|
|
return candidates
|
|
|
|
|
|
/datum/controller/subsystem/job/proc/give_random_job(mob/dead/new_player/player)
|
|
job_debug("GRJ: Giving random job, Player: [player]")
|
|
. = FALSE
|
|
for(var/datum/job/job as anything in shuffle(joinable_occupations))
|
|
if(QDELETED(player))
|
|
job_debug("GRJ: Player is deleted, aborting")
|
|
break
|
|
|
|
if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1)
|
|
job_debug("GRJ: Job lacks spawn positions to be eligible, Player: [player], Job: [job]")
|
|
continue
|
|
|
|
if(istype(job, get_job_type(overflow_role))) // We don't want to give him assistant, that's boring!
|
|
job_debug("GRJ: Skipping overflow role, Player: [player], Job: [job]")
|
|
continue
|
|
|
|
if(job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) //If you want a command position, select it!
|
|
job_debug("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!
|
|
job_debug("GRJ skipping Central Command role, Player: [player], Job: [job]")
|
|
continue
|
|
//SKYRAT EDIT END
|
|
|
|
// This check handles its own output to job_debug.
|
|
if(check_job_eligibility(player, job, "GRJ", add_job_to_log = TRUE) != JOB_AVAILABLE)
|
|
continue
|
|
|
|
if(assign_role(player, job, do_eligibility_checks = FALSE))
|
|
job_debug("GRJ: Random job given, Player: [player], Job: [job]")
|
|
return TRUE
|
|
|
|
job_debug("GRJ: Player eligible but assign_role failed, Player: [player], Job: [job]")
|
|
|
|
|
|
/datum/controller/subsystem/job/proc/reset_occupations()
|
|
job_debug("RO: 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(get_job_type(/datum/job/unassigned))
|
|
setup_occupations()
|
|
unassigned = list()
|
|
if(CONFIG_GET(flag/load_jobs_from_txt))
|
|
// Any errors with the configs has already been said, we don't need to repeat them here.
|
|
load_jobs_from_config(silent = TRUE)
|
|
set_overflow_role(overflow_role)
|
|
return
|
|
|
|
|
|
/*
|
|
* Forces a random Head of Staff role to be assigned to a random eligible player.
|
|
* Returns TRUE if a player was selected and assigned the role. FALSE otherwise.
|
|
*/
|
|
/datum/controller/subsystem/job/proc/force_one_head_assignment()
|
|
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 = find_occupation_candidates(job, level)
|
|
if(!candidates.len)
|
|
continue
|
|
var/mob/dead/new_player/candidate = pick(candidates)
|
|
// Eligibility checks done as part of find_occupation_candidates.
|
|
if(assign_role(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.
|
|
* Returns the number of Head positions assigned.
|
|
*
|
|
* Arguments:
|
|
* * level - One of the JP_LOW, JP_MEDIUM, JP_HIGH or JP_ANY defines. Attempts to find candidates with head jobs at that priority only.
|
|
*/
|
|
/datum/controller/subsystem/job/proc/fill_all_head_positions_at_priority(level)
|
|
. = 0
|
|
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 = find_occupation_candidates(job, level)
|
|
if(!candidates.len)
|
|
continue
|
|
|
|
var/mob/dead/new_player/candidate = pick(candidates)
|
|
|
|
// Eligibility checks done as part of find_occupation_candidates() above.
|
|
if(!assign_role(candidate, job, do_eligibility_checks = FALSE))
|
|
continue
|
|
|
|
.++
|
|
|
|
if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1)
|
|
job_debug("JOBS: Command Job is now full, Job: [job], Positions: [job.current_positions], Limit: [job.spawn_positions]")
|
|
|
|
/// Attempts to fill out all available AI positions.
|
|
/datum/controller/subsystem/job/proc/fill_ai_positions()
|
|
var/datum/job/ai_job = get_job(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 = find_occupation_candidates(ai_job, level)
|
|
if(candidates.len)
|
|
var/mob/dead/new_player/candidate = pick(candidates)
|
|
// Eligibility checks done as part of find_occupation_candidates
|
|
if(assign_role(candidate, get_job_type(/datum/job/ai), do_eligibility_checks = FALSE))
|
|
break
|
|
|
|
|
|
/** Proc divide_occupations
|
|
* 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/divide_occupations(pure = FALSE, allow_all = FALSE)
|
|
//Setup new player list and get the jobs list
|
|
job_debug("DO: Running, allow_all = [allow_all], pure = [pure]")
|
|
run_divide_occupation_pure = pure
|
|
SEND_SIGNAL(src, COMSIG_OCCUPATIONS_DIVIDED, pure, allow_all)
|
|
|
|
//Get the players who are ready
|
|
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
|
|
if(player.ready == PLAYER_READY_TO_PLAY && player.check_job_preferences(!pure) && player.mind && is_unassigned_job(player.mind.assigned_role))
|
|
unassigned += player
|
|
|
|
initial_players_to_assign = length(unassigned)
|
|
|
|
job_debug("DO: Player count to assign roles to: [initial_players_to_assign]")
|
|
|
|
//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/min_access_threshold = CONFIG_GET(number/minimal_access_threshold)
|
|
if(min_access_threshold)
|
|
if(min_access_threshold > initial_players_to_assign)
|
|
CONFIG_SET(flag/jobs_have_minimal_access, FALSE)
|
|
else
|
|
CONFIG_SET(flag/jobs_have_minimal_access, TRUE)
|
|
|
|
//Shuffle player list.
|
|
shuffle_inplace(unassigned)
|
|
|
|
handle_feedback_gathering()
|
|
|
|
// Assign any priority positions before all other standard job selections.
|
|
job_debug("DO: Assigning priority positions")
|
|
assign_priority_positions()
|
|
job_debug("DO: Priority assignment complete")
|
|
|
|
// The overflow role has limitless slots, plus having the Overflow box ticked in prefs should (with one exception) set the priority to JP_HIGH.
|
|
// So everyone with overflow enabled will get that job. Thus we can assign it immediately to all players that have it enabled.
|
|
job_debug("DO: Assigning early overflow roles")
|
|
assign_all_overflow_positions()
|
|
job_debug("DO: Early overflow roles assigned.")
|
|
|
|
// At this point we can assume the following:
|
|
// From assign_priority_positions()
|
|
// 1. If possible, any necessary job roles to allow Dynamic rulesets to execute (such as an AI for malf AI) are satisfied.
|
|
// 2. All Head of Staff roles with any player pref set to JP_HIGH are filled out.
|
|
// 3. If any player not selected by the above has any Head of Staff preference enabled at any JP_ level, there is at least one Head of Staff.
|
|
//
|
|
// From assign_all_overflow_positions()
|
|
// 4. Anyone with the overflow role enabled has been given the overflow role.
|
|
|
|
// Copy the joinable occupation list and filter out ineligible occupations due to above job assignments.
|
|
var/list/available_occupations = joinable_occupations.Copy()
|
|
var/datum/job_department/command_department = get_department_type(/datum/job_department/command)
|
|
|
|
for(var/datum/job/job in available_occupations)
|
|
// Make sure the job isn't filled. If it is, remove it from the list so it doesn't get checked.
|
|
if((job.current_positions >= job.spawn_positions) && job.spawn_positions != -1)
|
|
job_debug("DO: Job is now filled, Job: [job], Current: [job.current_positions], Limit: [job.spawn_positions]")
|
|
available_occupations -= job
|
|
continue
|
|
|
|
// Command jobs are handled via fill_all_head_positions_at_priority(...)
|
|
// Remove these jobs from the list of available occupations to prevent multiple players being assigned to the same
|
|
// limited role without constantly having to iterate over the available_occupations list and re-check them.
|
|
if(job in command_department?.department_jobs)
|
|
available_occupations -= job
|
|
|
|
job_debug("DO: Running standard job assignment")
|
|
|
|
for(var/level in level_order)
|
|
job_debug("JOBS: Filling in head roles, Level: [job_priority_level_to_string(level)]")
|
|
// Fill the head jobs first each level
|
|
fill_all_head_positions_at_priority(level)
|
|
|
|
// Loop through all unassigned players
|
|
for(var/mob/dead/new_player/player in unassigned)
|
|
if(!allow_all)
|
|
if(popcap_reached())
|
|
job_debug("JOBS: Popcap reached, trying to reject player: [player]")
|
|
try_reject_player(player)
|
|
|
|
job_debug("JOBS: Finding a job for player: [player], at job priority pref: [job_priority_level_to_string(level)]")
|
|
|
|
// Loop through all jobs and build a list of jobs this player could be eligible for.
|
|
var/list/possible_jobs = list()
|
|
for(var/datum/job/job in available_occupations)
|
|
// 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))
|
|
job_debug("JOBS: Job not enabled, Job: [job]")
|
|
continue
|
|
if(player_job_level != level)
|
|
job_debug("JOBS: Job enabled at different priority pref, Job: [job], TheirLevel: [job_priority_level_to_string(player_job_level)], ReqLevel: [job_priority_level_to_string(level)]")
|
|
continue
|
|
|
|
if(check_job_eligibility(player, job, "JOBS", add_job_to_log = TRUE) != JOB_AVAILABLE)
|
|
continue
|
|
|
|
possible_jobs += job
|
|
|
|
// If there are no possible jobs for them at this priority, skip them.
|
|
if(!length(possible_jobs))
|
|
job_debug("JOBS: Player not eligible for any available jobs at this priority level: [player]")
|
|
continue
|
|
|
|
// Otherwise, pick one of those jobs at random.
|
|
var/datum/job/picked_job = pick(possible_jobs)
|
|
|
|
job_debug("JOBS: Now assigning role to player: [player], Job:[picked_job.title]")
|
|
assign_role(player, picked_job, do_eligibility_checks = FALSE)
|
|
if((picked_job.current_positions >= picked_job.spawn_positions) && picked_job.spawn_positions != -1)
|
|
job_debug("JOBS: Job is now full, Job: [picked_job], Positions: [picked_job.current_positions], Limit: [picked_job.spawn_positions]")
|
|
available_occupations -= picked_job
|
|
|
|
job_debug("DO: Ending standard job assignment")
|
|
|
|
job_debug("DO: Handle unassigned")
|
|
// For any players that didn't get a job, fall back on their pref setting for what to do.
|
|
for(var/mob/dead/new_player/player in unassigned)
|
|
handle_unassigned(player, allow_all)
|
|
job_debug("DO: Ending handle unassigned")
|
|
|
|
job_debug("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(!give_random_job(player))
|
|
if(!assign_role(player, get_job_type(overflow_role))) //If everything is already filled, make them an assistant
|
|
job_debug("DO: Forced antagonist could not be assigned any random job or the overflow role. divide_occupations failed.")
|
|
job_debug("---------------------------------------------------")
|
|
run_divide_occupation_pure = FALSE
|
|
return FALSE //Living on the edge, the forced antagonist couldn't be assigned to overflow role (bans, client age) - just reroll
|
|
job_debug("DO: Ending handle unrejectable unassigned")
|
|
|
|
job_debug("All divide occupations tasks completed.")
|
|
job_debug("---------------------------------------------------")
|
|
run_divide_occupation_pure = FALSE
|
|
return TRUE
|
|
|
|
//We couldn't find a job from prefs for this guy.
|
|
/datum/controller/subsystem/job/proc/handle_unassigned(mob/dead/new_player/player, allow_all = FALSE)
|
|
var/jobless_role = player.client.prefs.read_preference(/datum/preference/choiced/jobless_role)
|
|
|
|
if(!allow_all)
|
|
if(popcap_reached())
|
|
job_debug("HU: Popcap reached, trying to reject player: [player]")
|
|
try_reject_player(player)
|
|
return
|
|
|
|
switch (jobless_role)
|
|
if (BEOVERFLOW)
|
|
var/datum/job/overflow_role_datum = get_job_type(overflow_role)
|
|
|
|
if((overflow_role_datum.current_positions >= overflow_role_datum.spawn_positions) && overflow_role_datum.spawn_positions != -1)
|
|
job_debug("HU: Overflow role player cap reached, trying to reject: [player]")
|
|
try_reject_player(player)
|
|
return
|
|
|
|
if(check_job_eligibility(player, overflow_role_datum, debug_prefix = "HU", add_job_to_log = TRUE) != JOB_AVAILABLE)
|
|
job_debug("HU: Player cannot be overflow, trying to reject: [player]")
|
|
try_reject_player(player)
|
|
return
|
|
|
|
if(!assign_role(player, overflow_role_datum, do_eligibility_checks = FALSE))
|
|
job_debug("HU: Player could not be assigned overflow role, trying to reject: [player]")
|
|
try_reject_player(player)
|
|
return
|
|
if (BERANDOMJOB)
|
|
if(!give_random_job(player))
|
|
job_debug("HU: Player cannot be given a random job, trying to reject: [player]")
|
|
try_reject_player(player)
|
|
return
|
|
if (RETURNTOLOBBY)
|
|
job_debug("HU: Player unable to be assigned job, return to lobby enabled: [player]")
|
|
try_reject_player(player)
|
|
return
|
|
else //Something gone wrong if we got here.
|
|
job_debug("HU: [player] has an invalid jobless_role var: [jobless_role]")
|
|
log_game("[player] has an invalid jobless_role var: [jobless_role]")
|
|
message_admins("[player] has an invalid jobless_role, this shouldn't happen.")
|
|
try_reject_player(player)
|
|
|
|
|
|
//Gives the player the stuff he should have with his rank
|
|
/datum/controller/subsystem/job/proc/equip_rank(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/alt_title = player_client?.prefs.alt_job_titles?[job.title] || job.title
|
|
// SKYRAT EDIT ADDITION END
|
|
equipping.job = job.title
|
|
|
|
SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job)
|
|
|
|
equipping.mind?.set_assigned_role_with_greeting(job, player_client, alt_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: equipping.mind?.set_assigned_role_with_greeting(job, player_client)
|
|
equipping.on_job_equipping(job, player_client)
|
|
job.announce_job(equipping, alt_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: job.announce_job(equipping)
|
|
|
|
if(player_client?.holder)
|
|
if(CONFIG_GET(flag/auto_deadmin_always) || (player_client.prefs?.toggles & DEADMIN_ALWAYS))
|
|
player_client.holder.auto_deadmin()
|
|
else
|
|
handle_auto_deadmin_roles(player_client, job.title)
|
|
|
|
setup_alt_job_items(equipping, 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 = get_job(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.get_job(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)
|
|
// BUBBER EDIT - Reduced from 10 max sec to 7 max sec due to departmental security being deactivated and replaced.
|
|
var/officer_positions = min(10, max(J.spawn_positions, round(unassigned.len / ssc))) //Scale between configured minimum and 10 officers
|
|
job_debug("SOP: 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/handle_feedback_gathering()
|
|
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
|
|
var/newbie = 0 //exp too low
|
|
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))
|
|
newbie++
|
|
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"))
|
|
SSblackbox.record_feedback("nested tally", "job_preferences", newbie, list("[job.title]", "newbie"))
|
|
|
|
/datum/controller/subsystem/job/proc/popcap_reached()
|
|
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/try_reject_player(mob/dead/new_player/player)
|
|
for(var/datum/dynamic_ruleset/roundstart/ruleset in SSdynamic.queued_rulesets)
|
|
if(player.mind in ruleset.selected_minds)
|
|
job_debug("RJCT: Player unable to be rejected due to being selected by dynamic, Player: [player], Ruleset: [ruleset]")
|
|
return FALSE
|
|
|
|
job_debug("RJCT: Player rejected, Player: [player]")
|
|
unassigned -= player
|
|
if(!run_divide_occupation_pure)
|
|
to_chat(player, span_infoplain("<b>You have failed to qualify for any job you desired.</b>"))
|
|
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_REF(recover_job), job)
|
|
|
|
/datum/controller/subsystem/job/proc/recover_job(datum/job/J)
|
|
var/datum/job/newjob = get_job(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/send_to_late_join(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()
|
|
var/area/shuttle/arrival/arrivals_area = GLOB.areas_by_type[/area/shuttle/arrival]
|
|
if(!isnull(arrivals_area))
|
|
var/list/turf/available_turfs = list()
|
|
for (var/list/zlevel_turfs as anything in arrivals_area.get_zlevel_turf_lists())
|
|
for (var/turf/arrivals_turf as anything in zlevel_turfs)
|
|
var/obj/structure/chair/shuttle_chair = locate() in arrivals_turf
|
|
if(!isnull(shuttle_chair))
|
|
return shuttle_chair
|
|
if(arrivals_turf.is_blocked_turf(TRUE))
|
|
continue
|
|
available_turfs += arrivals_turf
|
|
|
|
if(length(available_turfs))
|
|
return pick(available_turfs)
|
|
|
|
stack_trace("Unable to find last resort spawn point.")
|
|
return GET_ERROR_ROOM
|
|
|
|
/// Returns a list of minds of all heads of staff who are alive
|
|
/datum/controller/subsystem/job/proc/get_living_heads()
|
|
. = list()
|
|
for(var/datum/mind/head as anything in get_crewmember_minds())
|
|
if(!(head.assigned_role.job_flags & JOB_HEAD_OF_STAFF))
|
|
continue
|
|
if(isnull(head.current) || head.current.stat == DEAD)
|
|
continue
|
|
. += head
|
|
|
|
/// Returns a list of minds of all heads of staff
|
|
/datum/controller/subsystem/job/proc/get_all_heads()
|
|
. = list()
|
|
for(var/datum/mind/head as anything in get_crewmember_minds())
|
|
if(head.assigned_role.job_flags & JOB_HEAD_OF_STAFF)
|
|
. += head
|
|
|
|
/// Returns a list of minds of all security members who are alive
|
|
/datum/controller/subsystem/job/proc/get_living_sec()
|
|
. = list()
|
|
for(var/datum/mind/sec as anything in get_crewmember_minds())
|
|
if(!(sec.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY))
|
|
continue
|
|
if(isnull(sec.current) || sec.current.stat == DEAD)
|
|
continue
|
|
. += sec
|
|
|
|
/// Returns a list of minds of all security members
|
|
/datum/controller/subsystem/job/proc/get_all_sec()
|
|
. = list()
|
|
for(var/datum/mind/sec as anything in get_crewmember_minds())
|
|
if(sec.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
|
|
. += sec
|
|
|
|
/datum/controller/subsystem/job/proc/job_debug(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/paperslip/corporate/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/paperslip/corporate/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/paperslip/corporate/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/paperslip/corporate/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/folder/biscuit/confidential/spare_id_safe_code()
|
|
var/list/slots = list(
|
|
LOCATION_LPOCKET,
|
|
LOCATION_RPOCKET,
|
|
LOCATION_BACKPACK,
|
|
LOCATION_HANDS,
|
|
)
|
|
var/where = new_captain.equip_in_one_of_slots(paper, slots, FALSE, indirect_action = TRUE) || "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]."))
|
|
new_captain.add_mob_memory(/datum/memory/key/captains_spare_code, safe_code = SSid_access.spare_id_safe_code)
|
|
|
|
// 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/folder/biscuit/confidential/emergency_spare_id_safe_code())
|
|
safe_code_timer_id = null
|
|
safe_code_request_loc = null
|
|
|
|
/// Assigns roles that are considered high priority, either due to dynamic needing to force a specific role for a specific ruleset
|
|
/// or making sure roles critical to round progression exist where possible every shift.
|
|
/datum/controller/subsystem/job/proc/assign_priority_positions()
|
|
job_debug("APP: Assigning Dynamic ruleset forced occupations: [LAZYLEN(forced_occupations)]")
|
|
for(var/datum/mind/mind as anything in forced_occupations)
|
|
var/mob/dead/new_player = mind.current
|
|
// Eligibility checks already carried out as part of the dynamic ruleset trim_candidates proc.
|
|
// However no guarantee of game state between then and now, so don't skip eligibility checks on assign_role.
|
|
assign_role(new_player, get_job_type(LAZYACCESS(forced_occupations, mind)))
|
|
|
|
// Get JP_HIGH department Heads of Staff in place. Indirectly useful for the Revolution ruleset to have as many Heads as possible.
|
|
job_debug("APP: Assigning all JP_HIGH head of staff roles.")
|
|
var/head_count = fill_all_head_positions_at_priority(JP_HIGH)
|
|
|
|
// If nobody has JP_HIGH on a Head role, try to force at least one Head of Staff so every shift has the best chance
|
|
// of having at least one leadership role.
|
|
if(head_count == 0)
|
|
force_one_head_assignment()
|
|
|
|
// Fill out all AI positions.
|
|
job_debug("APP: Filling all AI positions")
|
|
fill_ai_positions()
|
|
|
|
/datum/controller/subsystem/job/proc/assign_all_overflow_positions()
|
|
job_debug("OVRFLW: Assigning all overflow roles.")
|
|
job_debug("OVRFLW: This shift's overflow role: [overflow_role]")
|
|
var/datum/job/overflow_datum = get_job_type(overflow_role)
|
|
|
|
// When the Overflow role changes for any reason, this allows players to set otherwise invalid job priority pref states.
|
|
// So if Assistant is the "usual" Overflow but it gets changed to Clown for a shift, players can set the Assistant role's priorities
|
|
// to JP_MEDIUM and JP_LOW. When the "usual" Overflow role comes back, it returns to an On option in the prefs menu but still
|
|
// keeps its old JP_MEDIUM or JP_LOW value in the background.
|
|
|
|
// Due to this prefs quirk, we actually don't want to find JP_HIGH candidates as it may exclude people with abnormal pref states that
|
|
// appear normal from the UI. By passing in JP_ANY, it will return all players that have the overflow job pref (which should be a toggle)
|
|
// set to any level.
|
|
var/list/overflow_candidates = find_occupation_candidates(overflow_datum, JP_ANY)
|
|
job_debug("OVRFLW: Attempting to assign the overflow role to [length(overflow_candidates)] players.")
|
|
for(var/mob/dead/new_player/player in overflow_candidates)
|
|
if((overflow_datum.current_positions >= overflow_datum.spawn_positions) && overflow_datum.spawn_positions != -1)
|
|
job_debug("OVRFLW: Overflow role cap reached, role only assigned to [overflow_datum.current_positions] players.")
|
|
job_debug("OVRFLW: Overflow Job is now full, Job: [overflow_datum], Positions: [overflow_datum.current_positions], Limit: [overflow_datum.spawn_positions]")
|
|
return
|
|
|
|
// Eligibility checks done as part of find_occupation_candidates, so skip them.
|
|
assign_role(player, get_job_type(overflow_role), do_eligibility_checks = FALSE)
|
|
job_debug("OVRFLW: Assigned overflow to player: [player]")
|
|
job_debug("OVRFLW: All overflow roles assigned.")
|
|
|
|
/// 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 job_debug log entries. For example, GRJ during give_random_job or DO during divide_occupations.
|
|
* * 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)
|
|
job_debug("[debug_prefix]: Player has no mind, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_GENERIC
|
|
|
|
if(possible_job.title in LAZYACCESS(prevented_occupations, player.mind))
|
|
job_debug("[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))
|
|
job_debug("[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)
|
|
job_debug("[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))
|
|
job_debug("[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
|
|
|
|
// Check for character age
|
|
var/client/player_client = GET_CLIENT(player)
|
|
if(isnum(possible_job.required_character_age) && possible_job.required_character_age > player_client.prefs.read_preference(/datum/preference/numeric/age))
|
|
job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_AGE)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_AGE
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
|
|
if(possible_job.has_banned_quirk(player.client.prefs))
|
|
job_debug("[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))
|
|
job_debug("[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))
|
|
job_debug("[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(CONFIG_GET(flag/min_flavor_text))
|
|
//BUBBER EDIT ADDITION: SILICON FLAVOR TEXT CHECK
|
|
var/uses_silicon_flavortext = (is_silicon_job(possible_job) && length_char(player.client?.prefs.read_preference(/datum/preference/text/silicon_flavor_text)) <= CONFIG_GET(number/silicon_flavor_text_character_requirement))
|
|
var/uses_normal_flavortext = (!is_silicon_job(possible_job) && length_char(player.client?.prefs.read_preference(/datum/preference/text/flavor_text)) <= CONFIG_GET(number/flavor_text_character_requirement))
|
|
if(uses_silicon_flavortext)
|
|
job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_FLAVOUR)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_FLAVOUR_SILICON
|
|
if(uses_normal_flavortext)
|
|
//BUBBER EDIT END: SILICON FLAVOR TEXT CHECK
|
|
job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_FLAVOUR)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_FLAVOUR
|
|
|
|
if(possible_job.has_banned_augment(player.client.prefs))
|
|
job_debug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_AUGMENT)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_AUGMENT
|
|
|
|
|
|
//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))
|
|
job_debug("[debug_prefix]: Player is qdeleted, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
|
|
return JOB_UNAVAILABLE_GENERIC
|
|
|
|
return JOB_AVAILABLE
|
|
|
|
/**
|
|
* Check if the station manifest has at least a certain amount of this staff type.
|
|
* If a matching head of staff is on the manifest, automatically passes (returns TRUE)
|
|
*
|
|
* Arguments:
|
|
* * crew_threshold - amount of crew to meet the requirement
|
|
* * jobs - a list of jobs that qualify the requirement
|
|
* * head_jobs - a list of head jobs that qualify the requirement
|
|
*
|
|
*/
|
|
/datum/controller/subsystem/job/proc/has_minimum_jobs(crew_threshold, list/jobs = list(), list/head_jobs = list())
|
|
var/employees = 0
|
|
for(var/datum/record/crew/target in GLOB.manifest.general)
|
|
if(target.trim in head_jobs)
|
|
return TRUE
|
|
if(target.trim in jobs)
|
|
employees++
|
|
|
|
if(employees > crew_threshold)
|
|
return TRUE
|
|
|
|
return FALSE
|