Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic lategame & Midround simulations #9858

Merged
merged 7 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions code/__DEFINES/dynamic.dm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
/// This ruleset will be logged in persistence, to reduce the chances of it repeatedly rolling several rounds in a row.
#define PERSISTENT_RULESET (1 << 5)

/// Can this ruleset be executed once we consider the round to be in the late game context?
/// We generally want lategame rulesets to be chaotic and antagonists which do not require a long setup in order to get started.
/// These are rulesets that should push the round to a close
#define LATEGAME_RULESET (1 << 6)

/// This is a "heavy" midround ruleset, and should be run later into the round
#define MIDROUND_RULESET_STYLE_HEAVY "Heavy"

Expand Down
60 changes: 33 additions & 27 deletions code/__HELPERS/roundend.dm
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,10 @@
parts += "[FOURSPACES][FOURSPACES][entry]<BR>"
parts += "[FOURSPACES]Executed rules:"
for(var/datum/dynamic_ruleset/rule in mode.executed_rules)
parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - <b>[rule.name]</b>: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat"
if (rule.lategame_spawned)
parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - <b>[rule.name]</b>: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat (Lategame, threat level ignored)"
else
parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - <b>[rule.name]</b>: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat"
return parts.Join("<br>")

/client/proc/roundend_report_file()
Expand Down Expand Up @@ -726,29 +729,32 @@


/datum/controller/subsystem/ticker/proc/sendtodiscord(var/survivors, var/escapees, var/integrity)
var/discordmsg = ""
discordmsg += "--------------ROUND END--------------\n"
discordmsg += "Server: [CONFIG_GET(string/servername)]\n"
discordmsg += "Round Number: [GLOB.round_id]\n"
discordmsg += "Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]\n"
discordmsg += "Players: [GLOB.player_list.len]\n"
discordmsg += "Survivors: [survivors]\n"
discordmsg += "Escapees: [escapees]\n"
discordmsg += "Integrity: [integrity]\n"
discordmsg += "Gamemode: [SSticker.mode.name]\n"
if(istype(SSticker.mode, /datum/game_mode/dynamic))
var/datum/game_mode/dynamic/mode = SSticker.mode
discordmsg += "Threat level: [mode.threat_level]\n"
discordmsg += "Threat left: [mode.mid_round_budget]\n"
discordmsg += "Executed rules:\n"
for(var/datum/dynamic_ruleset/rule in mode.executed_rules)
discordmsg += "[rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat\n"
var/list/ded = SSblackbox.first_death
if(ded)
discordmsg += "First Death: [ded["name"]], [ded["role"]], at [ded["area"]]\n"
var/last_words = ded["last_words"] ? "Their last words were: \"[ded["last_words"]]\"\n" : "They had no last words.\n"
discordmsg += "[last_words]\n"
else
discordmsg += "Nobody died!\n"
discordmsg += "--------------------------------------\n"
sendooc2ext(discordmsg)
var/discordmsg = ""
discordmsg += "--------------ROUND END--------------\n"
discordmsg += "Server: [CONFIG_GET(string/servername)]\n"
discordmsg += "Round Number: [GLOB.round_id]\n"
discordmsg += "Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]\n"
discordmsg += "Players: [GLOB.player_list.len]\n"
discordmsg += "Survivors: [survivors]\n"
discordmsg += "Escapees: [escapees]\n"
discordmsg += "Integrity: [integrity]\n"
discordmsg += "Gamemode: [SSticker.mode.name]\n"
if(istype(SSticker.mode, /datum/game_mode/dynamic))
var/datum/game_mode/dynamic/mode = SSticker.mode
discordmsg += "Threat level: [mode.threat_level]\n"
discordmsg += "Threat left: [mode.mid_round_budget]\n"
discordmsg += "Executed rules:\n"
for(var/datum/dynamic_ruleset/rule in mode.executed_rules)
if (rule.lategame_spawned)
discordmsg += "[rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat (Lategame, threat level ignored)\n"
else
discordmsg += "[rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat\n"
var/list/ded = SSblackbox.first_death
if(ded)
discordmsg += "First Death: [ded["name"]], [ded["role"]], at [ded["area"]]\n"
var/last_words = ded["last_words"] ? "Their last words were: \"[ded["last_words"]]\"\n" : "They had no last words.\n"
discordmsg += "[last_words]\n"
else
discordmsg += "Nobody died!\n"
discordmsg += "--------------------------------------\n"
sendooc2ext(discordmsg)
31 changes: 26 additions & 5 deletions code/game/gamemodes/dynamic/dynamic.dm
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// The upper bound for the midround roll time splits.
/// This number influences where to place midround rolls, making this larger
/// will make midround rolls less frequent, and vice versa.
/// A midround will never be able to roll farther than this.
/// Once this time has passed, only midround antags with the LATEGAME_RULESET
/// flag may roll, and these will roll independent of threat requirements.
var/midround_upper_bound = 100 MINUTES

