From 21109f3be2a32f1d16406e5b23d0af00dc1d05ff Mon Sep 17 00:00:00 2001 From: AffectedArc07 <25063394+AffectedArc07@users.noreply.github.com> Date: Thu, 3 Nov 2022 18:05:43 +0000 Subject: [PATCH] Improved antag fishing busting (#19542) --- code/__DEFINES/admin.dm | 3 + code/controllers/subsystem/jobs.dm | 8 +-- code/controllers/subsystem/ticker.dm | 83 ++++++++++++++++++++- code/game/gamemodes/game_mode.dm | 2 +- code/modules/admin/antaglog.dm | 103 +++++++++++++++++++++++++++ code/modules/mob/mob_helpers.dm | 22 ------ paradise.dme | 1 + 7 files changed, 192 insertions(+), 30 deletions(-) create mode 100644 code/modules/admin/antaglog.dm diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm index 60e3af760b2..2601ac89c3f 100644 --- a/code/__DEFINES/admin.dm +++ b/code/__DEFINES/admin.dm @@ -73,6 +73,9 @@ /// Note "ckey" for CID info tracking. Do not EVER update this. #define CIDTRACKING_PSUEDO_CKEY "ALICE-CIDTRACKING" +/// Note "ckey" for roundstart antag rolling tracking. Do not EVER update this. +#define ANTAGTRACKING_PSUEDO_CKEY "ALICE-ANTAGTRACKING" + // Connection types. These match enums in the SQL DB. Dont change them /// Client was let into the server #define CONNECTION_TYPE_ESTABLISHED "ESTABLISHED" diff --git a/code/controllers/subsystem/jobs.dm b/code/controllers/subsystem/jobs.dm index 5f005b67144..e3955cbfb10 100644 --- a/code/controllers/subsystem/jobs.dm +++ b/code/controllers/subsystem/jobs.dm @@ -276,12 +276,12 @@ SUBSYSTEM_DEF(jobs) //Get the players who are ready for(var/mob/new_player/player in GLOB.player_list) - if(player.ready && player.has_valid_preferences() && player.mind && !player.mind.assigned_role) + if(player.ready && player.mind && !player.mind.assigned_role) unassigned += player Debug("DO, Len: [unassigned.len]") - if(unassigned.len == 0) - return 0 + if(!length(unassigned)) + return FALSE //Shuffle players and jobs unassigned = shuffle(unassigned) @@ -392,7 +392,7 @@ SUBSYSTEM_DEF(jobs) unassigned -= player log_debug("Dividing Occupations took [stop_watch(watch)]s") - return 1 + return TRUE /datum/controller/subsystem/jobs/proc/AssignRank(mob/living/carbon/human/H, rank, joined_late = FALSE, log_to_db = TRUE) if(!H) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 13fb9d32905..b4c56bbeb90 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -65,6 +65,8 @@ SUBSYSTEM_DEF(ticker) var/real_reboot_time = 0 /// Datum used to generate the end of round scoreboard. var/datum/scoreboard/score = null + /// List of ckeys who had antag rolling issues flagged + var/list/flagged_antag_rollers = list() /datum/controller/subsystem/ticker/Initialize() login_music = pick(\ @@ -197,13 +199,35 @@ SUBSYSTEM_DEF(ticker) if(player.client.prefs.toggles2 & PREFTOGGLE_2_RANDOMSLOT) player.client.prefs.load_random_character_slot(player.client) + // Lets check if people who ready should or shouldnt be + for(var/mob/new_player/P in GLOB.player_list) + // Not logged in + if(!P.client) + continue + // Not ready + if(!P.ready) + continue + // Not set to return if nothing available + if(P.client.prefs.active_character.alternate_option != RETURN_TO_LOBBY) + continue + + var/has_antags = (length(P.client.prefs.be_special) > 0) + if(!P.client.prefs.active_character.check_any_job()) + to_chat(P, "You have no jobs enabled, along with return to lobby if job is unavailable. This makes you ineligible for any round start role, please update your job preferences.") + if(has_antags) + // We add these to a list so we can deal with them as a batch later + // A lot of DB tracking stuff needs doing, so we may as well async it + flagged_antag_rollers |= P.ckey + + P.ready = FALSE + //Configure mode and assign player to special mode stuff mode.pre_pre_setup() - var/can_continue - can_continue = mode.pre_setup() //Setup special modes + var/can_continue = FALSE + can_continue = mode.pre_setup() //Setup special modes. This also does the antag fishing checks. SSjobs.DivideOccupations() //Distribute jobs if(!can_continue) - qdel(mode) + QDEL_NULL(mode) to_chat(world, "Error setting up [GLOB.master_mode]. Reverting to pre-game lobby.") current_state = GAME_STATE_PREGAME force_start = FALSE @@ -307,6 +331,9 @@ SUBSYSTEM_DEF(ticker) #ifdef UNIT_TESTS RunUnitTests() #endif + + // Do this 10 second after roundstart because of roundstart lag, and make it more visible + addtimer(CALLBACK(src, .proc/handle_antagfishing_reporting), 10 SECONDS) return TRUE @@ -653,3 +680,53 @@ SUBSYSTEM_DEF(ticker) sleep(sound_length) world.Reboot() + +// Timers invoke this async +/datum/controller/subsystem/ticker/proc/handle_antagfishing_reporting() + // This needs the DB + if(!SSdbcore.IsConnected()) + return + // Dont need to do anything + if(!length(flagged_antag_rollers)) + return + + // Records themselves + var/list/datum/antag_record/records = list() + // Queries to load data (executed as async batch) + var/list/datum/db_query/load_queries = list() + // Queries to save data (executed as async batch) + var/list/datum/db_query/save_queries = list() + + + for(var/ckey in flagged_antag_rollers) + var/datum/antag_record/AR = new /datum/antag_record(ckey) + records[ckey] = AR + load_queries[ckey] = AR.get_load_query() + + // Explanation for parameters: + // TRUE: We want warnings if these fail + // FALSE: Do NOT qdel() queries here, otherwise they wont be read. At all. + // TRUE: This is an assoc list, so it needs to prepare for that + SSdbcore.MassExecute(load_queries, TRUE, FALSE, TRUE) + + // Report on things + var/list/log_text = list("The following players attempted to roll antag with no jobs (total infractions listed)") + + for(var/ckey in flagged_antag_rollers) + var/datum/antag_record/AR = records[ckey] + AR.handle_data(load_queries[ckey]) + save_queries[ckey] = AR.get_save_query() + + log_text += "- [ckey]: [AR.infraction_count]" + + log_text += "Investigation advised if there are a high number of infractions" + + message_admins(log_text.Join("
")) + + // Now do a ton of saves + SSdbcore.MassExecute(save_queries, TRUE, TRUE, TRUE) + + // And cleanup + QDEL_LIST_ASSOC_VAL(load_queries) + records.Cut() + flagged_antag_rollers.Cut() diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm index 193a6131670..55d07f78ce9 100644 --- a/code/game/gamemodes/game_mode.dm +++ b/code/game/gamemodes/game_mode.dm @@ -240,7 +240,7 @@ // Assemble a list of active players without jobbans. for(var/mob/new_player/player in GLOB.player_list) - if(player.client && player.ready && player.has_valid_preferences()) + if(player.client && player.ready) if(!jobban_isbanned(player, ROLE_SYNDICATE) && !jobban_isbanned(player, roletext)) if(player_old_enough_antag(player.client,role)) players += player diff --git a/code/modules/admin/antaglog.dm b/code/modules/admin/antaglog.dm new file mode 100644 index 00000000000..40bec7743a8 --- /dev/null +++ b/code/modules/admin/antaglog.dm @@ -0,0 +1,103 @@ +////////////////////////////////////////////////////////////////////////////////////// +// This file houses all the code for antag tracking datum serialization and deserialization +// +// If you are going to modify ANYTHING in here, please test it THOROUGHLY +// The serialization and deserialization here is so complicated that you WILL break something here +// PLEASE test things properly if you modify this file. -aa07 +// +////////////////////////////////////////////////////////////////////////////////////// + + +// All these defines are integral to the workings and mesh together with the database. +// DO NOT EDIT THESE UNDER ANY CIRCUMSTANCES EVER +#define ANTAGRECORD_FIRST_INFRACTION "First infraction: " +#define ANTAGRECORD_LAST_INFRACTION "Last infraction: " +#define ANTAGRECORD_TOTAL_INFRACTIONS "Total infractions: " + +/datum/antag_record + var/holder_ckey + var/first_infraction_date + var/last_infraction_date + var/infraction_count + var/has_note = FALSE + +/datum/antag_record/New(_holder_ckey) + holder_ckey = _holder_ckey + +/datum/antag_record/proc/handle_data(datum/db_query/Q) + var/notetxt = null + + while(Q.NextRow()) + notetxt = Q.item[1] + + if(notetxt) + has_note = TRUE + deserialize_and_load(notetxt) + infraction_count++ + else + // Sane defaults + first_infraction_date = SQLtime() + infraction_count = 1 + + last_infraction_date = SQLtime() + +// Seems pointless but it allows code to flow smoother +// Also I grew too dependant on languages that have nice syntax stuff +/datum/antag_record/proc/get_load_query() + return SSdbcore.NewQuery("SELECT notetext FROM notes WHERE ckey=:ckey AND adminckey=:ackey LIMIT 1", list( + "ckey" = holder_ckey, + "ackey" = ANTAGTRACKING_PSUEDO_CKEY + )) + +/* + Expected output below. These are parsed from raw_text by splitting by
+ [1] ANTAGRECORD_FIRST_INFRACTION + [2] ANTAGRECORD_LAST_INFRACTION + [3] ANTAGRECORD_TOTAL_INFRACTIONS +*/ + +/datum/antag_record/proc/deserialize_and_load(raw_text) + var/list/lines = splittext(raw_text, "
") + // Text + first_infraction_date = splittext(lines[1], ANTAGRECORD_FIRST_INFRACTION)[2] + last_infraction_date = splittext(lines[2], ANTAGRECORD_LAST_INFRACTION)[2] + // Number + infraction_count = text2num(splittext(lines[3], ANTAGRECORD_TOTAL_INFRACTIONS)[2]) // Make sure its a number + +/datum/antag_record/proc/get_save_query() + var/serialized_text + var/list/serialized_list = list() + serialized_list.len = 3 // Make it 3 off the bat + + serialized_list[1] = "[ANTAGRECORD_FIRST_INFRACTION][first_infraction_date]" + serialized_list[2] = "[ANTAGRECORD_LAST_INFRACTION][last_infraction_date]" + serialized_list[3] = "[ANTAGRECORD_TOTAL_INFRACTIONS][infraction_count]" + + serialized_text = serialized_list.Join("
") + + if(has_note) // They have a note. Update. + var/datum/db_query/update_existing_note = SSdbcore.NewQuery("UPDATE notes SET notetext=:nt, timestamp=NOW(), round_id=:rid WHERE ckey=:ckey AND adminckey=:ackey", list( + "nt" = serialized_text, + "rid" = GLOB.round_id, + "ckey" = holder_ckey, + "ackey" = ANTAGTRACKING_PSUEDO_CKEY + )) + + return update_existing_note + else + // They dont have a note. Make an insert query. + // To the person who asks "why dont you juse use add_note()" + // By making a query batch, we can use async magic to make it go faster + var/datum/db_query/query_noteadd = SSdbcore.NewQuery({" + INSERT INTO notes (ckey, timestamp, notetext, adminckey, server, round_id, automated) + VALUES (:targetckey, NOW(), :notetext, :adminkey, :server, :roundid, 1) + "}, list( + "targetckey" = holder_ckey, + "notetext" = serialized_text, + "adminkey" = ANTAGTRACKING_PSUEDO_CKEY, + "server" = GLOB.configuration.system.instance_id, + "roundid" = GLOB.round_id, + "automated" = TRUE + )) + + return query_noteadd diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 989652a2f7f..66706abb35f 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -730,28 +730,6 @@ GLOBAL_LIST_INIT(intents, list(INTENT_HELP,INTENT_DISARM,INTENT_GRAB,INTENT_HARM // Cast to 1/0 return !!(client.prefs.toggles & toggleflag) -// Used to make sure that a player has a valid job preference setup, used to knock players out of eligibility for anything if their prefs don't make sense. -// A "valid job preference setup" in this situation means at least having one job set to low, or not having "return to lobby" enabled -// Prevents "antag rolling" by setting antag prefs on, all jobs to never, and "return to lobby if preferences not availible" -// Doing so would previously allow you to roll for antag, then send you back to lobby if you didn't get an antag role -// This also does some admin notification and logging as well -/mob/proc/has_valid_preferences() - if(!client) - return FALSE //Not sure how this would get run without the mob having a client, but let's just be safe. - if(client.prefs.active_character.alternate_option != RETURN_TO_LOBBY) - return TRUE - // If they have antags enabled, they're potentially doing this on purpose instead of by accident. Notify admins if so. - var/has_antags = FALSE - if(client.prefs.be_special.len > 0) - has_antags = TRUE - if(!client.prefs.active_character.check_any_job()) - to_chat(src, "You have no jobs enabled, along with return to lobby if job is unavailable. This makes you ineligible for any round start role, please update your job preferences.") - if(has_antags) - log_admin("[src.ckey] just got booted back to lobby with no jobs, but antags enabled.") - message_admins("[src.ckey] just got booted back to lobby with no jobs enabled, but antag rolling enabled. Likely antag rolling abuse.") - return FALSE //This is the only case someone should actually be completely blocked from antag rolling as well - return TRUE - /** * Helper proc to determine if a mob can use emotes that make sound or not. */ diff --git a/paradise.dme b/paradise.dme index b2ea175cd50..6551663ecc1 100644 --- a/paradise.dme +++ b/paradise.dme @@ -1206,6 +1206,7 @@ #include "code\modules\admin\admin_memo.dm" #include "code\modules\admin\admin_ranks.dm" #include "code\modules\admin\admin_verbs.dm" +#include "code\modules\admin\antaglog.dm" #include "code\modules\admin\banjob.dm" #include "code\modules\admin\centcom_ban_db.dm" #include "code\modules\admin\cookielog.dm"