Tweaks Dynamic Latejoin Ruleset Execution (#75428)

## About The Pull Request

### Before: 

Dynamic wants a latejoin. 

Dynamic will collate all latejoin rulesets that are eligible to execute.

The latejoin-er is guaranteed to become an antagonist, so long as any
ruleset is valid.

It runs down the list of rulesets and tries to apply it until it
succeeds

Then the latejoin injection timer is incremented for the next time. 

<Details>

<Summary> This leads to the following:  </Summary>


![image](https://github.com/tgstation/tgstation/assets/51863163/904b81d2-033e-41d5-9073-860ccff8e6ca)

</Details> 

### After ( with this pr ): 

Dynamic wants a latejoin. 

Dynamic will collate all latejoin rulesets that are eligible to execute.

Dynamic will then select **one** of the valid rulesets, based on weight,
to attempt to apply to the incoming latejoiner.

If the latejoiner is not a valid candidate, such as not having that
antagonist enabled in their preferences, it will not reroll the ruleset.
Do not pass go, do not collect 200 dollars, do not get antag.

The latejoin injection timer will not be incremented, so the very next
latejoin will be checked the same, until one eventually succeeds.


![image](https://github.com/tgstation/tgstation/assets/51863163/77194f37-0781-4c76-82b9-21693f7ff557)

## Why It's Good For The Game

Dynamic latejoin handling is kinda poor, I believe Mothblocks has talked
about this a bit before but hasn't done much to it yet.

Compared to roundstart and latejoin, which can select out of everyone
which has the preference enabled, latejoin rulesets are not spoiled for
choice, which makes weighting the rulesets useless.

If you only have traitor enabled, the only latejoin antag you can be is
traitor. And if dynamic wants a latejoin, while you can only be traitor,
**you will ALWAYS become a traitor**. The weight of the traitor ruleset
or any other ruleset doesn't matter, because at the end of the day, the
only valid ruleset is traitor.

This makes latejoins much less of a guaranteed thing, and will
(hopefully) spread it out a bit more, reducing likelihood of rarer
latejoin antags.

## Changelog

🆑 Melbert
balance: You are now slightly less likely, statistically, to get an
antag role on latejoin
fix: Fixed latejoin antags being logged as "midround" antags
/🆑
This commit is contained in:
MrMelbert
2023-05-14 22:51:08 -05:00
committed by GitHub
parent 8cd0d05ee3
commit 26349eec50
5 changed files with 83 additions and 49 deletions
+5
View File
@@ -27,3 +27,8 @@
/// Max number of teams we can have for the abductor ruleset
#define ABDUCTOR_MAX_TEAMS 4
// Ruletype defines
#define ROUNDSTART_RULESET "Roundstart"
#define LATEJOIN_RULESET "Latejoin"
#define MIDROUND_RULESET "Midround"
+54 -29
View File
@@ -262,7 +262,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
return
forced_latejoin_rule = added_rule
log_admin("[key_name(usr)] set [added_rule] to proc on the next latejoin.")
message_admins("[key_name(usr)] set [added_rule] to proc on the next latejoin.")
message_admins("[key_name(usr)] set [added_rule] to proc on the next valid latejoin.")
else if(href_list["clear_forced_latejoin"])
forced_latejoin_rule = null
log_admin("[key_name(usr)] cleared the forced latejoin ruleset.")
@@ -680,38 +680,63 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
return
if (forced_latejoin_rule)
forced_latejoin_rule.candidates = list(newPlayer)
forced_latejoin_rule.trim_candidates()
forced_latejoin_rule.load_templates()
log_dynamic("Forcing ruleset [forced_latejoin_rule]")
if (forced_latejoin_rule.ready(TRUE))
if (!forced_latejoin_rule.repeatable)
latejoin_rules = remove_from_list(latejoin_rules, forced_latejoin_rule.type)
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
log_dynamic("Forcing specific [forced_latejoin_rule.ruletype] ruleset [forced_latejoin_rule].")
if(!handle_executing_latejoin(forced_latejoin_rule, newPlayer, forced = TRUE))
message_admins("The forced latejoin ruleset [forced_latejoin_rule.name] couldn't be executed \
as the most recent latejoin did not fulfill the ruleset's requirements.")
return
else if (latejoin_injection_cooldown < world.time && (forced_injection || prob(latejoin_roll_chance)))
forced_injection = FALSE
if(latejoin_injection_cooldown >= world.time && !forced_injection && !prob(latejoin_roll_chance))
return
var/list/drafted_rules = list()
for (var/datum/dynamic_ruleset/latejoin/rule in latejoin_rules)
if (!rule.weight)
continue
if (rule.acceptable(GLOB.alive_player_list.len, threat_level) && mid_round_budget >= rule.cost)
// No stacking : only one round-ender, unless threat level > stacking_limit.
if (threat_level < GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking)
if(rule.flags & HIGH_IMPACT_RULESET && high_impact_ruleset_executed)
continue
var/was_forced = forced_injection
forced_injection = FALSE
var/list/possible_latejoin_rules = list()
for (var/datum/dynamic_ruleset/latejoin/rule in latejoin_rules)
if(!rule.weight)
continue
if(mid_round_budget < rule.cost)
continue
if(!rule.acceptable(GLOB.alive_player_list.len, threat_level))
continue
possible_latejoin_rules[rule] = rule.get_weight()
rule.candidates = list(newPlayer)
rule.trim_candidates()
rule.load_templates()
if (rule.ready())
drafted_rules[rule] = rule.get_weight()
if(!length(possible_latejoin_rules))
log_dynamic("FAIL: [newPlayer] was selected to roll for a latejoin ruleset, but there were no valid rulesets.")
return
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
log_dynamic("[newPlayer] was selected to roll for a latejoin ruleset from the following list: [english_list(possible_latejoin_rules)].")
// You get one shot at becoming a latejoin antag, if it fails the next guy will try.
var/datum/dynamic_ruleset/latejoin/picked_rule = pick_ruleset(possible_latejoin_rules, max_allowed_attempts = 1)
if(isnull(picked_rule))
log_dynamic("FAIL: No valid rulset was selected for [newPlayer]'s latejoin[was_forced ? "" : ", the next player will be checked instead"].")
return
if(was_forced)
log_dynamic("Forcing random [picked_rule.ruletype] ruleset [picked_rule].")
handle_executing_latejoin(picked_rule, newPlayer, forced = was_forced)
/**
* This proc handles the execution of a latejoin ruleset, including removing it from latejoin rulesets if not repeatable,
* upping the injection cooldown, and starting a timer to execute the ruleset on delay.
*/
/datum/game_mode/dynamic/proc/handle_executing_latejoin(datum/dynamic_ruleset/ruleset, mob/living/carbon/human/only_candidate, forced = FALSE)
ruleset.candidates = list(only_candidate)
ruleset.trim_candidates()
ruleset.load_templates()
if (!ruleset.ready(forced))
log_dynamic("FAIL: [only_candidate] was selected to latejoin with the [ruleset] ruleset, \
but the ruleset failed to execute[length(ruleset.candidates) ? "":" as they were not a valid candiate"].")
return FALSE
if (!ruleset.repeatable)
latejoin_rules = remove_from_list(latejoin_rules, ruleset.type)
addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), ruleset), ruleset.delay)
if(!forced)
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
log_dynamic("A latejoin rulset triggered successfully, the next latejoin injection will happen at [latejoin_injection_cooldown] round time.")
return TRUE
/// Apply configurations to rule.
/datum/game_mode/dynamic/proc/configure_ruleset(datum/dynamic_ruleset/ruleset)
@@ -96,11 +96,11 @@
..()
/datum/dynamic_ruleset/roundstart // One or more of those drafted at roundstart
ruletype = "Roundstart"
ruletype = ROUNDSTART_RULESET
// Can be drafted when a player joins the server
/datum/dynamic_ruleset/latejoin
ruletype = "Latejoin"
ruletype = LATEJOIN_RULESET
/// 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
@@ -5,7 +5,7 @@
/// Midround Rulesets
/datum/dynamic_ruleset/midround // Can be drafted once in a while during a round
ruletype = "Midround"
ruletype = MIDROUND_RULESET
var/midround_ruleset_style
/// If the ruleset should be restricted from ghost roles.
var/restrict_ghost_roles = TRUE
+21 -17
View File
@@ -1,13 +1,25 @@
#define ADMIN_CANCEL_MIDROUND_TIME (10 SECONDS)
/// From a list of rulesets, returns one based on weight and availability.
/// Mutates the list that is passed into it to remove invalid rules.
/datum/game_mode/dynamic/proc/pick_ruleset(list/drafted_rules)
///
///
/**
* From a list of rulesets, returns one based on weight and availability.
* Mutates the list that is passed into it to remove invalid rules.
*
* * max_allowed_attempts - Allows you to configure how many times the proc will attempt to pick a ruleset before giving up.
*/
/datum/game_mode/dynamic/proc/pick_ruleset(list/drafted_rules, max_allowed_attempts = INFINITY)
if (only_ruleset_executed)
log_dynamic("FAIL: only_ruleset_executed")
return null
while (TRUE)
if(!length(drafted_rules))
log_dynamic("FAIL: pick ruleset supplied with an empty list of drafted rules.")
return null
var/attempts = 0
while (attempts < max_allowed_attempts)
attempts++
var/datum/dynamic_ruleset/rule = pick_weight(drafted_rules)
if (!rule)
var/list/leftover_rules = list()
@@ -15,10 +27,10 @@
leftover_rules += "[leftover_rule]"
log_dynamic("FAIL: No rulesets left to pick. Leftover rules: [leftover_rules.Join(", ")]")
return null
if (check_blocking(rule.blocking_rules, executed_rules))
log_dynamic("FAIL: [rule] can't execute as another rulset is blocking it.")
drafted_rules -= rule
if(drafted_rules.len <= 0)
return null
@@ -37,6 +49,8 @@
return rule
return null
/// Executes a random midround ruleset from the list of drafted rules.
/datum/game_mode/dynamic/proc/pick_midround_rule(list/drafted_rules, description)
log_dynamic("Rolling [drafted_rules.len] [description]")
@@ -68,28 +82,18 @@
midround_rules = remove_from_list(midround_rules, rule.type)
addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), rule), rule.delay)
/// Executes a random latejoin ruleset from the list of drafted rules.
/datum/game_mode/dynamic/proc/pick_latejoin_rule(list/drafted_rules)
var/datum/dynamic_ruleset/rule = pick_ruleset(drafted_rules)
if (isnull(rule))
return
if (!rule.repeatable)
latejoin_rules = remove_from_list(latejoin_rules, rule.type)
addtimer(CALLBACK(src, PROC_REF(execute_midround_latejoin_rule), rule), rule.delay)
return TRUE
/// Mainly here to facilitate delayed rulesets. All midround/latejoin rulesets are executed with a timered callback to this proc.
/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]")
rule.pre_execute(GLOB.alive_player_list.len)
if (rule.execute())
log_dynamic("Injected a [rule.ruletype == "latejoin" ? "latejoin" : "midround"] ruleset [rule.name].")
log_dynamic("Injected a [rule.ruletype] ruleset [rule.name].")
if(rule.flags & HIGH_IMPACT_RULESET)
high_impact_ruleset_executed = TRUE
else if(rule.flags & ONLY_RULESET)
only_ruleset_executed = TRUE
if(rule.ruletype == "Latejoin")
if(rule.ruletype == LATEJOIN_RULESET)
var/mob/M = pick(rule.candidates)
message_admins("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.")
log_dynamic("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.")