/// The distance between the chosen midround roll point (which is deterministic),
Expand Down Expand Up @@ -208,6 +209,15 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// Cached value of is_station_intact.
var/cached_station_intact = TRUE

/// If not null, use this instead of world.time
var/simulated_time = null
PowerfulBacon marked this conversation as resolved.
Show resolved Hide resolved

/// If we are running simulations and should treat nobody signing up as successful spawns
var/simulated = FALSE

/// Should we simulate there being more alive players than there actually are?
var/simulated_alive_players = 0

/// When the cached station intactness will expire.
COOLDOWN_DECLARE(intact_cache_expiry)

Expand Down Expand Up @@ -429,7 +439,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)

/datum/game_mode/dynamic/proc/set_cooldowns()
var/latejoin_injection_cooldown_middle = 0.5*(latejoin_delay_max + latejoin_delay_min)
latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + world.time
latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + get_time()

/datum/game_mode/dynamic/pre_setup()
if(CONFIG_GET(flag/dynamic_config_enabled))
Expand Down Expand Up @@ -604,7 +614,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
ruleset.trim_candidates()
var/added_threat = ruleset.scale_up(roundstart_pop_ready, scaled_times)

if(ruleset.pre_execute(roundstart_pop_ready))
if(simulated || ruleset.pre_execute(roundstart_pop_ready))
threat_log += "[worldtime2text()]: Roundstart [ruleset.name] spent [ruleset.cost + added_threat]. [ruleset.scaling_cost ? "Scaled up [ruleset.scaled_times]/[scaled_times] times." : ""]"
if(CHECK_BITFIELD(ruleset.flags, ONLY_RULESET))
only_ruleset_executed = TRUE
Expand Down Expand Up @@ -654,6 +664,9 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
if(threat_level < GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking && high_impact_ruleset_active())
return FALSE

// Stop respecting cost once we reach the late game stage
ignore_cost = ignore_cost || is_lategame()

var/population = current_players[CURRENT_LIVING_PLAYERS].len
if((new_rule.acceptable(population, threat_level) && (ignore_cost || new_rule.cost <= mid_round_budget)) || forced)
new_rule.trim_candidates()
Expand Down Expand Up @@ -728,7 +741,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/game_mode/dynamic, execute_midround_latejoin_rule), forced_latejoin_rule), forced_latejoin_rule.delay)
forced_latejoin_rule = null

else if (latejoin_injection_cooldown < world.time && (forced_injection || prob(latejoin_roll_chance)))
else if (latejoin_injection_cooldown < get_time() && (forced_injection || prob(latejoin_roll_chance)))
forced_injection = FALSE

var/list/drafted_rules = list()
Expand All @@ -750,7 +763,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)

