From 9efe1080902bd9f9b9f08786cdec3136f605478a Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Tue, 3 Oct 2023 21:34:19 +0100 Subject: [PATCH] Admin dynamic rule management (#78720) ## About The Pull Request ![image](https://github.com/tgstation/tgstation/assets/7483112/a538643e-8d84-48b1-9dc0-c29a90d3a46c) This PR adds a very ugly menu which allows admins to force dynamic rulesets to be enabled or disabled. Some admins asked if someone could add this and it seems reasonably useful. ![image](https://github.com/tgstation/tgstation/assets/7483112/2a88cceb-0cdc-4d92-b688-19cd7daf31ae) The information you have available becomes slightly more detailed once the game is actually running, because then we know how many players and how much threat there is. ![image](https://github.com/tgstation/tgstation/assets/7483112/91fdc7e7-dc3e-4b56-a2db-a33cb241e39b) I don't know why the weight of every midround is 0 when I run locally. It's fine. Unfortunately, the interface I have crafted is hideous. This is because it's not part of tgui and I am not very good at working with byond UIs without my beautiful typescript. It functions though. If anyone has any pointers I'll take 'em. "Force disabling" a ruleset simply ensures it will never run. "Force enabling" a ruleset disregards minimum population and minimum threat rules, but doesn't ensure that the ruleset will run. It might still be skipped due to low weight, insufficient threat to _buy_ it, or other disqualifying factors. ## Why It's Good For The Game It was an admin request so presumably they have some plans for it. It's a less-intrusive way of theming a round than disabling dynamic and running all the midround rules yourself. If they want to temporarily set the game to traitorling I guess they can do that. Or only heretic/cult. ## Changelog :cl: admin: Admins can turn off dynamic rulesets (or force them on despite not meeting the qualification criteria) on a per-round basis. /:cl: --- code/__DEFINES/dynamic.dm | 6 + code/game/gamemodes/dynamic/dynamic.dm | 2 + .../dynamic/dynamic_midround_rolling.dm | 6 +- .../gamemodes/dynamic/dynamic_rulesets.dm | 36 ++++-- .../dynamic/dynamic_rulesets_latejoin.dm | 4 +- .../dynamic/dynamic_rulesets_midround.dm | 14 +-- code/modules/admin/admin.dm | 108 ++++++++++++++++++ code/modules/admin/topic.dm | 33 ++++++ 8 files changed, 189 insertions(+), 20 deletions(-) diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm index f07b7e20b0f..07c833cdc49 100644 --- a/code/__DEFINES/dynamic.dm +++ b/code/__DEFINES/dynamic.dm @@ -32,3 +32,9 @@ #define ROUNDSTART_RULESET "Roundstart" #define LATEJOIN_RULESET "Latejoin" #define MIDROUND_RULESET "Midround" + +#define RULESET_NOT_FORCED "not forced" +/// Ruleset should run regardless of population and threat available +#define RULESET_FORCE_ENABLED "force enabled" +/// Ruleset should not run regardless of population and threat available +#define RULESET_FORCE_DISABLED "force disabled" diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm index 5a86050b13e..e5f6d9cd3d2 100644 --- a/code/game/gamemodes/dynamic/dynamic.dm +++ b/code/game/gamemodes/dynamic/dynamic.dm @@ -17,6 +17,8 @@ GLOBAL_LIST_EMPTY(dynamic_forced_roundstart_ruleset) GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) /// Modify the threat level for station traits before dynamic can be Initialized. List(instance = threat_reduction) GLOBAL_LIST_EMPTY(dynamic_station_traits) +/// Rulesets which have been forcibly enabled or disabled +GLOBAL_LIST_EMPTY(dynamic_forced_rulesets) /datum/game_mode/dynamic // Threat logging vars diff --git a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm index 5079035834b..c3e295ae875 100644 --- a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm +++ b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm @@ -45,7 +45,11 @@ continue if (!ruleset.acceptable(GLOB.alive_player_list.len, threat_level)) - log_dynamic("FAIL: [ruleset] is not acceptable with the current parameters. Alive players: [GLOB.alive_player_list.len], threat level: [threat_level]") + var/ruleset_forced = GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED + if (ruleset_forced == RULESET_NOT_FORCED) + log_dynamic("FAIL: [ruleset] is not acceptable with the current parameters. Alive players: [GLOB.alive_player_list.len], threat level: [threat_level]") + else + log_dynamic("FAIL: [ruleset] was disabled.") continue if (mid_round_budget < ruleset.cost) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm index 80b3ac17410..c9161963f24 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm @@ -106,23 +106,39 @@ /// By default, a rule is acceptable if it satisfies the threat level/population requirements. /// If your rule has extra checks, such as counting security officers, do that in ready() instead /datum/dynamic_ruleset/proc/acceptable(population = 0, threat_level = 0) - pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : mode.pop_per_requirement - indice_pop = min(requirements.len,round(population/pop_per_requirement)+1) + var/ruleset_forced = GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED + if (ruleset_forced != RULESET_NOT_FORCED) + if (ruleset_forced == RULESET_FORCE_ENABLED) + return TRUE + else + log_dynamic("FAIL: [src] was disabled in admin panel.") + return FALSE - if(minimum_players > population) - log_dynamic("FAIL: [src] failed acceptable: minimum_players ([minimum_players]) > population ([population])") + if(!is_valid_population(population)) + var/range = maximum_players > 0 ? "([minimum_players] - [maximum_players])" : "(minimum: [minimum_players])" + log_dynamic("FAIL: [src] failed acceptable: min/max players out of range [range] vs population ([population])") return FALSE - if(maximum_players > 0 && population > maximum_players) - log_dynamic("FAIL: [src] failed acceptable: maximum_players ([maximum_players]) < population ([population])") - return FALSE - - if (threat_level < requirements[indice_pop]) + if (!is_valid_threat(population, threat_level)) log_dynamic("FAIL: [src] failed acceptable: threat_level ([threat_level]) < requirement ([requirements[indice_pop]])") return FALSE return TRUE +/// Returns true if we have enough players to run +/datum/dynamic_ruleset/proc/is_valid_population(population) + if(minimum_players > population) + return FALSE + if(maximum_players > 0 && population > maximum_players) + return FALSE + return TRUE + +/// Sets the current threat indices and returns true if we're inside of them +/datum/dynamic_ruleset/proc/is_valid_threat(population, threat_level) + pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : mode.pop_per_requirement + indice_pop = min(requirements.len,round(population/pop_per_requirement)+1) + return threat_level >= requirements[indice_pop] + /// When picking rulesets, if dynamic picks the same one multiple times, it will "scale up". /// However, doing this blindly would result in lowpop rounds (think under 10 people) where over 80% of the crew is antags! /// This function is here to ensure the antag ratio is kept under control while scaling up. @@ -175,7 +191,7 @@ candidates = list() assigned = list() antag_datum = null - + /// Here you can perform any additional checks you want. (such as checking the map etc) /// Remember that on roundstart no one knows what their job is at this point. /// IMPORTANT: If ready() returns TRUE, that means pre_execute() or execute() should never fail! diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm index 11cbf45d2bf..887bf04276a 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm @@ -30,8 +30,8 @@ job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it var/threat = round(mode.threat_level/10) - - if (job_check < required_enemies[threat]) + var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED + if (!ruleset_forced && job_check < required_enemies[threat]) log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found") return FALSE diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm index 85c389c5b3f..93912e06899 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm @@ -108,8 +108,8 @@ job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it var/threat = round(mode.threat_level/10) - - if (job_check < required_enemies[threat]) + var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED + if (!ruleset_forced && job_check < required_enemies[threat]) log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found") return FALSE @@ -373,7 +373,7 @@ var/list/operative_cap = list(2,2,3,3,4,5,5,5,5,5) -/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat=0) +/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat_level=0) if (locate(/datum/dynamic_ruleset/roundstart/nuclear) in mode.executed_rules) return FALSE // Unavailable if nuke ops were already sent at roundstart indice_pop = min(operative_cap.len, round(living_players.len/5)+1) @@ -528,7 +528,7 @@ minimum_players = 15 repeatable = TRUE -/datum/dynamic_ruleset/midround/from_ghosts/nightmare/acceptable(population = 0, threat = 0) +/datum/dynamic_ruleset/midround/from_ghosts/nightmare/acceptable(population = 0, threat_level = 0) var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) //Checks if there's a single safe, dark tile on station. if(!spawn_loc) return FALSE @@ -705,7 +705,7 @@ spawn_locs = list() return ..() -/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat=0) +/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat_level=0) if(GLOB.dead_mob_list.len < dead_mobs_required) return FALSE return ..() @@ -768,7 +768,7 @@ minimum_players = 20 repeatable = TRUE -/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat=0) +/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat_level=0) if (SSmapping.is_planetary() || GLOB.light_pirate_gangs.len == 0) return FALSE return ..() @@ -790,7 +790,7 @@ minimum_players = 25 repeatable = TRUE -/datum/dynamic_ruleset/midround/dangerous_pirates/acceptable(population=0, threat=0) +/datum/dynamic_ruleset/midround/dangerous_pirates/acceptable(population=0, threat_level=0) if (SSmapping.is_planetary() || GLOB.heavy_pirate_gangs.len == 0) return FALSE return ..() diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index bc6942a9a11..2d2c692ed5f 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -21,6 +21,7 @@ var/dat = "
Game Panel

" if(SSticker.current_state <= GAME_STATE_PREGAME) + dat += "(Manage Dynamic Rulesets)
" dat += "(Force Roundstart Rulesets)
" if (GLOB.dynamic_forced_roundstart_ruleset.len > 0) for(var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset) @@ -30,6 +31,8 @@ dat += "
" if(SSticker.IsRoundInProgress()) dat += "(Game Mode Panel)
" + if(istype(SSticker.mode, /datum/game_mode/dynamic)) + dat += "(Manage Dynamic Rulesets)
" dat += {"
Create Object
@@ -143,6 +146,111 @@ user << browse(dat, "window=dyn_mode_options;size=900x650") +/datum/admins/proc/dynamic_ruleset_manager(mob/user) + if (SSticker.current_state > GAME_STATE_PREGAME && !istype(SSticker.mode, /datum/game_mode/dynamic)) + return // Not running dynamic + + var/dat = "

Dynamic Ruleset Management


\ + Change these options to forcibly enable or disable dynamic rulesets.
\ + Disabled rulesets will never run, even if they would otherwise be valid.
\ + Enabled rulesets will run even if the qualifying minimum of threat or player count is not present, this does not guarantee that they will necessarily be chosen (for example their weight may be set to 0 in config).
\ + \[force enable all / \ + force disable all / \ + reset all\]" + + if (SSticker.current_state <= GAME_STATE_PREGAME) // Don't bother displaying after the round has started + var/static/list/rulesets_by_context = list() + if (!length(rulesets_by_context)) + for (var/datum/dynamic_ruleset/rule as anything in subtypesof(/datum/dynamic_ruleset)) + if (initial(rule.name) == "") + continue + LAZYADD(rulesets_by_context[initial(rule.ruletype)], rule) + + dat += dynamic_ruleset_category_pre_start_display("Roundstart", rulesets_by_context[ROUNDSTART_RULESET]) + dat += dynamic_ruleset_category_pre_start_display("Latejoin", rulesets_by_context[LATEJOIN_RULESET]) + dat += dynamic_ruleset_category_pre_start_display("Midround", rulesets_by_context[MIDROUND_RULESET]) + user << browse(dat, "window=dyn_mode_options;size=900x650") + return + + var/datum/game_mode/dynamic/current_mode = SSticker.mode + var/pop_count = length(GLOB.alive_player_list) + var/threat_level = current_mode.threat_level + dat += dynamic_ruleset_category_during_round_display("Latejoin", current_mode.latejoin_rules, pop_count, threat_level) + dat += dynamic_ruleset_category_during_round_display("Midround", current_mode.midround_rules, pop_count, threat_level) + user << browse(dat, "window=dyn_mode_options;size=900x650") + +/datum/admins/proc/dynamic_ruleset_category_pre_start_display(title, list/rules) + var/dat = "

[title]

" + for (var/datum/dynamic_ruleset/rule as anything in rules) + var/forced = GLOB.dynamic_forced_rulesets[rule] || RULESET_NOT_FORCED + var/color = COLOR_BLACK + switch (forced) + if (RULESET_FORCE_ENABLED) + color = COLOR_GREEN + if (RULESET_FORCE_DISABLED) + color = COLOR_RED + dat += "" + dat += "
[initial(rule.name)]\[[forced]\]\[\ + force enabled /\ + force disabled /\ + reset\]
" + return dat + +/datum/admins/proc/dynamic_ruleset_category_during_round_display(title, list/rules, pop_count, threat_level) + var/dat = "

[title]

" + for (var/datum/dynamic_ruleset/rule as anything in rules) + var/active = rule.acceptable(population = pop_count, threat_level = threat_level) && rule.weight > 0 + var/forced = GLOB.dynamic_forced_rulesets[rule.type] || RULESET_NOT_FORCED + var/color = (active) ? COLOR_GREEN : COLOR_RED + var/explanation = "" + if (!active) + if (rule.weight <= 0) + explanation = " - Weight is zero" + else if (forced == RULESET_FORCE_DISABLED) + explanation = " - Forcibly disabled" + else if (forced == RULESET_FORCE_ENABLED) + explanation = " - Failed spawn conditions" + else if (!rule.is_valid_population(pop_count)) + explanation = " - Invalid player count" + else if (!rule.is_valid_threat(pop_count, threat_level)) + explanation = " - Insufficient threat" + else + explanation = " - Failed spawn conditions" + else if (forced == RULESET_FORCE_ENABLED) + explanation = " - Forcibly enabled" + active = active ? "Active" : "Inactive" + + dat += "\ + \ + " + dat += "
[rule.name]\[Weight : [rule.weight]\]\ + \[[active][explanation]\]\[\ + force enabled /\ + force disabled /\ + reset\]\[VV\]
" + return dat + + +/datum/admins/proc/force_all_rulesets(mob/user, force_value) + if (force_value == RULESET_NOT_FORCED) + GLOB.dynamic_forced_rulesets = list() + else + for (var/datum/dynamic_ruleset/rule as anything in subtypesof(/datum/dynamic_ruleset)) + GLOB.dynamic_forced_rulesets[rule] = force_value + var/logged_message = "[key_name(user)] set all dynamic rulesets to [force_value]." + log_admin(logged_message) + message_admins(logged_message) + dynamic_ruleset_manager(user) + +/datum/admins/proc/set_dynamic_ruleset_forced(mob/user, datum/dynamic_ruleset/type, force_value) + if (isnull(type)) + return + GLOB.dynamic_forced_rulesets[type] = force_value + dynamic_ruleset_manager(user) + var/logged_message = "[key_name(user)] set '[initial(type.name)] ([initial(type.ruletype)])' to [GLOB.dynamic_forced_rulesets[type]]." + log_admin(logged_message) + message_admins(logged_message) + /datum/admins/proc/create_or_modify_area() set category = "Debug" set name = "Create or modify area" diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index b585b5c9103..89503ef1f43 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -430,6 +430,39 @@ log_admin("[key_name(usr)] removed [rule] from the forced roundstart rulesets.") message_admins("[key_name(usr)] removed [rule] from the forced roundstart rulesets.", 1) + else if (href_list["f_dynamic_ruleset_manage"]) + if(!check_rights(R_ADMIN)) + return + dynamic_ruleset_manager(usr) + else if (href_list["f_dynamic_ruleset_force_all_on"]) + if(!check_rights(R_ADMIN)) + return + force_all_rulesets(usr, RULESET_FORCE_ENABLED) + else if (href_list["f_dynamic_ruleset_force_all_off"]) + if(!check_rights(R_ADMIN)) + return + force_all_rulesets(usr, RULESET_FORCE_DISABLED) + else if (href_list["f_dynamic_ruleset_force_all_reset"]) + if(!check_rights(R_ADMIN)) + return + force_all_rulesets(usr, RULESET_NOT_FORCED) + else if (href_list["f_dynamic_ruleset_force_on"]) + if(!check_rights(R_ADMIN)) + return + set_dynamic_ruleset_forced(usr, locate(href_list["f_dynamic_ruleset_force_on"]), RULESET_FORCE_ENABLED) + else if (href_list["f_dynamic_ruleset_force_off"]) + if(!check_rights(R_ADMIN)) + return + set_dynamic_ruleset_forced(usr, locate(href_list["f_dynamic_ruleset_force_off"]), RULESET_FORCE_DISABLED) + else if (href_list["f_dynamic_ruleset_force_reset"]) + if(!check_rights(R_ADMIN)) + return + set_dynamic_ruleset_forced(usr, locate(href_list["f_dynamic_ruleset_force_reset"]), RULESET_NOT_FORCED) + else if (href_list["f_inspect_ruleset"]) + if(!check_rights(R_ADMIN)) + return + usr.client.debug_variables(locate(href_list["f_inspect_ruleset"])) + else if (href_list["f_dynamic_options"]) if(!check_rights(R_ADMIN)) return