Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor job assignment code to improve job distribution variety #18879

Merged
merged 10 commits into from
May 16, 2024
5 changes: 5 additions & 0 deletions _std/defines/jobs.dm
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@
#define JOB_ENGINEERING "engineering"
#define JOB_CIVILIAN "civilian"
#define JOB_CREATED "created"

// Job categories
#define STAPLE_JOBS (1<<0)
#define SPECIAL_JOBS (1<<1)
#define HIDDEN_JOBS (1<<2)
149 changes: 149 additions & 0 deletions code/datums/controllers/job_controls.dm
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,155 @@ var/datum/job_controller/job_controls
return 1
return 0

/// Returns TRUE if a player is eligible to play a given job
proc/check_job_eligibility(mob/new_player/player, datum/job/job, valid_categories = STAPLE_JOBS | SPECIAL_JOBS | HIDDEN_JOBS)
if(!player?.client)
logTheThing(LOG_DEBUG, null, "<b>Jobs:</b> check job eligibility error - [player.last_ckey] has no client.")
return
if (!job)
logTheThing(LOG_DEBUG, null, "<b>Jobs:</b> check job eligibility error - [player.ckey] requested check with invalid job datum arg.")
return
if ((job.limit >= 0) && (job.assigned >= job.limit))
return
// prevent someone from trying to sneak their way into a job they shouldn't be able to choose
var/list/valid_jobs = list()
if (HAS_FLAG(valid_categories, STAPLE_JOBS))
valid_jobs.Add(src.staple_jobs)
if (HAS_FLAG(valid_categories, SPECIAL_JOBS))
valid_jobs.Add(src.special_jobs)
if (HAS_FLAG(valid_categories, HIDDEN_JOBS))
valid_jobs.Add(src.hidden_jobs)
if (!valid_jobs.Find(job))
logTheThing(LOG_DEBUG, null, "<b>Jobs:</b> check job eligibility error - [player.ckey] requested [job.name], but it was not found in list of valid jobs! (Flag value: [valid_categories]).")
return
var/datum/preferences/P = player.client.preferences
// antag job exemptions
if(player.mind?.is_antagonist())
if ((!job.allow_traitors && player.mind.special_role))
return
else if (!job.allow_spy_theft && (player.mind.special_role == ROLE_SPY_THIEF))
return
else if (istype(ticker?.mode, /datum/game_mode/revolution) && (job.cant_spawn_as_rev || ("loyalist" in P?.traitPreferences.traits_selected)))
return
else if ((istype(ticker?.mode, /datum/game_mode/conspiracy)) && job.cant_spawn_as_con)
return
else if ((!job.can_join_gangs) && (player.mind.special_role in list(ROLE_GANG_MEMBER,ROLE_GANG_LEADER)))
return
// job ban check
if (!job.no_jobban_from_this_job && jobban_isbanned(player, job.name))
logTheThing(LOG_DEBUG, null, "<b>Jobs:</b> check job eligibility error - [player.ckey] requested [job.name], but is job banned.")
return
// mentor only job check
if (job.mentor_only && !(player.ckey in mentors))
logTheThing(LOG_DEBUG, null, "<b>Jobs:</b> check job eligibility error - [player.ckey] requested [job.name], a mentor only job.")
return
// meant to prevent you from setting sec as fav and captain (or similar) as your only medium to ensure only captain traitor rounds
if (!job.allow_antag_fallthrough && player.antag_fallthrough)
return
// all of the 'serious' check have passed, ignore the rest of the requirements for random job rounds.
if (global.totally_random_jobs)
return TRUE

if (job.rounds_needed_to_play)
var/round_num = player.client.player.get_rounds_participated()
if (!isnull(round_num) && round_num < job.rounds_needed_to_play) //they havent played enough rounds!
return
if (job.needs_college && !player.has_medal("Unlike the director, I went to college"))
return
if (job.requires_whitelist && !NT.Find(ckey(player.mind.key)))
return
if (job.requires_supervisor_job && countJob(job.requires_supervisor_job) <= 0)
return
return TRUE