if (drafted_rules.len > 0 && pick_latejoin_rule(drafted_rules))
var/latejoin_injection_cooldown_middle = 0.5*(latejoin_delay_max + latejoin_delay_min)
latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + world.time
latejoin_injection_cooldown = round(clamp(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), latejoin_delay_min, latejoin_delay_max)) + get_time()

/// Apply configurations to rule.
/datum/game_mode/dynamic/proc/configure_ruleset(datum/dynamic_ruleset/ruleset)
Expand Down Expand Up @@ -850,11 +863,19 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
if (20 to INFINITY)
return rand(90, 100)

/datum/game_mode/dynamic/proc/is_lategame()
return (get_time() - SSticker.round_start_time) > midround_upper_bound

/// Log to messages and to the game
/datum/game_mode/dynamic/proc/dynamic_log(text)
if (simulated)
return
message_admins("DYNAMIC: [text]")
log_game("DYNAMIC: [text]")

/datum/game_mode/dynamic/proc/get_time()
return simulated_time || world.time

/// This sets the "don't roll after high impact rules die" flag
/// if the mode is dynamic, signalling that something major has happened
/// and that dynamic should NOT try to roll new antags, even if all the
Expand Down
4 changes: 2 additions & 2 deletions code/game/gamemodes/dynamic/dynamic_hijacking.dm
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@

var/time_range = rand(random_event_hijack_minimum, random_event_hijack_maximum)

if (world.time - last_midround_injection_attempt < time_range)
if (get_time() - last_midround_injection_attempt < time_range)
random_event_hijacked = HIJACKED_TOO_RECENT
dynamic_log("Random event [round_event_control.name] tried to roll, but the last midround injection \
was too recent. Heavy injection chance has been raised to [get_heavy_midround_injection_chance(dry_run = TRUE)]%.")
return CANCEL_PRE_RANDOM_EVENT

if (next_midround_injection() - world.time < time_range)
if (next_midround_injection() - get_time() < time_range)
dynamic_log("Random event [round_event_control.name] tried to roll, but the next midround injection is too soon.")
return CANCEL_PRE_RANDOM_EVENT
2 changes: 1 addition & 1 deletion code/game/gamemodes/dynamic/dynamic_logging.dm
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
var/datum/dynamic_snapshot/new_snapshot = new

new_snapshot.remaining_threat = mid_round_budget
new_snapshot.time = world.time
new_snapshot.time = get_time()
new_snapshot.alive_players = current_players[CURRENT_LIVING_PLAYERS].len
new_snapshot.dead_players = current_players[CURRENT_DEAD_PLAYERS].len
new_snapshot.observers = current_players[CURRENT_OBSERVERS].len
Expand Down
39 changes: 23 additions & 16 deletions code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@

return last_midround_injection_attempt + distance

/datum/game_mode/dynamic/proc/try_midround_roll()
if (!forced_injection && next_midround_injection() > world.time)
return
/datum/game_mode/dynamic/proc/try_midround_roll(force = FALSE)
if (!forced_injection && next_midround_injection() > get_time())
return null

if (GLOB.dynamic_forced_extended)
return
return null

if (EMERGENCY_ESCAPED_OR_ENDGAMED)
return
return null

var/spawn_heavy = prob(get_heavy_midround_injection_chance())

last_midround_injection_attempt = world.time
last_midround_injection_attempt = get_time()
next_midround_injection = null
forced_injection = FALSE

Expand All @@ -44,15 +44,15 @@
log_game("DYNAMIC: FAIL: [ruleset] has a weight of 0")
continue

if (!ruleset.acceptable(SSticker.mode.current_players[CURRENT_LIVING_PLAYERS].len, threat_level))
if (!ruleset.acceptable(simulated_alive_players || SSticker.mode.current_players[CURRENT_LIVING_PLAYERS].len, threat_level))
log_game("DYNAMIC: FAIL: [ruleset] is not acceptable with the current parameters. Alive players: [SSticker.mode.current_players[CURRENT_LIVING_PLAYERS].len], threat level: [threat_level]")
continue

