diff --git a/code/__DEFINES/dynamic.dm b/code/__DEFINES/dynamic.dm
index f77cfcf59c292..cb6f3d4218339 100644
--- a/code/__DEFINES/dynamic.dm
+++ b/code/__DEFINES/dynamic.dm
@@ -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"
diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm
index 5d795c7bf706a..68e32cdc7e40b 100644
--- a/code/__HELPERS/roundend.dm
+++ b/code/__HELPERS/roundend.dm
@@ -378,7 +378,10 @@
parts += "[FOURSPACES][FOURSPACES][entry]
"
parts += "[FOURSPACES]Executed rules:"
for(var/datum/dynamic_ruleset/rule in mode.executed_rules)
- parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat"
+ if (rule.lategame_spawned)
+ parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat (Lategame, threat level ignored)"
+ else
+ parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat"
return parts.Join("
")
/client/proc/roundend_report_file()
@@ -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)
diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm
index 4e35244ae9505..c030665dccf0c 100644
--- a/code/game/gamemodes/dynamic/dynamic.dm
+++ b/code/game/gamemodes/dynamic/dynamic.dm
@@ -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),
@@ -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
+
+ /// 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)
@@ -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))
@@ -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
@@ -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()
@@ -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()
@@ -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)
@@ -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
diff --git a/code/game/gamemodes/dynamic/dynamic_hijacking.dm b/code/game/gamemodes/dynamic/dynamic_hijacking.dm
index 7849fa7928da5..0de231ece1248 100644
--- a/code/game/gamemodes/dynamic/dynamic_hijacking.dm
+++ b/code/game/gamemodes/dynamic/dynamic_hijacking.dm
@@ -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
diff --git a/code/game/gamemodes/dynamic/dynamic_logging.dm b/code/game/gamemodes/dynamic/dynamic_logging.dm
index 08eb1330550d2..22510e9672913 100644
--- a/code/game/gamemodes/dynamic/dynamic_logging.dm
+++ b/code/game/gamemodes/dynamic/dynamic_logging.dm
@@ -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
diff --git a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
index 2d631a14667cd..a31f953be4da0 100644
--- a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
+++ b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
@@ -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
@@ -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
@@ -62,7 +62,7 @@
continue
ruleset.trim_candidates()
- if (!ruleset.ready())
+ if (!ruleset.ready(force))
log_game("DYNAMIC: FAIL: [ruleset] is not ready()")
continue
@@ -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)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
index eb48b7164ee71..20d725fddf863 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
@@ -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
@@ -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
@@ -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])
@@ -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.
@@ -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
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
index 2e1308adad7ad..55f540c4f796c 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
@@ -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)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 4d4a416a68c1c..5c8d249c19b2a 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -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))
@@ -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()
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
index d5032b56ff746..aefd66ce22290 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
@@ -546,11 +546,11 @@
var/rampupdelta = 5
/datum/dynamic_ruleset/roundstart/meteor/rule_process()
- if(nometeors || meteordelay > world.time - SSticker.round_start_time)
+ if(nometeors || meteordelay > (mode.simulated_time || world.time) - SSticker.round_start_time)
return
var/list/wavetype = GLOB.meteors_normal
- var/meteorminutes = (world.time - SSticker.round_start_time - meteordelay) / 10 / 60
+ var/meteorminutes = ((mode.simulated_time || world.time) - SSticker.round_start_time - meteordelay) / 10 / 60
if (prob(meteorminutes))
wavetype = GLOB.meteors_threatening
diff --git a/code/game/gamemodes/dynamic/dynamic_simulations.dm b/code/game/gamemodes/dynamic/dynamic_simulations.dm
index af1ed14f315b1..67b002bce5cc1 100644
--- a/code/game/gamemodes/dynamic/dynamic_simulations.dm
+++ b/code/game/gamemodes/dynamic/dynamic_simulations.dm
@@ -40,20 +40,55 @@
initialize_gamemode(config.forced_threat_level, config.roundstart_players)
create_candidates(config.roundstart_players)
gamemode.pre_setup()
+ gamemode.simulated = TRUE
var/total_antags = 0
for (var/_ruleset in gamemode.executed_rules)
var/datum/dynamic_ruleset/ruleset = _ruleset
total_antags += ruleset.assigned.len
+ var/midround_threat = gamemode.mid_round_budget
+
+ var/list/roundstart_rules = gamemode.executed_rules.Copy()
+
+ var/list/midround_rules = list()
+
+ // Generate midround threats
+ SSticker.round_start_time = 0
+ var/simulated_time = 1
+ gamemode.simulated_alive_players = config.roundstart_players
+ while (simulated_time < gamemode.midround_upper_bound)
+ // Simulate deaths and leaves
+ gamemode.simulated_alive_players = FLOOR(gamemode.simulated_alive_players * rand(90, 100) / 100, 1)
+ // Set the new world time
+ simulated_time = gamemode.next_midround_injection()
+ // Simulate an injection
+ gamemode.forced_injection = TRUE
+ // Set the simulated time
+ gamemode.simulated_time = simulated_time
+ // Run a midround injection
+ var/datum/dynamic_ruleset/simulated_result = gamemode.try_midround_roll(TRUE)
+ if (!simulated_result)
+ continue
+ midround_rules += list(list(
+ "ruleset" = simulated_result.name,
+ "weight" = simulated_result.weight,
+ "cost" = simulated_result.cost,
+ "execution_time" = simulated_time,
+ "remaining_threat" = gamemode.mid_round_budget,
+ "simulated_alive_players" = gamemode.simulated_alive_players,
+ "is_lategame" = gamemode.is_lategame()
+ ))
+
return list(
"roundstart_players" = config.roundstart_players,
"threat_level" = gamemode.threat_level,
"snapshot" = list(
"antag_percent" = total_antags / config.roundstart_players,
- "remaining_threat" = gamemode.mid_round_budget,
- "rulesets" = gamemode.executed_rules.Copy(),
+ "remaining_threat" = midround_threat,
+ "rulesets" = roundstart_rules,
),
+ "midround_rules" = midround_rules,
)
/datum/dynamic_simulation_config
@@ -96,6 +131,8 @@
WRITE_FILE(file("[GLOB.log_directory]/dynamic_simulations.json"), json_encode(outputs))
message_admins("Writing complete.")
+
+
/proc/export_dynamic_json_of(ruleset_list)
var/list/export = list()
diff --git a/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm b/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm
index 224a35c0708cf..94f364d48354d 100644
--- a/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm
+++ b/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm
@@ -24,13 +24,13 @@
if (ruleset.weight == 0)
continue
- if (ruleset.cost > max_threat_level)
+ if (ruleset.cost > max_threat_level && !is_lategame())
continue
if (!ruleset.acceptable(SSticker.mode.current_players[CURRENT_LIVING_PLAYERS].len, threat_level))
continue
- if (ruleset.minimum_round_time > world.time - SSticker.round_start_time)
+ if (ruleset.minimum_round_time > get_time() - SSticker.round_start_time)
continue
if(istype(ruleset, /datum/dynamic_ruleset/midround/from_ghosts) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT))
diff --git a/code/game/gamemodes/dynamic/ruleset_picking.dm b/code/game/gamemodes/dynamic/ruleset_picking.dm
index 50d975e67cb30..6859ffa0227c5 100644
--- a/code/game/gamemodes/dynamic/ruleset_picking.dm
+++ b/code/game/gamemodes/dynamic/ruleset_picking.dm
@@ -47,17 +47,20 @@
current_midround_rulesets = drafted_rules - rule
- midround_injection_timer_id = addtimer(
- CALLBACK(src, PROC_REF(execute_midround_rule), rule), \
- ADMIN_CANCEL_MIDROUND_TIME, \
- TIMER_STOPPABLE, \
- )
-
- log_game("DYNAMIC: [rule] ruleset executing...")
- message_admins("DYNAMIC: Executing midround ruleset [rule] in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME)]. \
- CANCEL | \
- SOMETHING ELSE")
- play_sound_to_all_admins('sound/effects/admin_alert.ogg')
+ if (!simulated)
+ midround_injection_timer_id = addtimer(
+ CALLBACK(src, PROC_REF(execute_midround_rule), rule), \
+ ADMIN_CANCEL_MIDROUND_TIME, \
+ TIMER_STOPPABLE, \
+ )
+
+ log_game("DYNAMIC: [rule] ruleset executing...")
+ message_admins("DYNAMIC: Executing midround ruleset [rule] in [DisplayTimeText(ADMIN_CANCEL_MIDROUND_TIME)]. \
+ CANCEL | \
+ SOMETHING ELSE")
+ play_sound_to_all_admins('sound/effects/admin_alert.ogg')
+ else
+ execute_midround_rule(rule)
return rule
@@ -67,7 +70,10 @@
midround_injection_timer_id = null
if (!rule.repeatable)
midround_rules = remove_from_list(midround_rules, rule.type)
- addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), rule), rule.delay)
+ if (!simulated)
+ addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), rule), rule.delay)
+ else
+ execute_midround_latejoin_rule(rule)
/// Executes a random latejoin ruleset from the list of drafted rules.
/datum/game_mode/dynamic/proc/pick_latejoin_rule(list/drafted_rules)
@@ -83,6 +89,13 @@
/datum/game_mode/dynamic/proc/execute_midround_latejoin_rule(sent_rule)
var/datum/dynamic_ruleset/rule = sent_rule
spend_midround_budget(rule.cost, threat_log, "[worldtime2text()]: [rule.ruletype] [rule.name]")
+ if (simulated)
+ if(rule.flags & ONLY_RULESET)
+ only_ruleset_executed = TRUE
+ executed_rules += rule
+ if (rule.persistent)
+ current_rules += rule
+ return TRUE
rule.pre_execute(current_players[CURRENT_LIVING_PLAYERS].len)
var/execute_result = rule.execute()
if(execute_result == DYNAMIC_EXECUTE_SUCCESS)