/// attempts to assign a player to a job from a list of either job datums or job strings
proc/try_assign_job_from_list(mob/player, list/jobs)
PRIVATE_PROC(TRUE)
RETURN_TYPE(/datum/job)
shuffle_list(jobs)
for(var/job_entry in jobs)
var/datum/job/job
if (istext(job_entry))
job = find_job_in_controller_by_string(job_entry)
else
job = job_entry
if (job && check_job_eligibility(player, job, STAPLE_JOBS))
player.mind.assigned_role = job.name
job.assigned++
return job
return

/// Assigns a player a job based on their preferences and job availability
proc/allocate_player_to_job_by_preference(mob/new_player/player)
RETURN_TYPE(/datum/job)
if (!player.client)
return
var/datum/preferences/player_preferences = player.client.preferences
if (!player_preferences)
return

if (totally_random_jobs)
var/datum/job/job = try_assign_job_from_list(player, staple_jobs)
if (!job) // what are you like, banned from everything...?
job = find_job_in_controller_by_path(/datum/job/civilian/staff_assistant) // very random
player.mind.assigned_role = job.name
job.assigned++
logTheThing(LOG_DEBUG, player, "<b>Jobs:</b> Assigned job: [job.name] (random job)")
return job

if (player_preferences.job_favorite)
var/datum/job/job = find_job_in_controller_by_string(player_preferences.job_favorite)
if (job)
// antag fall through flag set check
if ((!job.allow_traitors && player.mind.special_role))
player.antag_fallthrough = TRUE
else if (!job.allow_spy_theft && (player.mind.special_role == ROLE_SPY_THIEF))
player.antag_fallthrough = TRUE
else if ((!job.can_join_gangs) && (player.mind.special_role in list(ROLE_GANG_MEMBER,ROLE_GANG_LEADER)))
player.antag_fallthrough = TRUE

// try to assign fav job
if (check_job_eligibility(player, job, STAPLE_JOBS))
player.mind.assigned_role = job.name
job.assigned++
logTheThing(LOG_DEBUG, player, "<b>Jobs:</b> Assigned job: [job.name] (favorite job)")
return job

// If favorite job isn't available, check medium priority jobs
if (length(player_preferences.jobs_med_priority))
var/datum/job/job = try_assign_job_from_list(player, player_preferences.jobs_med_priority)
if (job)
logTheThing(LOG_DEBUG, player, "<b>Jobs:</b> Assigned job: [job.name] (medium priority job)")
return job

// If no medium priority jobs are available or suitable, check low priority jobs
if (length(player_preferences.jobs_low_priority))
var/datum/job/job = try_assign_job_from_list(player, player_preferences.jobs_low_priority)
if (job)
logTheThing(LOG_DEBUG, player, "<b>Jobs:</b> Assigned job: [job.name] (low priority job)")
return job

// look, we tried ok? Just be happy you work here at all.
var/list/low_priority_jobs = list()
for(var/datum/job/job in job_controls.staple_jobs)
if (job.low_priority_job)
low_priority_jobs += job
if (length(low_priority_jobs))
var/datum/job/job = pick(low_priority_jobs)
player.mind.assigned_role = job.name
job.assigned++
logTheThing(LOG_DEBUG, player, "<b>Jobs:</b> Assigned job: [job.name] (fallback job).")
return job

// staffie fallback
var/datum/job/fallback_job = find_job_in_controller_by_path(/datum/job/civilian/staff_assistant)
if(!fallback_job)
CRASH("Unable to locate the default fallback job in job controller. [player] has not been assigned a job!")
player.mind.assigned_role = fallback_job.name
fallback_job.assigned++
logTheThing(LOG_DEBUG, player, "<b>Jobs:</b> Assigned job: [fallback_job.name] (emergency fallback job)")
return fallback_job

