mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-31 11:57:12 +01:00
402 lines
15 KiB
Plaintext
402 lines
15 KiB
Plaintext
/datum/controller/subsystem/job
|
|
/// players who need jobs
|
|
var/list/mob/new_player/divide_unassigned
|
|
/// cached list of job priorities by the player
|
|
var/list/mob/new_player/divide_priorities
|
|
/// cached list of job overflow preferences by the player
|
|
var/list/mob/new_player/divide_overflows
|
|
|
|
/datum/controller/subsystem/job/proc/reset_occupations()
|
|
dispose_unassigned()
|
|
for(var/mob/new_player/NP in GLOB.player_list)
|
|
if(NP?.mind)
|
|
NP.mind.assigned_role = null
|
|
NP.mind.special_role = null
|
|
for(var/datum/prototype/role/job/job as anything in RSroles.legacy_all_job_datums())
|
|
job.current_positions = 0
|
|
|
|
/datum/controller/subsystem/job/proc/gather_unassigned()
|
|
//Get the players who are ready
|
|
divide_unassigned = list()
|
|
divide_priorities = list()
|
|
divide_overflows = list()
|
|
for(var/mob/new_player/player in GLOB.player_list)
|
|
if(!player.client) // git out
|
|
continue
|
|
if(player.client.persistent.ligma)
|
|
log_shadowban("[player] ([player.client]) roundstart blocked")
|
|
continue // bye :)
|
|
if(player.ready && player.mind && !player.mind.assigned_role)
|
|
divide_unassigned += player
|
|
var/list/priorities = player.client.effective_job_priorities()
|
|
var/overflow_mode = player.client.prefs.get_job_alternative()
|
|
divide_priorities[player] = priorities
|
|
divide_overflows[player] = overflow_mode
|
|
|
|
/datum/controller/subsystem/job/proc/dispose_unassigned()
|
|
divide_unassigned = null
|
|
divide_priorities = null
|
|
divide_overflows = null
|
|
|
|
// todo: "why did you not just move everything?"
|
|
// because although splitting everything into procs that re-query the prefs every single time
|
|
// to figure out what people want to be, which while readable and workable,
|
|
// is hellishly slow
|
|
// i'm going to figure out if i can speed it up BEFORE rewriting everything.
|
|
|
|
/*
|
|
/datum/controller/subsystem/job/proc/FindOccupationCandidates(datum/prototype/role/job/job, level, flag)
|
|
JobDebug("Running FOC, Job: [job], Level: [level], Flag: [flag]")
|
|
var/list/candidates = list()
|
|
for(var/mob/new_player/player in unassigned)
|
|
if(jobban_isbanned(player, job.title) || QDELETED(player))
|
|
JobDebug("FOC isbanned failed, Player: [player]")
|
|
continue
|
|
if(!job.player_old_enough(player.client))
|
|
JobDebug("FOC player not old enough, Player: [player]")
|
|
continue
|
|
if(job.required_playtime_remaining(player.client))
|
|
JobDebug("FOC player not enough xp, Player: [player]")
|
|
continue
|
|
if(!player.client.prefs.pref_species.qualifies_for_rank(job.title, player.client.prefs.features))
|
|
JobDebug("FOC non-human failed, Player: [player]")
|
|
continue
|
|
if(flag && (!(flag in player.client.prefs.be_special)))
|
|
JobDebug("FOC flag failed, Player: [player], Flag: [flag], ")
|
|
continue
|
|
if(player.mind && (job.title in player.mind.restricted_roles))
|
|
JobDebug("FOC incompatible with antagonist role, Player: [player]")
|
|
continue
|
|
if(player.client.prefs.job_preferences[job.title] == level)
|
|
JobDebug("FOC pass, Player: [player], Level:[level]")
|
|
candidates += player
|
|
return candidates
|
|
|
|
/datum/controller/subsystem/job/proc/GiveRandomJob(mob/new_player/player)
|
|
JobDebug("GRJ Giving random job, Player: [player]")
|
|
. = FALSE
|
|
for(var/datum/prototype/role/job/job in shuffle(GetAllJobs()))
|
|
if(!job)
|
|
continue
|
|
|
|
if(istype(job, GetJobName(SSjob.overflow_role))) // We don't want to give him assistant, that's boring!
|
|
continue
|
|
|
|
if(job.IsInDepartment(/datum/department/command)) //If you want a command position, select it!
|
|
continue
|
|
|
|
if(jobban_isbanned(player, job.title) || QDELETED(player))
|
|
if(QDELETED(player))
|
|
JobDebug("GRJ isbanned failed, Player deleted")
|
|
break
|
|
JobDebug("GRJ isbanned failed, Player: [player], Job: [job.title]")
|
|
continue
|
|
|
|
if(!job.player_old_enough(player.client))
|
|
JobDebug("GRJ player not old enough, Player: [player]")
|
|
continue
|
|
|
|
if(!player.client.prefs.pref_species.qualifies_for_rank(job.title, player.client.prefs.features))
|
|
JobDebug("GRJ non-human failed, Player: [player]")
|
|
continue
|
|
|
|
if(job.required_playtime_remaining(player.client))
|
|
JobDebug("GRJ player not enough xp, Player: [player]")
|
|
continue
|
|
|
|
if(player.mind && (job.title in player.mind.restricted_roles))
|
|
JobDebug("GRJ incompatible with antagonist role, Player: [player], Job: [job.title]")
|
|
continue
|
|
|
|
if((job.current_positions < job.roundstart_positions) || job.roundstart_positions == -1)
|
|
JobDebug("GRJ Random job given, Player: [player], Job: [job]")
|
|
if(Assign(player, job.title))
|
|
return TRUE
|
|
|
|
/datum/controller/subsystem/job/proc/reset_occupations()
|
|
JobDebug("Occupations reset.")
|
|
for(var/mob/new_player/player in GLOB.GLOB.player_list)
|
|
if((player) && (player.mind))
|
|
player.mind.assigned_role = null
|
|
player.mind.special_role = null
|
|
SSpersistence.antag_rep_change[player.ckey] = 0
|
|
SetupOccupations()
|
|
unassigned = list()
|
|
|
|
//This proc is called before the level loop of DivideOccupations() and will try to select a head, ignoring ALL non-head preferences for every level until
|
|
//it locates a head or runs out of levels to check
|
|
//This is basically to ensure that there's atleast a few heads in the round
|
|
/datum/controller/subsystem/job/proc/FillHeadPosition()
|
|
for(var/level in level_order)
|
|
for(var/datum/prototype/role/job/job as anything in GetDepartmentJobDatums(/datum/department/command))
|
|
if(!job)
|
|
continue
|
|
if((job.current_positions >= job.total_positions) && job.total_positions != -1)
|
|
continue
|
|
var/list/candidates = FindOccupationCandidates(job, level)
|
|
if(!candidates?.len)
|
|
continue
|
|
var/mob/new_player/candidate = pick(candidates)
|
|
if(Assign(candidate, job))
|
|
return 1
|
|
return 0
|
|
|
|
//This proc is called at the start of the level loop of DivideOccupations() and will cause head jobs to be checked before any other jobs of the same level
|
|
//This is also to ensure we get as many heads as possible
|
|
/datum/controller/subsystem/job/proc/CheckHeadPositions(level)
|
|
for(var/datum/prototype/role/job/job as anything in GetDepartmentJobDatums(/datum/department/command))
|
|
if(!job)
|
|
continue
|
|
if((job.current_positions >= job.total_positions) && job.total_positions != -1)
|
|
continue
|
|
var/list/candidates = FindOccupationCandidates(job, level)
|
|
if(!candidates?.len)
|
|
continue
|
|
var/mob/new_player/candidate = pick(candidates)
|
|
Assign(candidate, job)
|
|
|
|
/datum/controller/subsystem/job/proc/FillAIPosition()
|
|
var/ai_selected = 0
|
|
var/datum/prototype/role/job/job = GetJobType(/datum/prototype/role/job/ai)
|
|
if(!job)
|
|
return 0
|
|
for(var/i = job.total_positions, i > 0, i--)
|
|
for(var/level in level_order)
|
|
var/list/candidates = list()
|
|
candidates = FindOccupationCandidates(job, level)
|
|
if(candidates.len)
|
|
var/mob/new_player/candidate = pick(candidates)
|
|
if(Assign(candidate, "AI"))
|
|
ai_selected++
|
|
break
|
|
if(ai_selected)
|
|
return 1
|
|
return 0
|
|
|
|
/** Proc DivideOccupations
|
|
* fills var "assigned_role" for all ready players.
|
|
* This proc must not have any side effect besides of modifying "assigned_role".
|
|
**/
|
|
/datum/controller/subsystem/job/proc/DivideOccupations(list/required_jobs)
|
|
//Setup new player list and get the jobs list
|
|
JobDebug("Running DO")
|
|
|
|
//Holder for Triumvirate is stored in the SSticker, this just processes it
|
|
if(SSticker.triai)
|
|
for(var/datum/prototype/role/job/ai/A in GetAllJobs())
|
|
A.roundstart_positions = 3
|
|
var/left = 2
|
|
for(var/atom/movable/landmark/spawnpoint/job/ai/secondary/S in GetAllSpawnpoints())
|
|
if(!left)
|
|
break
|
|
left--
|
|
S.latejoin_active = TRUE
|
|
|
|
//Get the players who are ready
|
|
for(var/mob/new_player/player in GLOB.GLOB.player_list)
|
|
if(player.ready == PLAYER_READY_TO_PLAY && player.check_preferences() && player.mind && !player.mind.assigned_role)
|
|
unassigned += player
|
|
|
|
initial_players_to_assign = unassigned.len
|
|
|
|
JobDebug("DO, Len: [unassigned?.len]")
|
|
if(unassigned.len == 0)
|
|
return validate_required_jobs(required_jobs)
|
|
|
|
//Scale number of open security officer slots to population
|
|
setup_officer_positions()
|
|
|
|
//Jobs will have fewer access permissions if the number of players exceeds the threshold defined in game_options.txt
|
|
var/mat = CONFIG_GET(number/minimal_access_threshold)
|
|
if(mat)
|
|
if(mat > unassigned.len)
|
|
CONFIG_SET(flag/jobs_have_minimal_access, FALSE)
|
|
else
|
|
CONFIG_SET(flag/jobs_have_minimal_access, TRUE)
|
|
|
|
//Shuffle players and jobs
|
|
unassigned = shuffle(unassigned)
|
|
|
|
HandleFeedbackGathering()
|
|
|
|
//People who wants to be the overflow role, sure, go on.
|
|
JobDebug("DO, Running Overflow Check 1")
|
|
var/datum/prototype/role/job/overflow = GetJobName(SSjob.overflow_role)
|
|
var/list/overflow_candidates = FindOccupationCandidates(overflow, JP_LOW)
|
|
JobDebug("AC1, Candidates: [overflow_candidates?.len]")
|
|
for(var/mob/new_player/player in overflow_candidates)
|
|
JobDebug("AC1 pass, Player: [player]")
|
|
Assign(player, SSjob.overflow_role)
|
|
overflow_candidates -= player
|
|
JobDebug("DO, AC1 end")
|
|
|
|
//Select one head
|
|
JobDebug("DO, Running Head Check")
|
|
FillHeadPosition()
|
|
JobDebug("DO, Head Check end")
|
|
|
|
//Check for an AI
|
|
JobDebug("DO, Running AI Check")
|
|
FillAIPosition()
|
|
JobDebug("DO, AI Check end")
|
|
|
|
//Other jobs are now checked
|
|
JobDebug("DO, Running Standard Check")
|
|
|
|
|
|
// New job giving system by Donkie
|
|
// This will cause lots of more loops, but since it's only done once it shouldn't really matter much at all.
|
|
// Hopefully this will add more randomness and fairness to job giving.
|
|
|
|
// Loop through all levels from high to low
|
|
var/list/shuffledoccupations = shuffle(GetAllJobs())
|
|
for(var/level in level_order)
|
|
//Check the head jobs first each level
|
|
CheckHeadPositions(level)
|
|
|
|
// Loop through all unassigned players
|
|
for(var/mob/new_player/player in unassigned)
|
|
if(PopcapReached())
|
|
RejectPlayer(player)
|
|
|
|
// Loop through all jobs
|
|
for(var/datum/prototype/role/job/job in shuffledoccupations) // SHUFFLE ME BABY
|
|
if(!job)
|
|
continue
|
|
|
|
if(jobban_isbanned(player, job.title))
|
|
JobDebug("DO isbanned failed, Player: [player], Job:[job.title]")
|
|
continue
|
|
|
|
if(QDELETED(player))
|
|
JobDebug("DO player deleted during job ban check")
|
|
break
|
|
|
|
if(!job.player_old_enough(player.client))
|
|
JobDebug("DO player not old enough, Player: [player], Job:[job.title]")
|
|
continue
|
|
|
|
if(job.required_playtime_remaining(player.client))
|
|
JobDebug("DO player not enough xp, Player: [player], Job:[job.title]")
|
|
continue
|
|
|
|
if(!player.client.prefs.pref_species.qualifies_for_rank(job.title, player.client.prefs.features))
|
|
JobDebug("DO non-human failed, Player: [player], Job:[job.title]")
|
|
continue
|
|
|
|
if(player.mind && (job.title in player.mind.restricted_roles))
|
|
JobDebug("DO incompatible with antagonist role, Player: [player], Job:[job.title]")
|
|
continue
|
|
|
|
// If the player wants that job on this level, then try give it to him.
|
|
if(player.client.prefs.job_preferences[job.title] == level)
|
|
// If the job isn't filled
|
|
if((job.current_positions < job.roundstart_positions) || job.roundstart_positions == -1)
|
|
JobDebug("DO pass, Player: [player], Level:[level], Job:[job.title]")
|
|
Assign(player, job.title)
|
|
unassigned -= player
|
|
break
|
|
|
|
|
|
JobDebug("DO, Handling unassigned.")
|
|
// Hand out random jobs to the people who didn't get any in the last check
|
|
// Also makes sure that they got their preference correct
|
|
for(var/mob/new_player/player in unassigned)
|
|
HandleUnassigned(player)
|
|
|
|
JobDebug("DO, Handling unrejectable unassigned")
|
|
//Mop up people who can't leave.
|
|
for(var/mob/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(!Assign(player, SSjob.overflow_role)) //If everything is already filled, make them an assistant
|
|
return FALSE //Living on the edge, the forced antagonist couldn't be assigned to overflow role (bans, client age) - just reroll
|
|
|
|
return validate_required_jobs(required_jobs)
|
|
|
|
/datum/controller/subsystem/job/proc/validate_required_jobs(list/required_jobs)
|
|
if(!required_jobs.len)
|
|
return TRUE
|
|
for(var/required_group in required_jobs)
|
|
var/group_ok = TRUE
|
|
for(var/rank in required_group)
|
|
var/datum/prototype/role/job/J = GetJobName(rank)
|
|
if(!J)
|
|
SSticker.mode.setup_error = "Invalid job [rank] in gamemode required jobs."
|
|
return FALSE
|
|
if(J.current_positions < required_group[rank])
|
|
group_ok = FALSE
|
|
break
|
|
if(group_ok)
|
|
return TRUE
|
|
SSticker.mode.setup_error = "Required jobs not present."
|
|
return FALSE
|
|
|
|
//We couldn't find a job from prefs for this guy.
|
|
/datum/controller/subsystem/job/proc/HandleUnassigned(mob/new_player/player)
|
|
if(PopcapReached())
|
|
RejectPlayer(player)
|
|
else if(player.client.prefs.joblessrole == BEOVERFLOW)
|
|
var/allowed_to_be_a_loser = !jobban_isbanned(player, SSjob.overflow_role)
|
|
if(QDELETED(player) || !allowed_to_be_a_loser)
|
|
RejectPlayer(player)
|
|
else
|
|
if(!Assign(player, SSjob.overflow_role))
|
|
RejectPlayer(player)
|
|
else if(player.client.prefs.joblessrole == BERANDOMJOB)
|
|
if(!GiveRandomJob(player))
|
|
RejectPlayer(player)
|
|
else if(player.client.prefs.joblessrole == RETURNTOLOBBY)
|
|
RejectPlayer(player)
|
|
else //Something gone wrong if we got here.
|
|
var/message = "DO: [player] fell through handling unassigned"
|
|
JobDebug(message)
|
|
log_game(message)
|
|
message_admins(message)
|
|
RejectPlayer(player)
|
|
|
|
|
|
/datum/controller/subsystem/job/proc/setup_officer_positions()
|
|
var/datum/prototype/role/job/J = SSjob.GetJobType(/datum/prototype/role/job/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.roundstart_positions > 0)
|
|
var/officer_positions = min(12, max(J.roundstart_positions, round(unassigned.len / ssc))) //Scale between configured minimum and 12 officers
|
|
JobDebug("Setting open security officer positions to [officer_positions]")
|
|
J.total_positions = officer_positions
|
|
J.roundstart_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/RejectPlayer(mob/new_player/player)
|
|
if(player.mind && player.mind.special_role)
|
|
return
|
|
if(PopcapReached())
|
|
JobDebug("Popcap overflow Check observer located, Player: [player]")
|
|
JobDebug("Player rejected :[player]")
|
|
to_chat(player, "<b>You have failed to qualify for any job you desired.</b>")
|
|
unassigned -= player
|
|
player.ready = PLAYER_NOT_READY
|
|
|
|
/datum/controller/subsystem/job/proc/PopcapReached()
|
|
var/hpc = CONFIG_GET(number/hard_popcap)
|
|
var/epc = CONFIG_GET(number/extreme_popcap)
|
|
if(hpc || epc)
|
|
var/relevent_cap = max(hpc, epc)
|
|
if((initial_players_to_assign - unassigned.len) >= relevent_cap)
|
|
return TRUE
|
|
return FALSE
|
|
*/
|