Changes job config format to use TOML, and add tools for migrating existing format (#70199)

This commit is contained in:
san7890
2022-10-18 15:51:24 -06:00
committed by GitHub
parent 468809e38f
commit 901662f59b
49 changed files with 574 additions and 61 deletions

View File

@@ -88,6 +88,8 @@
LoadMOTD()
LoadPolicy()
LoadChatFilter()
if(CONFIG_GET(flag/load_jobs_from_txt))
validate_job_config()
loaded = TRUE
@@ -468,6 +470,31 @@ Example config:
var/regex_filter = whitespace_split != "" ? "([whitespace_split]|[word_bounds])" : word_bounds
return regex(regex_filter, "i")
/// Check to ensure that the jobconfig is valid/in-date.
/datum/controller/configuration/proc/validate_job_config()
var/config_toml = "[directory]/jobconfig.toml"
var/config_txt = "[directory]/jobs.txt"
var/message = "Notify Server Operators: "
log_config("Validating config file jobconfig.toml...")
if(!fexists(file(config_toml)))
SSjob.legacy_mode = TRUE
message += "jobconfig.toml not found, falling back to legacy mode (using jobs.txt). To surpress this warning, generate a jobconfig.toml by running the verb 'Generate Job Configuration' in the Server tab.\n\
From there, you can then add it to the /config folder of your server to have it take effect for future rounds."
if(!fexists(file(config_txt)))
message += "\n\nFailed to set up legacy mode, jobs.txt not found! Codebase defaults will be used. If you do not wish to use this system, please disable it by commenting out the LOAD_JOBS_FROM_TXT config flag."
log_config(message)
DelayedMessageAdmins(span_notice(message))
return
var/list/result = rustg_raw_read_toml_file(config_toml)
if(!result["success"])
message += "The job config (jobconfig.toml) is not configured correctly! [result["content"]]"
log_config(message)
DelayedMessageAdmins(span_notice(message))
//Message admins when you can.
/datum/controller/configuration/proc/DelayedMessageAdmins(text)
addtimer(CALLBACK(GLOBAL_PROC, /proc/message_admins, text), 0)

View File

@@ -62,12 +62,26 @@ SUBSYSTEM_DEF(job)
/// Dictionary that maps job priorities to low/medium/high. Keys have to be number-strings as assoc lists cannot be indexed by integers. Set in setup_job_lists.
var/list/job_priorities_to_strings
/// Are we using the old job config system (txt) or the new job config system (TOML)? IF we are going to use the txt file, then we are in "legacy mode", and this will flip to TRUE.
var/legacy_mode = FALSE
/// 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) four 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## As time goes on, more config options may be added to this file.\n\
## You can use the admin verb 'Generate Job Configuration' in-game to auto-regenerate this config as a downloadable file without having to manually edit this file if we add more jobs or more things you can edit here.\n\
## It will always respect prior-existing values in the config, but will appropriately add more fields when they generate.\n## It's strongly advised you create your own version of this file rather than use the one provisioned on the codebase.\n\n\
## The game will not read any line that is commented out with a '#', as to allow you to defer to codebase defaults.\n## If you want to override the codebase values, add the value and then uncomment that line by removing the # from the job key's name.\n\
## Ensure that the key is flush, do not introduce any whitespaces when you uncomment a key. For example:\n## \"# Total Positions\" should always be changed to \"Total Positions\", no additional spacing. \n\
## Best of luck editing!\n"
/datum/controller/subsystem/job/Initialize()
setup_job_lists()
if(!length(all_occupations))
SetupOccupations()
if(CONFIG_GET(flag/load_jobs_from_txt))
LoadJobs()
load_jobs_from_config()
set_overflow_role(CONFIG_GET(string/overflow_job))
return SS_INIT_SUCCESS
@@ -590,14 +604,203 @@ 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"
/datum/controller/subsystem/job/proc/LoadJobs()
var/jobstext = file2text("[global.config.directory]/jobs.txt")
for(var/datum/job/job as anything in joinable_occupations)
var/regex/jobs = new("[job.title]=(-1|\\d+),(-1|\\d+)")
jobs.Find(jobstext)
job.total_positions = text2num(jobs.group[1])
job.spawn_positions = text2num(jobs.group[2])
/// 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]
if(default_positions)
occupation.total_positions = default_positions
if(starting_positions)
occupation.spawn_positions = starting_positions
if(playtime_requirements)
occupation.exp_requirements = playtime_requirements
if(required_account_age)
occupation.minimal_player_age = required_account_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,
)
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,
)
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
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]
// 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.
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,
)
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,
)
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
/datum/controller/subsystem/job/proc/HandleFeedbackGathering()
for(var/datum/job/job as anything in joinable_occupations)