diff --git a/code/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm index cca828c3f1c..374c0e890c6 100644 --- a/code/__DEFINES/jobs.dm +++ b/code/__DEFINES/jobs.dm @@ -6,7 +6,7 @@ #define JOB_UNAVAILABLE_SLOTFULL 5 /// Job unavailable due to incompatibility with an antag role. #define JOB_UNAVAILABLE_ANTAG_INCOMPAT 6 -/// Checks for character age. +/// Checks for character age. #define JOB_UNAVAILABLE_AGE 7 /// Used when the `get_job_unavailable_error_message` proc can't make sense of a given code. @@ -19,6 +19,12 @@ #define JOB_DISPLAY_ORDER_DEFAULT 0 +// Keys for jobconfig.toml +#define JOB_CONFIG_PLAYTIME_REQUIREMENTS "Playtime Requirements" +#define JOB_CONFIG_REQUIRED_ACCOUNT_AGE "Required Account Age" +#define JOB_CONFIG_REQUIRED_CHARACTER_AGE "Required Character Age" +#define JOB_CONFIG_SPAWN_POSITIONS "Spawn Positions" +#define JOB_CONFIG_TOTAL_POSITIONS "Total Positions" /** * ======================= diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm index 1a4f3cd4011..10c6cc4284f 100644 --- a/code/controllers/configuration/configuration.dm +++ b/code/controllers/configuration/configuration.dm @@ -98,6 +98,9 @@ LoadChatFilter() if(CONFIG_GET(flag/load_jobs_from_txt)) validate_job_config() + if(SSjob.initialized) // in case we're reloading from disk after initialization, wanna make sure the changes update in the ongoing shift + SSjob.load_jobs_from_config() + if(CONFIG_GET(flag/usewhitelist)) load_whitelist() diff --git a/code/controllers/configuration/entries/jobs.dm b/code/controllers/configuration/entries/jobs.dm new file mode 100644 index 00000000000..99a3ab7a701 --- /dev/null +++ b/code/controllers/configuration/entries/jobs.dm @@ -0,0 +1,185 @@ +// This file pretty much just handles all of the interactions between jobconfig.toml and the codebase. This is started by work originating in SSconfig, so I'm okay with it being here. + +/// Returns an associated list of all of the job config types that we have in the codebase. +/datum/controller/subsystem/job/proc/generate_config_singletons() + var/returnable_list = list() + for(var/datum/job_config_type/config_datum as anything in subtypesof(/datum/job_config_type)) + returnable_list[initial(config_datum.name)] = new config_datum + + return returnable_list + +/// Sets all of the job datum configurable values to what they've been set to in the config file, jobconfig.toml. +/datum/controller/subsystem/job/proc/load_jobs_from_config() + if(!length(job_config_datum_singletons)) + stack_trace("SSjob tried to load jobs from config, but the config singletons were not initialized! Likely tried to load jobs before SSjob was initialized.") + return + + if(legacy_mode) + legacy_load() + return + + var/toml_path = "[global.config.directory]/jobconfig.toml" + var/list/job_config = rustg_read_toml_file(toml_path) + + for(var/datum/job/occupation as anything in joinable_occupations) + var/job_key = occupation.config_tag + if(!job_config[job_key]) // Job isn't listed, skip it. + // List both job_title and job_key in case they de-sync over time. + message_admins(span_notice("[occupation.title] (with config key [job_key]) is missing from jobconfig.toml! Using codebase defaults.")) + continue + + for(var/config_datum_key in job_config_datum_singletons) + var/datum/job_config_type/config_datum = job_config_datum_singletons[config_datum_key] + var/config_value = job_config[job_key][config_datum_key] + config_datum.set_current_value(occupation, config_value) + +/// Operates the legacy jobs.txt parser to load jobs from the old config system. +/datum/controller/subsystem/job/proc/legacy_load() + var/jobsfile = file("[global.config.directory]/jobs.txt") + if(!fexists(jobsfile)) // sanity with a trace + stack_trace("Despite SSconfig setting SSjob.legacy_mode to TRUE, jobs.txt was not found in the config directory! Something has gone terribly wrong!") + return + var/jobstext = file2text(jobsfile) + for(var/datum/job/occupation as anything in joinable_occupations) + var/regex/parser = new("[occupation.title]=(-1|\\d+),(-1|\\d+)") + parser.Find(jobstext) + occupation.total_positions = text2num(parser.group[1]) + occupation.spawn_positions = text2num(parser.group[2]) + +/// Will generate a new jobconfig.toml file if one does not exist, or if one does exist, will migrate the old jobs.txt file into the new TOML format for download +/// Returns TRUE if a file is successfully generated, FALSE otherwise. +/datum/controller/subsystem/job/proc/generate_config(mob/user) + var/toml_path = "[global.config.directory]/jobconfig.toml" + var/jobstext = "[global.config.directory]/jobs.txt" + config_documentation = initial(config_documentation) // Reset to default juuuuust in case. + + if(fexists(file(toml_path))) + to_chat(user, span_notice("Generating new jobconfig.toml, pulling from the old config settings.")) + if(!regenerate_job_config(user)) + return FALSE + return TRUE + + if(fexists(file(jobstext))) // Generate the new TOML format, migrating from the text format. + to_chat(user, span_notice("Found jobs.txt in config directory! Generating jobconfig.toml from it.")) + if(!import_config_from_txt(user)) + return FALSE + return TRUE + + // Generate the new TOML format, using codebase defaults. + to_chat(user, span_notice("Generating new jobconfig.toml, using codebase defaults.")) + var/list/file_data = list() + for(var/datum/job/occupation as anything in joinable_occupations) + file_data[occupation.config_tag] = generate_blank_job_config(occupation) + + if(!export_toml(user, file_data)) + return FALSE + + return TRUE + +/// Loads the job config from the TXT and creates a new TOML file from it. +/// Returns TRUE if a file is successfully generated, FALSE otherwise. +/datum/controller/subsystem/job/proc/import_config_from_txt(mob/user) + var/unrolled_jobs_txt = file2text(file("[global.config.directory]/jobs.txt")) // walter i'm dying (get the file from the string, then parse it into a larger text string) + var/list/file_data = list() + config_documentation += "\n\n## This TOML was migrated from jobs.txt. All variables are COMMENTED and will not load by default! Please verify to ensure that they are correct, and uncomment the key as you want, comparing it to the old config.\n\n" // small warning + + for(var/datum/job/occupation as anything in joinable_occupations) + var/regex/parser = new("[occupation.title]=(-1|\\d+),(-1|\\d+)") // TXT system used the occupation's name, we convert it to the new config_key system here. + parser.Find(unrolled_jobs_txt) + + var/default_positions = text2num(parser.group[1]) + var/starting_positions = text2num(parser.group[2]) + + // Playtime Requirements and Required Account Age are new and we want to see it migrated, so we will just pull codebase defaults for them. + // Remember, every time we write the TOML from scratch, we want to have it commented out by default to ensure that the server operator knows that they are overriding the codebase defaults when they remove the comment. + var/list/working_list = list( + "# [JOB_CONFIG_TOTAL_POSITIONS]" = default_positions, + "# [JOB_CONFIG_SPAWN_POSITIONS]" = starting_positions, + ) + + working_list += generate_job_config_excluding_legacy(occupation) + file_data[occupation.config_tag] = working_list + + if(!export_toml(user, file_data)) + return FALSE + + return TRUE + +/// If we add a new job or more fields to config a job with, quickly spin up a brand new config that inherits all of your old settings, but adds the new job with codebase defaults. +/// Returns TRUE if a file is successfully generated, FALSE otherwise. +/datum/controller/subsystem/job/proc/regenerate_job_config(mob/user) + var/toml_path = "[global.config.directory]/jobconfig.toml" + var/list/file_data = list() + + if(!fexists(file(toml_path))) // You need an existing (valid) TOML for this to work. Sanity check if someone calls this directly instead of through 'Generate Job Configuration' verb. + to_chat(user, span_notice("No jobconfig.toml found in the config folder! If this is not expected, please notify a server operator or coders. You may need to generate a new config file by running 'Generate Job Configuration' from the Server tab.")) + return FALSE + + var/list/job_config = rustg_read_toml_file(toml_path) + for(var/datum/job/occupation as anything in joinable_occupations) + var/job_key = occupation.config_tag + + if(file_data[job_key]) + stack_trace("We were about to over-write a job key that already exists in file_data while generating a new jobconfig.toml! This should not happen! Verify you do not have any duplicate job keys in your codebase!") + continue + + // When we regenerate, we want to make sure commented stuff stays commented, but we also want to migrate information that remains uncommented. So, let's make sure we keep that pattern. + if(!job_config[job_key]) + to_chat(user, span_notice("New job [occupation.title] (using key [job_key]) detected! Adding to jobconfig.toml using default codebase values...")) + file_data[job_key] = generate_blank_job_config(occupation) + continue + + var/list/working_list = list() + for(var/config_datum_key in job_config_datum_singletons) + var/datum/job_config_type/config_datum = job_config_datum_singletons[config_datum_key] + var/config_read_value = job_config[job_key][config_datum_key] + if(!config_datum.validate_value(config_read_value)) + working_list += list( + "# [config_datum_key]" = config_datum.get_current_value(occupation), // note that this doesn't make a real comment, it just creates a string mismatch. + ) + else + working_list += list( + "[config_datum_key]" = config_read_value, + ) + + file_data[job_key] = working_list + + if(!export_toml(user, file_data)) + return FALSE + + return TRUE + +/// This will just return a list for a completely new job that doesn't need to be migrated from an old config (completely new). Just done here to reduce copypasta +/datum/controller/subsystem/job/proc/generate_blank_job_config(datum/job/new_occupation) + var/returnable_list = list() + for(var/config_datum_key in job_config_datum_singletons) + var/datum/job_config_type/config_datum = job_config_datum_singletons[config_datum_key] + // Remember, every time we write the TOML from scratch, we want to have it commented out by default. + // This is to ensure that the server operator knows that they are overriding codebase defaults when they remove the comment. + // Having comments mean that we allow server operators to defer to codebase standards when they deem acceptable. They must uncomment to override the codebase default. + returnable_list += list( + "# [config_datum_key]" = config_datum.get_current_value(new_occupation), + ) + + return returnable_list + +/// Like `generate_blank_job_config`, but we opt-out of adding the legacy variables in case we handle it elsewhere. +/datum/controller/subsystem/job/proc/generate_job_config_excluding_legacy(datum/job/new_occupation) + var/list/returnable_list = list() + // make a quick list to ensure we don't double-dip total_positions and spawn_positions, but still get future config types in + var/list/datums_to_read = job_config_datum_singletons - list(JOB_CONFIG_TOTAL_POSITIONS, JOB_CONFIG_SPAWN_POSITIONS) + for(var/config_datum_key in datums_to_read) + var/datum/job_config_type/config_datum = job_config_datum_singletons[config_datum_key] + returnable_list += list( + "# [config_datum_key]" = config_datum.get_current_value(new_occupation), + ) + + return returnable_list + +/// Proc that we call to generate a new jobconfig.toml file and send it to the requesting client. Returns TRUE if a file is successfully generated. +/datum/controller/subsystem/job/proc/export_toml(mob/user, data) + var/file_location = "data/jobconfig.toml" // store it in the data folder server-side so we can FTP it to the client. + var/payload = "[config_documentation]\n[rustg_toml_encode(data)]" + rustg_file_write(payload, file_location) + DIRECT_OUTPUT(user, ftp(file(file_location), "jobconfig.toml")) + return TRUE diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm index 493a64ebfd5..414aa575931 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -67,25 +67,30 @@ SUBSYSTEM_DEF(job) /// 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 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. Zero = turned off for this job.\n## As time goes on, more config options may be added to this file.\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\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\ + ## 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)) SetupOccupations() if(CONFIG_GET(flag/load_jobs_from_txt)) load_jobs_from_config() - set_overflow_role(CONFIG_GET(string/overflow_job)) + 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 @@ -638,221 +643,6 @@ SUBSYSTEM_DEF(job) else //We ran out of spare locker spawns! break -#define TOTAL_POSITIONS "Total Positions" -#define SPAWN_POSITIONS "Spawn Positions" -#define PLAYTIME_REQUIREMENTS "Playtime Requirements" -#define REQUIRED_ACCOUNT_AGE "Required Account Age" -#define REQUIRED_CHARACTER_AGE "Required Character Age" - -/// Called in jobs subsystem initialize if LOAD_JOBS_FROM_TXT config flag is set: reads jobconfig.toml (or if in legacy mode, jobs.txt) to set all of the datum's values to what the server operator wants. -/datum/controller/subsystem/job/proc/load_jobs_from_config() - var/toml_file = "[global.config.directory]/jobconfig.toml" - - if(!legacy_mode) // this flag is set during the setup of SSconfig, and all warnings were handled there. - var/job_config = rustg_read_toml_file(toml_file) - - for(var/datum/job/occupation as anything in joinable_occupations) - var/job_title = occupation.title - var/job_key = occupation.config_tag - if(!job_config[job_key]) // Job isn't listed, skip it. - message_admins(span_notice("[job_title] (with config key [job_key]) is missing from jobconfig.toml! Using codebase defaults.")) // List both job_title and job_key in case they de-sync over time. - continue - - // If the value is commented out, we assume that the server operate did not want to override the codebase default values, so we skip it. - var/default_positions = job_config[job_key][TOTAL_POSITIONS] - var/starting_positions = job_config[job_key][SPAWN_POSITIONS] - var/playtime_requirements = job_config[job_key][PLAYTIME_REQUIREMENTS] - var/required_account_age = job_config[job_key][REQUIRED_ACCOUNT_AGE] - var/required_character_age = job_config[job_key][REQUIRED_CHARACTER_AGE] - - if(default_positions || default_positions == 0) // We need to account for jobs that were intentionally turned off via config too. - occupation.total_positions = default_positions - if(starting_positions || starting_positions == 0) - occupation.spawn_positions = starting_positions - if(playtime_requirements || playtime_requirements == 0) - occupation.exp_requirements = playtime_requirements - if(required_account_age || required_account_age == 0) - occupation.minimal_player_age = required_account_age - if(required_character_age || required_character_age == 0) - occupation.required_character_age = required_character_age - - return - - else // legacy mode, so just run the old parser. - var/jobsfile = file("[global.config.directory]/jobs.txt") - if(!fexists(jobsfile)) // sanity with a trace - stack_trace("Despite SSconfig setting SSjob.legacy_mode to TRUE, jobs.txt was not found in the config directory! Something has gone terribly wrong!") - return - var/jobstext = file2text(jobsfile) - for(var/datum/job/occupation as anything in joinable_occupations) - var/regex/parser = new("[occupation.title]=(-1|\\d+),(-1|\\d+)") - parser.Find(jobstext) - occupation.total_positions = text2num(parser.group[1]) - occupation.spawn_positions = text2num(parser.group[2]) - -/// Called from an admin debug verb that generates the jobconfig.toml file and then allows the end user to download it to their machine. Returns TRUE if a file is successfully generated, FALSE otherwise. -/datum/controller/subsystem/job/proc/generate_config(mob/user) - var/toml_file = "[global.config.directory]/jobconfig.toml" - var/jobstext = "[global.config.directory]/jobs.txt" - var/list/file_data = list() - config_documentation = initial(config_documentation) // Reset to default juuuuust in case. - - if(fexists(file(toml_file))) - to_chat(src, span_notice("Generating new jobconfig.toml, pulling from the old config settings.")) - if(!regenerate_job_config(user)) - return FALSE - return TRUE - - if(fexists(file(jobstext))) // Generate the new TOML format, migrating from the text format. - to_chat(user, span_notice("Found jobs.txt in config directory! Generating jobconfig.toml from it.")) - jobstext = file2text(file(jobstext)) // walter i'm dying (get the file from the string, then parse it into a larger text string) - config_documentation += "\n\n## This TOML was migrated from jobs.txt. All variables are COMMENTED and will not load by default! Please verify to ensure that they are correct, and uncomment the key as you want, comparing it to the old config.\n\n" // small warning - for(var/datum/job/occupation as anything in joinable_occupations) - var/job_key = occupation.config_tag - var/regex/parser = new("[occupation.title]=(-1|\\d+),(-1|\\d+)") // TXT system used the occupation's name, we convert it to the new config_key system here. - parser.Find(jobstext) - - var/default_positions = text2num(parser.group[1]) - var/starting_positions = text2num(parser.group[2]) - - // Playtime Requirements and Required Account Age are new and we want to see it migrated, so we will just pull codebase defaults for them. - // Remember, every time we write the TOML from scratch, we want to have it commented out by default to ensure that the server operator is knows that they codebase defaults when they remove the comment. - file_data["[job_key]"] = list( - "# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements, - "# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age, - "# [TOTAL_POSITIONS]" = default_positions, - "# [SPAWN_POSITIONS]" = starting_positions, - ) - - if(!export_toml(user, file_data)) - return FALSE - return TRUE - - else // Generate the new TOML format, using codebase defaults. - to_chat(user, span_notice("Generating new jobconfig.toml, using codebase defaults.")) - for(var/datum/job/occupation as anything in joinable_occupations) - var/job_key = occupation.config_tag - // Remember, every time we write the TOML from scratch, we want to have it commented out by default to ensure that the server operator is knows that they override codebase defaults when they remove the comment. - // Having comments mean that we allow server operators to defer to codebase standards when they deem acceptable. They must uncomment to override the codebase default. - if(is_assistant_job(occupation)) // there's a concession made in jobs.txt that we should just rapidly account for here I KNOW I KNOW. - file_data["[job_key]"] = list( - "# [TOTAL_POSITIONS]" = -1, - "# [SPAWN_POSITIONS]" = -1, - "# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements, - "# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age, - "# [REQUIRED_CHARACTER_AGE]" = occupation.required_character_age, - ) - continue - // Generate new config from codebase defaults. - file_data["[job_key]"] = list( - "# [TOTAL_POSITIONS]" = occupation.total_positions, - "# [SPAWN_POSITIONS]" = occupation.spawn_positions, - "# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements, - "# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age, - "# [REQUIRED_CHARACTER_AGE]" = occupation.required_character_age, - ) - if(!export_toml(user, file_data)) - return FALSE - return TRUE - -/// If we add a new job or more fields to config a job with, quickly spin up a brand new config that inherits all of your old settings, but adds the new job with codebase defaults. Returns TRUE if a file is successfully generated, FALSE otherwise. -/datum/controller/subsystem/job/proc/regenerate_job_config(mob/user) - var/toml_file = "[global.config.directory]/jobconfig.toml" - var/list/file_data = list() - - if(!fexists(file(toml_file))) // You need an existing (valid) TOML for this to work. Sanity check if someone calls this directly instead of through 'Generate Job Configuration' verb. - to_chat(user, span_notice("No jobconfig.toml found in the config folder! If this is not expected, please notify a server operator or coders. You may need to generate a new config file by running 'Generate Job Configuration' from the Server tab.")) - return FALSE - - var/job_config = rustg_read_toml_file(toml_file) - for(var/datum/job/occupation as anything in joinable_occupations) - var/job_name = occupation.title - var/job_key = occupation.config_tag - - // When we regenerate, we want to make sure commented stuff stays commented, but we also want to migrate information that remains uncommented. So, let's make sure we keep that pattern. - if(job_config["[job_key]"]) // Let's see if any data for this job exists. - var/default_positions = job_config[job_key][TOTAL_POSITIONS] - var/starting_positions = job_config[job_key][SPAWN_POSITIONS] - var/playtime_requirements = job_config[job_key][PLAYTIME_REQUIREMENTS] - var/required_account_age = job_config[job_key][REQUIRED_ACCOUNT_AGE] - var/required_character_age = job_config[job_key][REQUIRED_CHARACTER_AGE] - - if(file_data["[job_key]"]) // Sanity, let's just make sure we don't overwrite anything or add any dupe keys. We also unit test for this, but eh, you never know sometimes. - stack_trace("We were about to over-write a job key that already exists in file_data while generating a new jobconfig.toml! This should not happen! Verify you do not have any duplicate job keys in your codebase!") - continue - if(default_positions) // If the variable exists, we want to ensure it migrated into the new TOML uncommented, to allow for flush migration. - file_data["[job_key]"] += list( - TOTAL_POSITIONS = default_positions, - ) - else // If we can't find anything for this variable, then we just throw in the codebase default with it commented out. - file_data["[job_key]"] += list( - "# [TOTAL_POSITIONS]" = occupation.total_positions, - ) - - if(starting_positions) // Same pattern as above. - file_data["[job_key]"] += list( - SPAWN_POSITIONS = starting_positions, - ) - else - file_data["[job_key]"] += list( - "# [SPAWN_POSITIONS]" = occupation.spawn_positions, - ) - - if(playtime_requirements) // Same pattern as above. - file_data["[job_key]"] += list( - PLAYTIME_REQUIREMENTS = playtime_requirements, - ) - else - file_data["[job_key]"] += list( - "# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements, - ) - - if(required_account_age) // Same pattern as above. - file_data["[job_key]"] += list( - REQUIRED_ACCOUNT_AGE = required_account_age, - ) - else - file_data["[job_key]"] += list( - "# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age, - ) - if(required_character_age) // Same pattern as above. - file_data["[job_key]"] += list( - REQUIRED_CHARACTER_AGE = required_character_age, - ) - else - file_data["[job_key]"] += list( - "# [REQUIRED_CHARACTER_AGE]" = occupation.required_character_age, - ) - continue - else - to_chat(user, span_notice("New job [job_name] (using key [job_key]) detected! Adding to jobconfig.toml using default codebase values...")) - // Commented out keys here in case server operators wish to defer to codebase defaults. - file_data["[job_key]"] = list( - "# [TOTAL_POSITIONS]" = occupation.total_positions, - "# [SPAWN_POSITIONS]" = occupation.spawn_positions, - "# [PLAYTIME_REQUIREMENTS]" = occupation.exp_requirements, - "# [REQUIRED_ACCOUNT_AGE]" = occupation.minimal_player_age, - "# [REQUIRED_CHARACTER_AGE]" = occupation.required_character_age, - ) - - if(!export_toml(user, file_data)) - return FALSE - return TRUE - -/// Proc that we call to generate a new jobconfig.toml file and send it to the requesting client. Returns TRUE if a file is successfully generated. -/datum/controller/subsystem/job/proc/export_toml(mob/user, data) - var/file_location = "data/jobconfig.toml" // store it in the data folder server-side so we can FTP it to the client. - var/payload = "[config_documentation]\n[rustg_toml_encode(data)]" - rustg_file_write(payload, file_location) - DIRECT_OUTPUT(user, ftp(file(file_location), "jobconfig.toml")) - return TRUE - -#undef TOTAL_POSITIONS -#undef SPAWN_POSITIONS -#undef PLAYTIME_REQUIREMENTS -#undef REQUIRED_ACCOUNT_AGE -#undef REQUIRED_CHARACTER_AGE - /datum/controller/subsystem/job/proc/HandleFeedbackGathering() for(var/datum/job/job as anything in joinable_occupations) var/high = 0 //high diff --git a/code/datums/job_configs/_job_configs.dm b/code/datums/job_configs/_job_configs.dm new file mode 100644 index 00000000000..84e2cb4ec0a --- /dev/null +++ b/code/datums/job_configs/_job_configs.dm @@ -0,0 +1,42 @@ +// This contains the types of configurations that can be set for each job. +// Just add a new datum (and add the define name to __defines/jobs.dm) and the applicable procs and you should be good to go. +// Remember, there's a verb in the Server tab called "Generate Job Configuration" that will generate the config file for you. +// You don't need to waste time copy-pasting values if you add a datum here. Just use that verb and it'll do it for you. +// Use the verb. Use the verb. It's your life you're wasting otherwise. + +/// Lightweight datum simply used to store the applicable config type for each job such that the whole system is a tad bit more flexible. +/datum/job_config_type + /// The name that will be used in the config file. This is also the key for the accessing the singleton. + /// Use the JOB_CONFIG_* defines in __defines/jobs.dm to make sure you don't typo. + var/name = "DEFAULT" + + /// The name of the variable on the job datum that we will be accessing. + var/datum_var_name = "type" // we use this as the default because A) it always exists and B) if we try and modify it, we runtime. perfect for what we need + +/datum/job_config_type/New() + . = ..() + if(PERFORM_ALL_TESTS(focus_only/missing_job_datum_variables)) + var/datum/job/test_occupation = new() + if(!test_occupation.vars.Find(datum_var_name)) + stack_trace("'[datum_var_name]' is not a valid variable on /datum/job!") + qdel(test_occupation) + +/// Simply gets the value of the config type for a given job. There can be overrides for special instances on subtypes. +/datum/job_config_type/proc/get_current_value(datum/job/occupation) + return occupation.vars[datum_var_name] + +/// Validate the value of the config type for a given job. There can be overrides for special instances on subtypes. +/// Isn't meant for in-depth logic, just bare-bones sanity checks. Like: is this number a number? Is this string a string? Any sanity thing involving a specific job datum goes in set_current_value. +/// Will return TRUE if the value is valid, FALSE if it is not. +/datum/job_config_type/proc/validate_value(value) + SHOULD_CALL_PARENT(FALSE) + stack_trace("Attempted to validate value for the default job config! You're doing something wrong!!") + return FALSE + +/// This is the proc that we actually invoke to set the config-based values for each job. Is also intended to handle all in-depth logic checks pertient to the job datum itself. +/// Return TRUE if the value was set successfully (or if expected behavior did indeed occur), FALSE if it was not. +/datum/job_config_type/proc/set_current_value(datum/job/occupation, value) + if(!validate_value(value)) + return FALSE + occupation.vars[datum_var_name] = value + return TRUE diff --git a/code/datums/job_configs/default_positions.dm b/code/datums/job_configs/default_positions.dm new file mode 100644 index 00000000000..67f2c2bfbfa --- /dev/null +++ b/code/datums/job_configs/default_positions.dm @@ -0,0 +1,10 @@ +/// The number of positions a job can have at any given time. +/datum/job_config_type/default_positions + name = JOB_CONFIG_TOTAL_POSITIONS + datum_var_name = "total_positions" + +/datum/job_config_type/default_positions/validate_value(value) + if(isnum(value)) + return TRUE + return FALSE + diff --git a/code/datums/job_configs/playtime_requirements.dm b/code/datums/job_configs/playtime_requirements.dm new file mode 100644 index 00000000000..cb129792d41 --- /dev/null +++ b/code/datums/job_configs/playtime_requirements.dm @@ -0,0 +1,9 @@ +/// The amount of playtime required to join a job (minutes). +/datum/job_config_type/playtime_requirements + name = JOB_CONFIG_PLAYTIME_REQUIREMENTS + datum_var_name = "exp_requirements" + +/datum/job_config_type/playtime_requirements/validate_value(value) + if(isnum(value)) + return TRUE + return FALSE diff --git a/code/datums/job_configs/required_account_age.dm b/code/datums/job_configs/required_account_age.dm new file mode 100644 index 00000000000..46d7a0e6489 --- /dev/null +++ b/code/datums/job_configs/required_account_age.dm @@ -0,0 +1,10 @@ +/// The amount of time required to have an account to join a job (days). +/datum/job_config_type/required_account_age + name = JOB_CONFIG_REQUIRED_ACCOUNT_AGE + datum_var_name = "minimal_player_age" + +/datum/job_config_type/required_account_age/validate_value(value) + if(isnum(value)) + return TRUE + return FALSE + diff --git a/code/datums/job_configs/required_character_age.dm b/code/datums/job_configs/required_character_age.dm new file mode 100644 index 00000000000..3ac22301f40 --- /dev/null +++ b/code/datums/job_configs/required_character_age.dm @@ -0,0 +1,35 @@ +/// The required age a character must be to join a job (which is in years). +/datum/job_config_type/required_character_age + name = JOB_CONFIG_REQUIRED_CHARACTER_AGE + datum_var_name = "required_character_age" + +/datum/job_config_type/required_character_age/get_current_value(datum/job/occupation) + . = ..() + + if(isnum(.)) + return . + + return 0 + +/datum/job_config_type/required_character_age/validate_value(value) + if(isnum(value)) + return TRUE + return FALSE + +/datum/job_config_type/required_character_age/set_current_value(datum/job/occupation, value) + if(!validate_value(value)) + return FALSE + + if(value > AGE_MIN && value < AGE_MAX) + occupation.required_character_age = value + return TRUE + + if(value == 0) + occupation.required_character_age = null // they're opting out of the codebase-set required character age, so set it to null since that's what the code needs to ignore it + return TRUE + + var/error_string = "Invalid value for [name] for [occupation.title] (with config tag [occupation.config_tag])! Value must be between [AGE_MIN] and [AGE_MAX]!" + error_string += "\n[occupation.title]'s required age will remain the default value of [occupation.required_character_age || "0 (OFF)"]!" + log_config(error_string) + log_job_debug(error_string) + return FALSE diff --git a/code/datums/job_configs/starting_positions.dm b/code/datums/job_configs/starting_positions.dm new file mode 100644 index 00000000000..4d9caa9a8fa --- /dev/null +++ b/code/datums/job_configs/starting_positions.dm @@ -0,0 +1,10 @@ +/// The number of positions a job can have at the start of the round. +/datum/job_config_type/starting_positions + name = JOB_CONFIG_SPAWN_POSITIONS + datum_var_name = "spawn_positions" + +/datum/job_config_type/starting_positions/validate_value(value) + if(isnum(value)) + return TRUE + return FALSE + diff --git a/code/modules/unit_tests/focus_only_tests.dm b/code/modules/unit_tests/focus_only_tests.dm index 18b2cfbdcd5..95953baa9db 100644 --- a/code/modules/unit_tests/focus_only_tests.dm +++ b/code/modules/unit_tests/focus_only_tests.dm @@ -35,3 +35,6 @@ /// Checks for bad icon / icon state setups in cooking crafting menu /datum/unit_test/focus_only/bad_cooking_crafting_icons + +/// Checks to ensure that variables expected to exist in a job datum (for config reasons) actually exist +/datum/unit_test/focus_only/missing_job_datum_variables diff --git a/config/game_options.txt b/config/game_options.txt index 1bf33d2a30c..24b013b3fee 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -445,10 +445,12 @@ MORGUE_CADAVER_OTHER_SPECIES_PROBABILITY 50 #MORGUE_CADAVER_OVERRIDE_SPECIES lizard -##Overflow job. Default is assistant +##Overflow job. Default is assistant. +## NOTE: This will overwrite anything you put in jobconfig.toml! OVERFLOW_JOB Assistant ## Overflow slot cap. Set to -1 for unlimited. If limited, it will still open up if every other job is full. +## NOTE: This will overwrite anything you put in jobconfig.toml! OVERFLOW_CAP -1 ## Uncomment to bring back old grey suit assistants instead of the now default rainbow colored assistants. diff --git a/config/jobconfig.toml b/config/jobconfig.toml index 257ad59e913..fb9cfdc561e 100644 --- a/config/jobconfig.toml +++ b/config/jobconfig.toml @@ -5,7 +5,8 @@ ## 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. ## Playtime Requirements is in minutes, and the job will unlock when a player reaches that amount of time. ## 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. -## Required Character Age checks if your character is sutable for this job. 0 = disabled. +## 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. + ## As time goes on, more config options may be added to this file. ## 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. ## It will always respect prior-existing values in the config, but will appropriately add more fields when they generate. diff --git a/tgstation.dme b/tgstation.dme index 02b8c696601..37451783ac3 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -618,6 +618,7 @@ #include "code\controllers\configuration\entries\game_options.dm" #include "code\controllers\configuration\entries\general.dm" #include "code\controllers\configuration\entries\interview.dm" +#include "code\controllers\configuration\entries\jobs.dm" #include "code\controllers\configuration\entries\lua.dm" #include "code\controllers\configuration\entries\resources.dm" #include "code\controllers\subsystem\achievements.dm" @@ -1397,6 +1398,12 @@ #include "code\datums\id_trim\outfits.dm" #include "code\datums\id_trim\ruins.dm" #include "code\datums\id_trim\syndicate.dm" +#include "code\datums\job_configs\_job_configs.dm" +#include "code\datums\job_configs\default_positions.dm" +#include "code\datums\job_configs\playtime_requirements.dm" +#include "code\datums\job_configs\required_account_age.dm" +#include "code\datums\job_configs\required_character_age.dm" +#include "code\datums\job_configs\starting_positions.dm" #include "code\datums\keybinding\_defines.dm" #include "code\datums\keybinding\_keybindings.dm" #include "code\datums\keybinding\admin.dm"