proc/job_creator()
src.check_user_changed()
var/list/dat = list("<html><body><title>Job Creation</title>")
Expand Down
35 changes: 5 additions & 30 deletions code/mob/new_player.dm
Original file line number Diff line number Diff line change
Expand Up @@ -271,31 +271,6 @@ var/global/datum/mutex/limited/latespawning = new(5 SECONDS)
else if(!href_list["late_join"])
new_player_panel()

proc/IsJobAvailable(var/datum/job/JOB)
if(!ticker || !ticker.mode)
return 0
if (!JOB || !istype(JOB,/datum/job/) || JOB.limit == 0)
return 0
if (!JOB.no_jobban_from_this_job && jobban_isbanned(src,JOB.name))
return 0
if (JOB.requires_supervisor_job && countJob(JOB.requires_supervisor_job) <= 0)
return 0
if (JOB.requires_whitelist)
if (!(src.ckey in NT))
return 0
if (JOB.mentor_only)
if (!(src.ckey in mentors))
return 0
if (JOB.needs_college && !src.has_medal("Unlike the director, I went to college"))
return 0
if (JOB.rounds_needed_to_play && (src.client && src.client.player))
var/round_num = src.client.player.get_rounds_participated()
if (!isnull(round_num) && round_num < JOB.rounds_needed_to_play) //they havent played enough rounds!
return 0
if (JOB.limit < 0 || JOB.assigned < JOB.limit)
return 1
return 0

proc/IsSiliconAvailableForLateJoin(var/mob/living/silicon/S)
if (isdead(S))
return 0
Expand All @@ -321,7 +296,7 @@ var/global/datum/mutex/limited/latespawning = new(5 SECONDS)
return
global.latespawning.lock()

if (JOB && (force || IsJobAvailable(JOB)))
if (JOB && (force || job_controls.check_job_eligibility(src, JOB, STAPLE_JOBS | SPECIAL_JOBS)))
var/mob/character = create_character(JOB, JOB.allow_traitors)
if (isnull(character))
global.latespawning.unlock()
Expand Down Expand Up @@ -496,7 +471,7 @@ var/global/datum/mutex/limited/latespawning = new(5 SECONDS)
if (J.no_late_join)
return
var/limit = J.limit
if (!IsJobAvailable(J))
if (!job_controls.check_job_eligibility(src, J, STAPLE_JOBS | SPECIAL_JOBS))
// Show unavailable jobs, but no joining them
limit = 0

Expand Down Expand Up @@ -694,11 +669,11 @@ a.latejoin-card:hover {
dat += {"<tr><td colspan='2'>&nbsp;</td></tr><tr><th colspan='2'>Special Jobs</th></tr>"}

for(var/datum/job/special/J in job_controls.special_jobs)
if (IsJobAvailable(J) && !J.no_late_join)
if (job_controls.check_job_eligibility(src, J, SPECIAL_JOBS) && !J.no_late_join)
dat += LateJoinLink(J)

for(var/datum/job/created/J in job_controls.special_jobs)
if (IsJobAvailable(J) && !J.no_late_join)
if (job_controls.check_job_eligibility(src, J, SPECIAL_JOBS) && !J.no_late_join)
dat += LateJoinLink(J)

dat += "</table></div>"
Expand Down Expand Up @@ -728,7 +703,7 @@ a.latejoin-card:hover {
D.limit = -1
C.enabled_jobs += D
for (var/datum/job/J in C.enabled_jobs)
if (IsJobAvailable(J) && !J.no_late_join)
if (job_controls.check_job_eligibility(src, J, STAPLE_JOBS|SPECIAL_JOBS) && !J.no_late_join)
var/hover_text = J.short_description || "Join the round as [J.name]."
dat += "<tr><td style='width:100%'>"
dat += {"<a href='byond://?src=\ref[src];SelectedJob=\ref[J];latejoin=prompt' title='[hover_text]'><font color=[J.linkcolor]>[J.name]</font></a> ([J.assigned][J.limit == -1 ? "" : "/[J.limit]"])<br>"}
Expand Down