diff --git a/code/controllers/configuration/config_entry.dm b/code/controllers/configuration/config_entry.dm new file mode 100644 index 0000000000..b439743a79 --- /dev/null +++ b/code/controllers/configuration/config_entry.dm @@ -0,0 +1,198 @@ +#undef CURRENT_RESIDENT_FILE + +#define LIST_MODE_NUM 0 +#define LIST_MODE_TEXT 1 +#define LIST_MODE_FLAG 2 + +/datum/config_entry + var/name //read-only, this is determined by the last portion of the derived entry type + var/value + var/default //read-only, just set value directly + + var/resident_file //the file which this belongs to, must be set + var/modified = FALSE //set to TRUE if the default has been overridden by a config entry + + var/protection = NONE + var/abstract_type = /datum/config_entry //do not instantiate if type matches this + + var/dupes_allowed = FALSE + +/datum/config_entry/New() + if(!resident_file) + CRASH("Config entry [type] has no resident_file set") + if(type == abstract_type) + CRASH("Abstract config entry [type] instatiated!") + name = lowertext(type2top(type)) + if(islist(value)) + var/list/L = value + default = L.Copy() + else + default = value + +/datum/config_entry/Destroy() + config.RemoveEntry(src) + return ..() + +/datum/config_entry/can_vv_get(var_name) + . = ..() + if(var_name == "value" || var_name == "default") + . &= !(protection & CONFIG_ENTRY_HIDDEN) + +/datum/config_entry/vv_edit_var(var_name, var_value) + var/static/list/banned_edits = list("name", "default", "resident_file", "protection", "abstract_type", "modified", "dupes_allowed") + if(var_name == "value") + if(protection & CONFIG_ENTRY_LOCKED) + return FALSE + . = ValidateAndSet("[var_value]") + if(.) + var_edited = TRUE + return + if(var_name in banned_edits) + return FALSE + return ..() + +/datum/config_entry/proc/VASProcCallGuard(str_val) + . = !(IsAdminAdvancedProcCall() && GLOB.LastAdminCalledProc == "ValidateAndSet" && GLOB.LastAdminCalledTargetRef == "\ref[src]") + if(!.) + log_admin_private("Config set of [type] to [str_val] attempted by [key_name(usr)]") + +/datum/config_entry/proc/ValidateAndSet(str_val) + VASProcCallGuard(str_val) + CRASH("Invalid config entry type!") + +/datum/config_entry/proc/ValidateKeyedList(str_val, list_mode, splitter) + str_val = trim(str_val) + var/key_pos = findtext(str_val, splitter) + var/key_name = null + var/key_value = null + + if(key_pos || list_mode == LIST_MODE_FLAG) + key_name = lowertext(copytext(str_val, 1, key_pos)) + key_value = copytext(str_val, key_pos + 1) + var/temp + var/continue_check + switch(list_mode) + if(LIST_MODE_FLAG) + temp = TRUE + continue_check = TRUE + if(LIST_MODE_NUM) + temp = text2num(key_value) + continue_check = !isnull(temp) + if(LIST_MODE_TEXT) + temp = key_value + continue_check = temp + if(continue_check && ValidateKeyName(key_name)) + value[key_name] = temp + return TRUE + return FALSE + +/datum/config_entry/proc/ValidateKeyName(key_name) + return TRUE + +/datum/config_entry/string + value = "" + abstract_type = /datum/config_entry/string + var/auto_trim = TRUE + +/datum/config_entry/string/vv_edit_var(var_name, var_value) + return var_name != "auto_trim" && ..() + +/datum/config_entry/string/ValidateAndSet(str_val) + if(!VASProcCallGuard(str_val)) + return FALSE + value = auto_trim ? trim(str_val) : str_val + return TRUE + +/datum/config_entry/number + value = 0 + abstract_type = /datum/config_entry/number + var/integer = TRUE + var/max_val = INFINITY + var/min_val = -INFINITY + +/datum/config_entry/number/ValidateAndSet(str_val) + if(!VASProcCallGuard(str_val)) + return FALSE + var/temp = text2num(trim(str_val)) + if(!isnull(temp)) + value = Clamp(integer ? round(temp) : temp, min_val, max_val) + if(value != temp && !var_edited) + log_config("Changing [name] from [temp] to [value]!") + return TRUE + return FALSE + +/datum/config_entry/number/vv_edit_var(var_name, var_value) + var/static/list/banned_edits = list("max_val", "min_val", "integer") + return !(var_name in banned_edits) && ..() + +/datum/config_entry/flag + value = FALSE + abstract_type = /datum/config_entry/flag + +/datum/config_entry/flag/ValidateAndSet(str_val) + if(!VASProcCallGuard(str_val)) + return FALSE + value = text2num(trim(str_val)) != 0 + return TRUE + +/datum/config_entry/number_list + abstract_type = /datum/config_entry/number_list + value = list() + +/datum/config_entry/number_list/ValidateAndSet(str_val) + if(!VASProcCallGuard(str_val)) + return FALSE + str_val = trim(str_val) + var/list/new_list = list() + var/list/values = splittext(str_val," ") + for(var/I in values) + var/temp = text2num(I) + if(isnull(temp)) + return FALSE + new_list += temp + if(!new_list.len) + return FALSE + value = new_list + return TRUE + +/datum/config_entry/keyed_flag_list + abstract_type = /datum/config_entry/keyed_flag_list + value = list() + dupes_allowed = TRUE + +/datum/config_entry/keyed_flag_list/ValidateAndSet(str_val) + if(!VASProcCallGuard(str_val)) + return FALSE + return ValidateKeyedList(str_val, LIST_MODE_FLAG, " ") + +/datum/config_entry/keyed_number_list + abstract_type = /datum/config_entry/keyed_number_list + value = list() + dupes_allowed = TRUE + var/splitter = " " + +/datum/config_entry/keyed_number_list/vv_edit_var(var_name, var_value) + return var_name != "splitter" && ..() + +/datum/config_entry/keyed_number_list/ValidateAndSet(str_val) + if(!VASProcCallGuard(str_val)) + return FALSE + return ValidateKeyedList(str_val, LIST_MODE_NUM, splitter) + +/datum/config_entry/keyed_string_list + abstract_type = /datum/config_entry/keyed_string_list + value = list() + dupes_allowed = TRUE + var/splitter = " " + +/datum/config_entry/keyed_string_list/vv_edit_var(var_name, var_value) + return var_name != "splitter" && ..() + +/datum/config_entry/keyed_string_list/ValidateAndSet(str_val) + if(!VASProcCallGuard(str_val)) + return FALSE + return ValidateKeyedList(str_val, LIST_MODE_TEXT, splitter) + +#undef LIST_MODE_NUM +#undef LIST_MODE_TEXT +#undef LIST_MODE_FLAG diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm new file mode 100644 index 0000000000..8df012e5de --- /dev/null +++ b/code/controllers/configuration/configuration.dm @@ -0,0 +1,287 @@ +GLOBAL_VAR_INIT(config_dir, "config/") +GLOBAL_PROTECT(config_dir) + +/datum/controller/configuration + name = "Configuration" + + var/hiding_entries_by_type = TRUE //Set for readability, admins can set this to FALSE if they want to debug it + var/list/entries + var/list/entries_by_type + + var/list/maplist + var/datum/map_config/defaultmap + + var/list/modes // allowed modes + var/list/gamemode_cache + var/list/votable_modes // votable modes + var/list/mode_names + var/list/mode_reports + var/list/mode_false_report_weight + +/datum/controller/configuration/New() + config = src + var/list/config_files = InitEntries() + LoadModes() + for(var/I in config_files) + LoadEntries(I) + if(Get(/datum/config_entry/flag/maprotation)) + loadmaplist(CONFIG_MAPS_FILE) + +/datum/controller/configuration/Destroy() + entries_by_type.Cut() + QDEL_LIST_ASSOC_VAL(entries) + QDEL_LIST_ASSOC_VAL(maplist) + QDEL_NULL(defaultmap) + + config = null + + return ..() + +/datum/controller/configuration/proc/InitEntries() + var/list/_entries = list() + entries = _entries + var/list/_entries_by_type = list() + entries_by_type = _entries_by_type + + . = list() + + for(var/I in typesof(/datum/config_entry)) //typesof is faster in this case + var/datum/config_entry/E = I + if(initial(E.abstract_type) == I) + continue + E = new I + _entries_by_type[I] = E + var/esname = E.name + var/datum/config_entry/test = _entries[esname] + if(test) + log_config("Error: [test.type] has the same name as [E.type]: [esname]! Not initializing [E.type]!") + qdel(E) + continue + _entries[esname] = E + .[E.resident_file] = TRUE + +/datum/controller/configuration/proc/RemoveEntry(datum/config_entry/CE) + entries -= CE.name + entries_by_type -= CE.type + +/datum/controller/configuration/proc/LoadEntries(filename) + log_config("Loading config file [filename]...") + var/list/lines = world.file2list("[GLOB.config_dir][filename]") + var/list/_entries = entries + for(var/L in lines) + if(!L) + continue + + if(copytext(L, 1, 2) == "#") + continue + + var/pos = findtext(L, " ") + var/entry = null + var/value = null + + if(pos) + entry = lowertext(copytext(L, 1, pos)) + value = copytext(L, pos + 1) + else + entry = lowertext(L) + + if(!entry) + continue + + var/datum/config_entry/E = _entries[entry] + if(!E) + log_config("Unknown setting in configuration: '[entry]'") + continue + + if(filename != E.resident_file) + log_config("Found [entry] in [filename] when it should have been in [E.resident_file]! Ignoring.") + continue + + var/validated = E.ValidateAndSet(value) + if(!validated) + log_config("Failed to validate setting \"[value]\" for [entry]") + else if(E.modified && !E.dupes_allowed) + log_config("Duplicate setting for [entry] ([value]) detected! Using latest.") + + if(validated) + E.modified = TRUE + +/datum/controller/configuration/can_vv_get(var_name) + return (var_name != "entries_by_type" || !hiding_entries_by_type) && ..() + +/datum/controller/configuration/vv_edit_var(var_name, var_value) + return !(var_name in list("entries_by_type", "entries")) && ..() + +/datum/controller/configuration/stat_entry() + if(!statclick) + statclick = new/obj/effect/statclick/debug(null, "Edit", src) + stat("[name]:", statclick) + +/datum/controller/configuration/proc/Get(entry_type) + if(IsAdminAdvancedProcCall() && GLOB.LastAdminCalledProc == "Get" && GLOB.LastAdminCalledTargetRef == "\ref[src]") + log_admin_private("Config access of [entry_type] attempted by [key_name(usr)]") + return + var/datum/config_entry/E = entry_type + var/entry_is_abstract = initial(E.abstract_type) == entry_type + if(entry_is_abstract) + CRASH("Tried to retrieve an abstract config_entry: [entry_type]") + E = entries_by_type[entry_type] + if(!E) + CRASH("Missing config entry for [entry_type]!") + return E.value + +/datum/controller/configuration/proc/Set(entry_type, new_val) + if(IsAdminAdvancedProcCall() && GLOB.LastAdminCalledProc == "Set" && GLOB.LastAdminCalledTargetRef == "\ref[src]") + log_admin_private("Config rewrite of [entry_type] to [new_val] attempted by [key_name(usr)]") + return + var/datum/config_entry/E = entry_type + var/entry_is_abstract = initial(E.abstract_type) == entry_type + if(entry_is_abstract) + CRASH("Tried to retrieve an abstract config_entry: [entry_type]") + E = entries_by_type[entry_type] + if(!E) + CRASH("Missing config entry for [entry_type]!") + return E.ValidateAndSet(new_val) + +/datum/controller/configuration/proc/LoadModes() + gamemode_cache = typecacheof(/datum/game_mode, TRUE) + modes = list() + mode_names = list() + mode_reports = list() + mode_false_report_weight = list() + votable_modes = list() + var/list/probabilities = Get(/datum/config_entry/keyed_number_list/probability) + for(var/T in gamemode_cache) + // I wish I didn't have to instance the game modes in order to look up + // their information, but it is the only way (at least that I know of). + var/datum/game_mode/M = new T() + + if(M.config_tag) + if(!(M.config_tag in modes)) // ensure each mode is added only once + modes += M.config_tag + mode_names[M.config_tag] = M.name + probabilities[M.config_tag] = M.probability + mode_reports[M.config_tag] = M.generate_report() + mode_false_report_weight[M.config_tag] = M.false_report_weight + if(M.votable) + votable_modes += M.config_tag + qdel(M) + votable_modes += "secret" + +/datum/controller/configuration/proc/loadmaplist(filename) + filename = "[GLOB.config_dir][filename]" + var/list/Lines = world.file2list(filename) + + var/datum/map_config/currentmap = null + for(var/t in Lines) + if(!t) + continue + + t = trim(t) + if(length(t) == 0) + continue + else if(copytext(t, 1, 2) == "#") + continue + + var/pos = findtext(t, " ") + var/command = null + var/data = null + + if(pos) + command = lowertext(copytext(t, 1, pos)) + data = copytext(t, pos + 1) + else + command = lowertext(t) + + if(!command) + continue + + if (!currentmap && command != "map") + continue + + switch (command) + if ("map") + currentmap = new ("_maps/[data].json") + if(currentmap.defaulted) + log_config("Failed to load map config for [data]!") + if ("minplayers","minplayer") + currentmap.config_min_users = text2num(data) + if ("maxplayers","maxplayer") + currentmap.config_max_users = text2num(data) + if ("weight","voteweight") + currentmap.voteweight = text2num(data) + if ("default","defaultmap") + defaultmap = currentmap + if ("endmap") + LAZYINITLIST(maplist) + maplist[currentmap.map_name] = currentmap + currentmap = null + if ("disabled") + currentmap = null + else + WRITE_FILE(GLOB.config_error_log, "Unknown command in map vote config: '[command]'") + + +/datum/controller/configuration/proc/pick_mode(mode_name) + // I wish I didn't have to instance the game modes in order to look up + // their information, but it is the only way (at least that I know of). + // ^ This guy didn't try hard enough + for(var/T in gamemode_cache) + var/datum/game_mode/M = T + var/ct = initial(M.config_tag) + if(ct && ct == mode_name) + return new T + return new /datum/game_mode/extended() + +/datum/controller/configuration/proc/get_runnable_modes() + var/list/datum/game_mode/runnable_modes = new + var/list/probabilities = Get(/datum/config_entry/keyed_number_list/probability) + var/list/min_pop = Get(/datum/config_entry/keyed_number_list/min_pop) + var/list/max_pop = Get(/datum/config_entry/keyed_number_list/max_pop) + var/list/repeated_mode_adjust = Get(/datum/config_entry/number_list/repeated_mode_adjust) + for(var/T in gamemode_cache) + var/datum/game_mode/M = new T() + if(!(M.config_tag in modes)) + qdel(M) + continue + if(probabilities[M.config_tag]<=0) + qdel(M) + continue + if(min_pop[M.config_tag]) + M.required_players = min_pop[M.config_tag] + if(max_pop[M.config_tag]) + M.maximum_players = max_pop[M.config_tag] + if(M.can_start()) + var/final_weight = probabilities[M.config_tag] + if(SSpersistence.saved_modes.len == 3 && repeated_mode_adjust.len == 3) + var/recent_round = min(SSpersistence.saved_modes.Find(M.config_tag),3) + var/adjustment = 0 + while(recent_round) + adjustment += repeated_mode_adjust[recent_round] + recent_round = SSpersistence.saved_modes.Find(M.config_tag,recent_round+1,0) + final_weight *= ((100-adjustment)/100) + runnable_modes[M] = final_weight + return runnable_modes + +/datum/controller/configuration/proc/get_runnable_midround_modes(crew) + var/list/datum/game_mode/runnable_modes = new + var/list/probabilities = Get(/datum/config_entry/keyed_number_list/probability) + var/list/min_pop = Get(/datum/config_entry/keyed_number_list/min_pop) + var/list/max_pop = Get(/datum/config_entry/keyed_number_list/max_pop) + for(var/T in (gamemode_cache - SSticker.mode.type)) + var/datum/game_mode/M = new T() + if(!(M.config_tag in modes)) + qdel(M) + continue + if(probabilities[M.config_tag]<=0) + qdel(M) + continue + if(min_pop[M.config_tag]) + M.required_players = min_pop[M.config_tag] + if(max_pop[M.config_tag]) + M.maximum_players = max_pop[M.config_tag] + if(M.required_players <= crew) + if(M.maximum_players >= 0 && M.maximum_players < crew) + continue + runnable_modes[M] = probabilities[M.config_tag] + return runnable_modes diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index f8131ec5c7..cb5ce19d68 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -96,10 +96,16 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that to_chat(usr, .) SSblackbox.add_details("admin_verb","Advanced ProcCall") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -GLOBAL_VAR_INIT(AdminProcCaller, null) +GLOBAL_VAR(AdminProcCaller) GLOBAL_PROTECT(AdminProcCaller) GLOBAL_VAR_INIT(AdminProcCallCount, 0) GLOBAL_PROTECT(AdminProcCallCount) +GLOBAL_VAR(LastAdminCalledTargetRef) +GLOBAL_PROTECT(LastAdminCalledTargetRef) +GLOBAL_VAR(LastAdminCalledTarget) +GLOBAL_PROTECT(LastAdminCalledTarget) +GLOBAL_VAR(LastAdminCalledProc) +GLOBAL_PROTECT(LastAdminCalledProc) /proc/WrapAdminProcCall(target, procname, list/arguments) var/current_caller = GLOB.AdminProcCaller @@ -108,6 +114,9 @@ GLOBAL_PROTECT(AdminProcCallCount) to_chat(usr, "Another set of admin called procs are still running, your proc will be run after theirs finish.") UNTIL(!GLOB.AdminProcCaller) to_chat(usr, "Running your proc") + GLOB.LastAdminCalledProc = procname + if(target != GLOBAL_PROC) + GLOB.LastAdminCalledTargetRef = "\ref[target]" GLOB.AdminProcCaller = ckey //if this runtimes, too bad for you ++GLOB.AdminProcCallCount . = world.WrapAdminProcCall(target, procname, arguments)