diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 4807da6ec42..c55fec2cd87 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -71,6 +71,7 @@
#define INIT_ORDER_ICON_SMOOTHING -5
#define INIT_ORDER_OVERLAY -6
#define INIT_ORDER_XKEYSCORE -10
+#define INIT_ORDER_STICKY_BAN -10
#define INIT_ORDER_TICKETS -10
#define INIT_ORDER_LIGHTING -20
#define INIT_ORDER_SHUTTLE -21
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 1c4d569507e..199d4cf276a 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -84,7 +84,8 @@ var/list/admin_verbs_admin = list(
)
var/list/admin_verbs_ban = list(
/client/proc/unban_panel,
- /client/proc/jobbans
+ /client/proc/jobbans,
+ /client/proc/stickybanpanel
)
var/list/admin_verbs_sounds = list(
/client/proc/play_local_sound,
@@ -363,7 +364,7 @@ var/list/admin_verbs_ticket = list(
/client/proc/player_panel()
set name = "Player Panel"
- set category = null
+ set category = "Admin"
if(!check_rights(R_ADMIN))
return
diff --git a/code/modules/admin/stickyban.dm b/code/modules/admin/stickyban.dm
new file mode 100644
index 00000000000..61759f2e36d
--- /dev/null
+++ b/code/modules/admin/stickyban.dm
@@ -0,0 +1,210 @@
+/datum/admins/proc/stickyban(action,data)
+ if(!check_rights(R_BAN))
+ return
+
+ switch(action)
+ if("show")
+ stickyban_show()
+ return
+ if("add")
+ var/list/ban = list()
+ var/ckey
+ ban["admin"] = usr.key
+ ban["type"] = list("sticky")
+ ban["reason"] = "(InGameBan)([usr.key])" //this will be displayed in dd only
+
+ if(data["ckey"])
+ ckey = ckey(data["ckey"])
+ else
+ ckey = clean_input("Ckey","Ckey","")
+ if(!ckey)
+ return
+ ckey = ckey(ckey)
+ if(get_stickyban_from_ckey(ckey))
+ to_chat(usr, "Error: Can not add a stickyban: User already has a current sticky ban")
+
+ if(data["reason"])
+ ban["message"] = data["reason"]
+ else
+ var/reason = clean_input("Reason","Reason","Ban Evasion")
+ if(!reason)
+ return
+ ban["message"] = "[reason]"
+
+ world.SetConfig("ban", ckey, list2stickyban(ban))
+
+ log_admin("[key_name(usr)] has stickybanned [ckey].\nReason: [ban["message"]]")
+ message_admins("[key_name_admin(usr)] has stickybanned [ckey].\nReason: [ban["message"]]")
+
+ if("remove")
+ if(!data["ckey"])
+ return
+ var/ckey = data["ckey"]
+
+ var/ban = get_stickyban_from_ckey(ckey)
+ if(!ban)
+ to_chat(usr, "Error: No sticky ban for [ckey] found!")
+ return
+ if(alert("Are you sure you want to remove the sticky ban on [ckey]?","Are you sure","Yes","No") == "No")
+ return
+ if(!get_stickyban_from_ckey(ckey))
+ to_chat(usr, "Error: The ban disappeared.")
+ return
+ world.SetConfig("ban", ckey, null)
+
+ log_admin("[key_name(usr)] removed [ckey]'s stickyban")
+ message_admins("[key_name_admin(usr)] removed [ckey]'s stickyban")
+
+ if("remove_alt")
+ if(!data["ckey"])
+ return
+ var/ckey = data["ckey"]
+ if(!data["alt"])
+ return
+ var/alt = ckey(data["alt"])
+ var/ban = get_stickyban_from_ckey(ckey)
+ if(!ban)
+ to_chat(usr, "Error: No sticky ban for [ckey] found!")
+ return
+
+ var/found = 0
+ //we have to do it this way because byond keeps the case in its sticky ban matches WHY!!!
+ for(var/key in ban["keys"])
+ if(ckey(key) == alt)
+ found = 1
+ break
+
+ if(!found)
+ to_chat(usr, "Error: [alt] is not linked to [ckey]'s sticky ban!")
+ return
+
+ if(alert("Are you sure you want to disassociate [alt] from [ckey]'s sticky ban? \nNote: Nothing stops byond from re-linking them","Are you sure","Yes","No") == "No")
+ return
+
+ //we have to do this again incase something changes
+ ban = get_stickyban_from_ckey(ckey)
+ if(!ban)
+ to_chat(usr, "Error: The ban disappeared.")
+ return
+
+ found = 0
+ for(var/key in ban["keys"])
+ if(ckey(key) == alt)
+ ban["keys"] -= key
+ found = 1
+ break
+
+ if(!found)
+ to_chat(usr, "Error: [alt] link to [ckey]'s sticky ban disappeared.")
+ return
+
+ world.SetConfig("ban",ckey,list2stickyban(ban))
+
+ log_admin("[key_name(usr)] has disassociated [alt] from [ckey]'s sticky ban")
+ message_admins("[key_name_admin(usr)] has disassociated [alt] from [ckey]'s sticky ban")
+
+ if("edit")
+ if(!data["ckey"])
+ return
+ var/ckey = data["ckey"]
+ var/ban = get_stickyban_from_ckey(ckey)
+ if(!ban)
+ to_chat(usr, "Error: No sticky ban for [ckey] found!")
+ return
+ var/oldreason = ban["message"]
+ var/reason = clean_input("Reason","Reason","[ban["message"]]")
+ if(!reason || reason == oldreason)
+ return
+ //we have to do this again incase something changed while we waited for input
+ ban = get_stickyban_from_ckey(ckey)
+ if(!ban)
+ to_chat(usr, "Error: The ban disappeared.")
+ return
+ ban["message"] = "[reason]"
+
+ world.SetConfig("ban",ckey,list2stickyban(ban))
+
+ log_admin("[key_name(usr)] has edited [ckey]'s sticky ban reason from [oldreason] to [reason]")
+ message_admins("[key_name_admin(usr)] has edited [ckey]'s sticky ban reason from [oldreason] to [reason]")
+
+ spawn(10)
+ stickyban_show()
+
+/datum/admins/proc/stickyban_gethtml(ckey, ban)
+ . = "\[-\][ckey]
"
+ . += "[ban["message"]] \[Edit\]
"
+ if(ban["admin"])
+ . += "[ban["admin"]]
"
+ else
+ . += "LEGACY
"
+ . += "Caught keys
\n"
+ for(var/key in ban["keys"])
+ if(ckey(key) == ckey)
+ continue
+ . += "
\n"
+
+/datum/admins/proc/stickyban_show()
+ if(!check_rights(R_BAN))
+ return
+
+ var/list/bans = sortList(world.GetConfig("ban"))
+ var/banhtml = ""
+ for(var/key in bans)
+ var/ckey = ckey(key)
+ var/ban = stickyban2list(world.GetConfig("ban",key))
+ banhtml += "
\n"
+ banhtml += stickyban_gethtml(ckey,ban)
+
+ var/html = {"
+
+ All Sticky Bans:
\[+\]
+ [banhtml]
+
+ "}
+ usr << browse(html,"window=stickybans;size=700x400")
+
+/proc/get_stickyban_from_ckey(var/ckey)
+ if(!ckey)
+ return null
+ ckey = ckey(ckey)
+ . = null
+ for(var/key in world.GetConfig("ban"))
+ if(ckey(key) == ckey)
+ . = stickyban2list(world.GetConfig("ban",key))
+ break
+
+/proc/stickyban2list(var/ban)
+ if(!ban)
+ return null
+ . = params2list(ban)
+ .["keys"] = splittext(.["keys"], ",")
+ .["type"] = splittext(.["type"], ",")
+ .["IP"] = splittext(.["IP"], ",")
+ .["computer_id"] = splittext(.["computer_id"], ",")
+
+/proc/list2stickyban(var/list/ban)
+ if(!ban || !islist(ban))
+ return null
+ . = ban.Copy()
+ if(.["keys"])
+ .["keys"] = jointext(.["keys"], ",")
+ if(.["type"])
+ .["type"] = jointext(.["type"], ",")
+ if(.["IP"])
+ .["IP"] = jointext(.["IP"], ",")
+ if(.["computer_id"])
+ .["computer_id"] = jointext(.["computer_id"], ",")
+ . = list2params(.)
+
+/client/proc/stickybanpanel()
+ set name = "Sticky Ban Panel"
+ set category = "Admin"
+
+ if(!check_rights(R_BAN))
+ return
+
+ holder.stickyban_show()
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 3e4f5b374da..9aaef943421 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -37,6 +37,9 @@
return
var/ticketID = text2num(href_list["openmentorticket"])
SSmentor_tickets.showDetailUI(usr, ticketID)
+
+ if(href_list["stickyban"])
+ stickyban(href_list["stickyban"],href_list)
if(href_list["makeAntag"])
switch(href_list["makeAntag"])
diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index 46dc9ec5b6a..aa22dfe944c 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -1044,7 +1044,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
msg += "