diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm
index 2be7efceb9ad..311c8e368f00 100644
--- a/code/__DEFINES/admin.dm
+++ b/code/__DEFINES/admin.dm
@@ -65,10 +65,11 @@ var/global/list/admin_cooldowns_list = list(
#define R_WHITELIST 8192
#define R_EVENT 16384
#define R_LOG 32768
+#define R_MENTOR 65536
-#define R_MAXPERMISSION 32768 //This holds the maximum value for a permission. It is used in iteration, so keep it updated.
+#define R_MAXPERMISSION 65536 //This holds the maximum value for a permission. It is used in iteration, so keep it updated.
-#define R_HOST 65535
+#define R_HOST 131071
#define ADMIN_RANK_ROUND "Temporary Round Admin"
#define ADMIN_RANK_SANDBOX "Sandbox Admin"
diff --git a/code/__DEFINES/bridge.dm b/code/__DEFINES/bridge.dm
index 54140a9dd076..0463826ccf1b 100644
--- a/code/__DEFINES/bridge.dm
+++ b/code/__DEFINES/bridge.dm
@@ -31,6 +31,9 @@
#define BRIDGE_COLOR_BRIDGE "#adb3f0"
+//Mhelp tickets
+#define BRIDGE_COLOR_MENTORLOG "#6e713e"
+
//mention types, can be mappet to specific groups
//if not listed - bot will try to find and slap user
#define BRIDGE_MENTION_HERE "here"
diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm
index e6f6ee88218a..5a8186242270 100644
--- a/code/__HELPERS/type2type.dm
+++ b/code/__HELPERS/type2type.dm
@@ -218,6 +218,7 @@
if(rights & R_WHITELIST) . += "[seperator]+WHITELIST"
if(rights & R_EVENT) . += "[seperator]+EVENT"
if(rights & R_LOG) . += "[seperator]+LOG"
+ if(rights & R_MENTOR) . += "[seperator]+MENTOR"
return .
// heat2color functions. Adapted from: http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
diff --git a/code/datums/keybinding/client.dm b/code/datums/keybinding/client.dm
index 698b989453d2..2d3283f6c4d3 100644
--- a/code/datums/keybinding/client.dm
+++ b/code/datums/keybinding/client.dm
@@ -20,7 +20,7 @@
description = "Ask an mentors for help."
/datum/keybinding/client/mentor_help/down(client/user)
- user.get_mentorhelp()
+ user.mentorhelp()
return TRUE
/datum/keybinding/client/screenshot
diff --git a/code/game/world.dm b/code/game/world.dm
index 24b4a543e987..520005238fba 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -66,6 +66,7 @@ var/global/it_is_a_snow_day = FALSE
data_core = new /obj/effect/datacore()
paiController = new /datum/paiController()
ahelp_tickets = new
+ mhelp_tickets = new
SetRoundID()
base_commit_sha = GetGitMasterCommit(1)
@@ -185,7 +186,7 @@ var/global/world_topic_spam_protect_time = world.timeofday
if (packet_data)
if(packet_data["announce"] == "")
return receive_net_announce(packet_data, addr)
- if(packet_data["bridge"] == "" && addr == "127.0.0.1") //
+ if(packet_data["bridge"] == "" && addr == "127.0.0.1") //
bridge2game(packet_data)
return "bridge=1" // no return data in topic, feedback should be send only through bridge
@@ -662,7 +663,7 @@ var/global/failed_db_connections = 0
packet_data["secret"] = "SECRET"
log_href("WTOPIC: NET ANNOUNCE: \"[list2params(packet_data)]\", from:[sender]")
-
+
return proccess_net_announce(packet_data["type"], packet_data, sender)
/world/proc/proccess_net_announce(type, list/data, sender)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index c88211e92e56..ab081dd2d241 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -16,6 +16,8 @@ var/global/list/admin_verbs_admin = list(
/client/proc/colorooc, //allows us to set a custom colour for everythign we say in ooc,
/client/proc/admin_ghost, //allows us to ghost/reenter body at will,
/client/proc/toggle_view_range, //changes how far we can see,
+ /client/proc/cmd_mwntor_pm_context, //right-click adminPM interface,
+ /client/proc/cmd_mentor_pm_panel,
/client/proc/cmd_admin_pm_context, //right-click adminPM interface,
/client/proc/cmd_admin_pm_panel, //admin-pm list,
/client/proc/cmd_admin_subtle_message, //send an message to somebody as a 'voice in their head',
@@ -480,7 +482,7 @@ var/global/list/admin_verbs_hideable = list(
if(!config.sql_enabled)
to_chat(usr, "SQL database is disabled. Setup it or use native Byond bans.")
return
-
+
holder.DB_ban_panel()
feedback_add_details("admin_verb","UBP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
return
diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm
index 9a47e1268e5f..056e36baea78 100644
--- a/code/modules/admin/verbs/adminhelp.dm
+++ b/code/modules/admin/verbs/adminhelp.dm
@@ -87,6 +87,7 @@ var/global/datum/admin_help_tickets/ahelp_tickets
//Tickets statpanel
/datum/admin_help_tickets/proc/stat_entry()
var/num_disconnected = 0
+ stat("== Admin Tickets ==")
stat("Active Tickets:", astatclick.update("[active_tickets.len]"))
for(var/I in active_tickets)
var/datum/admin_help/AH = I
@@ -259,6 +260,7 @@ var/global/datum/admin_help_tickets/ahelp_tickets
. += " (IC)"
. += " (CLOSE)"
. += " (RSLVE)"
+ . += " (HANDLE)"
//private
/datum/admin_help/proc/LinkedReplyName(ref_src)
@@ -277,7 +279,7 @@ var/global/datum/admin_help_tickets/ahelp_tickets
/datum/admin_help/proc/MessageNoRecipient(msg)
var/ref_src = "\ref[src]"
//Message to be sent to all admins
- var/admin_msg = "Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]: [msg]"
+ var/admin_msg = " Admin Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]: [msg]"
AddInteraction("[LinkedReplyName(ref_src)]: [msg]")
@@ -316,7 +318,7 @@ var/global/datum/admin_help_tickets/ahelp_tickets
log_admin_private(msg)
world.send2bridge(
type = list(BRIDGE_ADMINLOG),
- attachment_title = "**Ticket #[id]** reopened by **[key_name(usr)]**",
+ attachment_title = "**Admin Ticket #[id]** reopened by **[key_name(usr)]**",
attachment_color = BRIDGE_COLOR_ADMINLOG,
)
TicketPanel() //can only be done from here, so refresh it
@@ -346,7 +348,7 @@ var/global/datum/admin_help_tickets/ahelp_tickets
log_admin_private(msg)
world.send2bridge(
type = list(BRIDGE_ADMINLOG),
- attachment_title = "**Ticket #[id]** closed by **[key_name(usr)]**",
+ attachment_title = "**Admin Ticket #[id]** closed by **[key_name(usr)]**",
attachment_color = BRIDGE_COLOR_ADMINLOG,
)
@@ -385,7 +387,7 @@ var/global/datum/admin_help_tickets/ahelp_tickets
var/msg = "- AdminHelp Rejected! -
" + \
"Your admin help was rejected. The adminhelp verb has been returned to you so that you may try again.
" + \
"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."
-
+
to_chat_admin_pm(initiator, msg)
var/msg = "Ticket [TicketHref("#[id]")] rejected by [key_name]"
@@ -422,6 +424,27 @@ var/global/datum/admin_help_tickets/ahelp_tickets
AddInteraction("Marked as IC issue by [key_name]")
Resolve(silent = TRUE)
+/datum/admin_help/proc/HandleIssue()
+ if(state != AHELP_ACTIVE)
+ return
+
+ var/msg = "Ваш AdminHelp рассматривает: [key_name(usr,FALSE,FALSE)], пожалуйста, будьте терпеливы."
+
+ if(initiator)
+ to_chat(initiator, msg)
+
+ feedback_inc("ahelp_handling")
+ msg = "Ticket [TicketHref("#[id]")] being handled by **[key_name(usr)]**"
+ message_admins(msg)
+ log_admin(msg)
+ world.send2bridge(
+ type = list(BRIDGE_ADMINLOG),
+ attachment_title = "**Тикет #[id]** раcсматривает: **[key_name(usr)]**",
+ attachment_color = BRIDGE_COLOR_ADMINLOG,
+
+ )
+ AddInteraction("[key_name_admin(usr)] is now handling this ticket.")
+
//Show the ticket panel
/datum/admin_help/proc/TicketPanel()
var/list/dat = list("
Ticket #[id]")
@@ -498,6 +521,8 @@ var/global/datum/admin_help_tickets/ahelp_tickets
Close()
if("resolve")
Resolve()
+ if("handleissue")
+ HandleIssue()
if("reopen")
Reopen()
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 5614b68d3049..9d6d9739f4af 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -222,6 +222,7 @@ var/global/list/blacklisted_builds = list(
tgui_panel = new(src)
global.ahelp_tickets?.ClientLogin(src)
+ global.mhelp_tickets?.ClientLogin(src)
//Admin Authorisation
holder = admin_datums[ckey]
@@ -354,7 +355,10 @@ var/global/list/blacklisted_builds = list(
admins -= src
global.ahelp_tickets?.ClientLogout(src)
directory -= ckey
- mentors -= src
+ if(holder)
+ holder.owner = null
+ mentors -= src
+ global.mhelp_tickets?.ClientLogout(src)
clients -= src
QDEL_LIST_ASSOC_VAL(char_render_holders)
LAZYREMOVE(movingmob?.clients_in_contents, src)
diff --git a/code/modules/mentor/mentor.dm b/code/modules/mentor/mentor.dm
index aad498cde41d..63373f350f02 100644
--- a/code/modules/mentor/mentor.dm
+++ b/code/modules/mentor/mentor.dm
@@ -1,5 +1,10 @@
-/var/list/mentor_ckeys = list()//all server mentors list
-/var/list/mentors = list() //online mentors
+/client
+ var/datum/mentor/mentorholder = null
+
+var/list/mentor_ckeys = list()
+var/global/list/mentors = list()
+
+var/list/mentor_verbs_default = list()
/world/proc/load_mentors()
mentor_ckeys.Cut()
@@ -38,11 +43,168 @@
if(directory[ckey])
mentors += directory[ckey]
-/proc/message_mentors(msg, observer_only = FALSE, emphasize = FALSE)
- var/style = "admin"
- if (emphasize)
- style += " emphasized"
- msg = "MENTOR LOG: [msg]"
+/datum/mentor
+ var/client/owner = null
+
+/datum/mentor/New(ckey)
+ if(!ckey)
+ error("Mentor datum created without a ckey argument. Datum has been deleted")
+ qdel(src)
+ return
+ mentor_ckeys[ckey] = src
+
+/datum/mentor/proc/associate(client/C)
+ if(istype(C))
+ owner = C
+ owner.mentorholder = src
+ owner.add_mentor_verbs()
+ mentors |= C
+
+/datum/mentor/proc/disassociate()
+ if(owner)
+ mentors -= owner
+ owner.remove_mentor_verbs()
+ owner.mentorholder = null
+ mentor_ckeys[owner.ckey] = null
+ qdel(src)
+
+/client/proc/add_mentor_verbs()
+ if(mentorholder)
+ verbs += mentor_verbs_default
+
+/client/proc/remove_mentor_verbs()
+ if(mentorholder)
+ verbs -= mentor_verbs_default
+
+/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"])
+ else
+ to_chat(C, "Ticket [mhelp_ref] has been deleted!")
+
+ if (href_list["mhelp_tickets"])
+ mhelp_tickets.BrowseTickets(text2num(href_list["mhelp_tickets"]))
+
+
+/datum/mentor/Topic(href, href_list)
+ ..()
+ if (usr.client != src.owner || (!usr.client.mentorholder))
+ log_admin("[key_name(usr)] tried to illegally use mentor functions.")
+ message_admins("[usr.key] tried to illegally use mentor functions.")
+ return
+
+ mentor_commands(href, href_list, usr)
+
+/client/proc/cmd_mhelp_reply(whom)
+ if(prefs.muted & MUTE_PM)
+ to_chat(src, "Error: Mentor-PM: You are unable to use admin PM-s (muted).")
+ return
+ var/client/C
+ if(istext(whom))
+ C = directory[whom]
+ else if(istype(whom,/client))
+ C = whom
+ if(!C)
+ if(has_mentor_powers(src))
+ to_chat(src, "Error: Mentor-PM: Client not found.")
+ return
+
+ var/datum/mentor_help/MH = C.current_mentorhelp
+
+ if(MH)
+ message_mentors("[src] has started replying to [C]'s mentor help.")
+ var/msg = input(src,"Message:", "Private message to [C]")
+ if (!msg)
+ message_mentors("[src] has cancelled their reply to [C]'s mentor help.")
+ return
+ cmd_mentor_pm(whom, msg, MH)
+
+/proc/has_mentor_powers(client/C)
+ return C.holder || C.mentorholder
+
+// This not really a great place to put it, but this verb replaces adminhelp in hotkeys so that people requesting help can select the type they need
+// You can still directly adminhelp if necessary, this ONLY replaces the inbuilt hotkeys
+
+
+/client/proc/cmd_mentor_pm(whom, msg, datum/mentor_help/MH)
+ set category = "Admin"
+ set name = "Mentor-PM"
+
+ if(prefs.muted & MUTE_PM)
+ to_chat(src, "Error: Mentor-PM: You are unable to use admin PM-s (muted).")
+ return
+
+ //Not a mentor and no open ticket
+ if(!has_mentor_powers(src) && !current_mentorhelp)
+ to_chat(src, "You can no longer reply to this ticket, please open another one by using the Mentorhelp verb if need be.")
+ to_chat(src, "Message: [msg]")
+ return
+
+ var/client/recipient
+
+ if(istext(whom))
+ recipient = directory[whom]
+
+ else if(istype(whom,/client))
+ recipient = whom
+ //get message text, limit it's length.and clean/escape html
+ if(!msg)
+ msg = input(src,"Message:", "Mentor-PM to [whom]")
+
+ if(!msg)
+ return
+
+ if(prefs.muted & MUTE_PM)
+ to_chat(src, "Error: Mentor-PM: You are unable to use admin PM-s (muted).")
+ return
+
+ if(!recipient)
+ if(has_mentor_powers(src))
+ to_chat(src, "Error:Mentor-PM: Client not found.")
+ to_chat(src, msg)
+ else
+ log_admin("Mentorhelp: [key_name(src)]: [msg]")
+ current_mentorhelp.MessageNoRecipient(msg)
+ return
+
+ //Has mentor powers but the recipient no longer has an open ticket
+ if(has_mentor_powers(src) && !recipient.current_mentorhelp)
+ to_chat(src, "You can no longer reply to this ticket.")
+ to_chat(src, "Message: [msg]")
+ return
+
+ if (src.handle_spam_prevention(msg,MUTE_PM))
+ return
+
+ msg = trim(sanitize(copytext(msg,1,MAX_MESSAGE_LEN)))
+ if(!msg)
+ return
+
+ var/interaction_message = "Mentor-PM from-[src] to-[recipient]: [msg]"
+
+ 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)
+
+ // 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)
+
+ to_chat(recipient, "Mentor-PM from-[src]: [msg]")
+ to_chat(src, "Mentor-PM to-[recipient]: [msg]")
+
+ log_admin("[key_name(src)]->[key_name(recipient)]: [msg]")
+
for(var/client/C in mentors)
- if(!observer_only || (observer_only && isobserver(C.mob)))
- to_chat(C, msg)
+ if (C != recipient && C != src)
+ to_chat(C, interaction_message)
+ for(var/client/C in admins)
+ if (C != recipient && C != src)
+ to_chat(C, interaction_message)
diff --git a/code/modules/mentor/mentorhelp(old).dm b/code/modules/mentor/mentorhelp(old).dm
new file mode 100644
index 000000000000..2b0ae3ee5676
--- /dev/null
+++ b/code/modules/mentor/mentorhelp(old).dm
@@ -0,0 +1,74 @@
+// /client/verb/mentorhelp(msg as text)
+// set category = "Admin"
+// set name = "Mentorhelp"
+
+// if(!mob || !msg)
+// return
+
+// if(say_disabled) //This is here to try to identify lag problems
+// to_chat(usr, "Speech is currently admin-disabled.")
+// return
+
+// if(mob.mind && mob.mind.special_role && !(src in mentors))
+// to_chat(usr, "You cannot ask mentors for help while being antag. File a ticket instead if you wish question this to admins.")
+// return
+
+// //handle muting and automuting
+// if(prefs.muted & MUTE_PM || IS_ON_ADMIN_CD(src, ADMIN_CD_PM))
+// to_chat(src, "Error: Mentor-PM: You cannot send mentorhelps (Muted).")
+// return
+// if(handle_spam_prevention(msg, ADMIN_CD_PM))
+// return
+
+// msg = sanitize(msg)
+// if(!msg)
+// return
+
+// var/ai_found = isAI(mob)
+// var/ref_mob = "\ref[mob]"
+
+// var/prefix = "MHELP"
+// var/colour = "maroon"
+
+// //send this msg to all admins
+// var/admin_number_afk = 0
+// for(var/client/X as anything in admins)
+// if(R_ADMIN & X.holder.rights)
+// if(X.is_afk())
+// admin_number_afk++
+// X.mob.playsound_local(null, X.bwoink_sound, VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
+// to_chat(X, "[prefix]: [get_options_bar(mob, 2, 1, 1, MHELP_REPLY, TRUE)][ai_found ? " (CL)" : ""]: [msg]")
+
+// var/mentor_number_afk = 0
+// var/jump = null
+// for(var/client/X in mentors)
+// if(X.is_afk())
+// mentor_number_afk++
+// if(isobserver(X.mob))
+// jump = "(JMP) "
+// X.mob.playsound_local(null, X.bwoink_sound, VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
+// to_chat(X, "[prefix]: [key_name(src, 1, 0, 0, MHELP_REPLY, TRUE)][jump]: [msg]")
+
+// mentorhelped = TRUE //Determines if they get the message to reply by clicking the name.
+
+// //show it to the person mentorhelping too
+// to_chat(src, "PM to-Mentors: [msg]")
+
+// var/mentor_number_present = mentors.len - mentor_number_afk
+// var/admin_number_present = admins.len - admin_number_afk
+
+// world.send2bridge(
+// type = list(BRIDGE_ADMINLOG),
+// attachment_title = "MENTOR HELP",
+// attachment_msg = "**[key_name(src)]:** [msg]",
+// attachment_color = BRIDGE_COLOR_ADMINLOG,
+// )
+
+// feedback_add_details("admin_verb", "MH") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+// log_admin("[prefix]: [key_name(src)]: [msg] - heard by [mentor_number_present] non-AFK mentors and [admin_number_present] non-AFK admins.")
+
+// /client/proc/get_mentorhelp()
+// var/msg = sanitize(input(src, null, "mentorhelp \"text\"") as text|null)
+// if(!msg)
+// return
+// mentorhelp(msg)
diff --git a/code/modules/mentor/mentorhelp.dm b/code/modules/mentor/mentorhelp.dm
index cf3cbe63c3fc..0f0945abf068 100644
--- a/code/modules/mentor/mentorhelp.dm
+++ b/code/modules/mentor/mentorhelp.dm
@@ -1,74 +1,471 @@
-/client/verb/mentorhelp(msg as text)
- set category = "Admin"
- set name = "Mentorhelp"
+/client/var/datum/mentor_help/current_mentorhelp
+
+//
+//TICKET MANAGER
+//
+
+var/global/datum/mentor_help_tickets/mhelp_tickets
+
+/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 ..()
+
+/datum/mentor_help_tickets/proc/TicketsByCKey(ckey)
+ . = list()
+ var/list/lists = list(active_tickets, resolved_tickets)
+ for(var/I in lists)
+ for(var/datum/mentor_help/MH in I)
+ if(MH.initiator_ckey == ckey)
+ . += MH
+
+//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
- if(!mob || !msg)
+//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("[title]")
+ dat += "Refresh
"
+ for(var/datum/mentor_help/MH as anything in l2b)
+ dat += "Ticket #[MH.id]: [MH.initiator_ckey]: [MH.name]
"
- if(say_disabled) //This is here to try to identify lag problems
- to_chat(usr, "Speech is currently admin-disabled.")
+ var/datum/browser/popup = new(usr, "mhelp_list[state]", null, 600, 480, null, CSS_THEME_LIGHT)
+ popup.set_content(dat.Join())
+ popup.open()
+
+//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/I in active_tickets)
+ var/datum/admin_help/MH = I
+ 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()
+ global.mhelp_tickets.BrowseTickets(current_state)
+
+//
+// # Mentorhelp Ticket
+//
+
+/datum/mentor_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 (ticks) at which the ticket was opened
+ var/opened_at
+ /// The time (ticks) at which the ticket was closed
+ var/closed_at
+ /// The time (timeofday) at which the ticket was opened
+ var/opened_at_server
+ /// The time (timeofday) at which the ticket was closed
+ var/closed_at_server
+ //semi-misnomer, it's the person who mhelped/was bwoinked
+ var/client/initiator
+ /// The ckey of the initiator
+ var/initiator_ckey
+ /// The key name of the initiator
+ var/initiator_key_name
+ /// use AddInteraction() or, preferably, admin_ticket_log()
+ var/list/_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
+
+//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, is_mwoink)
+ //clean the input msg
+ msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN))
+ if(!msg || !C || !C.mob)
+ qdel(src)
return
- if(mob.mind && mob.mind.special_role && !(src in mentors))
- to_chat(usr, "You cannot ask mentors for help while being antag. File a ticket instead if you wish question this to admins.")
+ 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
+ stack_trace("Multiple mhelp current_tickets")
+ initiator.current_mentorhelp.AddInteraction("Ticket erroneously left open by code")
+ initiator.current_mentorhelp.Resolve()
+ initiator.current_mentorhelp = src
+
+ statclick = new(null, src)
+ _interactions = list()
+
+ if(is_mwoink)
+ AddInteraction("[key_name_admin(usr)] PM'd [LinkedReplyName()]")
+ message_admins("Ticket [TicketHref("#[id]")] created")
+ else
+ MessageNoRecipient(msg)
+ //show it to the person adminhelping too
+ to_chat(C, "Mentor-PM to-Mentors: [name]")
+
+ world.send2bridge(
+ type = list(BRIDGE_ADMINLOG),
+ attachment_title = "**Ментор тикет #[id]** создан: **[key_name(initiator)]**",
+ attachment_msg = name,
+ attachment_color = BRIDGE_COLOR_MENTORLOG,
+ )
+
+ global.mhelp_tickets.active_tickets += src
+
+/datum/mentor_help/Destroy()
+ RemoveActive()
+ global.mhelp_tickets.resolved_tickets -= src
+ return ..()
+
+/datum/mentor_help/proc/AddInteraction(formatted_message)
+ _interactions += "[time_stamp()]: [formatted_message]"
+
+//private
+/datum/mentor_help/proc/ClosureLinks(ref_src)
+ if(!ref_src)
+ ref_src = "\ref[src]"
+ . = " (RSLVE)"
+
+/datum/mentor_help/proc/EscalateToAdmins(ref_src)
+ if(!ref_src)
+ ref_src = "\ref[src]"
+ . = " (ESCALATE)"
+
+//private
+/datum/mentor_help/proc/LinkedReplyName(ref_src)
+ if(!ref_src)
+ ref_src = "\ref[src]"
+ return "[initiator_key_name]"
+
+//private
+/datum/mentor_help/proc/TicketHref(msg, ref_src, action = "ticket")
+ if(!ref_src)
+ ref_src = "\ref[src]"
+ return "[msg]"
+
+//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 = "Mentor Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)] [EscalateToAdmins(ref_src)]: [msg]"
+ AddInteraction("[LinkedReplyName(ref_src)]: [msg]")
+ if(initiator)
+ giveadminhelpverb(initiator.ckey)
+
+ initiator.mob.playsound_local(null, 'sound/effects/mentorhelp.ogg', VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
+ message_mentors(chat_msg)
+
+//Reopen a closed ticket
+/datum/mentor_help/proc/Reopen()
+ if(state == AHELP_ACTIVE)
+ to_chat(usr, "This ticket is already open.")
return
- //handle muting and automuting
- if(prefs.muted & MUTE_PM || IS_ON_ADMIN_CD(src, ADMIN_CD_PM))
- to_chat(src, "Error: Mentor-PM: You cannot send mentorhelps (Muted).")
+ if(global.mhelp_tickets.CKey2ActiveTicket(initiator_ckey))
+ to_chat(usr, "This user already has an active ticket, cannot reopen this one.")
return
- if(handle_spam_prevention(msg, ADMIN_CD_PM))
+
+ statclick = new(null, src)
+ global.mhelp_tickets.active_tickets += src
+ global.mhelp_tickets.resolved_tickets -= src
+ state = AHELP_ACTIVE
+ closed_at = null
+ closed_at_server = null
+ if(initiator)
+ initiator.current_mentorhelp = src
+
+ AddInteraction("Reopened by [key_name(usr)]")
+ if(initiator)
+ to_chat(initiator, "Ticket [TicketHref("#[id]")] was reopened by [key_name(usr)].")
+ var/msg = "Ticket [TicketHref("#[id]")] reopened by [key_name(usr)]."
+ message_mentors(msg)
+ log_admin(msg)
+ world.send2bridge(
+ type = list(BRIDGE_ADMINLOG),
+ attachment_title = "**Mentor Ticket #[id]** reopened by **[key_name(usr)]**",
+ attachment_color = BRIDGE_COLOR_MENTORLOG,
+ )
+ 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
+ closed_at_server = world.timeofday
+ QDEL_NULL(statclick)
+ global.mhelp_tickets.active_tickets -= src
+ if(initiator && initiator.current_mentorhelp == src)
+ initiator.current_mentorhelp = null
- msg = sanitize(msg)
- if(!msg)
+//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
+ global.mhelp_tickets.ListInsert(src)
+ AddInteraction("Resolved by [key_name(usr)].")
+ if(initiator)
+ to_chat(initiator, "Ticket [TicketHref("#[id]")] was marked resolved by [key_name(usr)].")
+ if(!silent)
+ feedback_inc("mhelp_resolve")
+ var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name(usr)]"
+ message_mentors(msg)
+ log_admin(msg)
+ world.send2bridge(
+ type = list(BRIDGE_ADMINLOG),
+ attachment_title = "**Mentor Ticket #[id]** closed by **[key_name(usr)]**",
+ attachment_color = BRIDGE_COLOR_MENTORLOG,
+ )
+
+//Show the ticket panel
+/datum/mentor_help/proc/TicketPanel()
+ tgui_interact(usr.client.mob)
- var/ai_found = isAI(mob)
- var/ref_mob = "\ref[mob]"
+/datum/mentor_help/proc/TicketPanelLegacy()
+ var/list/dat = list("Ticket #[id]")
+ var/ref_src = "\ref[src]"
+ dat += "Mentor Help Ticket #[id]: [LinkedReplyName(ref_src)]
"
+ dat += "State: "
+ switch(state)
+ if(AHELP_ACTIVE)
+ dat += "OPEN"
+ if(AHELP_RESOLVED)
+ dat += "RESOLVED"
+ else
+ dat += "UNKNOWN"
+ dat += "[TAB][TicketHref("Refresh", ref_src)]"
+ if(state != AHELP_ACTIVE)
+ dat += "[TAB][TicketHref("Reopen", ref_src, "reopen")]"
+ dat += "
Opened at: [time_stamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)"
+ if(closed_at)
+ dat += "
Closed at: [time_stamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)"
+ dat += "
"
+ if(initiator)
+ dat += "Actions: [Context(ref_src)]
"
+ else
+ dat += "DISCONNECTED[TAB][ClosureLinks(ref_src)]
"
+ dat += "
Log:
"
+ for(var/I in _interactions)
+ dat += "[I]
"
- var/prefix = "MHELP"
- var/colour = "maroon"
+ usr << browse(dat.Join(), "window=mhelp[id];size=620x480")
- //send this msg to all admins
- var/admin_number_afk = 0
- for(var/client/X as anything in admins)
- if(R_ADMIN & X.holder.rights)
- if(X.is_afk())
- admin_number_afk++
- X.mob.playsound_local(null, X.bwoink_sound, VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
- to_chat(X, "[prefix]: [get_options_bar(mob, 2, 1, 1, MHELP_REPLY, TRUE)][ai_found ? " (CL)" : ""]: [msg]")
+ // Append any tickets also opened by this user if relevant
+ var/list/related_tickets = global.mhelp_tickets.TicketsByCKey(initiator_ckey)
+ if (related_tickets.len > 1)
+ dat += "
Other Tickets by [initiator_ckey]
"
+ for (var/datum/mentor_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]
"
- var/mentor_number_afk = 0
- var/jump = null
- for(var/client/X in mentors)
- if(X.is_afk())
- mentor_number_afk++
- if(isobserver(X.mob))
- jump = "(JMP) "
- X.mob.playsound_local(null, X.bwoink_sound, VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
- to_chat(X, "[prefix]: [key_name(src, 1, 0, 0, MHELP_REPLY, TRUE)][jump]: [msg]")
+ var/datum/browser/popup = new(usr, "mhelp[id]", null, 620, 480, null, CSS_THEME_LIGHT)
+ popup.set_content(dat.Join())
+ popup.open()
- mentorhelped = TRUE //Determines if they get the message to reply by clicking the name.
+/**
+ * Renders the current status of the ticket into a displayable string
+ */
+/datum/mentor_help/proc/ticket_status()
+ switch(state)
+ if(AHELP_ACTIVE)
+ return "OPEN"
+ if(AHELP_RESOLVED)
+ return "RESOLVED"
+ else
+ stack_trace("Invalid ticket state: [state]")
+ return "INVALID, CALL A CODER"
- //show it to the person mentorhelping too
- to_chat(src, "PM to-Mentors: [msg]")
+//Kick ticket to admins
+/datum/mentor_help/proc/Escalate()
+ if(tgui_alert(usr, "Вы действительно хотите передать этот тикет администраторам? Если вы это сделаете, то вы и другие менторы больше не смогут с ним взаимодействовать.","Передать тикет админам?",list("Да","Нет")) != "Да")
+ 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, "Error: client not found, unable to escalate.")
+ return
+ var/datum/admin_help/AH = new /datum/admin_help(src.name, src.initiator, FALSE)
+ message_mentors("[key_name(usr)] escalated Ticket [TicketHref("#[id]")]")
+ log_admin("[key_name(usr)] escalated mentorhelp [src.name]")
+ to_chat(src.initiator, "[key_name(usr)] escalated your mentorhelp to admins.")
+ AH._interactions = src._interactions
+ global.mhelp_tickets.active_tickets -= src
+ global.mhelp_tickets.resolved_tickets -= src
+ qdel(src)
- var/mentor_number_present = mentors.len - mentor_number_afk
- var/admin_number_present = admins.len - admin_number_afk
+/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)
+ . += EscalateToAdmins(ref_src)
- world.send2bridge(
- type = list(BRIDGE_ADMINLOG),
- attachment_title = "MENTOR HELP",
- attachment_msg = "**[key_name(src)]:** [msg]",
- attachment_color = BRIDGE_COLOR_ADMINLOG,
- )
+//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/atom_init(mapload, datum/mentor_help/MH)
+ mhelp_datum = MH
+ . = ..()
+
+/obj/effect/statclick/mhelp/update()
+ return ..(mhelp_datum.name)
- feedback_add_details("admin_verb", "MH") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
- log_admin("[prefix]: [key_name(src)]: [msg] - heard by [mentor_number_present] non-AFK mentors and [admin_number_present] non-AFK admins.")
+/obj/effect/statclick/mhelp/Click()
+ mhelp_datum.TicketPanel()
-/client/proc/get_mentorhelp()
- var/msg = sanitize(input(src, null, "mentorhelp \"text\"") as text|null)
+/obj/effect/statclick/mhelp/Destroy()
+ mhelp_datum = null
+ return ..()
+
+//
+// CLIENT PROCS
+//
+
+/client/verb/mentorhelp()
+ set category = "Admin"
+ set name = "Mentorhelp"
+
+ if(say_disabled) //This is here to try to identify lag problems
+ to_chat(usr, "Speech is currently admin-disabled.")
+ return
+
+ //handle muting and automuting
+ if(prefs.muted & MUTE_PM)
+ to_chat(src, "Error: Mentor-PM: You cannot send adminhelps (Muted).")
+ return
+ if(handle_spam_prevention())
+ return
+ var/msg = sanitize(input(src, "Please describe your game problem concisely and an mentor will help as soon as they're able.", "Mentorhelp contents") as message|null)
if(!msg)
return
- mentorhelp(msg)
+
+ //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, "Mentor-PM to-Mentors: [msg]")
+ return
+ else
+ to_chat(usr, "Ticket not found, creating new one...")
+ else
+ current_mentorhelp.AddInteraction("[key_name(usr)] opened a new ticket.")
+ current_mentorhelp.Resolve()
+
+ new /datum/mentor_help(msg, src, FALSE)
+
+//admin proc
+
+/proc/message_mentors(var/msg)
+ msg = " [msg]"
+
+ for(var/client/C in mentors)
+ to_chat(C, msg)
+ for(var/client/C in admins)
+ to_chat(C, msg)
diff --git a/code/modules/mentor/mentorpm(old).dm b/code/modules/mentor/mentorpm(old).dm
new file mode 100644
index 000000000000..3689af6f8a88
--- /dev/null
+++ b/code/modules/mentor/mentorpm(old).dm
@@ -0,0 +1,88 @@
+/client/proc/cmd_mentor_pm(client/C, msg)
+ if(prefs.muted & MUTE_PM || IS_ON_ADMIN_CD(src, ADMIN_CD_PM))
+ to_chat(src, "Error: Private-Message: You are unable to use PM-s (muted).")
+ return
+
+ if(!isclient(C))
+ if(holder)
+ to_chat(src, "Error: Private-Message: Client not found.")
+ else
+ mentorhelp(msg) //admin/mentor we are replying to left. mentorhelp instead
+ return
+
+ //get message text, limit it's length.and clean/escape html
+ if(!msg)
+ msg = sanitize(input(src,"Message:", "Private message to [key_name(C, 0, holder ? 1 : 0, holder ? 1 : 0)]") as text|null)
+
+ if(!msg)
+ return
+ if(!C)
+ if(holder)
+ to_chat(src, "Error: Admin-PM: Client not found.")
+ else
+ mentorhelp(msg) //admin/mentor we are replying to has vanished, mentorhelp instead
+ return
+
+ if (handle_spam_prevention(msg, ADMIN_CD_PM))
+ return
+
+ var/recieve_color = "purple"
+ var/send_pm_type = " "
+ var/recieve_pm_type = "Player"
+
+ if(holder)
+ //mentor PMs are maroon
+ //PMs sent from admins display their rank
+ if(C.holder && (holder.rights & R_ADMIN))
+ recieve_color = "red"
+ else
+ recieve_color = "maroon"
+ send_pm_type = holder.rank + " "
+ if(!C.holder && holder && holder.fakekey)
+ recieve_pm_type = "Admin"
+ else
+ recieve_pm_type = holder.rank
+ else if(src in mentors)
+ recieve_color = "maroon"
+ send_pm_type = "Mentor "
+ recieve_pm_type = "Mentor"
+ else if(!C.holder && !(C in mentors))
+ to_chat(src, "Error: Admin-PM: Non-admin to non-admin PM communication is forbidden.")
+ return
+
+ var/recieve_message = ""
+
+ if(((src in mentors) || holder) && !C.holder)
+ recieve_message = "-- Нажмите на имя [recieve_pm_type]'а для ответа --\n"
+ if(C.mentorhelped)
+ to_chat(C, recieve_message)
+ C.mentorhelped = FALSE
+
+ recieve_message = "[recieve_pm_type] PM from-[get_options_bar(src, C.holder ? 1 : 0, C.holder ? 1 : 0, 1, null, TRUE)]: [msg]"
+ to_chat(C, recieve_message)
+ to_chat(src, "[send_pm_type]PM to-[get_options_bar(C, holder ? 1 : 0, holder ? 1 : 0, 1, null, TRUE)]: [msg]")
+
+ //play the recieving admin the adminhelp sound (if they have them enabled)
+ //non-admins shouldn't be able to disable this
+ C.mob.playsound_local(null, C.bwoink_sound, VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
+
+ log_admin("PM: [key_name(src)]->[key_name(C)]: [msg]")
+ world.send2bridge(
+ type = list(BRIDGE_ADMINLOG),
+ attachment_title = "MENTOR PM",
+ attachment_msg = "**[key_name(src)]->[key_name(C)]:** [msg]",
+ attachment_color = BRIDGE_COLOR_ADMINLOG,
+ )
+
+ //we don't use message_admins here because the sender/receiver might get it too
+ for(var/client/X as anything in admins)
+ //check client/X is an admin and isn't the sender or recipient
+ if(X == C || X == src)
+ continue
+ if(X.key != key && X.key != C.key && X.holder.rights & R_ADMIN)
+ to_chat(X, "PM: [key_name(src, X, 0)]->[key_name(C, X, 0)]: [msg]")//inform X
+ for(var/client/X in mentors)
+ if(X == C || X == src)
+ continue
+ if(X.key != key && X.key != C.key && !C.holder && !src.holder)
+ to_chat(X, "PM: [key_name(src, X, 0, 0)]->[key_name(C, X, 0, 0)]: [msg]")//inform X
diff --git a/code/modules/mentor/mentorpm.dm b/code/modules/mentor/mentorpm.dm
index 3689af6f8a88..494344af9f0b 100644
--- a/code/modules/mentor/mentorpm.dm
+++ b/code/modules/mentor/mentorpm.dm
@@ -1,88 +1,172 @@
-/client/proc/cmd_mentor_pm(client/C, msg)
- if(prefs.muted & MUTE_PM || IS_ON_ADMIN_CD(src, ADMIN_CD_PM))
- to_chat(src, "Error: Private-Message: You are unable to use PM-s (muted).")
+//allows right clicking mobs to send an admin PM to their client, forwards the selected mob's client to cmd_admin_pm
+/client/proc/cmd_admin_pm_context(mob/M as mob in mob_list)
+ set category = null
+ set name = "Mentor PM Mob"
+ if(!check_rights(R_ADMIN))
+ to_chat_admin_pm(src, "Error: Mentor-PM-Context: Only administrators may use this command.")
return
+ if( !ismob(M) || !M.client ) return
+ cmd_admin_pm(M.client,null)
+ feedback_add_details("admin_verb","APMM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
- if(!isclient(C))
- if(holder)
- to_chat(src, "Error: Private-Message: Client not found.")
+//shows a list of clients we could send PMs to, then forwards our choice to cmd_admin_pm
+/client/proc/cmd_admin_pm_panel()
+ set category = "Admin"
+ set name = "Admin PM"
+ if(!check_rights(R_ADMIN))
+ to_chat_admin_pm(src, "Error: Mentor-PM-Panel: Only administrators may use this command.")
+ return
+ var/list/client/targets[0]
+ for(var/client/T)
+ if(T.mob)
+ if(isnewplayer(T.mob))
+ targets["(New Player) - [T]"] = T
+ else if(isobserver(T.mob))
+ targets["[T.mob.name](Ghost) - [T]"] = T
+ else
+ targets["[T.mob.real_name](as [T.mob.name]) - [T]"] = T
else
- mentorhelp(msg) //admin/mentor we are replying to left. mentorhelp instead
+ targets["(No Mob) - [T]"] = T
+ var/list/sorted = sortList(targets)
+ var/target = input(src,"To whom shall we send a message?","Admin PM",null) in sorted|null
+ cmd_admin_pm(targets[target],null)
+ feedback_add_details("admin_verb","APM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+
+/client/proc/cmd_ahelp_reply(whom, reply_type)
+ if(prefs.muted & MUTE_PM || IS_ON_ADMIN_CD(src, ADMIN_CD_PM))
+ to_chat_admin_pm(src, "Error: Admin-PM: You are unable to use admin PM-s (muted).")
+ return
+ var/client/C
+ if(isclient(whom))
+ C = whom
+ if(!C)
+ if(holder)
+ to_chat_admin_pm(src, "Error: Admin-PM: Client not found.")
return
+ if(src in mentors)
+ message_mentors("[key_name(src, 0, 0, 0)] has started replying to [key_name(C, 0, 0, 0)]'s help request.")
+ message_admins("[key_name_admin(src)] has started replying to [key_name(C, 0, 0)]'s help request.")
+ var/msg = sanitize(input(src,"Message:", "Private message to [key_name(C, 0, 0)]") as text|null)
+ if (!msg)
+ message_admins("[key_name_admin(src)] has cancelled their reply to [key_name(C, 0, 0)]'s help request.")
+ if(src in mentors)
+ message_mentors("[key_name(src, 0, 0, 0)] has cancelled their reply to [key_name(C, 0, 0, 0)]'s help request.")
+ return
+ if(reply_type != MHELP_REPLY)
+ cmd_admin_pm(whom, msg)
+ else
+ if(!holder && mob.mind && mob.mind.special_role && !(src in mentors))
+ to_chat_admin_pm(src, "You cannot ask mentors for help while being antag. File a ticket instead if you wish question this to admins.")
+ return
+ cmd_mentor_pm(whom, msg)
+
+//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)
+ if(prefs.muted & MUTE_PM || IS_ON_ADMIN_CD(src, ADMIN_CD_PM))
+ to_chat_admin_pm(src, "Error: Private-Message: You are unable to use PM-s (muted).")
+ return
+
+ if(!holder && !current_ticket) //no ticket? https://www.youtube.com/watch?v=iHSPf6x1Fdo
+ to_chat_admin_pm(src, "You can no longer reply to this ticket, please open another one by using the Adminhelp verb if need be.")
+ if(msg)
+ to_chat_admin_pm(src, "Message: [msg]")
+ return
+
+ var/client/recipient
+ if(isclient(whom))
+ recipient = whom
+
+ if(!recipient)
+ if(holder)
+ to_chat_admin_pm(src, "Error: Admin-PM: Client not found.")
+ if(msg)
+ to_chat_admin_pm(src, "Returned message: [msg]") // this just returns original msg back, so you can copy and paste again or whatever.
+ return
+ else if(msg) // you want to continue if there's no message instead of returning now
+ current_ticket.MessageNoRecipient(msg)
+ return
//get message text, limit it's length.and clean/escape html
if(!msg)
- msg = sanitize(input(src,"Message:", "Private message to [key_name(C, 0, holder ? 1 : 0, holder ? 1 : 0)]") as text|null)
-
+ msg = sanitize(input(src,"Message:", "Private message to [key_name(recipient, 0, holder ? 1 : 0, holder ? 1 : 0)]") as text|null)
if(!msg)
return
- if(!C)
+
+ if(prefs.muted & MUTE_PM || IS_ON_ADMIN_CD(src, ADMIN_CD_PM)) // maybe client were muted while typing input.
+ to_chat_admin_pm(src, "Error: Admin-PM: You are unable to use admin PM-s (muted).")
+ return
+
+ if(!recipient)
if(holder)
- to_chat(src, "Error: Admin-PM: Client not found.")
+ to_chat_admin_pm(src, "Error: Admin-PM: Client not found.")
+ to_chat_admin_pm(src, "Returned message: [msg]")
else
- mentorhelp(msg) //admin/mentor we are replying to has vanished, mentorhelp instead
+ current_ticket.MessageNoRecipient(msg)
return
- if (handle_spam_prevention(msg, ADMIN_CD_PM))
+ if (handle_spam_prevention(msg,ADMIN_CD_PM))
return
- var/recieve_color = "purple"
- var/send_pm_type = " "
- var/recieve_pm_type = "Player"
+ if(recipient.holder)
+ if(holder) //both are admins
+ to_chat_admin_pm(recipient, "Admin PM from-[key_name(src, recipient, 1)]: [msg]")
- if(holder)
- //mentor PMs are maroon
- //PMs sent from admins display their rank
- if(C.holder && (holder.rights & R_ADMIN))
- recieve_color = "red"
- else
- recieve_color = "maroon"
- send_pm_type = holder.rank + " "
- if(!C.holder && holder && holder.fakekey)
- recieve_pm_type = "Admin"
- else
- recieve_pm_type = holder.rank
- else if(src in mentors)
- recieve_color = "maroon"
- send_pm_type = "Mentor "
- recieve_pm_type = "Mentor"
- else if(!C.holder && !(C in mentors))
- to_chat(src, "Error: Admin-PM: Non-admin to non-admin PM communication is forbidden.")
- return
+ to_chat_admin_pm(src, "Admin PM to-[key_name(recipient, src, 1)]: [msg]")
- var/recieve_message = ""
+ //omg this is dumb, just fill in both their tickets
+ var/interaction_message = "PM from-[key_name(src, recipient, 1)] to-[key_name(recipient, src, 1)]: [msg]"
+ admin_ticket_log(src, interaction_message)
+ if(recipient != src) //reeee
+ admin_ticket_log(recipient, interaction_message)
- if(((src in mentors) || holder) && !C.holder)
- recieve_message = "-- Нажмите на имя [recieve_pm_type]'а для ответа --\n"
- if(C.mentorhelped)
- to_chat(C, recieve_message)
- C.mentorhelped = FALSE
+ else //recipient is an admin but sender is not
+ if(!current_ticket)
+ to_chat_admin_pm(src, "You can no longer reply to this ticket, please open another one by using the Adminhelp verb if need be.")
- recieve_message = "[recieve_pm_type] PM from-[get_options_bar(src, C.holder ? 1 : 0, C.holder ? 1 : 0, 1, null, TRUE)]: [msg]"
- to_chat(C, recieve_message)
- to_chat(src, "[send_pm_type]PM to-[get_options_bar(C, holder ? 1 : 0, holder ? 1 : 0, 1, null, TRUE)]: [msg]")
+ to_chat_admin_pm(src, "Message: [msg]")
+ return
+ else
+ var/replymsg = "Reply PM from-[key_name(src, recipient, 1)]: [msg]"
- //play the recieving admin the adminhelp sound (if they have them enabled)
- //non-admins shouldn't be able to disable this
- C.mob.playsound_local(null, C.bwoink_sound, VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
+ to_chat_admin_pm(recipient, "[replymsg]")
+ to_chat_admin_pm(src, "PM to-Admins: [msg]")
+
+ admin_ticket_log(src, "[replymsg]")
+
+ //play the receiving admin the adminhelp sound (if they have them enabled)
+ recipient.mob.playsound_local(null, recipient.bwoink_sound, VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
+
+ 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)
+
+ var/recipmsg = "-- Administrator private message --
" + \
+ "Admin PM from-[key_name(src, recipient, 0)]: [msg]
" + \
+ "Нажмите на имя администратора для ответа."
+ to_chat_admin_pm(recipient, recipmsg)
+ to_chat_admin_pm(src, "Admin PM to-[key_name(recipient, src, 1)]: [msg]")
+
+ admin_ticket_log(recipient, "PM From [key_name_admin(src)]: [msg]")
+
+ //always play non-admin recipients the adminhelp sound
+ recipient.mob.playsound_local(null, 'sound/effects/adminhelp.ogg', VOL_NOTIFICATIONS, vary = FALSE, ignore_environment = TRUE)
+
+ else //neither are admins
+ to_chat_admin_pm(src, "Error: Admin-PM: Non-admin to non-admin PM communication is forbidden.")
+ return
- log_admin("PM: [key_name(src)]->[key_name(C)]: [msg]")
world.send2bridge(
type = list(BRIDGE_ADMINLOG),
- attachment_title = "MENTOR PM",
- attachment_msg = "**[key_name(src)]->[key_name(C)]:** [msg]",
+ attachment_title = "PM",
+ attachment_msg = "**[key_name(src)]->[key_name(recipient)]:** [msg]",
attachment_color = BRIDGE_COLOR_ADMINLOG,
)
-
+ window_flash(recipient)
+ log_admin_private("[key_name(src)]->[key_name(recipient)]: [msg]")
//we don't use message_admins here because the sender/receiver might get it too
- for(var/client/X as anything in admins)
- //check client/X is an admin and isn't the sender or recipient
- if(X == C || X == src)
- continue
- if(X.key != key && X.key != C.key && X.holder.rights & R_ADMIN)
- to_chat(X, "PM: [key_name(src, X, 0)]->[key_name(C, X, 0)]: [msg]")//inform X
- for(var/client/X in mentors)
- if(X == C || X == src)
- continue
- if(X.key != key && X.key != C.key && !C.holder && !src.holder)
- to_chat(X, "PM: [key_name(src, X, 0, 0)]->[key_name(C, X, 0, 0)]: [msg]")//inform X
+ for(var/client/X in global.admins)
+ if(X.key != key && X.key != recipient.key) //check client/X is an admin and isn't the sender or recipient
+ to_chat_admin_pm(X, "PM: [key_name(src, 1, 0)]->[key_name(recipient, 1, 0)]: [msg]")
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 3299899d139f..fe815de46e40 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -679,6 +679,11 @@ note dizziness decrements automatically in the mob's Life() proc.
if(client && client.holder)
if(statpanel("Tickets"))
global.ahelp_tickets.stat_entry()
+
+ if(has_mentor_powers(client))
+ if(statpanel("Tickets"))
+ global.mhelp_tickets.stat_entry()
+
if(client.holder.rights & R_ADMIN)
if(statpanel("MC"))
stat("CPU:", "[world.cpu]")
diff --git a/code/modules/tgui/states/mentor.dm b/code/modules/tgui/states/mentor.dm
new file mode 100644
index 000000000000..f7409e081601
--- /dev/null
+++ b/code/modules/tgui/states/mentor.dm
@@ -0,0 +1,12 @@
+ /**
+ * tgui state: mentor_state
+ *
+ * Checks that the user is an mentor.
+ **/
+
+var/global/datum/tgui_state/mentor_state/mentor_state = new
+
+/datum/tgui_state/mentor_state/can_use_topic(src_object, mob/user)
+ if(has_mentor_powers(user.client))
+ return UI_INTERACTIVE
+ return UI_CLOSE
diff --git a/sound/effects/mentorhelp.ogg b/sound/effects/mentorhelp.ogg
new file mode 100644
index 000000000000..ed784c038228
Binary files /dev/null and b/sound/effects/mentorhelp.ogg differ
diff --git a/taucetistation.dme b/taucetistation.dme
index a94c7d845e88..1974455d3f30 100644
--- a/taucetistation.dme
+++ b/taucetistation.dme
@@ -1824,8 +1824,8 @@
#include "code\modules\media\broadcast\transmitters\broadcast.dm"
#include "code\modules\memories\memories.dm"
#include "code\modules\mentor\mentor.dm"
-#include "code\modules\mentor\mentorhelp.dm"
#include "code\modules\mentor\mentorpm.dm"
+#include "code\modules\mentor\mentorhelp.dm"
#include "code\modules\metahelp\metahelp.dm"
#include "code\modules\minigames\minesweeper.dm"
#include "code\modules\mining\abandonedcrates.dm"
@@ -2507,6 +2507,7 @@
#include "code\modules\tgui\states\interactive_reach.dm"
#include "code\modules\tgui\states\inventory.dm"
#include "code\modules\tgui\states\machinery_state.dm"
+#include "code\modules\tgui\states\mentor.dm"
#include "code\modules\tgui\states\not_incapacitated.dm"
#include "code\modules\tgui\states\notcontained.dm"
#include "code\modules\tgui\states\observer.dm"
diff --git a/tgui/packages/tgui/interfaces/MentorTicketPanel.tsx b/tgui/packages/tgui/interfaces/MentorTicketPanel.tsx
new file mode 100644
index 000000000000..b071c4fa0d8f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/MentorTicketPanel.tsx
@@ -0,0 +1,84 @@
+/* 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(context);
+ const {
+ id,
+ title,
+ name,
+ state,
+ opened_at,
+ closed_at,
+ opened_at_date,
+ closed_at_date,
+ actions,
+ log,
+ } = data;
+ return (
+
+
+
+
+
+
+ );
+};