if (mid_round_budget < ruleset.cost)
if (mid_round_budget < ruleset.cost && !is_lategame())
log_game("DYNAMIC: FAIL: [ruleset] is too expensive, and cannot be bought. Midround budget: [mid_round_budget], ruleset cost: [ruleset.cost]")
continue

if (ruleset.minimum_round_time > world.time - SSticker.round_start_time)
if (ruleset.minimum_round_time > get_time() - SSticker.round_start_time)
log_game("DYNAMIC: FAIL: [ruleset] is trying to run too early. Minimum round time: [ruleset.minimum_round_time], current round time: [world.time - SSticker.round_start_time]")
continue

Expand All @@ -62,7 +62,7 @@
continue

ruleset.trim_candidates()
if (!ruleset.ready())
if (!ruleset.ready(force))
log_game("DYNAMIC: FAIL: [ruleset] is not ready()")
continue

Expand All @@ -76,13 +76,20 @@

log_game("DYNAMIC: Rolling [spawn_heavy ? "HEAVY" : "LIGHT"]... [heavy_light_log_count]")

if (spawn_heavy && drafted_heavies.len > 0 && pick_midround_rule(drafted_heavies, "heavy rulesets"))
// Attempt to draft a heavy ruleset
if (spawn_heavy && drafted_heavies.len > 0)
. = pick_midround_rule(drafted_heavies, "heavy rulesets")
if (.)
return
if (drafted_lights.len <= 0)
dynamic_log("No midround rulesets could be drafted as there were no drafted rules. ([heavy_light_log_count])")
return
. = pick_midround_rule(drafted_lights, "light rulesets")
if (!.)
dynamic_log("No midround rulesets could be drafted as pick midround rules returned nothing. ([heavy_light_log_count])")
return
else if (drafted_lights.len > 0 && pick_midround_rule(drafted_lights, "light rulesets"))
if (spawn_heavy)
dynamic_log("A heavy ruleset was intended to roll, but there weren't any available. [heavy_light_log_count]")
else
dynamic_log("No midround rulesets could be drafted. ([heavy_light_log_count])")
if (spawn_heavy)
dynamic_log("A heavy ruleset was intended to roll, but there weren't any available. [heavy_light_log_count]")

/// Gets the chance for a heavy ruleset midround injection, the dry_run argument is only used for forced injection.
/datum/game_mode/dynamic/proc/get_heavy_midround_injection_chance(dry_run)
Expand Down
12 changes: 11 additions & 1 deletion code/game/gamemodes/dynamic/dynamic_rulesets.dm
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
/// Whether repeated_mode_adjust weight changes have been logged already.
var/logged_repeated_mode_adjust = FALSE

/// Was this ruleset spawned from the lategame mode?
var/lategame_spawned = FALSE


/datum/dynamic_ruleset/New(datum/game_mode/dynamic/dynamic_mode)
// Rulesets can be instantiated more than once, such as when an admin clicks
Expand All @@ -91,6 +94,7 @@
SHOULD_NOT_OVERRIDE(TRUE)

mode = dynamic_mode
lategame_spawned = mode.is_lategame()
..()

/datum/dynamic_ruleset/roundstart // One or more of those drafted at roundstart
Expand All @@ -112,6 +116,9 @@
log_game("DYNAMIC: FAIL: [src] failed acceptable: maximum_players ([maximum_players]) < population ([population])")
return FALSE

if (mode.is_lategame())
return (flags & LATEGAME_RULESET)

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)
if (threat_level < requirements[indice_pop])
Expand Down Expand Up @@ -228,6 +235,9 @@

