Files
Bubberstation/code/modules/admin/verbs/adminhelp.dm
Gandalf 4375bd7432 The Opposing Force (OPFOR) - Ready for review/testing (#10093)
* OPFOR PART ONE

* 0

* E

* E

* 0

* E

* e

* Update opposing_force_datum.dm

* E

* E

* Admin stuffs

* Admin control GUI

* E

* Chat

* 0

* Update opposing_force_datum.dm

* System messages, better admin chat procs, UI fixes.

* Update _basemap.dm

* who thought holding a ref to the client was a good fucking idea?

* 0

* Update opposing_force_datum.dm

* Update modular_skyrat/modules/opposing_force/code/opposing_force_datum.dm

* Equipment tab

* equipment part 2

* Tidy up, finishing up equipments panel, some font fixes

* clarity and continuity

* Update opposing_force_datum.dm

* more logic fixes

* Update opposing_force_datum.dm

* weeee

* Update opposing_force_datum.dm

* chat commands

* 0

* ee

* Update opposing_force_datum.dm

* Itemspawning

* Assassinate objectives only need one kill

* new commands

* Update opposing_force_datum.dm

* f

* weee roundend report

* E

* ew

* E

* E

* more fixes

* E

* E

* Update opposing_force_datum.dm

* Update opposing_force_datum.dm

* Fixes

* request opfor

* Update admin_procs.dm

* Update adminhelp.dm

* Update opposing_force_datum.dm

* fex

* admin info panel, approve checks, queue position checks

* panel improvements, better logging format

* Barboza equipment list Vol 1 (#10386)

* Update opposing_force_equipment.dm

* silly little changes to the silly little code

i doubt this will be seen by anyone teehee

* OpFor Prep Balance

Shotguns have been prepped into opfor catalog. Model 23-37 is now an 8 rounder pump action, M2 is untouched as a semi auto peacekeeper variation and SAS14 is semi auto as it was meant to be

* OpFor Prep Balance

Shotguns have been prepped into opfor catalog. Model 23-37 is now an 8 rounder pump action, M2 is untouched as a semi auto peacekeeper variation and SAS14 is semi auto as it was meant to be. 23-37 keeps its beanbag load while M2 comes with slugs as a proper combat shotgun

* Gear Pass 1

* Mag Variety for SAS14

Since its gonna be an opfor exclusive why not add some choices. Refilling its ammo is nigh impossible so here comes new mags!

* Gear Pass 2

yes I 'code' in github web

* Gear Pass 3

just martial arts and some stuff for today, tomorrow i add the discord suggestions

* opfor equipment up the ass

woo spider

* descs for everyone

* g357

* altyn

* Gear Pass 4

syndicakes and nri

* Item Variety Document for Opfor

* gunman loadout

* gunman update

* Gear Pass 5

gear harder

* Update opposing_force_equipment.dm

* big typo fuckup fix

* lcoat desc fix

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update modular_skyrat/modules/opposing_force/code/opposing_force_equipment.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update modular_skyrat/modules/opposing_force/code/opposing_force_equipment.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update modular_skyrat/modules/opposing_force/code/opposing_force_equipment.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update modular_skyrat/modules/opposing_force/code/opposing_force_equipment.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update modular_skyrat/modules/opposing_force/code/opposing_force_equipment.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update modular_skyrat/modules/opposing_force/code/opposing_force_equipment.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update modular_skyrat/modules/opposing_force/code/opposing_force_equipment.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* DocNameChange

also no more shamwow

* Update opposing_force_equipment.dm

* Update modular_skyrat/modules/sec_haul/code/guns/guns.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update modular_skyrat/modules/sec_haul/code/guns/guns.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* Update items.dm

* Update opposing_force_equipment.dm

* Update modular_skyrat/modules/opposing_force/code/items.dm

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

* More Categories and Expanded Title

Adds loadouts and explosives tab, increases title limit to 40 instead of 20 for objectives

* Apply suggestions from code review

Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>

Co-authored-by: dawsonkeyes <76002401+dawsonkeyes@users.noreply.github.com>
Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>
Co-authored-by: Gandalf <9026500+Gandalf2k15@users.noreply.github.com>

* Automatic changelog generation for PR #10386 [ci skip]

* E

* Update opposing_force_equipment.dm

* E

* Update items.dm

* Vol2Remastered (#10413)

* Update modular_skyrat/modules/opposing_force/code/opposing_force_datum.dm

Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com>

* Bye bye high-frequency sword

* Fixed the space cash too

* Review comments

* Update counter.dm

* general antagonist ban

* opfor ban

* Update opposing_force_subsystem.dm

Co-authored-by: ErdinyoBarboza <erdinyobarboza@gmail.com>
Co-authored-by: dawsonkeyes <76002401+dawsonkeyes@users.noreply.github.com>
Co-authored-by: Tom <8881105+tf-4@users.noreply.github.com>
Co-authored-by: SkyratBot <59378654+SkyratBot@users.noreply.github.com>
Co-authored-by: GoldenAlpharex <jerego1234@hotmail.com>
Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com>
2022-01-20 04:15:15 +00:00

999 lines
36 KiB
Plaintext

/// Client var used for returning the ahelp verb
/client/var/adminhelptimerid = 0
/// Client var used for tracking the ticket the (usually) not-admin client is dealing with
/client/var/datum/admin_help/current_ticket
GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
/**
* # Adminhelp Ticket Manager
*/
/datum/admin_help_tickets
/// The set of all active tickets
var/list/active_tickets = list()
/// The set of all closed tickets
var/list/closed_tickets = list()
/// The set of all resolved tickets
var/list/resolved_tickets = list()
var/obj/effect/statclick/ticket_list/astatclick = new(null, null, AHELP_ACTIVE)
var/obj/effect/statclick/ticket_list/cstatclick = new(null, null, AHELP_CLOSED)
var/obj/effect/statclick/ticket_list/rstatclick = new(null, null, AHELP_RESOLVED)
/datum/admin_help_tickets/Destroy()
QDEL_LIST(active_tickets)
QDEL_LIST(closed_tickets)
QDEL_LIST(resolved_tickets)
QDEL_NULL(astatclick)
QDEL_NULL(cstatclick)
QDEL_NULL(rstatclick)
return ..()
/datum/admin_help_tickets/proc/TicketByID(id)
var/list/lists = list(active_tickets, closed_tickets, resolved_tickets)
for(var/I in lists)
for(var/J in I)
var/datum/admin_help/AH = J
if(AH.id == id)
return J
/datum/admin_help_tickets/proc/TicketsByCKey(ckey)
. = list()
var/list/lists = list(active_tickets, closed_tickets, resolved_tickets)
for(var/I in lists)
for(var/J in I)
var/datum/admin_help/AH = J
if(AH.initiator_ckey == ckey)
. += AH
//private
/datum/admin_help_tickets/proc/ListInsert(datum/admin_help/new_ticket)
var/list/ticket_list
switch(new_ticket.state)
if(AHELP_ACTIVE)
ticket_list = active_tickets
if(AHELP_CLOSED)
ticket_list = closed_tickets
if(AHELP_RESOLVED)
ticket_list = resolved_tickets
else
CRASH("Invalid ticket state: [new_ticket.state]")
var/num_closed = ticket_list.len
if(num_closed)
for(var/I in 1 to num_closed)
var/datum/admin_help/AH = ticket_list[I]
if(AH.id > new_ticket.id)
ticket_list.Insert(I, new_ticket)
return
ticket_list += new_ticket
//opens the ticket listings for one of the 3 states
/datum/admin_help_tickets/proc/BrowseTickets(state)
var/list/l2b
var/title
switch(state)
if(AHELP_ACTIVE)
l2b = active_tickets
title = "Active Tickets"
if(AHELP_CLOSED)
l2b = closed_tickets
title = "Closed Tickets"
if(AHELP_RESOLVED)
l2b = resolved_tickets
title = "Resolved Tickets"
if(!l2b)
return
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>[title]</title></head>")
dat += "<A href='?_src_=holder;[HrefToken()];ahelp_tickets=[state]'>Refresh</A><br><br>"
for(var/I in l2b)
var/datum/admin_help/AH = I
dat += "[span_adminnotice("[span_adminhelp("Ticket #[AH.id]")]: <A href='?_src_=holder;[HrefToken()];ahelp=[REF(AH)];ahelp_action=ticket'>[AH.initiator_key_name]: [AH.name]</A>")]<br>"
usr << browse(dat.Join(), "window=ahelp_list[state];size=600x480")
//Tickets statpanel
/datum/admin_help_tickets/proc/stat_entry()
SHOULD_CALL_PARENT(TRUE)
SHOULD_NOT_SLEEP(TRUE)
var/list/L = list()
var/num_disconnected = 0
L[++L.len] = list("Active Tickets:", "[astatclick.update("[active_tickets.len]")]", null, REF(astatclick))
astatclick.update("[active_tickets.len]")
for(var/I in active_tickets)
var/datum/admin_help/AH = I
if(AH.initiator)
var/obj/effect/statclick/updated = AH.statclick.update()
//L[++L.len] = list("#[AH.id]. [AH.initiator_key_name]:", "[updated.name]", REF(AH)) //ORIGINAL
L[++L.len] = list("[AH.handler ? "H-[AH.handler]" : ""]#[AH.id]. [AH.initiator_key_name]:", "[updated.name]", REF(AH)) //SKYRAT EDIT CHANGE - ADMIN
else
++num_disconnected
if(num_disconnected)
L[++L.len] = list("Disconnected:", "[astatclick.update("[num_disconnected]")]", null, REF(astatclick))
L[++L.len] = list("Closed Tickets:", "[cstatclick.update("[closed_tickets.len]")]", null, REF(cstatclick))
L[++L.len] = list("Resolved Tickets:", "[rstatclick.update("[resolved_tickets.len]")]", null, REF(rstatclick))
//SKYRAT EDIT ADDITION
if(LAZYLEN(SSopposing_force.submitted_applications))
for(var/datum/opposing_force/opposing_force as anything in SSopposing_force.submitted_applications)
L[++L.len] = list("[opposing_force.handling_admin ? "H-[opposing_force.handling_admin]. " : ""]OPFOR: [opposing_force.ckey]", null, REF(opposing_force))
//SKYRAT EDIT END
return L
//Reassociate still open ticket if one exists
/datum/admin_help_tickets/proc/ClientLogin(client/C)
C.current_ticket = CKey2ActiveTicket(C.ckey)
if(C.current_ticket)
C.current_ticket.initiator = C
C.current_ticket.AddInteraction("Client reconnected.")
SSblackbox.LogAhelp(C.current_ticket.id, "Reconnected", "Client reconnected", C.ckey)
//Dissasociate ticket
/datum/admin_help_tickets/proc/ClientLogout(client/C)
if(C.current_ticket)
var/datum/admin_help/T = C.current_ticket
T.AddInteraction("Client disconnected.")
//Gotta async this cause clients only logout on destroy, and sleeping in destroy is disgusting
INVOKE_ASYNC(SSblackbox, /datum/controller/subsystem/blackbox/proc/LogAhelp, T, "Disconnected", "Client disconnected", C.ckey)
T.initiator = null
//Get a ticket given a ckey
/datum/admin_help_tickets/proc/CKey2ActiveTicket(ckey)
for(var/I in active_tickets)
var/datum/admin_help/AH = I
if(AH.initiator_ckey == ckey)
return AH
//
//TICKET LIST STATCLICK
//
/obj/effect/statclick/ticket_list
var/current_state
/obj/effect/statclick/ticket_list/Initialize(mapload, name, state)
. = ..()
current_state = state
/obj/effect/statclick/ticket_list/Click()
if (!usr.client?.holder)
message_admins("[key_name_admin(usr)] non-holder clicked on a ticket list statclick! ([src])")
log_game("[key_name(usr)] non-holder clicked on a ticket list statclick! ([src])")
return
GLOB.ahelp_tickets.BrowseTickets(current_state)
//called by admin topic
/obj/effect/statclick/ticket_list/proc/Action()
Click()
/**
* # Adminhelp Ticket
*/
/datum/admin_help
/// Unique ID of the ticket
var/id
/// The current name of the ticket
var/name
/// The current state of the ticket
var/state = AHELP_ACTIVE
/// The time at which the ticket was opened
var/opened_at
/// The time at which the ticket was closed
var/closed_at
/// Semi-misnomer, it's the person who ahelped/was bwoinked
var/client/initiator
/// The ckey of the initiator
var/initiator_ckey
/// The key name of the initiator
var/initiator_key_name
/// If any admins were online when the ticket was initialized
var/heard_by_no_admins = FALSE
/// The collection of interactions with this ticket. Use AddInteraction() or, preferably, admin_ticket_log()
var/list/ticket_interactions
/// Statclick holder for the ticket
var/obj/effect/statclick/ahelp/statclick
/// Static counter used for generating each ticket ID
var/static/ticket_counter = 0
/// The list of clients currently responding to the opening ticket before it gets a response
var/list/opening_responders
/**
* Call this on its own to create a ticket, don't manually assign current_ticket
*
* Arguments:
* * msg_raw - The first message of this admin_help: used for the initial title of the ticket
* * is_bwoink - Boolean operator, TRUE if this ticket was started by an admin PM
*/
/datum/admin_help/New(msg_raw, client/C, is_bwoink, client/admin_C, urgent = FALSE) //SKYRAT EDIT CHANGE
//clean the input msg
var/msg = sanitize(copytext_char(msg_raw, 1, MAX_MESSAGE_LEN))
if(!msg || !C || !C.mob)
qdel(src)
return
//SKYRAT EDIT ADDITION BEGIN
if(admin_C && is_bwoink)
handler = "[admin_C.ckey]"
//SKYRAT EDIT END
id = ++ticket_counter
opened_at = world.time
name = copytext_char(msg, 1, 100)
initiator = C
initiator_ckey = initiator.ckey
initiator_key_name = key_name(initiator, FALSE, TRUE)
if(initiator.current_ticket) //This is a bug
stack_trace("Multiple ahelp current_tickets")
initiator.current_ticket.AddInteraction("Ticket erroneously left open by code")
initiator.current_ticket.Close()
initiator.current_ticket = src
TimeoutVerb()
statclick = new(null, src)
ticket_interactions = list()
_interactions_player = list() // SKYRAT EDIT ADDITION -- Player ticket viewing
if(is_bwoink)
AddInteraction("<font color='blue'>[key_name_admin(usr)] PM'd [LinkedReplyName()]</font>")
AddInteractionPlayer("<font color='blue'>[key_name_admin(usr, FALSE)] PM'd [LinkedReplyName()]</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
message_admins("<font color='blue'>Ticket [TicketHref("#[id]")] created</font>")
else
MessageNoRecipient(msg_raw, urgent)
send_message_to_tgs(msg, urgent)
GLOB.ahelp_tickets.active_tickets += src
/datum/admin_help/proc/send_message_to_tgs(message, urgent = FALSE)
var/message_to_send = message
if(urgent)
var/extra_message_to_send = "[message] - Requested an admin"
var/extra_message = CONFIG_GET(string/urgent_ahelp_message)
if(extra_message)
extra_message_to_send += " ([extra_message])"
to_chat(initiator, span_boldwarning("Notified admins to prioritize your ticket"))
send2adminchat_webhook("RELAY: [initiator_ckey] | Ticket #[id]: [extra_message_to_send]")
//send it to TGS if nobody is on and tell us how many were on
var/admin_number_present = send2tgs_adminless_only(initiator_ckey, "Ticket #[id]: [message_to_send]")
log_admin_private("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.")
if(admin_number_present <= 0)
to_chat(initiator, span_notice("No active admins are online, your adminhelp was sent to admins who are available through IRC or Discord."), confidential = TRUE)
heard_by_no_admins = TRUE
/proc/send2adminchat_webhook(message)
if(!CONFIG_GET(string/adminhelp_webhook_url))
return
var/message_content = replacetext(replacetext(message, "\proper", ""), "\improper", "")
message_content = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ")
var/list/webhook_info = list()
webhook_info["content"] = message_content
if(CONFIG_GET(string/adminhelp_webhook_name))
webhook_info["username"] = CONFIG_GET(string/adminhelp_webhook_name)
if(CONFIG_GET(string/adminhelp_webhook_pfp))
webhook_info["avatar_url"] = CONFIG_GET(string/adminhelp_webhook_pfp)
// Uncomment when servers are moved to TGS4
// send2chat("[initiator_ckey] | [message_content]", "ahelp", TRUE)
var/list/headers = list()
headers["Content-Type"] = "application/json"
var/datum/http_request/request = new()
request.prepare(RUSTG_HTTP_METHOD_POST, CONFIG_GET(string/adminhelp_webhook_url), json_encode(webhook_info), headers, "tmp/response.json")
request.begin_async()
/datum/admin_help/Destroy()
RemoveActive()
GLOB.ahelp_tickets.closed_tickets -= src
GLOB.ahelp_tickets.resolved_tickets -= src
return ..()
/datum/admin_help/proc/AddInteraction(formatted_message)
if(heard_by_no_admins && usr && usr.ckey != initiator_ckey)
heard_by_no_admins = FALSE
send2adminchat(initiator_ckey, "Ticket #[id]: Answered by [key_name(usr)]")
ticket_interactions += "[time_stamp()]: [formatted_message]"
//Removes the ahelp verb and returns it after 2 minutes
/datum/admin_help/proc/TimeoutVerb()
remove_verb(initiator, /client/verb/adminhelp)
initiator.adminhelptimerid = addtimer(CALLBACK(initiator, /client/proc/giveadminhelpverb), 1200, TIMER_STOPPABLE) //2 minute cooldown of admin helps
//private
/datum/admin_help/proc/FullMonty(ref_src)
if(!ref_src)
ref_src = "[REF(src)]"
. = ADMIN_FULLMONTY_NONAME(initiator.mob)
if(state == AHELP_ACTIVE)
if (CONFIG_GET(flag/popup_admin_pm))
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];adminpopup=[REF(initiator)]'>POPUP</A>)"
. += ClosureLinks(ref_src)
//private
/datum/admin_help/proc/ClosureLinks(ref_src)
if(!ref_src)
ref_src = "[REF(src)]"
. = " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=reject'>REJT</A>)"
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=icissue'>IC</A>)"
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=close'>CLOSE</A>)"
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=resolve'>RSLVE</A>)"
. += " (<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=handleissue'>HANDLE</A>)" //SKYRAT EDIT ADDITION - ADMIN
//private
/datum/admin_help/proc/LinkedReplyName(ref_src)
if(!ref_src)
ref_src = "[REF(src)]"
return "<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=reply'>[initiator_key_name]</A>"
//private
/datum/admin_help/proc/TicketHref(msg, ref_src, action = "ticket")
if(!ref_src)
ref_src = "[REF(src)]"
return "<A HREF='?_src_=holder;[HrefToken(TRUE)];ahelp=[ref_src];ahelp_action=[action]'>[msg]</A>"
//message from the initiator without a target, all admins will see this
//won't bug irc/discord
/datum/admin_help/proc/MessageNoRecipient(msg, urgent = FALSE)
msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN))
var/ref_src = "[REF(src)]"
//Message to be sent to all admins
var/admin_msg = span_adminnotice(span_adminhelp("Ticket [TicketHref("#[id]", ref_src)]</span><b>: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]:</b> <span class='linkify'>[keywords_lookup(msg)]"))
AddInteraction("<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>")
AddInteractionPlayer("<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
log_admin_private("Ticket #[id]: [key_name(initiator)]: [msg]")
//send this msg to all admins
for(var/client/X in GLOB.admins)
if(X.prefs.toggles & SOUND_ADMINHELP)
SEND_SOUND(X, sound('sound/effects/adminhelp.ogg'))
window_flash(X, ignorepref = TRUE)
to_chat(X,
type = MESSAGE_TYPE_ADMINPM,
html = admin_msg,
confidential = TRUE)
//show it to the person adminhelping too
to_chat(initiator,
type = MESSAGE_TYPE_ADMINPM,
html = span_adminnotice("PM to-<b>Admins</b>: <span class='linkify'>[msg]</span>"),
confidential = TRUE)
SSblackbox.LogAhelp(id, "Ticket Opened", msg, null, initiator.ckey, urgent = urgent)
//Reopen a closed ticket
/datum/admin_help/proc/Reopen()
if(state == AHELP_ACTIVE)
to_chat(usr, span_warning("This ticket is already open."), confidential = TRUE)
return
if(GLOB.ahelp_tickets.CKey2ActiveTicket(initiator_ckey))
to_chat(usr, span_warning("This user already has an active ticket, cannot reopen this one."), confidential = TRUE)
return
statclick = new(null, src)
GLOB.ahelp_tickets.active_tickets += src
GLOB.ahelp_tickets.closed_tickets -= src
GLOB.ahelp_tickets.resolved_tickets -= src
switch(state)
if(AHELP_CLOSED)
SSblackbox.record_feedback("tally", "ahelp_stats", -1, "closed")
if(AHELP_RESOLVED)
SSblackbox.record_feedback("tally", "ahelp_stats", -1, "resolved")
state = AHELP_ACTIVE
closed_at = null
if(initiator)
initiator.current_ticket = src
AddInteraction("<font color='purple'>Reopened by [key_name_admin(usr)]</font>")
AddInteractionPlayer("<font color='purple'>Reopened by [key_name_admin(usr, FALSE)]</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
var/msg = span_adminhelp("Ticket [TicketHref("#[id]")] reopened by [key_name_admin(usr)].")
message_admins(msg)
log_admin_private(msg)
SSblackbox.LogAhelp(id, "Reopened", "Reopened by [usr.key]", usr.ckey)
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "reopened")
TicketPanel() //can only be done from here, so refresh it
//private
/datum/admin_help/proc/RemoveActive()
if(state != AHELP_ACTIVE)
return
closed_at = world.time
QDEL_NULL(statclick)
GLOB.ahelp_tickets.active_tickets -= src
if(initiator && initiator.current_ticket == src)
initiator.current_ticket = null
SEND_SIGNAL(src, COMSIG_ADMIN_HELP_MADE_INACTIVE)
//Mark open ticket as closed/meme
/datum/admin_help/proc/Close(key_name = key_name_admin(usr), silent = FALSE)
if(state != AHELP_ACTIVE)
return
//SKYRAT EDIT ADDITION BEGIN - ADMIN
if(handler && handler != usr.ckey)
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
if(!response || response == "No")
return
//SKYRAT EDIT ADDITION END
RemoveActive()
state = AHELP_CLOSED
GLOB.ahelp_tickets.ListInsert(src)
AddInteraction("<font color='red'>Closed by [key_name].</font>")
AddInteractionPlayer("<font color='red'>Closed by [key_name_admin(usr, FALSE)].</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
if(!silent)
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "closed")
var/msg = "Ticket [TicketHref("#[id]")] closed by [key_name]."
message_admins(msg)
SSblackbox.LogAhelp(id, "Closed", "Closed by [usr.key]", null, usr.ckey)
log_admin_private(msg)
//Mark open ticket as resolved/legitimate, returns ahelp verb
/datum/admin_help/proc/Resolve(key_name = key_name_admin(usr), silent = FALSE)
if(state != AHELP_ACTIVE)
return
//SKYRAT EDIT ADDITION BEGIN - ADMIN
if(handler && handler != usr.ckey)
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
if(!response || response == "No")
return
//SKYRAT EDIT ADDITION END
RemoveActive()
state = AHELP_RESOLVED
GLOB.ahelp_tickets.ListInsert(src)
addtimer(CALLBACK(initiator, /client/proc/giveadminhelpverb), 50)
AddInteraction("<font color='green'>Resolved by [key_name].</font>")
AddInteractionPlayer("<font color='green'>Resolved by [key_name_admin(usr, FALSE)].</font>") // SKYRAT EDIT ADDITION -- Player ticket viewing
to_chat(initiator, span_adminhelp("Your ticket has been resolved by an admin. The Adminhelp verb will be returned to you shortly."), confidential = TRUE)
if(!silent)
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "resolved")
var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name]"
message_admins(msg)
SSblackbox.LogAhelp(id, "Resolved", "Resolved by [usr.key]", null, usr.ckey)
log_admin_private(msg)
//Close and return ahelp verb, use if ticket is incoherent
/datum/admin_help/proc/Reject(key_name = key_name_admin(usr))
if(state != AHELP_ACTIVE)
return
//SKYRAT EDIT ADDITION BEGIN - ADMIN
if(handler && handler != usr.ckey)
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
if(!response || response == "No")
return
//SKYRAT EDIT ADDITION END
if(initiator)
initiator.giveadminhelpverb()
SEND_SOUND(initiator, sound('sound/effects/adminhelp.ogg'))
to_chat(initiator, "<font color='red' size='4'><b>- AdminHelp Rejected! -</b></font>", confidential = TRUE)
to_chat(initiator, "<font color='red'><b>Your admin help was rejected.</b> The adminhelp verb has been returned to you so that you may try again.</font>", confidential = TRUE)
to_chat(initiator, "Please try to be calm, clear, and descriptive in admin helps, do not assume the admin has seen any related events, and clearly state the names of anybody you are reporting.", confidential = TRUE)
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "rejected")
var/msg = "Ticket [TicketHref("#[id]")] rejected by [key_name]"
message_admins(msg)
log_admin_private(msg)
AddInteraction("Rejected by [key_name].")
AddInteractionPlayer("Rejected by [key_name_admin(usr, FALSE)].") // SKYRAT EDIT ADDITION -- Player ticket viewing
SSblackbox.LogAhelp(id, "Rejected", "Rejected by [usr.key]", null, usr.ckey)
Close(silent = TRUE)
//Resolve ticket with IC Issue message
/datum/admin_help/proc/ICIssue(key_name = key_name_admin(usr))
if(state != AHELP_ACTIVE)
return
//SKYRAT EDIT ADDITION BEGIN - ADMIN
if(handler && handler != usr.ckey)
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
if(!response || response == "No")
return
//SKYRAT EDIT ADDITION END
var/msg = "<font color='red' size='4'><b>- AdminHelp marked as IC issue! -</b></font><br>"
msg += "<font color='red'>Your issue has been determined by an administrator to be an in character issue and does NOT require administrator intervention at this time. For further resolution you should pursue options that are in character.</font>"
if(initiator)
to_chat(initiator, msg, confidential = TRUE)
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "IC")
msg = "Ticket [TicketHref("#[id]")] marked as IC by [key_name]"
message_admins(msg)
log_admin_private(msg)
AddInteraction("Marked as IC issue by [key_name]")
AddInteractionPlayer("Marked as IC issue by [key_name_admin(usr, FALSE)]") // SKYRAT EDIT ADDITION -- Player ticket viewing
SSblackbox.LogAhelp(id, "IC Issue", "Marked as IC issue by [usr.key]", null, usr.ckey)
Resolve(silent = TRUE)
//Show the ticket panel
/datum/admin_help/proc/TicketPanel()
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Ticket #[id]</title></head>")
var/ref_src = "[REF(src)]"
dat += "<h4>Admin Help Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
dat += "<b>State: [ticket_status()]</b>"
dat += "[FOURSPACES][TicketHref("Refresh", ref_src)][FOURSPACES][TicketHref("Re-Title", ref_src, "retitle")]"
if(state != AHELP_ACTIVE)
dat += "[FOURSPACES][TicketHref("Reopen", ref_src, "reopen")]"
dat += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [DisplayTimeText(world.time - opened_at)] ago)"
if(closed_at)
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [DisplayTimeText(world.time - closed_at)] ago)"
dat += "<br><br>"
if(initiator)
dat += "<b>Actions:</b> [FullMonty(ref_src)]<br>"
else
dat += "<b>DISCONNECTED</b>[FOURSPACES][ClosureLinks(ref_src)]<br>"
dat += "<br><b>Log:</b><br><br>"
for(var/I in ticket_interactions)
dat += "[I]<br>"
// Append any tickets also opened by this user if relevant
var/list/related_tickets = GLOB.ahelp_tickets.TicketsByCKey(initiator_ckey)
if (related_tickets.len > 1)
dat += "<br/><b>Other Tickets by User</b><br/>"
for (var/datum/admin_help/related_ticket in related_tickets)
if (related_ticket.id == id)
continue
dat += "[related_ticket.TicketHref("#[related_ticket.id]")] ([related_ticket.ticket_status()]): [related_ticket.name]<br/>"
usr << browse(dat.Join(), "window=ahelp[id];size=700x480")
/**
* Renders the current status of the ticket into a displayable string
*/
/datum/admin_help/proc/ticket_status()
switch(state)
if(AHELP_ACTIVE)
return "<font color='red'>OPEN</font>"
if(AHELP_RESOLVED)
return "<font color='green'>RESOLVED</font>"
if(AHELP_CLOSED)
return "CLOSED"
else
stack_trace("Invalid ticket state: [state]")
return "INVALID, CALL A CODER"
/datum/admin_help/proc/Retitle()
var/new_title = input(usr, "Enter a title for the ticket", "Rename Ticket", name) as text|null
if(new_title)
name = new_title
//not saying the original name cause it could be a long ass message
var/msg = "Ticket [TicketHref("#[id]")] titled [name] by [key_name_admin(usr)]"
message_admins(msg)
log_admin_private(msg)
TicketPanel() //we have to be here to do this
//Forwarded action from admin/Topic
/datum/admin_help/proc/Action(action)
testing("Ahelp action: [action]")
switch(action)
if("ticket")
TicketPanel()
if("retitle")
Retitle()
if("reject")
Reject()
if("reply")
HandleIssue() /// SKYRAT EDIT ADDITION - ADMIN HANDLE
usr.client.cmd_ahelp_reply(initiator)
if("icissue")
ICIssue()
if("close")
Close()
if("resolve")
Resolve()
if("reopen")
Reopen()
//SKYRAT EDIT ADDITION BEING - ADMIN
if("handleissue")
HandleIssue()
//SKYRAT EDIT ADDITION END
//
// TICKET STATCLICK
//
/obj/effect/statclick/ahelp
var/datum/admin_help/ahelp_datum
/obj/effect/statclick/ahelp/Initialize(mapload, datum/admin_help/AH)
ahelp_datum = AH
. = ..()
/obj/effect/statclick/ahelp/update()
return ..(ahelp_datum.name)
/obj/effect/statclick/ahelp/Click()
if (!usr.client?.holder)
message_admins("[key_name_admin(usr)] non-holder clicked on an ahelp statclick! ([src])")
log_game("[key_name(usr)] non-holder clicked on an ahelp statclick! ([src])")
return
ahelp_datum.TicketPanel()
/obj/effect/statclick/ahelp/Destroy()
ahelp_datum = null
return ..()
//
// CLIENT PROCS
//
/client/proc/giveadminhelpverb()
add_verb(src, /client/verb/adminhelp)
deltimer(adminhelptimerid)
adminhelptimerid = 0
GLOBAL_DATUM_INIT(admin_help_ui_handler, /datum/admin_help_ui_handler, new)
/datum/admin_help_ui_handler
var/list/ahelp_cooldowns = list()
/datum/admin_help_ui_handler/ui_state(mob/user)
return GLOB.always_state
/datum/admin_help_ui_handler/ui_data(mob/user)
. = list()
var/list/admins = get_admin_counts(R_BAN)
.["adminCount"] = length(admins["present"])
/datum/admin_help_ui_handler/ui_static_data(mob/user)
. = list()
.["bannedFromUrgentAhelp"] = is_banned_from(user.ckey, "Urgent Adminhelp")
.["urgentAhelpPromptMessage"] = CONFIG_GET(string/urgent_ahelp_user_prompt)
var/webhook_url = CONFIG_GET(string/adminhelp_webhook_url)
if(webhook_url)
.["urgentAhelpEnabled"] = TRUE
/datum/admin_help_ui_handler/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Adminhelp")
ui.open()
ui.set_autoupdate(FALSE)
/datum/admin_help_ui_handler/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/client/user_client = usr.client
var/message = sanitize_text(trim(params["message"]))
var/urgent = !!params["urgent"]
var/list/admins = get_admin_counts(R_BAN)
if(length(admins["present"]) != 0 || is_banned_from(user_client.ckey, "Urgent Adminhelp"))
urgent = FALSE
if(user_client.adminhelptimerid)
return
perform_adminhelp(user_client, message, urgent)
ui.close()
/datum/admin_help_ui_handler/proc/perform_adminhelp(client/user_client, message, urgent)
if(GLOB.say_disabled) //This is here to try to identify lag problems
to_chat(usr, span_danger("Speech is currently admin-disabled."), confidential = TRUE)
return
if(!message)
return
//handle muting and automuting
if(user_client.prefs.muted & MUTE_ADMINHELP)
to_chat(user_client, span_danger("Error: Admin-PM: You cannot send adminhelps (Muted)."), confidential = TRUE)
return
if(user_client.handle_spam_prevention(message, MUTE_ADMINHELP))
return
SSblackbox.record_feedback("tally", "admin_verb", 1, "Adminhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
if(urgent)
if(!COOLDOWN_FINISHED(src, ahelp_cooldowns?[user_client.ckey]))
urgent = FALSE // Prevent abuse
else
COOLDOWN_START(src, ahelp_cooldowns[user_client.ckey], CONFIG_GET(number/urgent_ahelp_cooldown) * (1 SECONDS))
if(user_client.current_ticket)
user_client.current_ticket.TimeoutVerb()
if(urgent)
var/sanitized_message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN))
user_client.current_ticket.send_message_to_tgs(sanitized_message, urgent = TRUE)
user_client.current_ticket.MessageNoRecipient(message, urgent)
return
//new /datum/admin_help(message, user_client, FALSE, urgent) //ORIGINAL
new /datum/admin_help(message, user_client, FALSE, urgent=urgent) //SKYRAT EDIT CHANGE - ADMIN
/client/verb/no_tgui_adminhelp(message as message)
set name = "NoTguiAdminhelp"
set hidden = TRUE
if(adminhelptimerid)
return
message = trim(message)
GLOB.admin_help_ui_handler.perform_adminhelp(src, message, FALSE)
/client/verb/adminhelp()
set category = "Admin"
set name = "Adminhelp"
GLOB.admin_help_ui_handler.ui_interact(mob)
to_chat(src, span_boldnotice("Adminhelp failing to open or work? <a href='?src=[REF(src)];tguiless_adminhelp=1'>Click here</a>"))
//
// LOGGING
//
//Use this proc when an admin takes action that may be related to an open ticket on what
//what can be a client, ckey, or mob
//log_in_blackbox: Whether or not this message with the blackbox system.
//If disabled, this message should be logged with a different proc call
// /proc/admin_ticket_log(what, message, log_in_blackbox = TRUE) // SKYRAT EDIT ORIGINAL
/proc/admin_ticket_log(what, message, log_in_blackbox = TRUE, admin_only = TRUE) // SKYRAT EDIT CHANGE -- Player ticket viewing
var/client/C
var/mob/Mob = what
if(istype(Mob))
C = Mob.client
else
C = what
if(istype(C) && C.current_ticket)
C.current_ticket.AddInteraction(message)
// SKYRAT EDIT ADDITION START -- Player ticket viewing
if(!admin_only)
C.current_ticket.AddInteractionPlayer(message)
// SKYRAT EDIT ADDITION END
if(log_in_blackbox)
SSblackbox.LogAhelp(C.current_ticket.id, "Interaction", message, C.ckey, usr.ckey)
return C.current_ticket
if(istext(what)) //ckey
var/datum/admin_help/AH = GLOB.ahelp_tickets.CKey2ActiveTicket(what)
if(AH)
AH.AddInteraction(message)
// SKYRAT EDIT ADDITION START -- Player ticket viewing
if(!admin_only)
AH.AddInteractionPlayer(message)
// SKYRAT EDIT ADDITION END
if(log_in_blackbox)
SSblackbox.LogAhelp(AH.id, "Interaction", message, what, usr.ckey)
return AH
//
// HELPER PROCS
//
/proc/get_admin_counts(requiredflags = R_BAN)
. = list("total" = list(), "noflags" = list(), "afk" = list(), "stealth" = list(), "present" = list())
for(var/client/X in GLOB.admins)
.["total"] += X
if(requiredflags != NONE && !check_rights_for(X, requiredflags))
.["noflags"] += X
else if(X.is_afk())
.["afk"] += X
else if(X.holder.fakekey)
.["stealth"] += X
else
.["present"] += X
/proc/send2tgs_adminless_only(source, msg, requiredflags = R_BAN)
var/list/adm = get_admin_counts(requiredflags)
var/list/activemins = adm["present"]
. = activemins.len
if(. <= 0)
var/final = ""
var/list/afkmins = adm["afk"]
var/list/stealthmins = adm["stealth"]
var/list/powerlessmins = adm["noflags"]
var/list/allmins = adm["total"]
if(!afkmins.len && !stealthmins.len && !powerlessmins.len)
final = "[msg] - No admins online"
else
final = "[msg] - All admins stealthed\[[english_list(stealthmins)]\], AFK\[[english_list(afkmins)]\], or lacks +BAN\[[english_list(powerlessmins)]\]! Total: [allmins.len] "
send2adminchat(source,final)
send2otherserver(source,final)
/**
* Sends a message to a set of cross-communications-enabled servers using world topic calls
*
* Arguments:
* * source - Who sent this message
* * msg - The message body
* * type - The type of message, becomes the topic command under the hood
* * target_servers - A collection of servers to send the message to, defined in config
* * additional_data - An (optional) associated list of extra parameters and data to send with this world topic call
*/
/proc/send2otherserver(source, msg, type = "Ahelp", target_servers, list/additional_data = list())
if(!CONFIG_GET(string/comms_key))
debug_world_log("Server cross-comms message not sent for lack of configured key")
return
var/our_id = CONFIG_GET(string/cross_comms_name)
additional_data["message_sender"] = source
additional_data["message"] = msg
additional_data["source"] = "([our_id])"
additional_data += type
var/list/servers = CONFIG_GET(keyed_list/cross_server)
// SKYRAT EDTI ADDITION
if(target_servers == "all")
for(var/server in servers)
if(server == our_id)
continue
world.send_cross_comms(server, additional_data)
else //SKYRAT EDIT END
for(var/I in servers)
if(I == our_id) //No sending to ourselves
continue
if(target_servers && !(I in target_servers))
continue
world.send_cross_comms(I, additional_data)
/// Sends a message to a given cross comms server by name (by name for security).
/world/proc/send_cross_comms(server_name, list/message, auth = TRUE)
set waitfor = FALSE
if (auth)
var/comms_key = CONFIG_GET(string/comms_key)
if(!comms_key)
debug_world_log("Server cross-comms message not sent for lack of configured key")
return
message["key"] = comms_key
var/list/servers = CONFIG_GET(keyed_list/cross_server)
var/server_url = servers[server_name]
if (!server_url)
CRASH("Invalid cross comms config: [server_name]")
world.Export("[server_url]?[list2params(message)]")
/proc/tgsadminwho()
var/list/message = list("Admins: ")
var/list/admin_keys = list()
for(var/adm in GLOB.admins)
var/client/C = adm
admin_keys += "[C][C.holder.fakekey ? "(Stealth)" : ""][C.is_afk() ? "(AFK)" : ""]"
for(var/admin in admin_keys)
if(LAZYLEN(message) > 1)
message += ", [admin]"
else
message += "[admin]"
return jointext(message, "")
/proc/keywords_lookup(msg,external)
//This is a list of words which are ignored by the parser when comparing message contents for names. MUST BE IN LOWER CASE!
var/list/adminhelp_ignored_words = list("unknown","the","a","an","of","monkey","alien","as", "i")
//explode the input msg into a list
var/list/msglist = splittext(msg, " ")
//generate keywords lookup
var/list/surnames = list()
var/list/forenames = list()
var/list/ckeys = list()
var/founds = ""
for(var/mob/M in GLOB.mob_list)
var/list/indexing = list(M.real_name, M.name)
if(M.mind)
indexing += M.mind.name
for(var/string in indexing)
var/list/L = splittext(string, " ")
var/surname_found = 0
//surnames
for(var/i=L.len, i>=1, i--)
var/word = ckey(L[i])
if(word)
surnames[word] = M
surname_found = i
break
//forenames
for(var/i in 1 to surname_found-1)
var/word = ckey(L[i])
if(word)
forenames[word] = M
//ckeys
ckeys[M.ckey] = M
var/ai_found = 0
msg = ""
var/list/mobs_found = list()
for(var/original_word in msglist)
var/word = ckey(original_word)
if(word)
if(!(word in adminhelp_ignored_words))
if(word == "ai")
ai_found = 1
else
var/mob/found = ckeys[word]
if(!found)
found = surnames[word]
if(!found)
found = forenames[word]
if(found)
if(!(found in mobs_found))
mobs_found += found
if(!ai_found && isAI(found))
ai_found = 1
var/is_antag = 0
if(is_special_character(found))
is_antag = 1
founds += "Name: [found.name]([found.real_name]) Key: [found.key] Ckey: [found.ckey] [is_antag ? "(Antag)" : null] "
msg += "[original_word]<font size='1' color='[is_antag ? "red" : "black"]'>(<A HREF='?_src_=holder;[HrefToken(TRUE)];adminmoreinfo=[REF(found)]'>?</A>|<A HREF='?_src_=holder;[HrefToken(TRUE)];adminplayerobservefollow=[REF(found)]'>F</A>)</font> "
continue
msg += "[original_word] "
if(external)
if(founds == "")
return "Search Failed"
else
return founds
return msg
/proc/get_mob_by_name(msg)
//This is a list of words which are ignored by the parser when comparing message contents for names. MUST BE IN LOWER CASE!
var/list/ignored_words = list("unknown","the","a","an","of","monkey","alien","as", "i")
//explode the input msg into a list
var/list/msglist = splittext(msg, " ")
//who might fit the shoe
var/list/potential_hits = list()
for(var/i in GLOB.mob_list)
var/mob/M = i
var/list/nameWords = list()
if(!M.mind)
continue
for(var/string in splittext(lowertext(M.real_name), " "))
if(!(string in ignored_words))
nameWords += string
for(var/string in splittext(lowertext(M.name), " "))
if(!(string in ignored_words))
nameWords += string
for(var/string in nameWords)
if(string in msglist)
potential_hits += M
break
return potential_hits
/**
* Checks a given message to see if any of the words contain an active admin's ckey with an @ before it
*
* Returns nothing if no pings are found, otherwise returns an associative list with ckey -> client
* Also modifies msg to underline the pings, then stores them in the key [ADMINSAY_PING_UNDERLINE_NAME_INDEX] for returning
*
* Arguments:
* * msg - the message being scanned
*/
/proc/check_admin_pings(msg)
//explode the input msg into a list
var/list/msglist = splittext(msg, " ")
var/list/admins_to_ping = list()
var/i = 0
for(var/word in msglist)
i++
if(!length(word))
continue
if(word[1] != "@")
continue
var/ckey_check = lowertext(copytext(word, 2))
var/client/client_check = GLOB.directory[ckey_check]
if(client_check?.holder)
msglist[i] = "<u>[word]</u>"
admins_to_ping[ckey_check] = client_check
if(length(admins_to_ping))
admins_to_ping[ADMINSAY_PING_UNDERLINE_NAME_INDEX] = jointext(msglist, " ") // without tuples, we must make do!
return admins_to_ping