Assorted changes to job assignment code and logging. Runtime free, guaranteed or your money back. Price: $£0. (#85947)

## About The Previous Pull Request

#85308 reverted by #85929


![image](https://github.com/user-attachments/assets/e7518dcb-a60a-4bf1-a3d4-a5a8966d8633)

~~Causes the round to not start when a player isn't eligible for any
jobs at a specific priority level due to runtimes trying to `pick()`
from an empty list aborting the entire job assignment stack.~~
(Fixed???? by
https://github.com/tgstation/tgstation/pull/85947/commits/e0e9f2f430079d4ab7097abe12e75f934131a638)

Maybe we should test merge this for a mo just to make sure no more
cheeky runtimes pop up before merging.

## About The Pull Request

This PR does a couple of minor things:
Makes the job debug logging a bit easier to follow.
Minorly brings some SSjob code up to code standards, converting proc
names to snake_case and doing some otherm is cleanup.
Refactored some stuff into different procs, updated some comments.

And some major things:
Changes the job assignment logic.
Old behaviour
> Assign dynamic priority roles
> Force one Head of Staff (if possible)
> Assign all AIs
> Assign overflow roles (bugged in 2 ways)
> Shuffle the available jobs list once, at the start of the random job
assignment loop
> Pick and assign random jobs for random players from High prefs down,
with a priority on Head of Staff roles
> Handle everyone that couldn't be assigned a random job

New behaviour
> Assign dynamic priority roles
> Assign all Head of Staff roles to players with High prefs
> If no Head of Staff was made in the above way, force one Head of Staff
(if possible)
> Assign all AIs
> Assign overflow roles (fixed)
> Prioritise and fill unfilled head roles at each job priority pref
level, from High prefs down.
> Build a list of all jobs that each unassigned player could be eligible
for at the above pref level.
> Pick a job from that list at random and assign it to the player.
> Handle everyone that couldn't be assigned a random job.

In reality there should be little impact on overall job assignment, the
code changes read more as semantics. For example, the priority check for
filling Head slots will have the same candidate pool in both old and new
versions, but in the new version we're more clearly saying that Heads
are important and we want to prioritise filling them for the sake of
round progression even though the outcome in new and old is the same.

A key change will lead to an increase in assistants - Overflow fixes.

Currently the code block to do early assignments to the Overflow role
doesn't work - or works but not as you'd expect. The idea was is that
because enabling the Overflow role in the prefs menu is an On/Off toggle
that sets the job to High priority when enabled and prevents any other
High priority pref, players that have the Overflow role enabled will
**always** get it. It's their highest priority job with infinite slots.
So we do a pass right at the start to give everyone with the Overflow
role enabled that role and save us wasting time later on in random job
code giving them that same role but with more work.

The problem is the code for this only assigns the Overflow role to
people with it set to Low priority in their prefs, resulting in log
readouts like:
```
[2024-07-27 09:49:43.469] DEBUG-JOB: DO, Running Overflow Check 1
[2024-07-27 09:49:43.469] DEBUG-JOB: Running FOC, Job: /datum/job/assistant, Level: Low Priority
[2024-07-27 09:49:43.472] DEBUG-JOB: FOC player job enabled at wrong level, Player: Radioprague, TheirLevel: Medium Priority, ReqLevel: Low Priority
[2024-07-27 09:49:43.472] DEBUG-JOB: FOC player job enabled at wrong level, Player: Caluan, TheirLevel: High Priority, ReqLevel: Low Priority
[2024-07-27 09:49:43.473] DEBUG-JOB: FOC player job enabled at wrong level, Player: Caractaser, TheirLevel: High Priority, ReqLevel: Low Priority
[2024-07-27 09:49:43.473] DEBUG-JOB: FOC player job enabled at wrong level, Player: Apsua, TheirLevel: High Priority, ReqLevel: Low Priority
[2024-07-27 09:49:43.475] DEBUG-JOB: FOC player job enabled at wrong level, Player: Bebrus2, TheirLevel: Medium Priority, ReqLevel: Low Priority
[2024-07-27 09:49:43.475] DEBUG-JOB: AC1, Candidates: 0
```
Where nobody gets pre-assigned the overflow role because their prefs are
all set to the High priority from being toggled... Except wait a second,
some people have it at Medium priority when it should just be a No
Role/High Priority Role toggle?

And herein we meet a problem. My hypothesis is that traits and stuff
that change the overflow have allowed players to set the "ordinary"
overflow role of Assistant to Medium and/or Low priority.

This still shows as enabled in the prefs menu, but leads to an outcome
where a player with assistant enabled is assigned Cook instead.
```
[2024-07-27 09:49:47.775] DEBUG-JOB: DO, Running Overflow Check 1
[2024-07-27 09:49:47.775] DEBUG-JOB: Running FOC, Job: /datum/job/assistant, Level: Low Priority
...
[2024-07-27 09:49:43.475] DEBUG-JOB: FOC player job enabled at wrong level, Player: Bebrus2, TheirLevel: Medium Priority, ReqLevel: Low Priority
...
[2024-07-27 09:49:47.987] DEBUG-JOB: Running AR, Player: Bebrus2, Job: /datum/job/cook, LateJoin: 0
```

So players with the Overflow job pref set to Low (an unexpected state,
should be disabled or High) would be guaranteed to get that role if none
of the higher priority Head of Staff/AI/Dynamic roles took over via the
bugged "force overflow for people with the pref enabled" proc.

Players with the Overflow job pref set to High would be guaranteed to
get that role if none of the higher priority Head of Staff/AI/Dynamic
roles took over via the random job assignment code giving them their
Highest priority role thanks to the infinite job slots of the Overflow.

And players with the Overflow job pref set to Medium (an unexpected
state, should be disabled or High) would get Assistant if the shuffle
step of the available jobs list put Assisstant before any of the other
jobs they had prefs enabled for at Medium that weren't already filled,
otherwise they'd get another random job.

This code is now changed to ignore the priority the player has set when
looking for people to fill the overflow role. As long as it **is**
enabled, the player will get it unless they're forced into a dynamic
ruleset role (AI when malf rolls) or a Head of Staff role due to their
other prefs (they have RD set to med or low, and no other player has a
Head of Staff at high so they get randomly picked and miss the overflow
role).

This will increase the number of assistants in shifts where their pref
state has Assisstant in the bugged Medium priority, but doesn't change
it for bugged Low and not-bugged High/On priority.

On the other side of the coin, we have how the random jobs are picked.
They're kinda not random, and I noticed this reading the logs then
reading the code.

The list of available jobs to pick from is randomly shuffled - but only
**once**. All players pull from a list of jobs in the same order. So you
end up with a log block like this:
```
[2024-07-27 09:49:47.985] DEBUG-JOB: DO pass, Player: Pierow, Level:3, Job:Botanist
[2024-07-27 09:49:47.985] DEBUG-JOB: Running AR, Player: Pierow, Job: /datum/job/botanist, LateJoin: 0
[2024-07-27 09:49:47.985] DEBUG-JOB: Player: Pierow is now Rank: Botanist, JCP:0, JPL:2
[2024-07-27 09:49:47.986] DEBUG-JOB: DO pass, Player: Daddos, Level:3, Job:Botanist
[2024-07-27 09:49:47.986] DEBUG-JOB: Running AR, Player: Daddos, Job: /datum/job/botanist, LateJoin: 0
[2024-07-27 09:49:47.986] DEBUG-JOB: Player: Daddos is now Rank: Botanist, JCP:1, JPL:2
[2024-07-27 09:49:47.986] DEBUG-JOB: FOC job filled and not overflow, Player: Bebrus2, Job: /datum/job/botanist, Current: 2, Limit: 2
[2024-07-27 09:49:47.987] DEBUG-JOB: FOC player job not enabled, Player: Bebrus2
[2024-07-27 09:49:47.987] DEBUG-JOB: DO pass, Player: Bebrus2, Level:3, Job:Cook
[2024-07-27 09:49:47.987] DEBUG-JOB: Running AR, Player: Bebrus2, Job: /datum/job/cook, LateJoin: 0
[2024-07-27 09:49:47.988] DEBUG-JOB: Player: Bebrus2 is now Rank: Cook, JCP:0, JPL:1
[2024-07-27 09:49:47.988] DEBUG-JOB: FOC player job not enabled, Player: Redwizz
[2024-07-27 09:49:47.988] DEBUG-JOB: FOC job filled and not overflow, Player: Redwizz, Job: /datum/job/cook, Current: 1, Limit: 1
```

The list is shuffled into an order of something like `list("Scientist",
"Botanist", "Cook", "Sec Officer", ...)` then iterated over for each
player. So every random job selection goes:
> "Does Player1 have Scientist enabled and at the right priority? No?
Okay, Botanist? Yes? You get botanist."
> "Does Player2 have Scientist enabled and at the right priority? No?
Okay, Botanist? Yes? You get botanist."
> "Does Player3 have Scientist enabled and at the right priority? No?
Okay, Botanist has no slots left so we'll remove it from the list. Okay,
Cook? Yes? You get cook."
> "Does Player4 have Scientist enabled and at the right priority? No?
Okay, Cook has no slots left so we'll remove it from the list. Okay, Sec
Officer? ..."

This can lead to stacked individual departments if it gets randomly
rolled to the start of the list in the shuffle, and completely empty
departments if they end up at the end.

On high pop shifts this is probably less of an issue. Player prefs add
noise to this and as departments at the front fill up, those at the back
pick up some of the lower pref players.

But have you ever had a shift where there's just like... No fucking sec
even though there's tons of players? The logging (before I made changes
in this PR) was a bit ass, but my hypothesis there is that sec officer
was shuffled right at the end of the random job list, so every other
department was filled up before sec officers were picked.

To mitigate this, I made the list shuffle every single time the game
picks a random available job for the player. This should lead to a more
balanced selection of available jobs by avoiding situations where the
code is biased towards packing some departments by accident.
## Why It's Good For The Game

Overflow fixes mean people who go to their prefs and see the Overflow
Role is On will all have the same experience - They will be the Overflow
role.

More random random job selection should prevent individual departments
having a jobs be stacked when it would have otherwise been possible for
a more balanced selection but the code unintentially biased random
departments to be overstaffed and understaffed each shift.
## Changelog
🆑
fix: Having the Overflow Role set to On will properly ensure you get
that role at a High priority as intended by the game code.
fix: Job selection is now a little bit more random. Fixes an
unintentional bias in random job assignment that could lead to
feast-or-famine for roles where everyone is assigned one job and nobody
is assigned another job.
/🆑
This commit is contained in:
Timberpoes
2024-09-13 13:58:35 +02:00
committed by GitHub
parent 48f51a7c34
commit 6808a082eb
69 changed files with 328 additions and 295 deletions
@@ -526,7 +526,7 @@ SUBSYSTEM_DEF(dynamic)
//To new_player and such, and we want the datums to just free when the roundstart work is done
var/list/roundstart_rules = init_rulesets(/datum/dynamic_ruleset/roundstart)
SSjob.DivideOccupations(pure = TRUE, allow_all = TRUE)
SSjob.divide_occupations(pure = TRUE, allow_all = TRUE)
for(var/i in GLOB.new_player_list)
var/mob/dead/new_player/player = i
if(player.ready == PLAYER_READY_TO_PLAY && player.mind && player.check_preferences())
@@ -541,7 +541,7 @@ SUBSYSTEM_DEF(dynamic)
else
roundstart_pop_ready++
candidates.Add(player)
SSjob.ResetOccupations()
SSjob.reset_occupations()
log_dynamic("Listing [roundstart_rules.len] round start rulesets, and [candidates.len] players ready.")
if (candidates.len <= 0)
log_dynamic("[candidates.len] candidates.")
@@ -1018,7 +1018,7 @@ SUBSYSTEM_DEF(dynamic)
var/list/reopened_jobs = list()
for(var/mob/living/quitter in GLOB.suicided_mob_list)
var/datum/job/job = SSjob.GetJob(quitter.job)
var/datum/job/job = SSjob.get_job(quitter.job)
if(!job || !(job.job_flags & JOB_REOPEN_ON_ROUNDSTART_LOSS))
continue
if(!include_command && job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
@@ -283,7 +283,7 @@
if(length(exclusive_roles))
var/exclusive_candidate = FALSE
for(var/role in exclusive_roles)
var/datum/job/job = SSjob.GetJob(role)
var/datum/job/job = SSjob.get_job(role)
if((role in candidate_client.prefs.job_preferences) && SSjob.check_job_eligibility(candidate_player, job, "Dynamic Roundstart TC", add_job_to_log = TRUE) == JOB_AVAILABLE)
exclusive_candidate = TRUE
@@ -413,7 +413,7 @@
return ..()
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_setup(mob/new_character, index)
new_character.mind.set_assigned_role(SSjob.GetJobType(/datum/job/nuclear_operative))
new_character.mind.set_assigned_role(SSjob.get_job_type(/datum/job/nuclear_operative))
new_character.mind.special_role = ROLE_NUCLEAR_OPERATIVE
if(index == 1)
var/datum/antagonist/nukeop/leader/leader_antag_datum = new()
@@ -572,7 +572,7 @@
var/mob/living/carbon/human/new_nightmare = new (find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE))
player_mind.transfer_to(new_nightmare)
player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/nightmare))
player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/nightmare))
player_mind.special_role = ROLE_NIGHTMARE
player_mind.add_antag_datum(/datum/antagonist/nightmare)
new_nightmare.set_species(/datum/species/shadow/nightmare)
@@ -991,7 +991,7 @@
var/mob/living/carbon/human/voidwalker = new (space_turf)
player_mind.transfer_to(voidwalker)
player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/voidwalker))
player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/voidwalker))
player_mind.special_role = antag_flag
player_mind.add_antag_datum(antag_datum)
@@ -63,7 +63,7 @@ GLOBAL_VAR_INIT(revolutionary_win, FALSE)
flags = HIGH_IMPACT_RULESET
/datum/dynamic_ruleset/roundstart/malf_ai/ready(forced)
var/datum/job/ai_job = SSjob.GetJobType(/datum/job/ai)
var/datum/job/ai_job = SSjob.get_job_type(/datum/job/ai)
// If we're not forced, we're going to make sure we can actually have an AI in this shift,
if(!forced && min(ai_job.total_positions - ai_job.current_positions, ai_job.spawn_positions) <= 0)
@@ -75,7 +75,7 @@ GLOBAL_VAR_INIT(revolutionary_win, FALSE)
/datum/dynamic_ruleset/roundstart/malf_ai/pre_execute(population)
. = ..()
var/datum/job/ai_job = SSjob.GetJobType(/datum/job/ai)
var/datum/job/ai_job = SSjob.get_job_type(/datum/job/ai)
// Maybe a bit too pedantic, but there should never be more malf AIs than there are available positions, spawn positions or antag cap allocations.
var/num_malf = min(get_antag_cap(population), min(ai_job.total_positions - ai_job.current_positions, ai_job.spawn_positions))
for (var/i in 1 to num_malf)
@@ -296,7 +296,7 @@ GLOBAL_VAR_INIT(revolutionary_win, FALSE)
var/mob/M = pick_n_take(candidates)
if (M)
assigned += M.mind
M.mind.set_assigned_role(SSjob.GetJobType(/datum/job/space_wizard))
M.mind.set_assigned_role(SSjob.get_job_type(/datum/job/space_wizard))
M.mind.special_role = ROLE_WIZARD
return TRUE
@@ -429,7 +429,7 @@ GLOBAL_VAR_INIT(revolutionary_win, FALSE)
break
var/mob/M = pick_n_take(candidates)
assigned += M.mind
M.mind.set_assigned_role(SSjob.GetJobType(job_type))
M.mind.set_assigned_role(SSjob.get_job_type(job_type))
M.mind.special_role = required_role
return TRUE
+226 -194
View File
@@ -22,7 +22,7 @@ SUBSYSTEM_DEF(job)
var/list/unassigned = list() //Players who need jobs
var/initial_players_to_assign = 0 //used for checking against population caps
// Whether to run DivideOccupations pure so that there are no side-effects from calling it other than
// 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
@@ -31,7 +31,7 @@ SUBSYSTEM_DEF(job)
var/overflow_role = /datum/job/assistant
var/list/level_order = list(JP_HIGH,JP_MEDIUM,JP_LOW)
var/list/level_order = list(JP_HIGH, JP_MEDIUM, JP_LOW)
/// Lazylist of mob:occupation_string pairs.
var/list/dynamic_forced_occupations
@@ -88,7 +88,7 @@ SUBSYSTEM_DEF(job)
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))
SetupOccupations()
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.
@@ -108,9 +108,9 @@ SUBSYSTEM_DEF(job)
return overflow_jobs
/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)
var/datum/job/new_overflow = ispath(new_overflow_role) ? get_job_type(new_overflow_role) : get_job(new_overflow_role)
if(!new_overflow)
JobDebug("Failed to set new overflow role: [new_overflow_role]")
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)
@@ -121,17 +121,16 @@ SUBSYSTEM_DEF(job)
if(new_overflow.type == overflow_role)
return
var/datum/job/old_overflow = GetJobType(overflow_role)
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
JobDebug("Overflow role set to : [new_overflow.type]")
job_debug("SET_OVRFLW: Overflow role set to: [new_overflow.type]")
/datum/controller/subsystem/job/proc/SetupOccupations()
/datum/controller/subsystem/job/proc/setup_occupations()
name_occupations = list()
type_occupations = list()
@@ -205,20 +204,20 @@ SUBSYSTEM_DEF(job)
return TRUE
/datum/controller/subsystem/job/proc/GetJob(rank)
/datum/controller/subsystem/job/proc/get_job(rank)
if(!length(all_occupations))
SetupOccupations()
setup_occupations()
return name_occupations[rank]
/datum/controller/subsystem/job/proc/GetJobType(jobtype)
/datum/controller/subsystem/job/proc/get_job_type(jobtype)
RETURN_TYPE(/datum/job)
if(!length(all_occupations))
SetupOccupations()
setup_occupations()
return type_occupations[jobtype]
/datum/controller/subsystem/job/proc/get_department_type(department_type)
if(!length(all_occupations))
SetupOccupations()
setup_occupations()
return joinable_departments_by_type[department_type]
/**
@@ -230,89 +229,92 @@ SUBSYSTEM_DEF(job)
* * 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]")
/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)
JobDebug("AR has failed, player has no mind or job is null, Player: [player], Rank: [isnull(job) ? "null" : job.type]")
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
JobDebug("Player: [player] is now Rank: [job.title], JCP:[job.current_positions], JPL:[latejoin ? job.total_positions : job.spawn_positions]")
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/FindOccupationCandidates(datum/job/job, level)
JobDebug("Running FOC, Job: [job], Level: [job_priority_level_to_string(level)]")
/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)
JobDebug("FOC player no longer exists.")
job_debug("FOC: Player no longer exists.")
continue
if(!player.client)
JobDebug("FOC player client no longer exists, Player: [player]")
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))
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)]")
job_debug("FOC: Player job not enabled, Player: [player]")
continue
// This check handles its own output to JobDebug.
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.
JobDebug("FOC pass, Player: [player], Level: [job_priority_level_to_string(level)]")
job_debug("FOC: Player eligible, 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]")
/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))
JobDebug("GRJ player is deleted, aborting")
job_debug("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]")
job_debug("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]")
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!
JobDebug("GRJ skipping command role, Player: [player], Job: [job]")
job_debug("GRJ: Skipping command role, Player: [player], Job: [job]")
continue
// This check handles its own output to JobDebug.
// 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(AssignRole(player, job, do_eligibility_checks = FALSE))
JobDebug("GRJ Random job given, Player: [player], Job: [job]")
if(assign_role(player, job, do_eligibility_checks = FALSE))
job_debug("GRJ: Random job given, Player: [player], Job: [job]")
return TRUE
JobDebug("GRJ Player eligible but AssignRole failed, Player: [player], Job: [job]")
job_debug("GRJ: Player eligible but assign_role failed, Player: [player], Job: [job]")
/datum/controller/subsystem/job/proc/ResetOccupations()
JobDebug("Occupations reset.")
/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(GetJobType(/datum/job/unassigned))
player.mind.set_assigned_role(get_job_type(/datum/job/unassigned))
player.mind.special_role = null
SetupOccupations()
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.
@@ -321,12 +323,11 @@ SUBSYSTEM_DEF(job)
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.
/*
* 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/FillHeadPosition()
/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
@@ -334,60 +335,65 @@ SUBSYSTEM_DEF(job)
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)
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 FindOccupationCandidates.
if(AssignRole(candidate, job, do_eligibility_checks = FALSE))
// 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 or JP_HIGH defines. Attempts to find candidates with head jobs at this priority only.
* * 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/CheckHeadPositions(level)
/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
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)
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 FindOccupationCandidates
AssignRole(candidate, job, do_eligibility_checks = FALSE)
// Eligibility checks done as part of find_occupation_candidates
if(assign_role(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)
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 = FindOccupationCandidates(ai_job, level)
candidates = find_occupation_candidates(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))
// 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 DivideOccupations
/** 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/DivideOccupations(pure = FALSE, allow_all = FALSE)
/datum/controller/subsystem/job/proc/divide_occupations(pure = FALSE, allow_all = FALSE)
//Setup new player list and get the jobs list
JobDebug("Running DO, allow_all = [allow_all], pure = [pure]")
job_debug("DO: Running, allow_all = [allow_all], pure = [pure]")
run_divide_occupation_pure = pure
SEND_SIGNAL(src, COMSIG_OCCUPATIONS_DIVIDED, pure, allow_all)
@@ -397,162 +403,166 @@ SUBSYSTEM_DEF(job)
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
initial_players_to_assign = length(unassigned)
JobDebug("DO, Len: [unassigned.len]")
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/mat = CONFIG_GET(number/minimal_access_threshold)
if(mat)
if(mat > unassigned.len)
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 players and jobs
unassigned = shuffle(unassigned)
//Shuffle player list.
shuffle_inplace(unassigned)
HandleFeedbackGathering()
handle_feedback_gathering()
// Dynamic has picked a ruleset that requires enforcing some jobs before others.
JobDebug("DO, Assigning Priority Positions: [length(dynamic_forced_occupations)]")
// 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")
//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")
// 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.")
//Select one head
JobDebug("DO, Running Head Check")
FillHeadPosition()
JobDebug("DO, Head Check end")
// 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.
// Fill out any remaining AI positions.
JobDebug("DO, Running AI Check")
fill_ai_positions()
JobDebug("DO, AI Check end")
// Shuffle the joinable occupation list and filter out ineligible occupations due to above job assignments.
var/list/available_occupations = joinable_occupations.Copy()
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
//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.
job_debug("DO: Running standard job assignment")
// 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)
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(PopcapReached())
RejectPlayer(player)
if(popcap_reached())
job_debug("JOBS: Popcap reached, trying to reject player: [player]")
try_reject_player(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
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))
JobDebug("FOC player job not enabled, Player: [player]")
job_debug("JOBS: Job not enabled, Job: [job]")
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)]")
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, "DO", add_job_to_log = TRUE) != JOB_AVAILABLE)
if(check_job_eligibility(player, job, "JOBS", 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
possible_jobs += job
JobDebug("DO, Ending standard job assignment")
// 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
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
// 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)
HandleUnassigned(player, allow_all)
JobDebug("DO, Ending handle unassigned.")
handle_unassigned(player, allow_all)
job_debug("DO: Ending handle unassigned")
JobDebug("DO, Handle unrejectable 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(!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("---------------------------------------------------")
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
JobDebug("DO, Ending handle unrejectable unassigned")
job_debug("DO: Ending handle unrejectable unassigned")
JobDebug("All divide occupations tasks completed.")
JobDebug("---------------------------------------------------")
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/HandleUnassigned(mob/dead/new_player/player, allow_all = FALSE)
/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(PopcapReached())
RejectPlayer(player)
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 = GetJobType(overflow_role)
var/datum/job/overflow_role_datum = get_job_type(overflow_role)
if(check_job_eligibility(player, overflow_role_datum, debug_prefix = "HU", add_job_to_log = TRUE) != JOB_AVAILABLE)
RejectPlayer(player)
job_debug("HU: Player cannot be overflow, trying to reject: [player]")
try_reject_player(player)
return
if(!AssignRole(player, overflow_role_datum, do_eligibility_checks = FALSE))
RejectPlayer(player)
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(!GiveRandomJob(player))
RejectPlayer(player)
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)
RejectPlayer(player)
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.
var/message = "HU: [player] fell through handling unassigned"
JobDebug(message)
log_game(message)
message_admins(message)
RejectPlayer(player)
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/EquipRank(mob/living/equipping, datum/job/job, client/player_client)
/datum/controller/subsystem/job/proc/equip_rank(mob/living/equipping, datum/job/job, client/player_client)
equipping.job = job.title
SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job)
@@ -572,7 +582,7 @@ SUBSYSTEM_DEF(job)
/datum/controller/subsystem/job/proc/handle_auto_deadmin_roles(client/C, rank)
if(!C?.holder)
return TRUE
var/datum/job/job = GetJob(rank)
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
@@ -590,7 +600,7 @@ SUBSYSTEM_DEF(job)
return C.holder.auto_deadmin()
/datum/controller/subsystem/job/proc/setup_officer_positions()
var/datum/job/J = SSjob.GetJob(JOB_SECURITY_OFFICER)
var/datum/job/J = SSjob.get_job(JOB_SECURITY_OFFICER)
if(!J)
CRASH("setup_officer_positions(): Security officer job is missing")
@@ -598,7 +608,7 @@ SUBSYSTEM_DEF(job)
if(ssc > 0)
if(J.spawn_positions > 0)
var/officer_positions = min(12, max(J.spawn_positions, round(unassigned.len / ssc))) //Scale between configured minimum and 12 officers
JobDebug("Setting open security officer positions to [officer_positions]")
job_debug("SOP: Setting open security officer positions to [officer_positions]")
J.total_positions = officer_positions
J.spawn_positions = officer_positions
@@ -614,7 +624,7 @@ SUBSYSTEM_DEF(job)
else //We ran out of spare locker spawns!
break
/datum/controller/subsystem/job/proc/HandleFeedbackGathering()
/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
@@ -653,7 +663,7 @@ SUBSYSTEM_DEF(job)
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/PopcapReached()
/datum/controller/subsystem/job/proc/popcap_reached()
var/hpc = CONFIG_GET(number/hard_popcap)
var/epc = CONFIG_GET(number/extreme_popcap)
if(hpc || epc)
@@ -662,12 +672,12 @@ SUBSYSTEM_DEF(job)
return 1
return 0
/datum/controller/subsystem/job/proc/RejectPlayer(mob/dead/new_player/player)
/datum/controller/subsystem/job/proc/try_reject_player(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]")
job_debug("RJCT: Player unable to be rejected due to special_role, Player: [player], SpecialRole: [player.mind.special_role]")
return FALSE
job_debug("RJCT: Player rejected, Player: [player]")
unassigned -= player
if(!run_divide_occupation_pure)
to_chat(player, "<span class='infoplain'><b>You have failed to qualify for any job you desired.</b></span>")
@@ -679,10 +689,10 @@ SUBSYSTEM_DEF(job)
var/oldjobs = SSjob.all_occupations
sleep(2 SECONDS)
for (var/datum/job/job as anything in oldjobs)
INVOKE_ASYNC(src, PROC_REF(RecoverJob), job)
INVOKE_ASYNC(src, PROC_REF(recover_job), job)
/datum/controller/subsystem/job/proc/RecoverJob(datum/job/J)
var/datum/job/newjob = GetJob(J.title)
/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
@@ -699,7 +709,7 @@ SUBSYSTEM_DEF(job)
if(buckle && isliving(joining_mob))
buckle_mob(joining_mob, FALSE, FALSE)
/datum/controller/subsystem/job/proc/SendToLateJoin(mob/M, buckle = TRUE)
/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])
@@ -734,19 +744,6 @@ SUBSYSTEM_DEF(job)
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
podspawn(list(
"target" = spawn_turf,
"path" = /obj/structure/closet/supplypod/centcompod,
"spawn" = living_mob
))
/// Returns a list of minds of all heads of staff who are alive
/datum/controller/subsystem/job/proc/get_living_heads()
. = list()
@@ -781,7 +778,7 @@ SUBSYSTEM_DEF(job)
if(sec.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
. += sec
/datum/controller/subsystem/job/proc/JobDebug(message)
/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.
@@ -846,12 +843,47 @@ SUBSYSTEM_DEF(job)
safe_code_timer_id = null
safe_code_request_loc = null
/// Blindly assigns the required roles to every player in the dynamic_forced_occupations list.
/// 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: [length(dynamic_forced_occupations)]")
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]))
// 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(dynamic_forced_occupations[new_player]))
// 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)
for(var/mob/dead/new_player/player in overflow_candidates)
// 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)
@@ -869,40 +901,40 @@ SUBSYSTEM_DEF(job)
* 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.
* * 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)
JobDebug("[debug_prefix] player has no mind, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
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 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]" : ""]")
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))
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_ACCOUNTAGE, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
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)
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]" : ""]")
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))
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_BANNED, possible_job.title)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
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
if(possible_job.required_character_age > player.client.prefs.read_preference(/datum/preference/numeric/age) && possible_job.required_character_age != null)
JobDebug("[debug_prefix] Error: [get_job_unavailable_error_message(JOB_UNAVAILABLE_AGE)], Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
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
// 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]" : ""]")
job_debug("[debug_prefix]: Player is qdeleted, Player: [player][add_job_to_log ? ", Job: [possible_job]" : ""]")
return JOB_UNAVAILABLE_GENERIC
return JOB_AVAILABLE
+3 -3
View File
@@ -236,14 +236,14 @@ SUBSYSTEM_DEF(ticker)
can_continue = SSdynamic.pre_setup() //Choose antagonists
CHECK_TICK
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src)
can_continue = can_continue && SSjob.DivideOccupations() //Distribute jobs
can_continue = can_continue && SSjob.divide_occupations() //Distribute jobs
CHECK_TICK
if(!GLOB.Debug2)
if(!can_continue)
log_game("Game failed pre_setup")
to_chat(world, "<B>Error setting up game.</B> Reverting to pre-game lobby.")
SSjob.ResetOccupations()
SSjob.reset_occupations()
return FALSE
else
message_admins(span_notice("DEBUG: Bypassing prestart checks..."))
@@ -416,7 +416,7 @@ SUBSYSTEM_DEF(ticker)
continue
var/datum/job/player_assigned_role = new_player_living.mind.assigned_role
if(player_assigned_role.job_flags & JOB_EQUIP_RANK)
SSjob.EquipRank(new_player_living, player_assigned_role, new_player_mob.client)
SSjob.equip_rank(new_player_living, player_assigned_role, new_player_mob.client)
player_assigned_role.after_roundstart_spawn(new_player_living, new_player_mob.client)
if(picked_spare_id_candidate == new_player_mob)
captainless = FALSE