/// Checks if the ruleset is "dead", where all the antags are either dead or deconverted.
/datum/dynamic_ruleset/proc/is_dead()
// Don't let dead threats affect simulation results
if (mode.simulated)
return FALSE
for(var/datum/mind/mind in assigned)
var/mob/living/body = mind.current
// If they have no body, they're dead for realsies.
Expand All @@ -248,7 +258,7 @@
if(body.soul_departed() || mind.hellbound)
continue
// Are they in medbay or an operating table/stasis bed, and have been dead for less than 20 minutes? If so, they're probably being revived.
if(world.time <= (mind.last_death + 15 MINUTES) && (istype(get_area(body), /area/medical) || (locate(/obj/machinery/stasis) in body.loc) || (locate(/obj/structure/table/optable) in body.loc)))
if((mode.simulated_time || world.time) <= (mind.last_death + 15 MINUTES) && (istype(get_area(body), /area/medical) || (locate(/obj/machinery/stasis) in body.loc) || (locate(/obj/structure/table/optable) in body.loc)))
log_undead()
return FALSE
else
Expand Down
2 changes: 1 addition & 1 deletion code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
// As a consequence, latejoin heretics start out at a massive
// disadvantage if the round's been going on for a while.
// Let's give them some influence points when they arrive.
new_heretic.knowledge_points += round((world.time - SSticker.round_start_time) / new_heretic.passive_gain_timer)
new_heretic.knowledge_points += round(((mode.simulated_time || world.time) - SSticker.round_start_time) / new_heretic.passive_gain_timer)
// BUT let's not give smugglers a million points on arrival.
// Limit it to four missed passive gain cycles (4 points).
new_heretic.knowledge_points = min(new_heretic.knowledge_points, 5)
Expand Down
12 changes: 7 additions & 5 deletions code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@
weight = 1
cost = 15
requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED
flags = HIGH_IMPACT_RULESET|PERSISTENT_RULESET
flags = HIGH_IMPACT_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET

/datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE)
if(!length(GLOB.wizardstart))
Expand Down Expand Up @@ -367,7 +367,7 @@
weight = 3
cost = 12
minimum_players = 25
flags = HIGH_IMPACT_RULESET|INTACT_STATION_RULESET|PERSISTENT_RULESET
flags = HIGH_IMPACT_RULESET|INTACT_STATION_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET

/datum/dynamic_ruleset/midround/from_ghosts/blob/generate_ruleset_body(mob/applicant)
var/body = applicant.become_overmind()
Expand All @@ -391,7 +391,7 @@
weight = 3
cost = 12
minimum_players = 25
flags = HIGH_IMPACT_RULESET|INTACT_STATION_RULESET|PERSISTENT_RULESET
flags = HIGH_IMPACT_RULESET|INTACT_STATION_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET
var/list/vents

/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/acceptable(population=0, threat=0)
Expand Down Expand Up @@ -492,7 +492,7 @@
cost = 11
minimum_players = 25
repeatable = TRUE
flags = INTACT_STATION_RULESET|PERSISTENT_RULESET
flags = INTACT_STATION_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET
var/list/spawn_locs

/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/ready(forced = FALSE)
Expand Down Expand Up @@ -625,6 +625,7 @@
cost = 8
minimum_players = 27
repeatable = FALSE
flags = LATEGAME_RULESET

/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat=0)
if (!SSmapping.empty_space)
Expand Down Expand Up @@ -690,7 +691,7 @@
weight = 3
cost = 11
repeatable = TRUE
flags = INTACT_STATION_RULESET|PERSISTENT_RULESET
flags = INTACT_STATION_RULESET|PERSISTENT_RULESET|LATEGAME_RULESET
minimum_players = 27
var/fed = 1
var/list/vents
Expand Down Expand Up @@ -886,6 +887,7 @@
minimum_players = 20
repeatable = TRUE
blocking_rules = list(/datum/dynamic_ruleset/roundstart/nuclear, /datum/dynamic_ruleset/roundstart/clockcult)
flags = LATEGAME_RULESET
var/spawn_loc

/datum/dynamic_ruleset/midround/from_ghosts/ninja/ready(forced)
Expand Down
Loading