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)