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"