Tickets System

This commit is contained in:
Selis
2023-06-21 13:51:32 +02:00
parent 831110b53e
commit 67ba4a2de5
23 changed files with 1862 additions and 660 deletions
+22 -15
View File
@@ -29,19 +29,22 @@
check_antagonists()
return
if(href_list["ahelp"])
// CHOMPedit Start - Tickets System
if(href_list["ticket"])
if(!check_rights(R_ADMIN|R_MOD|R_DEBUG|R_EVENT))
return
var/ahelp_ref = href_list["ahelp"]
var/datum/admin_help/AH = locate(ahelp_ref)
if(AH)
AH.Action(href_list["ahelp_action"])
var/ticket_ref = href_list["ticket"]
var/datum/ticket/T = locate(ticket_ref)
if(T)
T.Action(href_list["ticket_action"])
else
to_chat(usr, "Ticket [ahelp_ref] has been deleted!")
to_chat(usr, "Ticket [ticket_ref] has been deleted!")
else if(href_list["ahelp_tickets"])
GLOB.ahelp_tickets.BrowseTickets(text2num(href_list["ahelp_tickets"]))
else if(href_list["tickets"])
GLOB.tickets.BrowseTickets(text2num(href_list["tickets"]))
// CHOMPedit End
mentor_commands(href, href_list, src)
@@ -166,7 +169,7 @@
if(admin_ranks.len)
new_rank = tgui_input_list(usr, "Please select a rank", "New rank", (admin_ranks|"*New Rank*"))
else
new_rank = tgui_input_list(usr, "Please select a rank", "New rank", list("Game Master","Head Admin","Game Admin", "Trial Admin", "Admin Observer","Moderator","Mentor","Badmin","Retired Admin","Event Manager","Developer","DevMod","*New Rank*")) //CHOMP Edit bandaid fix to assigning titles because we're having some funky database issues, I think. Other option is to manually edit database entry for someone's title.
new_rank = tgui_input_list(usr, "Please select a rank", "New rank", list("Game Master","Head Admin","Game Admin", "Trial Admin", "Admin Observer","Moderator","Mentor","Badmin","Retired Admin","Event Manager","Developer","DevMod","*New Rank*")) //CHOMP Edit bandaid fix to assigning titles because we're having some funky database issues, I think. Other option is to manually edit database entry for someone's title.
var/rights = 0
if(D)
@@ -935,10 +938,12 @@
to_chat(M, "<span class='filter_system warning'>No ban appeals URL has been set.</span>")
log_admin("[usr.client.ckey] has banned [M.ckey].\nReason: [reason]\nThis will be removed in [mins] minutes.")
message_admins("<font color='blue'>[usr.client.ckey] has banned [M.ckey].\nReason: [reason]\nThis will be removed in [mins] minutes.</font>")
var/datum/admin_help/AH = M.client ? M.client.current_ticket : null
if(AH)
AH.Resolve()
// CHOMPedit Start - Tickets System
var/datum/ticket/T = M.client ? M.client.current_ticket : null
if(T)
T.Resolve()
qdel(M.client)
// CHOMPedit End
//qdel(M) // See no reason why to delete mob. Important stuff can be lost. And ban can be lifted before round ends.
if("No")
if(!check_rights(R_BAN)) return
@@ -963,9 +968,11 @@
message_admins("<font color='blue'>[usr.client.ckey] has banned [M.ckey].\nReason: [reason]\nThis is a permanent ban.</font>")
feedback_inc("ban_perma",1)
DB_ban_record(BANTYPE_PERMA, M, -1, reason)
var/datum/admin_help/AH = M.client ? M.client.current_ticket : null
if(AH)
AH.Resolve()
// CHOMPedit Start - Tickets System
var/datum/ticket/T = M.client ? M.client.current_ticket : null
if(T)
T.Resolve()
// CHOMPedit End
qdel(M.client)
//qdel(M)
if("Cancel")
+7
View File
@@ -1,3 +1,10 @@
/*
CHOMPedit - This file has been excluded from the compilation.
Reason: Replaced with "Tickets System". Main logic has been moved to: modular_chomp/modules/tickets/tickets.dm
*/
/client/var/datum/admin_help/current_ticket //the current ticket the (usually) not-admin client is dealing with
//CHOMPEdit Begin
+7
View File
@@ -1,3 +1,10 @@
/*
CHOMPedit - This file has been excluded from the compilation.
Reason: Replaced with "Tickets System"
*/
/datum/admin_help/proc/send2adminchat()
if(!config.chat_webhook_url)
return
+6 -6
View File
@@ -53,19 +53,19 @@
to_chat(src, "<span class='pm warning'>Error: Admin-PM: Client not found.</span>")
return
var/datum/admin_help/AH = C.current_ticket
var/datum/ticket/T = C.current_ticket // CHOMPedit - Ticket System
if(AH)
if(T) // CHOMPedit - Ticket System
message_admins("<span class='pm'>[key_name_admin(src)] has started replying to [key_name(C, 0, 0)]'s admin help.</span>")
var/msg = tgui_input_text(src,"Message:", "Private message to [key_name(C, 0, 0)]")
if (!msg)
message_admins("<span class='pm'>[key_name_admin(src)] has cancelled their reply to [key_name(C, 0, 0)]'s admin help.</span>")
return
cmd_admin_pm(whom, msg, AH)
cmd_admin_pm(whom, msg, T) // CHOMPedit - Ticket System
//takes input from cmd_admin_pm_context, cmd_admin_pm_panel or /client/Topic and sends them a PM.
//Fetching a message if needed. src is the sender and C is the target client
/client/proc/cmd_admin_pm(whom, msg, datum/admin_help/AH)
/client/proc/cmd_admin_pm(whom, msg, datum/ticket/T) // CHOMPedit - Ticket System
if(prefs.muted & MUTE_ADMINHELP)
to_chat(src, "<span class='pm warning'>Error: Admin-PM: You are unable to use admin PM-s (muted).</span>")
return
@@ -171,7 +171,7 @@
else
if(holder) //sender is an admin but recipient is not. Do BIG RED TEXT
if(!recipient.current_ticket)
new /datum/admin_help(msg, recipient, TRUE)
new /datum/ticket(msg, recipient, TRUE, 0) // CHOMPedit - Ticket System
to_chat(recipient, "<span class='pm warning' size='4'><b>-- Administrator private message --</b></span>")
to_chat(recipient, "<span class='pm warning'>Admin PM from-<b>[key_name(src, recipient, 0)]</b>: [msg]</span>")
@@ -214,7 +214,7 @@
/proc/IrcPm(target,msg,sender)
var/client/C = GLOB.directory[target]
var/datum/admin_help/ticket = C ? C.current_ticket : GLOB.ahelp_tickets.CKey2ActiveTicket(target)
var/datum/ticket/ticket = C ? C.current_ticket : GLOB.tickets.CKey2ActiveTicket(target) // CHOMPedit - Ticket System
var/compliant_msg = trim(lowertext(msg))
var/irc_tagged = "[sender](IRC)"
var/list/splits = splittext(compliant_msg, " ")
+2 -4
View File
@@ -186,8 +186,7 @@
GLOB.clients += src
GLOB.directory[ckey] = src
GLOB.ahelp_tickets.ClientLogin(src)
GLOB.mhelp_tickets.ClientLogin(src)
GLOB.tickets.ClientLogin(src) // CHOMPedit - Tickets System
//Admin Authorisation
holder = admin_datums[ckey]
@@ -280,8 +279,7 @@
if (mentorholder)
mentorholder.owner = null
GLOB.mentors -= src
GLOB.ahelp_tickets.ClientLogout(src)
GLOB.mhelp_tickets.ClientLogout(src)
GLOB.tickets.ClientLogout(src) // CHOMPedit - Tickets System
GLOB.directory -= ckey
GLOB.clients -= src
return ..()
+27 -23
View File
@@ -116,16 +116,18 @@ var/list/mentor_verbs_default = list(
to_chat(C, create_text_tag("mentor", "MENTOR:", C) + " <span class='mentor_channel'><span class='name'>[src]</span>: <span class='message'>[msg]</span></span>")
/proc/mentor_commands(href, href_list, client/C)
if(href_list["mhelp"])
var/mhelp_ref = href_list["mhelp"]
var/datum/mentor_help/MH = locate(mhelp_ref)
if (MH && istype(MH, /datum/mentor_help))
MH.Action(href_list["mhelp_action"])
// CHOMPedit Start - Tickets System
if(href_list["ticket"])
var/ticket_ref = href_list["ticket"]
var/datum/ticket/T = locate(ticket_ref)
if (T && istype(T, /datum/ticket))
T.Action(href_list["ticket_action"])
else
to_chat(C, "Ticket [mhelp_ref] has been deleted!")
to_chat(C, "Ticket [ticket_ref] has been deleted!")
if (href_list["mhelp_tickets"])
GLOB.mhelp_tickets.BrowseTickets(text2num(href_list["mhelp_tickets"]))
if (href_list["tickets"])
GLOB.tickets.BrowseTickets(text2num(href_list["tickets"]))
// CHOMPedit End
/datum/mentor/Topic(href, href_list)
@@ -158,15 +160,15 @@ var/list/mentor_verbs_default = list(
to_chat(src, "<span class='pm warning'>Error: Mentor-PM: Client not found.</span>")
return
var/datum/mentor_help/MH = C.current_mentorhelp
var/datum/ticket/T = C.current_ticket // CHOMPedit - Ticket System
if(MH)
if(T) // CHOMPedit - Ticket System
message_mentors("<span class='mentor_channel'>[src] has started replying to [C]'s mentor help.</span>")
var/msg = tgui_input_text(src,"Message:", "Private message to [C]")
if (!msg)
message_mentors("<span class='mentor_channel'>[src] has cancelled their reply to [C]'s mentor help.</span>")
return
cmd_mentor_pm(whom, msg, MH)
cmd_mentor_pm(whom, msg, T) // CHOMPedit - Ticket System
/proc/has_mentor_powers(client/C)
return C.holder || C.mentorholder
@@ -189,7 +191,7 @@ var/list/mentor_verbs_default = list(
adminhelp(msg)
/client/proc/cmd_mentor_pm(whom, msg, datum/mentor_help/MH)
/client/proc/cmd_mentor_pm(whom, msg, datum/ticket/T) // CHOMPedit - Ticket System
set category = "Admin"
set name = "Mentor-PM"
set hidden = 1
@@ -199,7 +201,7 @@ var/list/mentor_verbs_default = list(
return
//Not a mentor and no open ticket
if(!has_mentor_powers(src) && !current_mentorhelp)
if(!has_mentor_powers(src) && !current_ticket) // CHOMPedit - Ticket System
to_chat(src, "<span class='pm warning'>You can no longer reply to this ticket, please open another one by using the Mentorhelp verb if need be.</span>")
to_chat(src, "<span class='pm notice'>Message: [msg]</span>")
return
@@ -229,11 +231,11 @@ var/list/mentor_verbs_default = list(
to_chat(src, msg)
else
log_admin("Mentorhelp: [key_name(src)]: [msg]")
current_mentorhelp.MessageNoRecipient(msg)
current_ticket.MessageNoRecipient(msg) // CHOMPedit - Ticket System
return
//Has mentor powers but the recipient no longer has an open ticket
if(has_mentor_powers(src) && !recipient.current_mentorhelp)
if(has_mentor_powers(src) && !recipient.current_ticket) // CHOMPedit - Ticket System
to_chat(src, "<span class='pm warning'>You can no longer reply to this ticket.</span>")
to_chat(src, "<span class='pm notice'>Message: [msg]</span>")
return
@@ -247,17 +249,19 @@ var/list/mentor_verbs_default = list(
var/interaction_message = "<span class='pm notice'>Mentor-PM from-<b>[src]</b> to-<b>[recipient]</b>: [msg]</span>"
if (recipient.current_mentorhelp && !has_mentor_powers(recipient))
recipient.current_mentorhelp.AddInteraction(interaction_message)
if (src.current_mentorhelp && !has_mentor_powers(src))
src.current_mentorhelp.AddInteraction(interaction_message)
// CHOMPedit Start - Ticket System
if (recipient.current_ticket && !has_mentor_powers(recipient))
recipient.current_ticket.AddInteraction(interaction_message)
if (src.current_ticket && !has_mentor_powers(src))
src.current_ticket.AddInteraction(interaction_message)
// It's a little fucky if they're both mentors, but while admins may need to adminhelp I don't really see any reason a mentor would have to mentorhelp since you can literally just ask any other mentors online
if (has_mentor_powers(recipient) && has_mentor_powers(src))
if (recipient.current_mentorhelp)
recipient.current_mentorhelp.AddInteraction(interaction_message)
if (src.current_mentorhelp)
src.current_mentorhelp.AddInteraction(interaction_message)
if (recipient.current_ticket)
recipient.current_ticket.AddInteraction(interaction_message)
if (src.current_ticket)
src.current_ticket.AddInteraction(interaction_message)
// CHOMPedit End
to_chat(recipient, "<i><span class='mentor'>Mentor-PM from-<b><a href='?mentorhelp_msg=\ref[src]'>[src]</a></b>: [msg]</span></i>")
to_chat(src, "<i><span class='mentor'>Mentor-PM to-<b>[recipient]</b>: [msg]</span></i>")
-476
View File
@@ -1,476 +0,0 @@
/client/var/datum/mentor_help/current_mentorhelp
//
//TICKET MANAGER
//
GLOBAL_DATUM_INIT(mhelp_tickets, /datum/mentor_help_tickets, new)
/datum/mentor_help_tickets
var/list/active_tickets = list()
var/list/resolved_tickets = list()
var/obj/effect/statclick/mticket_list/astatclick = new(null, null, AHELP_ACTIVE)
var/obj/effect/statclick/mticket_list/rstatclick = new(null, null, AHELP_RESOLVED)
/datum/mentor_help_tickets/Destroy()
QDEL_LIST(active_tickets)
QDEL_LIST(resolved_tickets)
QDEL_NULL(astatclick)
QDEL_NULL(rstatclick)
return ..()
//private
/datum/mentor_help_tickets/proc/ListInsert(datum/mentor_help/new_ticket)
var/list/mticket_list
switch(new_ticket.state)
if(AHELP_ACTIVE)
mticket_list = active_tickets
if(AHELP_RESOLVED)
mticket_list = resolved_tickets
else
CRASH("Invalid ticket state: [new_ticket.state]")
var/num_closed = mticket_list.len
if(num_closed)
for(var/I in 1 to num_closed)
var/datum/mentor_help/MH = mticket_list[I]
if(MH.id > new_ticket.id)
mticket_list.Insert(I, new_ticket)
return
mticket_list += new_ticket
//opens the ticket listings, only two states here
/datum/mentor_help_tickets/proc/BrowseTickets(state)
var/list/l2b
var/title
switch(state)
if(AHELP_ACTIVE)
l2b = active_tickets
title = "Active Tickets"
if(AHELP_RESOLVED)
l2b = resolved_tickets
title = "Resolved Tickets"
if(!l2b)
return
var/list/dat = list("<html><head><title>[title]</title></head>")
dat += "<A HREF='?_src_=mentorholder;[HrefToken()];mhelp_tickets=[state]'>Refresh</A><br><br>"
for(var/datum/mentor_help/MH as anything in l2b)
dat += "<span class='adminnotice'><span class='adminhelp'>Ticket #[MH.id]</span>: <A HREF='?_src_=mentorholder;mhelp=\ref[MH];[HrefToken()];mhelp_action=ticket'>[MH.initiator_ckey]: [MH.name]</A></span><br>"
usr << browse(dat.Join(), "window=mhelp_list[state];size=600x480")
//Tickets statpanel
/datum/mentor_help_tickets/proc/stat_entry()
var/num_disconnected = 0
stat("== Mentor Tickets ==")
stat("Active Tickets:", astatclick.update("[active_tickets.len]"))
for(var/datum/mentor_help/MH as anything in active_tickets)
if(MH.initiator)
stat("#[MH.id]. [MH.initiator_ckey]:", MH.statclick.update())
else
++num_disconnected
if(num_disconnected)
stat("Disconnected:", astatclick.update("[num_disconnected]"))
stat("Resolved Tickets:", rstatclick.update("[resolved_tickets.len]"))
//Reassociate still open ticket if one exists
/datum/mentor_help_tickets/proc/ClientLogin(client/C)
C.current_mentorhelp = CKey2ActiveTicket(C.ckey)
if(C.current_mentorhelp)
C.current_mentorhelp.AddInteraction("Client reconnected.")
C.current_mentorhelp.initiator = C
//Dissasociate ticket
/datum/mentor_help_tickets/proc/ClientLogout(client/C)
if(C.current_mentorhelp)
C.current_mentorhelp.AddInteraction("Client disconnected.")
C.current_mentorhelp.initiator = null
C.current_mentorhelp = null
//Get a ticket given a ckey
/datum/mentor_help_tickets/proc/CKey2ActiveTicket(ckey)
for(var/datum/admin_help/MH as anything in active_tickets)
if(MH.initiator_ckey == ckey)
return MH
//
//TICKET LIST STATCLICK
//
/obj/effect/statclick/mticket_list
var/current_state
/obj/effect/statclick/mticket_list/New(loc, name, state)
current_state = state
..()
/obj/effect/statclick/mticket_list/Click()
GLOB.mhelp_tickets.BrowseTickets(current_state)
//
//TICKET DATUM
//
/datum/mentor_help
var/id
var/name
var/state = AHELP_ACTIVE
var/opened_at
var/closed_at
var/client/initiator //semi-misnomer, it's the person who ahelped/was bwoinked
var/initiator_ckey
var/initiator_key_name
var/list/_interactions //use AddInteraction() or, preferably, admin_ticket_log()
var/obj/effect/statclick/ahelp/statclick
var/static/ticket_counter = 0
//call this on its own to create a ticket, don't manually assign current_mentorhelp
//msg is the title of the ticket: usually the ahelp text
/datum/mentor_help/New(msg, client/C)
//clean the input msg
msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN))
if(!msg || !C || !C.mob)
qdel(src)
return
id = ++ticket_counter
opened_at = world.time
name = msg
initiator = C
initiator_ckey = C.ckey
initiator_key_name = key_name(initiator, FALSE, TRUE)
if(initiator.current_mentorhelp) //This is a bug
log_debug("Ticket erroneously left open by code")
initiator.current_mentorhelp.AddInteraction("Ticket erroneously left open by code")
initiator.current_mentorhelp.Resolve()
initiator.current_mentorhelp = src
statclick = new(null, src)
_interactions = list()
log_admin("Mentorhelp: [key_name(C)]: [msg]")
MessageNoRecipient(msg)
//show it to the person adminhelping too
to_chat(C, "<i><span class='mentor'>Mentor-PM to-<b>Mentors</b>: [name]</span></i>")
GLOB.mhelp_tickets.active_tickets += src
/datum/mentor_help/Destroy()
RemoveActive()
GLOB.mhelp_tickets.resolved_tickets -= src
return ..()
/datum/mentor_help/proc/AddInteraction(formatted_message)
_interactions += "[gameTimestamp()]: [formatted_message]"
//private
/datum/mentor_help/proc/ClosureLinks(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
. = " (<A HREF='?_src_=mentorholder;mhelp=[ref_src];[HrefToken()];mhelp_action=resolve'>RSLVE</A>)"
//private
/datum/mentor_help/proc/LinkedReplyName(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
return "<A HREF='?_src_=mentorholder;mhelp=[ref_src];[HrefToken()];mhelp_action=reply'>[initiator_ckey]</A>"
//private
/datum/mentor_help/proc/TicketHref(msg, ref_src, action = "ticket")
if(!ref_src)
ref_src = "\ref[src]"
return "<A HREF='?_src_=mentorholder;mhelp=[ref_src];[HrefToken()];mhelp_action=[action]'>[msg]</A>"
//message from the initiator without a target, all people with mentor powers will see this
/datum/mentor_help/proc/MessageNoRecipient(msg)
var/ref_src = "\ref[src]"
var/chat_msg = "<span class='notice'>(<A HREF='?_src_=mentorholder;mhelp=[ref_src];[HrefToken()];mhelp_action=escalate'>ESCALATE</A>) Ticket [TicketHref("#[id]", ref_src)]<b>: [LinkedReplyName(ref_src)]:</b> [msg]</span>"
AddInteraction("<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>")
for (var/client/C in GLOB.mentors)
if (C.is_preference_enabled(/datum/client_preference/play_mentorhelp_ping))
C << 'sound/effects/mentorhelp.mp3'
for (var/client/C in GLOB.admins)
if (C.is_preference_enabled(/datum/client_preference/play_mentorhelp_ping))
C << 'sound/effects/mentorhelp.mp3'
message_mentors(chat_msg)
//Reopen a closed ticket
/datum/mentor_help/proc/Reopen()
if(state == AHELP_ACTIVE)
to_chat(usr, "<span class='warning'>This ticket is already open.</span>")
return
if(GLOB.mhelp_tickets.CKey2ActiveTicket(initiator_ckey))
to_chat(usr, "<span class='warning'>This user already has an active ticket, cannot reopen this one.</span>")
return
statclick = new(null, src)
GLOB.mhelp_tickets.active_tickets += src
GLOB.mhelp_tickets.resolved_tickets -= src
switch(state)
if(AHELP_RESOLVED)
feedback_dec("mhelp_resolve")
state = AHELP_ACTIVE
closed_at = null
if(initiator)
initiator.current_mentorhelp = src
AddInteraction("<font color='purple'>Reopened by [usr.ckey]</font>")
if(initiator)
to_chat(initiator, "<span class='filter_adminlog'><font color='purple'>Ticket [TicketHref("#[id]")] was reopened by [usr.ckey].</font></span>")
var/msg = "<span class='adminhelp'>Ticket [TicketHref("#[id]")] reopened by [usr.ckey].</span>"
message_mentors(msg)
log_admin(msg)
feedback_inc("mhelp_reopen")
TicketPanel() //can only be done from here, so refresh it
//private
/datum/mentor_help/proc/RemoveActive()
if(state != AHELP_ACTIVE)
return
closed_at = world.time
QDEL_NULL(statclick)
GLOB.mhelp_tickets.active_tickets -= src
if(initiator && initiator.current_mentorhelp == src)
initiator.current_mentorhelp = null
//Mark open ticket as resolved/legitimate, returns mentorhelp verb
/datum/mentor_help/proc/Resolve(silent = FALSE)
if(state != AHELP_ACTIVE)
return
RemoveActive()
state = AHELP_RESOLVED
GLOB.mhelp_tickets.ListInsert(src)
AddInteraction("<span class='filter_adminlog'><font color='green'>Resolved by [usr.ckey].</font></span>")
if(initiator)
to_chat(initiator, "<span class='filter_adminlog'><font color='green'>Ticket [TicketHref("#[id]")] was marked resolved by [usr.ckey].</font></span>")
if(!silent)
feedback_inc("mhelp_resolve")
var/msg = "Ticket [TicketHref("#[id]")] resolved by [usr.ckey]"
message_mentors(msg)
log_admin(msg)
//Show the ticket panel
/datum/mentor_help/proc/TicketPanel()
tgui_interact(usr.client.mob)
/datum/mentor_help/proc/TicketPanelLegacy()
var/list/dat = list("<html><head><title>Ticket #[id]</title></head>")
var/ref_src = "\ref[src]"
dat += "<h4>Mentor Help Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
dat += "<b>State: "
switch(state)
if(AHELP_ACTIVE)
dat += "<font color='red'>OPEN</font>"
if(AHELP_RESOLVED)
dat += "<font color='green'>RESOLVED</font>"
else
dat += "UNKNOWN"
dat += "</b>[GLOB.TAB][TicketHref("Refresh", ref_src)]"
if(state != AHELP_ACTIVE)
dat += "[GLOB.TAB][TicketHref("Reopen", ref_src, "reopen")]"
dat += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)"
if(closed_at)
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)"
dat += "<br><br>"
if(initiator)
dat += "<b>Actions:</b> [Context(ref_src)]<br>"
else
dat += "<b>DISCONNECTED</b>[GLOB.TAB][ClosureLinks(ref_src)]<br>"
dat += "<br><b>Log:</b><br><br>"
for(var/I in _interactions)
dat += "[I]<br>"
usr << browse(dat.Join(), "window=mhelp[id];size=620x480")
/datum/mentor_help/tgui_fallback(payload)
if(..())
return
TicketPanelLegacy()
/datum/mentor_help/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "MentorTicketPanel", "Ticket #[id] - [LinkedReplyName("\ref[src]")]")
ui.open()
/datum/mentor_help/tgui_state(mob/user)
return GLOB.tgui_mentor_state
/datum/mentor_help/tgui_data(mob/user)
var/list/data = list()
data["id"] = id
var/ref_src = "\ref[src]"
data["title"] = name
data["name"] = LinkedReplyName(ref_src)
switch(state)
if(AHELP_ACTIVE)
data["state"] = "open"
if(AHELP_RESOLVED)
data["state"] = "resolved"
else
data["state"] = "unknown"
data["opened_at"] = (world.time - opened_at)
data["closed_at"] = (world.time - closed_at)
data["opened_at_date"] = gameTimestamp(wtime = opened_at)
data["closed_at_date"] = gameTimestamp(wtime = closed_at)
data["actions"] = Context(ref_src)
data["log"] = _interactions
return data
/datum/mentor_help/tgui_act(action, params)
if(..())
return
switch(action)
if("escalate")
Escalate()
. = TRUE
if("reopen")
Reopen()
. = TRUE
if("legacy")
TicketPanelLegacy()
. = TRUE
//Kick ticket to admins
/datum/mentor_help/proc/Escalate()
if(tgui_alert(usr, "Really escalate this ticket to admins? No mentors will ever be able to interact with it again if you do.","Escalate",list("Yes","No")) != "Yes")
return
if (src.initiator == null) // You can't escalate a mentorhelp of someone who's logged out because it won't create the adminhelp properly
to_chat(usr, "<span class='pm warning'>Error: client not found, unable to escalate.</span>")
return
var/datum/admin_help/AH = new /datum/admin_help(src.name, src.initiator, FALSE)
message_mentors("[usr.ckey] escalated Ticket [TicketHref("#[id]")]")
log_admin("[key_name(usr)] escalated mentorhelp [src.name]")
to_chat(src.initiator, "<span class='mentor'>[usr.ckey] escalated your mentorhelp to admins.</span>")
AH._interactions = src._interactions
GLOB.mhelp_tickets.active_tickets -= src
GLOB.mhelp_tickets.resolved_tickets -= src
qdel(src)
/datum/mentor_help/proc/Context(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
if(state == AHELP_ACTIVE)
. += ClosureLinks(ref_src)
if(state != AHELP_RESOLVED)
. += " (<A HREF='?_src_=mentorholder;[HrefToken()];mhelp=[ref_src];mhelp_action=escalate'>ESCALATE</A>)"
//Forwarded action from admin/Topic OR mentor/Topic depending on which rank the caller has
/datum/mentor_help/proc/Action(action)
switch(action)
if("ticket")
TicketPanel()
if("reply")
usr.client.cmd_mhelp_reply(initiator)
if("resolve")
Resolve()
if("reopen")
Reopen()
if("escalate")
Escalate()
//
// TICKET STATCLICK
//
/obj/effect/statclick/mhelp
var/datum/mentor_help/mhelp_datum
/obj/effect/statclick/mhelp/New(loc, datum/mentor_help/MH)
mhelp_datum = MH
..(loc)
/obj/effect/statclick/mhelp/update()
return ..(mhelp_datum.name)
/obj/effect/statclick/mhelp/Click()
mhelp_datum.TicketPanel()
/obj/effect/statclick/mhelp/Destroy()
mhelp_datum = null
return ..()
//
// CLIENT PROCS
//
/client/verb/mentorhelp(msg as text)
set category = "Admin"
set name = "Mentorhelp"
if(say_disabled) //This is here to try to identify lag problems
to_chat(usr, "<span class='danger'>Speech is currently admin-disabled.</span>")
return
//handle muting and automuting
if(prefs.muted & MUTE_ADMINHELP)
to_chat(src, "<span class='danger'>Error: Mentor-PM: You cannot send adminhelps (Muted).</span>")
return
if(handle_spam_prevention(msg,MUTE_ADMINHELP))
return
if(!msg)
return
//remove out adminhelp verb temporarily to prevent spamming of admins.
src.verbs -= /client/verb/mentorhelp
spawn(600)
src.verbs += /client/verb/mentorhelp // 1 minute cool-down for mentorhelps
feedback_add_details("admin_verb","Mentorhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
if(current_mentorhelp)
if(tgui_alert(usr, "You already have a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) != "No")
if(current_mentorhelp)
log_admin("Mentorhelp: [key_name(src)]: [msg]")
current_mentorhelp.MessageNoRecipient(msg)
to_chat(usr, "<span class='adminnotice'><span class='mentor'>Mentor-PM to-<b>Mentors</b>: [msg]</span></span>")
return
else
to_chat(usr, "<span class='warning'>Ticket not found, creating new one...</span>")
else
current_mentorhelp.AddInteraction("[usr.ckey] opened a new ticket.")
current_mentorhelp.Resolve()
new /datum/mentor_help(msg, src, FALSE)
//admin proc
/client/proc/cmd_mentor_ticket_panel()
set name = "Mentor Ticket List"
set category = "Admin"
var/browse_to
switch(tgui_input_list(usr, "Display which ticket list?", "List Choice", list("Active Tickets", "Resolved Tickets")))
if("Active Tickets")
browse_to = AHELP_ACTIVE
if("Resolved Tickets")
browse_to = AHELP_RESOLVED
else
return
GLOB.mhelp_tickets.BrowseTickets(browse_to)
/proc/message_mentors(var/msg)
msg = "<span class='mentor_channel'><span class='prefix'>Mentor:</span> <span class=\"message\">[msg]</span></span>"
for(var/client/C in GLOB.mentors)
to_chat(C, msg)
for(var/client/C in GLOB.admins)
to_chat(C, msg)
+5 -4
View File
@@ -685,8 +685,9 @@
for(var/datum/controller/subsystem/SS in Master.subsystems)
SS.stat_entry()
if(statpanel("Tickets"))
GLOB.ahelp_tickets.stat_entry()
// CHOMPedit - Ticket System
//if(statpanel("Tickets"))
//GLOB.ahelp_tickets.stat_entry()
if(length(GLOB.sdql2_queries))
@@ -696,9 +697,9 @@
Q.generate_stat()
if(has_mentor_powers(client))
if(has_mentor_powers(client) || client.holder) // CHOMPedit - Ticket System
if(statpanel("Tickets"))
GLOB.mhelp_tickets.stat_entry()
GLOB.tickets.stat_entry() // CHOMPedit - Ticket System
if(listed_turf && client)
if(!TurfAdjacent(listed_turf))
+18
View File
@@ -0,0 +1,18 @@
/obj/screen/alert/open_ticket
icon = 'modular_chomp/icons/logo.dmi'
name = "Admin Chat Request"
desc = "A Administrator would like to chat with you. \
Click here to begin."
icon_state = "32x32"
var/clicked_once = FALSE
/obj/screen/alert/open_ticket/Click()
if(!usr || !usr.client) return
// Open a new chat with the user
var/datum/ticket_chat/TC = new()
TC.T = usr.client.current_ticket
TC.tgui_interact(usr.client.mob)
desc = "A Administrator would like to chat with you. \
Click here to continue the conversation."
@@ -0,0 +1,13 @@
/**
* tgui state: ticket_state
*
* Grants the user UI_INTERACTIVE, if a ticket is open.
**/
GLOBAL_DATUM_INIT(tgui_ticket_state, /datum/tgui_state/ticket_state, new)
/datum/tgui_state/ticket_state/can_use_topic(src_object, mob/user)
//if (user.client.current_ticket)
// return STATUS_INTERACTIVE
//return STATUS_CLOSE
return STATUS_INTERACTIVE
+173
View File
@@ -0,0 +1,173 @@
//
// CLIENT PROCS
//
/client/verb/mentorhelp(msg as text)
set category = "Admin"
set name = "Mentorhelp"
if(say_disabled) //This is here to try to identify lag problems
to_chat(usr, "<span class='danger'>Speech is currently admin-disabled.</span>")
return
//handle muting and automuting
if(prefs.muted & MUTE_ADMINHELP)
to_chat(src, "<span class='danger'>Error: Mentor-PM: You cannot send adminhelps (Muted).</span>")
return
if(handle_spam_prevention(msg,MUTE_ADMINHELP))
return
if(!msg)
return
//remove out adminhelp verb temporarily to prevent spamming of admins.
src.verbs -= /client/verb/mentorhelp
spawn(600)
src.verbs += /client/verb/mentorhelp // 1 minute cool-down for mentorhelps
feedback_add_details("admin_verb","Mentorhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
if(current_ticket)
if(tgui_alert(usr, "You already have a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) != "No")
if(current_ticket)
log_admin("Mentorhelp: [key_name(src)]: [msg]")
current_ticket.MessageNoRecipient(msg)
to_chat(usr, "<span class='adminnotice'><span class='mentor'>Mentor-PM to-<b>Mentors</b>: [msg]</span></span>")
return
else
to_chat(usr, "<span class='warning'>Ticket not found, creating new one...</span>")
else
current_ticket.AddInteraction("[usr.ckey] opened a new ticket.")
current_ticket.Resolve()
new /datum/ticket(msg, src, FALSE, 1)
//admin proc
/client/proc/cmd_mentor_ticket_panel()
set name = "Mentor Ticket List"
set category = "Admin"
var/browse_to
switch(tgui_input_list(usr, "Display which ticket list?", "List Choice", list("Active Tickets", "Resolved Tickets")))
if("Active Tickets")
browse_to = AHELP_ACTIVE
if("Resolved Tickets")
browse_to = AHELP_RESOLVED
else
return
GLOB.tickets.BrowseTickets(browse_to)
/proc/message_mentors(var/msg)
msg = "<span class='mentor_channel'><span class='prefix'>Mentor:</span> <span class=\"message\">[msg]</span></span>"
for(var/client/C in GLOB.mentors)
to_chat(C, msg)
for(var/client/C in GLOB.admins)
to_chat(C, msg)
//
// CLIENT PROCS
//
/client/verb/adminhelp(msg as text)
set category = "Admin"
set name = "Adminhelp"
if(say_disabled) //This is here to try to identify lag problems
to_chat(usr, "<span class='danger'>Speech is currently admin-disabled.</span>")
return
//handle muting and automuting
if(prefs.muted & MUTE_ADMINHELP)
to_chat(src, "<span class='danger'>Error: Admin-PM: You cannot send adminhelps (Muted).</span>")
return
if(handle_spam_prevention(msg,MUTE_ADMINHELP))
return
if(!msg)
return
//remove out adminhelp verb temporarily to prevent spamming of admins.
src.verbs -= /client/verb/adminhelp
spawn(1200)
src.verbs += /client/verb/adminhelp // 2 minute cool-down for adminhelps
feedback_add_details("admin_verb","Adminhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
if(current_ticket)
if(tgui_alert(usr, "You already have a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) != "No")
if(current_ticket)
current_ticket.MessageNoRecipient(msg)
to_chat(usr, "<span class='adminnotice'>PM to-<b>Admins</b>: [msg]</span>")
return
else
to_chat(usr, "<span class='warning'>Ticket not found, creating new one...</span>")
else
current_ticket.AddInteraction("[key_name_admin(usr)] opened a new ticket.")
current_ticket.Close()
new /datum/ticket(msg, src, FALSE, 0)
//admin proc
/client/proc/cmd_admin_ticket_panel()
set name = "Show Ticket List"
set category = "Admin"
if(!check_rights(R_ADMIN|R_MOD|R_DEBUG|R_EVENT, TRUE))
return
var/browse_to
switch(tgui_input_list(usr, "Display which ticket list?", "List Choice", list("Active Tickets", "Closed Tickets", "Resolved Tickets")))
if("Active Tickets")
browse_to = AHELP_ACTIVE
if("Closed Tickets")
browse_to = AHELP_CLOSED
if("Resolved Tickets")
browse_to = AHELP_RESOLVED
else
return
GLOB.tickets.BrowseTickets(browse_to)
//// VOREstation Additions Below
/datum/ticket/proc/send2adminchat()
if(!config.chat_webhook_url)
return
var/list/adm = get_admin_counts()
var/list/afkmins = adm["afk"]
var/list/allmins = adm["total"]
spawn(0) //Unreliable world.Exports()
var/query_string = "type=adminhelp"
query_string += "&key=[url_encode(config.chat_webhook_key)]"
query_string += "&from=[url_encode(key_name(initiator))]"
query_string += "&msg=[url_encode(html_decode(name))]"
query_string += "&admin_number=[allmins.len]"
query_string += "&admin_number_afk=[afkmins.len]"
world.Export("[config.chat_webhook_url]?[query_string]")
/client/verb/adminspice()
set category = "Admin"
set name = "Request Spice"
set desc = "Request admins to spice round up for you"
//handle muting and automuting
if(prefs.muted & MUTE_ADMINHELP)
to_chat(usr, "<span class='danger'>Error: You cannot request spice (muted from adminhelps).</span>")
return
if(tgui_alert(usr, "Are you sure you want to request the admins spice things up for you? You accept the consequences if you do.","Spicy!",list("Yes","No")) != "No")
message_admins("[ADMIN_FULLMONTY(usr)] has requested the round be spiced up a little.")
to_chat(usr, "<span class='notice'>You have requested some more spice in your round.</span>")
else
to_chat(usr, "<span class='notice'>Spice request cancelled.</span>")
return
//if they requested spice, then remove spice verb temporarily to prevent spamming
usr.verbs -= /client/verb/adminspice
spawn(10 MINUTES)
if(usr) // In case we left in the 10 minute cooldown
usr.verbs += /client/verb/adminspice // 10 minute cool-down for spice request
@@ -0,0 +1,807 @@
/*
CHOMPedit - This file has been excluded from the compilation.
Reason: Replaced with "Tickets System". Main logic has been moved to: modular_chomp/modules/tickets/tickets.dm
*/
/client/var/datum/ticket/current_ticket //the current ticket the (usually) not-admin client is dealing with
/client/var/datum/ticket/selected_ticket //the current ticket being viewed in the Tickets Panel (usually) admin/mentor client
// CHOMPEdit Begin
/proc/get_ahelp_channel()
var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs)
if(istype(api) && config.ahelp_channel_tag)
for(var/datum/tgs_chat_channel/channel in api.chat_channels)
if(channel.custom_tag == config.ahelp_channel_tag)
return list(channel)
return 0
/proc/ahelp_discord_message(var/message)
if(!message)
return
if(config.discord_ahelps_disabled)
return
var/datum/tgs_chat_channel/ahelp_channel = get_ahelp_channel()
if(ahelp_channel)
world.TgsChatBroadcast(message,ahelp_channel)
else
world.TgsTargetedChatBroadcast(message,TRUE)
// CHOMPEdit End
//
//TICKET MANAGER
//
GLOBAL_DATUM_INIT(tickets, /datum/tickets, new)
/datum/tickets
var/list/active_tickets = list()
var/list/closed_tickets = list()
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/tickets/Destroy()
QDEL_LIST(active_tickets)
QDEL_LIST(closed_tickets)
QDEL_LIST(resolved_tickets)
QDEL_NULL(astatclick)
QDEL_NULL(cstatclick)
QDEL_NULL(rstatclick)
return ..()
//private
/datum/tickets/proc/ListInsert(datum/ticket/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/ticket/T = ticket_list[I]
if(T.id > new_ticket.id)
ticket_list.Insert(I, new_ticket)
return
ticket_list += new_ticket
/datum/tickets/proc/BrowseTickets(state)
tgui_interact(usr)
//opens the ticket listings for one of the 3 states
/datum/tickets/proc/BrowseTicketsLegacy(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><title>[title]</title></head>")
dat += "<A HREF='?_src_=holder;[HrefToken()];ahelp_tickets=[state]'>Refresh</A><br><br>"
for(var/datum/ticket/T as anything in l2b)
dat += "<span class='adminnotice'><span class='adminhelp'>Ticket #[T.id]</span>: <A HREF='?_src_=holder;ahelp=\ref[T];[HrefToken()];ahelp_action=ticket'>[T.initiator_key_name]: [T.name]</A></span><br>"
usr << browse(dat.Join(), "window=ahelp_list[state];size=600x480")
//Tickets statpanel
/datum/tickets/proc/stat_entry()
var/num_disconnected = 0
stat("== Tickets ==")
stat("Active Tickets:", astatclick.update("[active_tickets.len]"))
for(var/datum/ticket/T as anything in active_tickets)
if(T.initiator)
var/type = "N/A"
switch(T.level)
if(0)
type = "ADM"
if(1)
type = "MEN"
if(usr.client.holder || T.level > 0)
stat("\[[type]\] #[T.id]. [T.initiator_key_name]:", T.statclick.update())
else
++num_disconnected
if(num_disconnected)
stat("Disconnected:", astatclick.update("[num_disconnected]"))
stat("Closed Tickets:", cstatclick.update("[closed_tickets.len]"))
stat("Resolved Tickets:", rstatclick.update("[resolved_tickets.len]"))
//Reassociate still open ticket if one exists
/datum/tickets/proc/ClientLogin(client/C)
C.current_ticket = CKey2ActiveTicket(C.ckey)
if(C.current_ticket)
C.current_ticket.AddInteraction("Client reconnected.")
C.current_ticket.initiator = C
//Dissasociate ticket
/datum/tickets/proc/ClientLogout(client/C)
if(C.current_ticket)
C.current_ticket.AddInteraction("Client disconnected.")
C.current_ticket.initiator = null
C.current_ticket = null
//Get a ticket given a ckey
/datum/tickets/proc/CKey2ActiveTicket(ckey)
if(!usr?.client.holder || !has_mentor_powers(usr?.client))
message_admins("[usr] has attempted to look up a ticket with CKEY [ckey] without sufficent privileges.")
return
for(var/datum/ticket/T as anything in active_tickets)
if(T.initiator_ckey == ckey)
return T
//Get a ticket by ticket id
/datum/tickets/proc/ID2Ticket(id)
if(!usr?.client.holder || !has_mentor_powers(usr?.client))
message_admins("[usr] has attempted to look up a ticket with ID [id] without sufficent privileges.")
return
for(var/datum/ticket/T as anything in active_tickets)
if(T.id == id)
return T
for(var/datum/ticket/T as anything in resolved_tickets)
if(T.id == id)
return T
for(var/datum/ticket/T as anything in closed_tickets)
if(T.id == id)
return T
//
//TICKET LIST STATCLICK
//
/obj/effect/statclick/ticket_list
var/current_state
/obj/effect/statclick/ticket_list/New(loc, name, state)
current_state = state
..()
/obj/effect/statclick/ticket_list/Click()
GLOB.tickets.BrowseTickets(current_state)
//
//TICKET DATUM
//
/datum/ticket
var/id
var/name
var/level = 0 // 0 = Admin, 1 = Mentor
var/list/tags
var/state = AHELP_ACTIVE
var/opened_at
var/closed_at
var/client/initiator //semi-misnomer, it's the person who ahelped/was bwoinked
var/handler = "/Unassigned\\" // The admin handling the ticket
var/initiator_ckey
var/initiator_key_name
var/list/_interactions //use AddInteraction() or, preferably, admin_ticket_log()
var/obj/effect/statclick/ticket/statclick
var/static/ticket_counter = 0
/*
//call this on its own to create a ticket, don't manually assign current_mentorhelp
//msg is the title of the ticket: usually the ahelp text
/datum/mentor_help/New(msg, client/C)
initiator_ckey = C.ckey
initiator_key_name = key_name(initiator, FALSE, TRUE)
if(initiator.current_mentorhelp) //This is a bug
log_debug("Ticket erroneously left open by code")
initiator.current_mentorhelp.AddInteraction("Ticket erroneously left open by code")
initiator.current_mentorhelp.Resolve()
initiator.current_mentorhelp = src
statclick = new(null, src)
_interactions = list()
log_admin("Mentorhelp: [key_name(C)]: [msg]")
MessageNoRecipient(msg)
//show it to the person adminhelping too
to_chat(C, "<i><span class='mentor'>Mentor-PM to-<b>Mentors</b>: [name]</span></i>")
GLOB.mhelp_tickets.active_tickets += src */
/**
* public
*
* Create a new Ticket.
* Call this on its own to create a ticket, don't manually assign current_ticket
*
* required msg string The title of the ticket: usually the ahelp text
* required C client The object or datum which owns the UI.
* required is_bwoink boolean TRUE if this ticket was started by an admin PM
* required level integer The level of the ticket. 0 = Admin, 1 = Mentor
*/
/datum/ticket/New(msg, client/C, is_bwoink, ticket_level)
//clean the input msg
msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN))
if(!msg || !C || !C.mob)
qdel(src)
return
id = ++ticket_counter
opened_at = world.time
name = msg
level = ticket_level
initiator = C
initiator_ckey = initiator.ckey
initiator_key_name = key_name(initiator, FALSE, TRUE)
if(initiator.current_ticket) //This is a bug
log_debug("Multiple ahelp current_tickets")
initiator.current_ticket.AddInteraction("Ticket erroneously left open by code")
initiator.current_ticket.Close()
initiator.current_ticket = src
var/parsed_message = keywords_lookup(msg)
statclick = new(null, src)
_interactions = list()
if(is_bwoink)
AddInteraction("<font color='blue'>[key_name_admin(usr)] PM'd [LinkedReplyName()]</font>")
message_admins("<font color='blue'>Ticket [TicketHref("#[id]")] created</font>")
else
MessageNoRecipient(parsed_message)
send2adminchat() //VOREStation Add
//show it to the person adminhelping too
to_chat(C, "<span class='adminnotice'>PM to-<b>Admins</b>: [name]</span>")
//send it to irc if nobody is on and tell us how many were on
var/admin_number_present = send2irc_adminless_only(initiator_ckey, name)
log_admin("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.")
if(admin_number_present <= 0)
to_chat(C, "<span class='notice'>No active admins are online, your adminhelp was sent to the admin discord.</span>") //VOREStation Edit
send2adminchat() //VOREStation Add
//YW EDIT START
var/list/adm = get_admin_counts()
var/list/activemins = adm["present"]
var activeMins = activemins.len
if(is_bwoink)
ahelp_discord_message("ADMINHELP: FROM: [key_name_admin(usr)] TO [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") //CHOMPEdit
else
ahelp_discord_message("ADMINHELP: FROM: [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") //CHOMPEdit
//YW EDIT END
// Also send it to discord since that's the hip cool thing now.
SSwebhooks.send(
WEBHOOK_AHELP_SENT,
list(
"name" = "Ticket ([id]) (Game ID: [game_id]) ticket opened.",
"body" = "[key_name(initiator)] has opened a ticket. \n[msg]",
"color" = COLOR_WEBHOOK_POOR
)
)
GLOB.tickets.active_tickets += src
// Open a new chat with the user
//var/datum/ticket_chat/TC = new()
//TC.T = src
//TC.tgui_interact(C.mob)
C.mob.throw_alert("open ticket", /obj/screen/alert/open_ticket)
/datum/ticket/Destroy()
RemoveActive()
GLOB.tickets.closed_tickets -= src
GLOB.tickets.resolved_tickets -= src
return ..()
/datum/ticket/proc/AddInteraction(formatted_message)
var/curinteraction = "[gameTimestamp()]: [formatted_message]"
if(config.discord_ahelps_all) //CHOMPEdit
ahelp_discord_message("ADMINHELP: TICKETID:[id] [strip_html_properly(curinteraction)]") //CHOMPEdit
_interactions += curinteraction
/datum/ticket/proc/TicketPanel()
tgui_interact(usr.client.mob)
//private
/datum/ticket/proc/FullMonty(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
if(initiator && initiator.mob)
. = ADMIN_FULLMONTY_NONAME(initiator.mob)
else
. = "Initiator disconnected."
if(state == AHELP_ACTIVE)
. += ClosureLinks(ref_src)
//private
/datum/ticket/proc/ClosureLinks(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
if(level == 0)
. = " (<A HREF='?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=reject'>REJT</A>)"
. += " (<A HREF='?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=icissue'>IC</A>)"
. += " (<A HREF='?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=close'>CLOSE</A>)"
. += " (<A HREF='?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=resolve'>RSLVE</A>)"
. += " (<A HREF='?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=handleissue'>HANDLE</A>)"
else
. = " (<A HREF='?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=resolve'>RSLVE</A>)"
//private
/datum/ticket/proc/LinkedReplyName(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
return "<A HREF='?_src_=holder;ticket=[ref_src];ticket_action=reply'>[initiator_key_name]</A>"
//private
/datum/ticket/proc/TicketHref(msg, ref_src, action = "ticket")
if(!ref_src)
ref_src = "\ref[src]"
return "<A HREF='?_src_=holder;ticket=[ref_src];[HrefToken(TRUE)];ticket_action=[action]'>[msg]</A>"
/*
var/chat_msg = "<span class='notice'>(<A HREF='?_src_=mentorholder;mhelp=[ref_src];[HrefToken(TRUE)];mhelp_action=escalate'>ESCALATE</A>) Ticket [TicketHref("#[id]", ref_src)]<b>: [LinkedReplyName(ref_src)]:</b> [msg]</span>"
*/
//message from the initiator without a target, all admins will see this
//won't bug irc
/datum/ticket/proc/MessageNoRecipient(msg)
var/ref_src = "\ref[src]"
var/chat_msg = "<span class='adminnotice'><span class='adminhelp'>Ticket [TicketHref("#[id]", ref_src)]</span><b>: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]:</b> [msg]</span>"
AddInteraction("<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>")
//send this msg to all admins
if(level == 1)
for (var/client/C in GLOB.mentors)
if (C.is_preference_enabled(/datum/client_preference/play_mentorhelp_ping))
C << 'sound/effects/mentorhelp.mp3'
for (var/client/C in GLOB.admins)
if (C.is_preference_enabled(/datum/client_preference/play_mentorhelp_ping))
C << 'sound/effects/mentorhelp.mp3'
message_mentors(chat_msg)
else if(level == 0)
for(var/client/X in GLOB.admins)
if(X.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping))
X << 'sound/effects/adminhelp.ogg'
window_flash(X)
to_chat(X, chat_msg)
/*
//Reopen a closed ticket
/datum/mentor_help/proc/Reopen()
switch(state)
if(AHELP_RESOLVED)
feedback_dec("mhelp_resolve")
AddInteraction("<font color='purple'>Reopened by [usr.ckey]</font>")
if(initiator)
to_chat(initiator, "<span class='filter_adminlog'><font color='purple'>Ticket [TicketHref("#[id]")] was reopened by [usr.ckey].</font></span>")
var/msg = "<span class='adminhelp'>Ticket [TicketHref("#[id]")] reopened by [usr.ckey].</span>"
message_mentors(msg)
log_admin(msg)
*/
//Reopen a closed ticket
/datum/ticket/proc/Reopen()
if(state == AHELP_ACTIVE)
to_chat(usr, "<span class='warning'>This ticket is already open.</span>")
return
if(GLOB.tickets.CKey2ActiveTicket(initiator_ckey))
to_chat(usr, "<span class='warning'>This user already has an active ticket, cannot reopen this one.</span>")
return
statclick = new(null, src)
GLOB.tickets.active_tickets += src
GLOB.tickets.closed_tickets -= src
GLOB.tickets.resolved_tickets -= src
switch(state)
if(AHELP_CLOSED)
feedback_dec("ticket_close")
if(AHELP_RESOLVED)
feedback_dec("ticket_resolve")
state = AHELP_ACTIVE
closed_at = null
if(initiator)
initiator.current_ticket = src
AddInteraction("<font color='purple'>Reopened by [key_name_admin(usr)]</font>")
if(initiator)
to_chat(initiator, "<span class='filter_adminlog'><font color='purple'>Ticket [TicketHref("#[id]")] was reopened by [key_name(usr,FALSE,FALSE)].</font></span>")
var/msg = "<span class='adminhelp'>Ticket [TicketHref("#[id]")] reopened by [key_name_admin(usr)].</span>"
message_admins(msg)
log_admin(msg)
feedback_inc("ticket_reopen")
TicketPanel() //can only be done from here, so refresh it
SSwebhooks.send(
WEBHOOK_AHELP_SENT,
list(
"name" = "Ticket ([id]) (Game ID: [game_id]) reopened.",
"body" = "Reopened by [key_name(usr)]."
)
)
//private
/datum/ticket/proc/RemoveActive()
if(state != AHELP_ACTIVE)
return
closed_at = world.time
QDEL_NULL(statclick)
GLOB.tickets.active_tickets -= src
if(initiator && initiator.current_ticket == src)
initiator.current_ticket = null
//Mark open ticket as closed/meme
/datum/ticket/proc/Close(silent = FALSE)
if(state != AHELP_ACTIVE)
return
RemoveActive()
state = AHELP_CLOSED
GLOB.tickets.ListInsert(src)
AddInteraction("<span class='filter_adminlog'><font color='red'>Closed by [key_name_admin(usr)].</font></span>")
if(initiator)
to_chat(initiator, "<span class='filter_adminlog'><font color='red'>Ticket [TicketHref("#[id]")] was closed by [key_name(usr,FALSE,FALSE)].</font></span>")
if(!silent)
feedback_inc("ahelp_close")
var/msg = "Ticket [TicketHref("#[id]")] closed by [key_name_admin(usr)]."
message_admins(msg)
log_admin(msg)
SSwebhooks.send(
WEBHOOK_AHELP_SENT,
list(
"name" = "Ticket ([id]) (Game ID: [game_id]) closed.",
"body" = "Closed by [key_name(usr)].",
"color" = COLOR_WEBHOOK_BAD
)
)
initiator.mob.clear_alert("open ticket")
//Mark open ticket as resolved/legitimate, returns ahelp verb
/datum/ticket/proc/Resolve(silent = FALSE)
if(state != AHELP_ACTIVE)
return
RemoveActive()
state = AHELP_RESOLVED
GLOB.tickets.ListInsert(src)
AddInteraction("<span class='filter_adminlog'><font color='green'>Resolved by [key_name_admin(usr)].</font></span>")
if(initiator)
to_chat(initiator, "<span class='filter_adminlog'><font color='green'>Ticket [TicketHref("#[id]")] was marked resolved by [key_name(usr,FALSE,FALSE)].</font></span>")
if(!silent)
feedback_inc("ticket_resolve")
var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name_admin(usr)]"
if(type == 1)
message_mentors(msg)
else if (type == 0)
message_admins(msg)
log_admin(msg)
if(type == 1)
SSwebhooks.send(
WEBHOOK_AHELP_SENT,
list(
"name" = "Ticket ([id]) (Game ID: [game_id]) resolved.",
"body" = "Marked as Resolved by [key_name(usr)].",
"color" = COLOR_WEBHOOK_GOOD
)
)
//Close and return ahelp verb, use if ticket is incoherent
/datum/ticket/proc/Reject(key_name = key_name_admin(usr))
if(state != AHELP_ACTIVE)
return
if(initiator)
if(initiator.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping))
initiator << 'sound/effects/adminhelp.ogg'
to_chat(initiator, "<span class='filter_pm'><font color='red' size='4'><b>- AdminHelp Rejected! -</b></font><br>\
<font color='red'><b>Your admin help was rejected.</b></font><br>\
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.</span>")
feedback_inc("ahelp_reject")
var/msg = "Ticket [TicketHref("#[id]")] rejected by [key_name_admin(usr)]"
message_admins(msg)
log_admin(msg)
AddInteraction("Rejected by [key_name_admin(usr)].")
Close(silent = TRUE)
SSwebhooks.send(
WEBHOOK_AHELP_SENT,
list(
"name" = "Ticket ([id]) (Game ID: [game_id]) rejected.",
"body" = "Rejected by [key_name(usr)].",
"color" = COLOR_WEBHOOK_BAD
)
)
//Resolve ticket with IC Issue message
/datum/ticket/proc/ICIssue(key_name = key_name_admin(usr))
if(state != AHELP_ACTIVE)
return
var/msg = "<span class='filter_pm'><font color='red' size='4'><b>- AdminHelp marked as IC issue! -</b></font><br>"
msg += "<font color='red'><b>This is something that can be solved ICly, and does not currently require staff intervention.</b></font><br>"
msg += "<font color='red'>Your AdminHelp may also be unanswerable due to ongoing events.</font></span>"
if(initiator)
to_chat(initiator, msg)
feedback_inc("ahelp_icissue")
msg = "Ticket [TicketHref("#[id]")] marked as IC by [key_name_admin(usr)]"
message_admins(msg)
log_admin(msg)
AddInteraction("Marked as IC issue by [key_name_admin(usr)]")
Resolve(silent = TRUE)
SSwebhooks.send(
WEBHOOK_AHELP_SENT,
list(
"name" = "Ticket ([id]) (Game ID: [game_id]) marked as IC issue.",
"body" = "Marked as IC Issue by [key_name(usr)].",
"color" = COLOR_WEBHOOK_BAD
)
)
//Resolve ticket with IC Issue message
/datum/ticket/proc/HandleIssue()
if(state != AHELP_ACTIVE)
return
if(handler == key_name(usr, FALSE, TRUE))
to_chat("<font color='red'>You are already handling this ticket.</font>")
return
var/msg = "<font color='red'>Your AdminHelp is being handled by [key_name(usr,FALSE,FALSE)] please be patient.</font>"
if(initiator)
to_chat(initiator, msg)
feedback_inc("ahelp_handling")
msg = "Ticket [TicketHref("#[id]")] being handled by [key_name(usr,FALSE,FALSE)]"
message_admins(msg)
log_admin(msg)
AddInteraction("[key_name_admin(usr)] is now handling this ticket.")
handler = key_name(usr, FALSE, TRUE)
SSwebhooks.send(
WEBHOOK_AHELP_SENT,
list(
"name" = "Ticket ([id]) (Game ID: [game_id]) being handled.",
"body" = "[key_name(usr)] is now handling the ticket."
)
)
/datum/ticket/proc/Retitle()
var/new_title = tgui_input_text(usr, "Enter a title for the ticket", "Rename Ticket", name)
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(msg)
TicketPanel() //we have to be here to do this
//Kick ticket to next level
/datum/ticket/proc/Escalate()
if(tgui_alert(usr, "Really escalate this ticket to admins? No mentors will ever be able to interact with it again if you do.","Escalate",list("Yes","No")) != "Yes")
return
if (src.initiator == null) // You can't escalate a mentorhelp of someone who's logged out because it won't create the adminhelp properly
to_chat(usr, "<span class='pm warning'>Error: client not found, unable to escalate.</span>")
return
level = level - 1
message_mentors("[usr.ckey] escalated Ticket [TicketHref("#[id]")]")
log_admin("[key_name(usr)] escalated ticket [src.name]")
to_chat(src.initiator, "<span class='mentor'>[usr.ckey] escalated your ticket to admins.</span>")
/datum/ticket/proc/Context(ref_src)
if(!ref_src)
ref_src = "\ref[src]"
if(state == AHELP_ACTIVE)
. += ClosureLinks(ref_src)
if(state != AHELP_RESOLVED)
. += " (<A HREF='?_src_=mentorholder;mhelp=[ref_src];mhelp_action=escalate'>ESCALATE</A>)"
//Forwarded action from admin/Topic
/datum/ticket/proc/Action(action)
testing("Ahelp action: [action]")
switch(action)
if("ticket")
TicketPanel()
if("retitle")
Retitle()
if("reject")
Reject()
if("reply")
usr.client.cmd_ahelp_reply(initiator)
if("icissue")
ICIssue()
if("close")
Close()
if("resolve")
Resolve()
if("handleissue")
HandleIssue()
if("reopen")
Reopen()
if("escalate")
Escalate()
//
// TICKET STATCLICK
//
/obj/effect/statclick/ticket
var/datum/ticket/ticket_datum
/obj/effect/statclick/ticket/New(loc, datum/ticket/T)
ticket_datum = T
..(loc)
/obj/effect/statclick/ticket/update()
return ..(ticket_datum.name)
/obj/effect/statclick/ticket/Click()
ticket_datum.TicketPanel()
/obj/effect/statclick/ticket/Destroy()
ticket_datum = null
return ..()
//
// 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
/proc/admin_ticket_log(what, message)
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)
return C.current_ticket
if(istext(what)) //ckey
var/datum/ticket/T = GLOB.tickets.CKey2ActiveTicket(what)
if(T)
T.AddInteraction(message)
return T
//
// 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 != 0 && !check_rights(rights_required = requiredflags, show_msg = FALSE, C = X))
.["noflags"] += X
else if(X.is_afk())
.["afk"] += X
else if(X.holder.fakekey)
.["stealth"] += X
else
.["present"] += X
/proc/send2irc_adminless_only(source, msg, requiredflags = R_BAN)
var/list/adm = get_admin_counts()
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] "
send2irc(source,final)
/proc/ircadminwho()
var/list/message = list("Admins: ")
var/list/admin_keys = list()
for(var/client/C as anything in GLOB.admins)
admin_keys += "[C][C.holder.fakekey ? "(Stealth)" : ""][C.is_afk() ? "(AFK)" : ""]"
for(var/admin in admin_keys)
if(LAZYLEN(admin_keys) > 1)
message += ", [admin]"
else
message += "[admin]"
return jointext(message, "")
/proc/keywords_lookup(msg,irc)
//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 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=1, i<surname_found, i++)
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(found.mind && found.mind.special_role)
is_antag = 1
founds += "Name: [found.name]([found.real_name]) Ckey: [found.ckey] [is_antag ? "(Antag)" : null] "
msg += "[original_word]<font size='1' color='[is_antag ? "red" : "black"]'>(<A HREF='?_src_=holder;adminmoreinfo=\ref[found]'>?</A>|<A HREF='?_src_=holder;adminplayerobservefollow=\ref[found]'>F</A>)</font> "
continue
msg += "[original_word] "
if(irc)
if(founds == "")
return "Search Failed"
else
return founds
return msg
@@ -0,0 +1,44 @@
//
//PLAYERSIDE TICKET UI
//
/datum/ticket_chat
var/datum/ticket/T
/datum/ticket_chat/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "TicketChat", "Ticket #[T.id] - [T.LinkedReplyName("\ref[T]")]")
ui.open()
user.clear_alert("open ticket")
/datum/ticket_chat/tgui_close(mob/user)
. = ..()
if(user.client.current_ticket)
user.throw_alert("open ticket", /obj/screen/alert/open_ticket)
/datum/ticket_chat/tgui_state(mob/user)
return GLOB.tgui_ticket_state
/datum/ticket_chat/tgui_data(mob/user)
var/list/data = list()
data["id"] = T.id
data["level"] = T.level
data["handler"] = T.handler
data["log"] = T._interactions
return data
/datum/ticket_chat/tgui_act(action, params)
if(..())
return
switch(action)
if("send_msg")
if(params["msg"])
return
usr.client.cmd_admin_pm(usr.client, sanitize(params["msg"]), T)
. = TRUE
@@ -0,0 +1,319 @@
//
//TICKET MANAGER
//
/datum/tickets/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "TicketsPanel", "Tickets")
ui.open()
/datum/tickets/tgui_state(mob/user)
return GLOB.tgui_admin_state
/datum/tickets/proc/get_ticket_state(state)
var/ticket_state
switch(state)
if(AHELP_ACTIVE)
ticket_state = "open"
// TODO: Mentor tickets cannot be resolved
if(AHELP_RESOLVED)
ticket_state = "resolved"
if(AHELP_CLOSED)
ticket_state = "closed"
else
ticket_state = "unknown"
return ticket_state
/datum/tickets/tgui_data(mob/user)
var/list/data = list()
var/list/tickets = list()
var/selected_ticket = null
if(user.client.selected_ticket)
var/datum/ticket/T = user.client.selected_ticket
selected_ticket = list(
"id" = T.id,
"name" = T.LinkedReplyName(),
"state" = get_ticket_state(T.state),
"level" = T.level,
"handler" = T.handler,
"opened_at" = (world.time - T.opened_at),
"closed_at" = (world.time - T.closed_at),
"opened_at_date" = gameTimestamp(wtime = T.opened_at),
"closed_at_date" = gameTimestamp(wtime = T.closed_at),
"actions" = T.FullMonty(),
"log" = T._interactions,
)
for(var/datum/ticket/T as anything in GLOB.tickets.active_tickets)
if(user.client.holder || (has_mentor_powers(user.client) && T.level > 0))
tickets.Add(list(list(
"id" = T.id,
"name" = T.initiator_key_name,
"state" = get_ticket_state(T.state),
"level" = T.level,
"handler" = T.handler,
"opened_at" = (world.time - T.opened_at),
"closed_at" = (world.time - T.closed_at),
"opened_at_date" = gameTimestamp(wtime = T.opened_at),
"closed_at_date" = gameTimestamp(wtime = T.closed_at),
)))
for(var/datum/ticket/T as anything in GLOB.tickets.closed_tickets)
if(user.client.holder || (has_mentor_powers(user.client) && T.level > 0))
tickets.Add(list(list(
"id" = T.id,
"name" = T.initiator_key_name,
"state" = get_ticket_state(T.state),
"level" = T.level,
"handler" = T.handler,
"opened_at" = (world.time - T.opened_at),
"closed_at" = (world.time - T.closed_at),
"opened_at_date" = gameTimestamp(wtime = T.opened_at),
"closed_at_date" = gameTimestamp(wtime = T.closed_at),
)))
for(var/datum/ticket/T as anything in GLOB.tickets.resolved_tickets)
if(user.client.holder || (has_mentor_powers(user.client) && T.level > 0))
tickets.Add(list(list(
"id" = T.id,
"name" = T.initiator_key_name,
"state" = get_ticket_state(T.state),
"level" = T.level,
"handler" = T.handler,
"opened_at" = (world.time - T.opened_at),
"closed_at" = (world.time - T.closed_at),
"opened_at_date" = gameTimestamp(wtime = T.opened_at),
"closed_at_date" = gameTimestamp(wtime = T.closed_at),
)))
data["tickets"] = tickets
data["selected_ticket"] = selected_ticket
return data
/datum/tickets/tgui_act(action, params, datum/tgui/ui)
if(..())
return
switch(action)
if("legacy")
var/choice = tgui_input_list(usr, "Which tickets do you want to list?", "Tickets", list("Active", "Closed", "Resolved"))
TicketListLegacy(choice)
. = TRUE
if("new_ticket")
var/list/ckeys
for(var/client/C in GLOB.clients)
ckeys += C.ckey
var/ckey = tgui_input_list(usr, "Please select the ckey of the user.", "Select CKEY", ckeys)
if(!ckey)
return
var/client/player
for(var/client/C in GLOB.clients)
if(C.ckey == ckey)
player = C
if(!player)
to_chat(usr, "<span class='warning'>Ckey ([ckey]) not online.</span>")
return
var/msg = tgui_input_text(usr, "What should the initial text be?", "New Ticket")
if(!msg)
to_chat(usr, "<span class='warning'>Ticket message cannot be empty.</span>")
return
var/level = tgui_input_text(usr, "What level is the ticket? 0 = ADMIN, 1 = MENTOR", "Ticket Level")
if(level != "0" && level != "1")
to_chat(usr, "<span class='warning'>Invalid ticket level: [level].</span>")
return
feedback_add_details("admin_verb","Admincreatedticket") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
if(player.current_ticket)
if(tgui_alert(usr, "The player already has a ticket open. Is this for the same issue?","Duplicate?",list("Yes","No")) != "No")
if(player.current_ticket)
player.current_ticket.MessageNoRecipient(msg)
to_chat(usr, "<span class='adminnotice'>PM to-<b>Admins</b>: [msg]</span>")
return
else
to_chat(usr, "<span class='warning'>Ticket not found, creating new one...</span>")
else
player.current_ticket.AddInteraction("[key_name_admin(usr)] opened a new ticket.")
player.current_ticket.Close()
new /datum/ticket(msg, player, TRUE, level)
. = TRUE
if("pick_ticket")
var/datum/ticket/T = ID2Ticket(params["ticket_id"])
usr.client.selected_ticket = T
. = TRUE
if("retitle_ticket")
usr.client.selected_ticket.Retitle()
. = TRUE
if("reopen_ticket")
usr.client.selected_ticket.Reopen()
. = TRUE
if("undock_ticket")
usr.client.selected_ticket.tgui_interact(usr)
usr.client.selected_ticket = null
. = TRUE
/datum/tickets/tgui_fallback(payload)
if(..())
return
var/choice = tgui_input_list(usr, "Which tickets do you want to list?", "Tickets", list("Active", "Closed", "Resolved"))
TicketListLegacy(choice)
//
//TICKET DATUM
//
/datum/ticket/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Ticket", "Ticket #[id] - [LinkedReplyName("\ref[src]")]")
ui.open()
/datum/ticket/tgui_state(mob/user)
return GLOB.tgui_admin_state
/datum/ticket/tgui_data(mob/user)
var/list/data = list()
data["id"] = id
var/ref_src = "\ref[src]"
data["title"] = name
data["name"] = LinkedReplyName(ref_src)
switch(state)
if(AHELP_ACTIVE)
data["state"] = "open"
// TODO: Mentor tickets cannot be resolved
if(AHELP_RESOLVED)
data["state"] = "resolved"
if(AHELP_CLOSED)
data["state"] = "closed"
else
data["state"] = "unknown"
data["level"] = level
data["handler"] = handler
data["opened_at"] = (world.time - opened_at)
data["closed_at"] = (world.time - closed_at)
data["opened_at_date"] = gameTimestamp(wtime = opened_at)
data["closed_at_date"] = gameTimestamp(wtime = closed_at)
data["actions"] = FullMonty(ref_src)
data["log"] = _interactions
return data
/datum/ticket/tgui_act(action, params)
if(..())
return
switch(action)
if("retitle")
Retitle()
. = TRUE
if("reopen")
Reopen()
. = TRUE
if("legacy")
TicketPanelLegacy()
. = TRUE
/datum/ticket/tgui_fallback(payload)
if(..())
return
TicketPanelLegacy()
/*
/datum/mentor_help/proc/TicketPanelLegacy()
var/list/dat = list("<html><head><title>Ticket #[id]</title></head>")
var/ref_src = "\ref[src]"
dat += "<h4>Mentor Help Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
dat += "<b>State: "
switch(state)
if(AHELP_ACTIVE)
dat += "<font color='red'>OPEN</font>"
if(AHELP_RESOLVED)
dat += "<font color='green'>RESOLVED</font>"
else
dat += "UNKNOWN"
dat += "</b>[GLOB.TAB][TicketHref("Refresh", ref_src)]"
if(state != AHELP_ACTIVE)
dat += "[GLOB.TAB][TicketHref("Reopen", ref_src, "reopen")]"
dat += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)"
if(closed_at)
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)"
dat += "<br><br>"
if(initiator)
dat += "<b>Actions:</b> [Context(ref_src)]<br>"
else
dat += "<b>DISCONNECTED</b>[GLOB.TAB][ClosureLinks(ref_src)]<br>"
dat += "<br><b>Log:</b><br><br>"
for(var/I in _interactions)
dat += "[I]<br>"
usr << browse(dat.Join(), "window=mhelp[id];size=620x480") */
/datum/ticket/proc/TicketPanelLegacy()
var/list/dat = list("<html><head><title>Ticket #[id]</title></head>")
var/ref_src = "\ref[src]"
dat += "<h4>Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
dat += "<b>State: "
switch(state)
if(AHELP_ACTIVE)
dat += "<font color='red'>OPEN</font>"
if(AHELP_RESOLVED)
dat += "<font color='green'>RESOLVED</font>"
if(AHELP_CLOSED)
dat += "CLOSED"
else
dat += "UNKNOWN"
dat += "</b>[GLOB.TAB][TicketHref("Refresh", ref_src)][GLOB.TAB][TicketHref("Re-Title", ref_src, "retitle")]"
if(state != AHELP_ACTIVE)
dat += "[GLOB.TAB][TicketHref("Reopen", ref_src, "reopen")]"
dat += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)"
if(closed_at)
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)"
dat += "<br><br>"
if(initiator)
dat += "<b>Actions:</b> [FullMonty(ref_src)]<br>"
else
dat += "<b>DISCONNECTED</b>[GLOB.TAB][ClosureLinks(ref_src)]<br>"
dat += "<br><b>Log:</b><br><br>"
for(var/I in _interactions)
dat += "[I]<br>"
usr << browse(dat.Join(), "window=ahelp[id];size=620x480")
/datum/tickets/proc/TicketListLegacy(var/state)
var/list/dat = list("<html><head><title>[state] Tickets</title></head>")
var/tickets_found = 0
if(state == "Active")
for(var/datum/ticket/T as anything in GLOB.tickets.active_tickets)
dat += "[T.level == 0 ? "Adminhelp" : "Mentorhelp"] - [T.TicketHref("#[T.id] - [T.initiator_ckey]: [T.name]")]"
tickets_found++
else if(state == "Closed")
for(var/datum/ticket/T as anything in GLOB.tickets.closed_tickets)
dat += "[T.level == 0 ? "Adminhelp" : "Mentorhelp"] - [T.TicketHref("#[T.id] - [T.initiator_ckey]: [T.name]")]"
tickets_found++
else if(state == "Resolved")
for(var/datum/ticket/T as anything in GLOB.tickets.resolved_tickets)
dat += "[T.level == 0 ? "Adminhelp" : "Mentorhelp"] - [T.TicketHref("#[T.id] - [T.initiator_ckey]: [T.name]")]"
tickets_found++
if(tickets_found == 0)
dat += "No [state] tickets found."
usr << browse(dat.Join(), "window=ahelp-list;size=250x350")
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+3 -3
View File
@@ -4,10 +4,10 @@
"version": "4.4.1",
"type": "module",
"dependencies": {
"axios": "^0.27.2",
"glob": "^8.0.3",
"axios": "^0.21.1",
"glob": "^7.1.7",
"source-map": "^0.7.3",
"stacktrace-parser": "^0.1.10",
"ws": "^8.6.0"
"ws": "^7.5.3"
}
}
@@ -1,84 +0,0 @@
/* eslint react/no-danger: "off" */
import { useBackend } from '../backend';
import { Box, Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
const State = {
'open': 'Open',
'resolved': 'Resolved',
'unknown': 'Unknown',
};
type Data = {
id: number;
title: string;
name: string;
state: string;
opened_at: number;
closed_at: number;
opened_at_date: string;
closed_at_date: string;
actions: string;
log: string[];
};
export const MentorTicketPanel = (props, context) => {
const { act, data } = useBackend<Data>(context);
const {
id,
title,
name,
state,
opened_at,
closed_at,
opened_at_date,
closed_at_date,
actions,
log,
} = data;
return (
<Window width={900} height={600}>
<Window.Content scrollable>
<Section
title={'Ticket #' + id}
buttons={
<Box nowrap>
<Button
icon="arrow-up"
content="Escalate"
onClick={() => act('escalate')}
/>{' '}
<Button content="Legacy UI" onClick={() => act('legacy')} />
</Box>
}>
<LabeledList>
<LabeledList.Item label="Mentor Help Ticket">
#{id}: <div dangerouslySetInnerHTML={{ __html: name }} />
</LabeledList.Item>
<LabeledList.Item label="State">{State[state]}</LabeledList.Item>
{State[state] === State.open ? (
<LabeledList.Item label="Opened At">
{opened_at_date} ({Math.round((opened_at / 600) * 10) / 10}{' '}
minutes ago.)
</LabeledList.Item>
) : (
<LabeledList.Item label="Closed At">
{closed_at_date} ({Math.round((closed_at / 600) * 10) / 10}{' '}
minutes ago.){' '}
<Button content="Reopen" onClick={() => act('reopen')} />
</LabeledList.Item>
)}
<LabeledList.Item label="Actions">
<div dangerouslySetInnerHTML={{ __html: actions }} />
</LabeledList.Item>
<LabeledList.Item label="Log">
{Object.keys(log).map((L) => (
<div dangerouslySetInnerHTML={{ __html: log[L] }} />
))}
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
@@ -3,6 +3,22 @@ import { useBackend } from '../backend';
import { Box, Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
const Level = {
0: 'Adminhelp',
1: 'Mentorhelp',
2: 'GM Request',
};
const LevelColor = {
0: 'red',
1: 'green',
2: 'pink',
};
const Tag = {
'example': 'Example',
};
const State = {
'open': 'Open',
'resolved': 'Resolved',
@@ -15,6 +31,8 @@ type Data = {
title: string;
name: string;
state: string;
level: number;
handler: string;
opened_at: number;
closed_at: number;
opened_at_date: string;
@@ -23,13 +41,15 @@ type Data = {
log: string[];
};
export const AdminTicketPanel = (props, context) => {
export const Ticket = (props, context) => {
const { act, data } = useBackend<Data>(context);
const {
id,
title,
name,
state,
level,
handler,
opened_at,
closed_at,
opened_at_date,
@@ -49,14 +69,17 @@ export const AdminTicketPanel = (props, context) => {
content="Rename Ticket"
onClick={() => act('retitle')}
/>{' '}
<Button content="Legacy UI" onClick={() => act('legacy')} />
<Button content="Legacy UI" onClick={() => act('legacy')} />{' '}
<Button content={Level[level]} color={LevelColor[level]} />
</Box>
}>
<LabeledList>
<LabeledList.Item label="Admin Help Ticket">
<LabeledList.Item label="Ticket ID">
#{id}: <div dangerouslySetInnerHTML={{ __html: name }} />
</LabeledList.Item>
<LabeledList.Item label="Type">{Level[level]}</LabeledList.Item>
<LabeledList.Item label="State">{State[state]}</LabeledList.Item>
<LabeledList.Item label="Assignee">{handler}</LabeledList.Item>
{State[state] === State.open ? (
<LabeledList.Item label="Opened At">
{opened_at_date} ({Math.round((opened_at / 600) * 10) / 10}{' '}
@@ -0,0 +1,101 @@
/* eslint react/no-danger: "off" */
import { KEY_ENTER } from '../../common/keycodes';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Divider, Flex, Input, LabeledList, Section } from '../components';
import { Window } from '../layouts';
const Level = {
0: 'Adminhelp',
1: 'Mentorhelp',
2: 'GM Request',
};
const LevelColor = {
0: 'red',
1: 'green',
2: 'pink',
};
const Tag = {
'example': 'Example',
};
const State = {
'open': 'Open',
'resolved': 'Resolved',
'closed': 'Closed',
'unknown': 'Unknown',
};
type Data = {
id: number;
level: number;
handler: string;
log: string[];
};
export const TicketChat = (props, context) => {
const { act, data } = useBackend<Data>(context);
const [ticketChat, setTicketChat] = useLocalState(context, 'ticketChat', '');
const { id, level, handler, log } = data;
return (
<Window width={900} height={600}>
<Window.Content>
<Section
title={'Ticket #' + id}
buttons={
<Box nowrap>
<Button content={Level[level]} color={LevelColor[level]} />
</Box>
}>
<LabeledList>
<LabeledList.Item label="Assignee">{handler}</LabeledList.Item>
<LabeledList.Item label="Log" />
</LabeledList>
<Divider />
<Flex direction="column">
<Flex.Item>
<Flex>
<Flex.Item grow>
<Input
fluid
placeholder="Enter a message..."
value={ticketChat}
onInput={(e, value) => setTicketChat(value)}
onKeyDown={(event) => {
const keyCode = window.event
? event.which
: event.keyCode;
if (keyCode === KEY_ENTER) {
act('send_msg', { msg: ticketChat });
setTicketChat('');
}
}}
/>
</Flex.Item>
<Flex.Item>
<Button
content="Send"
onClick={() => {
act('send_msg', { msg: ticketChat });
setTicketChat('');
}}
/>
</Flex.Item>
</Flex>
</Flex.Item>
<Divider />
<Flex.Item>
{Object.keys(log)
.slice(0)
.reverse()
.map((L) => (
<div dangerouslySetInnerHTML={{ __html: log[L] }} />
))}
</Flex.Item>
</Flex>
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,259 @@
/* eslint react/no-danger: "off" */
import { BooleanLike } from '../../common/react';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Divider, Dropdown, Flex, Icon, LabeledList, Section, Tabs } from '../components';
import { Window } from '../layouts';
const Level = {
0: 'Admin',
1: 'Mentor',
2: 'All Levels',
};
const LevelColor = {
0: 'red',
1: 'green',
2: 'pink',
};
const Tag = {
'example': 'Example',
};
const State = {
'open': 'Open',
'resolved': 'Resolved',
'closed': 'Closed',
'unknown': 'Unknown',
'all': 'All States',
};
const StateColor = {
'open': 'white',
'resolved': 'green',
'closed': 'grey',
'unknown': 'orange',
};
type Data = {
tickets: Ticket[];
selected_ticket: Ticket;
};
type Ticket = {
id: number;
title: string;
name: string;
state: string;
level: number;
handler: string;
opened_at: number;
closed_at: number;
opened_at_date: string;
closed_at_date: string;
actions: string;
log: string[];
ref: string;
selected: BooleanLike;
};
const getFilteredTickets = (
tickets: Ticket[],
state: string,
level: number
): Ticket[] => {
let result: Ticket[] = [];
tickets.forEach((t) => {
if (
(t.state === state || state === 'all') &&
(t.level === level || level === 2)
) {
result.push(t);
}
});
return result;
};
export const TicketsPanel = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { tickets, selected_ticket } = data;
const [stateFilter, setStateFilter] = useLocalState(
context,
'stateFilter',
'open'
);
const [levelFilter, setLevelFilter] = useLocalState(
context,
'levelFilter',
2
);
let filtered_tickets = getFilteredTickets(tickets, stateFilter, levelFilter);
return (
<Window width={900} height={600}>
<Window.Content>
<Flex>
<Flex.Item shrink>
<Section title="Filter">
<Dropdown
width="100%"
maxHeight="160px"
options={Object.values(State)}
selected={State[stateFilter]}
onSelected={(val) =>
setStateFilter(
Object.keys(State)[Object.values(State).indexOf(val)]
)
}
/>
<Divider />
<Dropdown
width="100%"
maxHeight="160px"
options={Object.values(Level)}
selected={Level[levelFilter]}
onSelected={(val) =>
setLevelFilter(Object.values(Level).indexOf(val))
}
/>
</Section>
<Section title="Tickets" scrollable>
<Tabs vertical>
<Tabs.Tab onClick={() => act('new_ticket')}>
New Ticket
<Icon name="plus" ml={0.5} />
</Tabs.Tab>
<Divider />
{filtered_tickets.map((ticket) => (
<Tabs.Tab
key={ticket.id}
selected={ticket.id === selected_ticket?.id}
onClick={() =>
act('pick_ticket', { ticket_id: ticket.id })
}>
<Box inline>
<Box>
<Button
content={Level[ticket.level]}
color={LevelColor[ticket.level]}
/>{' '}
{ticket.name}
</Box>
<Box fontSize={0.9} textColor={StateColor[ticket.state]}>
State: {State[ticket.state]} | Assignee:{' '}
{ticket.handler}
</Box>
</Box>
</Tabs.Tab>
))}
</Tabs>
</Section>
</Flex.Item>
<Flex.Item grow>
{(selected_ticket && (
<Section
title={'Ticket #' + selected_ticket.id}
buttons={
<Box nowrap>
<Button
icon="arrow-up"
content="Undock"
onClick={() => act('undock_ticket')}
/>{' '}
<Button
icon="pen"
content="Rename Ticket"
onClick={() => act('retitle_ticket')}
/>{' '}
<Button content="Legacy UI" onClick={() => act('legacy')} />{' '}
<Button
content={Level[selected_ticket.level]}
color={LevelColor[selected_ticket.level]}
/>
</Box>
}>
<LabeledList>
<LabeledList.Item label="Ticket ID">
#{selected_ticket.id}:{' '}
<div
dangerouslySetInnerHTML={{ __html: selected_ticket.name }}
/>
</LabeledList.Item>
<LabeledList.Item label="Type">
{Level[selected_ticket.level]}
</LabeledList.Item>
<LabeledList.Item label="State">
{State[selected_ticket.state]}
</LabeledList.Item>
<LabeledList.Item label="Assignee">
{selected_ticket.handler}
</LabeledList.Item>
{State[selected_ticket.state] === State.open ? (
<LabeledList.Item label="Opened At">
{selected_ticket.opened_at_date} (
{Math.round((selected_ticket.opened_at / 600) * 10) / 10}{' '}
minutes ago.)
</LabeledList.Item>
) : (
<LabeledList.Item label="Closed At">
{selected_ticket.closed_at_date} (
{Math.round((selected_ticket.closed_at / 600) * 10) / 10}{' '}
minutes ago.){' '}
<Button
content="Reopen"
onClick={() => act('reopen_ticket')}
/>
</LabeledList.Item>
)}
<LabeledList.Item label="Actions">
<div
dangerouslySetInnerHTML={{
__html: selected_ticket.actions,
}}
/>
</LabeledList.Item>
<LabeledList.Item label="Log">
{Object.keys(selected_ticket.log).map((L) => (
<Box
dangerouslySetInnerHTML={{
__html: selected_ticket.log[L],
}}
/>
))}
</LabeledList.Item>
</LabeledList>
</Section>
)) || (
<Section
title="No ticket selected"
buttons={
<Box nowrap>
<Button
disabled
icon="arrow-up"
content="Undock"
onClick={() => act('undock_ticket')}
/>{' '}
<Button
disabled
icon="pen"
content="Rename Ticket"
onClick={() => act('retitle_ticket')}
/>{' '}
<Button content="Legacy UI" onClick={() => act('legacy')} />
</Box>
}>
Please select a ticket on the left to view its details.
</Section>
)}
</Flex.Item>
</Flex>
</Window.Content>
</Window>
);
};
File diff suppressed because one or more lines are too long
+15 -38
View File
@@ -2983,13 +2983,12 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^0.27.2":
version: 0.27.2
resolution: "axios@npm:0.27.2"
"axios@npm:^0.21.1":
version: 0.21.4
resolution: "axios@npm:0.21.4"
dependencies:
follow-redirects: ^1.14.9
form-data: ^4.0.0
checksum: 38cb7540465fe8c4102850c4368053c21683af85c5fdf0ea619f9628abbcb59415d1e22ebc8a6390d2bbc9b58a9806c874f139767389c862ec9b772235f06854
follow-redirects: ^1.14.0
checksum: 44245f24ac971e7458f3120c92f9d66d1fc695e8b97019139de5b0cc65d9b8104647db01e5f46917728edfc0cfd88eb30fc4c55e6053eef4ace76768ce95ff3c
languageName: node
linkType: hard
@@ -4059,9 +4058,9 @@ __metadata:
linkType: hard
"electron-to-chromium@npm:^1.4.431":
version: 1.4.433
resolution: "electron-to-chromium@npm:1.4.433"
checksum: 106e3bc2fb4ee5eddd4b141363900d5cd731c7579aa6bebd02509c52d6b598a1684aba1b75791e838dfa54dec0a40ddd17ea01199041ea46310aafb206395e43
version: 1.4.434
resolution: "electron-to-chromium@npm:1.4.434"
checksum: b35ef09571167e65ba867f7d7c3fc3ed7696ccd82313c172fd400e8ada1568a9f310791854ff0b57abbe8869abacae71d32b2f9e9460b2f4ef1eff86661ad075
languageName: node
linkType: hard
@@ -4918,7 +4917,7 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.14.9":
"follow-redirects@npm:^1.14.0":
version: 1.15.2
resolution: "follow-redirects@npm:1.15.2"
peerDependenciesMeta:
@@ -5162,7 +5161,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.2.0":
"glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0":
version: 7.2.3
resolution: "glob@npm:7.2.3"
dependencies:
@@ -5176,19 +5175,6 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^8.0.3":
version: 8.1.0
resolution: "glob@npm:8.1.0"
dependencies:
fs.realpath: ^1.0.0
inflight: ^1.0.4
inherits: 2
minimatch: ^5.0.1
once: ^1.3.0
checksum: 92fbea3221a7d12075f26f0227abac435de868dd0736a17170663783296d0dd8d3d532a5672b4488a439bf5d7fb85cdd07c11185d6cd39184f0385cbdfb86a47
languageName: node
linkType: hard
"globals@npm:^11.1.0":
version: 11.12.0
resolution: "globals@npm:11.12.0"
@@ -7073,15 +7059,6 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^5.0.1":
version: 5.1.6
resolution: "minimatch@npm:5.1.6"
dependencies:
brace-expansion: ^2.0.1
checksum: 7564208ef81d7065a370f788d337cd80a689e981042cb9a1d0e6580b6c6a8c9279eba80010516e258835a988363f99f54a6f711a315089b8b42694f5da9d0d77
languageName: node
linkType: hard
"minimatch@npm:^9.0.1":
version: 9.0.1
resolution: "minimatch@npm:9.0.1"
@@ -9344,11 +9321,11 @@ __metadata:
version: 0.0.0-use.local
resolution: "tgui-dev-server@workspace:packages/tgui-dev-server"
dependencies:
axios: ^0.27.2
glob: ^8.0.3
axios: ^0.21.1
glob: ^7.1.7
source-map: ^0.7.3
stacktrace-parser: ^0.1.10
ws: ^8.6.0
ws: ^7.5.3
languageName: unknown
linkType: soft
@@ -10299,7 +10276,7 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^7.3.1":
"ws@npm:^7.3.1, ws@npm:^7.5.3":
version: 7.5.9
resolution: "ws@npm:7.5.9"
peerDependencies:
@@ -10314,7 +10291,7 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.2.3, ws@npm:^8.6.0":
"ws@npm:^8.2.3":
version: 8.13.0
resolution: "ws@npm:8.13.0"
peerDependencies:
+7 -3
View File
@@ -1845,8 +1845,6 @@
#include "code\modules\admin\secrets\random_events\trigger_cordical_borer_infestation.dm"
#include "code\modules\admin\secrets\random_events\trigger_xenomorph_infestation.dm"
#include "code\modules\admin\verbs\admin_ch.dm"
#include "code\modules\admin\verbs\adminhelp.dm"
#include "code\modules\admin\verbs\adminhelp_vr.dm"
#include "code\modules\admin\verbs\adminjump.dm"
#include "code\modules\admin\verbs\adminpm.dm"
#include "code\modules\admin\verbs\adminsay.dm"
@@ -2834,7 +2832,6 @@
#include "code\modules\media\mediamanager.dm"
#include "code\modules\media\walkpod.dm"
#include "code\modules\mentor\mentor.dm"
#include "code\modules\mentor\mentorhelp.dm"
#include "code\modules\metric\activity.dm"
#include "code\modules\metric\count.dm"
#include "code\modules\metric\department.dm"
@@ -4506,6 +4503,7 @@
#include "code\ZAS\Zone.dm"
#include "interface\interface.dm"
#include "interface\skin.dmf"
#include "maps\runtime_station.dm"
#include "maps\atoll\atoll_decals.dm"
#include "maps\atoll\atoll_objs.dm"
#include "maps\atoll\atoll_turfs.dm"
@@ -4547,6 +4545,7 @@
#include "modular_chomp\code\global.dm"
#include "modular_chomp\code\_HELPERS\icons\flatten.dm"
#include "modular_chomp\code\_HELPERS\type2type\color.dm"
#include "modular_chomp\code\_onclick\hud\alert.dm"
#include "modular_chomp\code\ATMOSPHERICS\atmospherics.dm"
#include "modular_chomp\code\datums\autolathe\arms.dm"
#include "modular_chomp\code\datums\autolathe\engineering_ch.dm"
@@ -4733,6 +4732,11 @@
#include "modular_chomp\code\modules\resleeving\machines.dm"
#include "modular_chomp\code\modules\shieldgen\shield_capacitor.dm"
#include "modular_chomp\code\modules\shieldgen\shield_gen.dm"
#include "modular_chomp\code\modules\tgui\states\ticket.dm"
#include "modular_chomp\code\modules\tickets\procs.dm"
#include "modular_chomp\code\modules\tickets\tickets.dm"
#include "modular_chomp\code\modules\tickets\tickets_player_ui.dm"
#include "modular_chomp\code\modules\tickets\tickets_ui.dm"
#include "modular_chomp\code\modules\vore\eating\exportpanel_ch.dm"
#include "modular_chomp\code\modules\vore\resizing\resize.dm"
#include "modular_chomp\code\modules\xenobio\machinery\monkey_processor.dm"