From a7002ecdf1ae7bb7068de486a2a53b1397d1db95 Mon Sep 17 00:00:00 2001 From: lm40 Date: Fri, 12 Aug 2022 22:29:15 -0400 Subject: [PATCH] Adds mentors Adds mentors --- code/_global_vars/mobs.dm | 2 + code/controllers/configuration.dm | 12 +- code/game/world.dm | 27 +- code/modules/admin/admin_verb_lists_vr.dm | 6 +- code/modules/admin/topic.dm | 11 + code/modules/client/client procs.dm | 16 + code/modules/client/verbs/who.dm | 24 +- code/modules/mentor/mentor.dm | 234 +++++++++++++ code/modules/mentor/mentorhelp.dm | 407 ++++++++++++++++++++++ code/modules/vchat/css/ss13styles.css | 1 + code/stylesheet.dm | 1 + icons/chattags.dmi | Bin 1674 -> 1869 bytes sound/effects/mentorhelp.mp3 | Bin 0 -> 7334 bytes vorestation.dme | 2 + 14 files changed, 722 insertions(+), 21 deletions(-) create mode 100644 code/modules/mentor/mentor.dm create mode 100644 code/modules/mentor/mentorhelp.dm create mode 100644 sound/effects/mentorhelp.mp3 diff --git a/code/_global_vars/mobs.dm b/code/_global_vars/mobs.dm index 7e60dc71ea..9e1027bc88 100644 --- a/code/_global_vars/mobs.dm +++ b/code/_global_vars/mobs.dm @@ -1,5 +1,7 @@ GLOBAL_LIST_EMPTY(admins) //all clients whom are admins GLOBAL_PROTECT(admins) +GLOBAL_LIST_EMPTY(mentors) +GLOBAL_PROTECT(mentors) GLOBAL_LIST_EMPTY(deadmins) //all ckeys who have used the de-admin verb. GLOBAL_LIST_EMPTY(stealthminID) GLOBAL_LIST_EMPTY(directory) //all ckeys with associated client diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index 2da3f71025..2ebe6ea8c1 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -78,6 +78,7 @@ var/list/gamemode_cache = list() var/static/kick_inactive = 0 //force disconnect for inactive players after this many minutes, if non-0 var/static/show_mods = 0 var/static/show_devs = 0 + var/static/show_mentors = 0 var/static/show_event_managers = 0 var/static/mods_can_tempban = 0 var/static/mods_can_job_tempban = 0 @@ -216,7 +217,7 @@ var/list/gamemode_cache = list() var/python_path = "" //Path to the python executable. Defaults to "python" on windows and "/usr/bin/env python2" on unix var/use_lib_nudge = 0 //Use the C library nudge instead of the python nudge. var/use_overmap = 0 - + var/static/list/engine_map = list("Supermatter Engine", "Edison's Bane") // Comma separated list of engines to choose from. Blank means fully random. // Event settings @@ -289,7 +290,7 @@ var/list/gamemode_cache = list() // How strictly the loadout enforces object species whitelists var/loadout_whitelist = LOADOUT_WHITELIST_LAX - + var/static/vgs_access_identifier = null // VOREStation Edit - VGS var/static/vgs_server_port = null // VOREStation Edit - VGS @@ -652,6 +653,9 @@ var/list/gamemode_cache = list() if("show_devs") config.show_devs = 1 + if("show_mentors") + config.show_mentors = 1 + if("show_event_managers") config.show_event_managers = 1 @@ -955,10 +959,10 @@ var/list/gamemode_cache = list() if("suggested_byond_version") config.suggested_byond_version = text2num(value) - + if("suggested_byond_build") config.suggested_byond_build = text2num(value) - + // VOREStation Edit Start - Can't be in _vr file because it is loaded too late. if("vgs_access_identifier") config.vgs_access_identifier = value diff --git a/code/game/world.dm b/code/game/world.dm index b962eb028f..01416b57ba 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -113,7 +113,7 @@ var/world_topic_spam_protect_time = world.timeofday s["players"] = 0 s["stationtime"] = stationtime2text() s["roundduration"] = roundduration2text() - s["map"] = strip_improper(using_map.full_name) //Done to remove the non-UTF-8 text macros + s["map"] = strip_improper(using_map.full_name) //Done to remove the non-UTF-8 text macros if(input["status"] == "2") // Shiny new hip status. var/active = 0 @@ -494,12 +494,9 @@ var/world_topic_spam_protect_time = world.timeofday if (copytext(line, 1, 2) == ";") continue - var/title = "Mentor" - var/rights = admin_ranks[title] - var/ckey = copytext(line, 1, length(line)+1) - var/datum/admins/D = new /datum/admins(title, rights, ckey) - D.associate(GLOB.directory[ckey]) + var/datum/mentor/M = new /datum/mentor(ckey) + M.associate(GLOB.directory[ckey]) /world/proc/update_status() var/s = "" @@ -529,7 +526,7 @@ var/world_topic_spam_protect_time = world.timeofday features += config.abandon_allowed ? "respawn" : "no respawn" features += config.persistence_disabled ? "persistence disabled" : "persistence enabled" - + features += config.persistence_ignore_mapload ? "persistence mapload disabled" : "persistence mapload enabled" if (config && config.allow_vote_mode) @@ -663,7 +660,7 @@ var/failed_old_db_connections = 0 if(dbcon_old?.IsConnected()) results += "WARNING: dbcon_old is connected, not touching it, but is this intentional?" - + if(!config.sql_enabled) results += "stopping because config.sql_enabled = false" else @@ -672,7 +669,7 @@ var/failed_old_db_connections = 0 results += "SUCCESS: set up a connection successfully with setup_database_connection()" else results += "FAIL: failed to connect to the database with setup_database_connection()" - + results += "-- DB Reset End --" to_world_log(results.Join("\n")) @@ -681,11 +678,11 @@ var/failed_old_db_connections = 0 if(!istype(GLOB.players_by_zlevel, /list)) GLOB.players_by_zlevel = new /list(world.maxz, 0) GLOB.living_players_by_zlevel = new /list(world.maxz, 0) - + while(GLOB.players_by_zlevel.len < world.maxz) GLOB.players_by_zlevel.len++ GLOB.players_by_zlevel[GLOB.players_by_zlevel.len] = list() - + GLOB.living_players_by_zlevel.len++ GLOB.living_players_by_zlevel[GLOB.living_players_by_zlevel.len] = list() @@ -727,10 +724,10 @@ var/global/game_id = null game_id = "" var/list/c = list( - "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", - "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", - "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", + "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" ) var/l = c.len diff --git a/code/modules/admin/admin_verb_lists_vr.dm b/code/modules/admin/admin_verb_lists_vr.dm index b871c3315b..3d8ec234fa 100644 --- a/code/modules/admin/admin_verb_lists_vr.dm +++ b/code/modules/admin/admin_verb_lists_vr.dm @@ -7,6 +7,8 @@ var/list/admin_verbs_default = list( /client/proc/cmd_admin_say, //VOREStation Add, /client/proc/cmd_mod_say, //VOREStation Add, /client/proc/cmd_event_say, //VOREStation Add, + /client/proc/cmd_mentor_ticket_panel, + /client/proc/cmd_mentor_say // /client/proc/hide_verbs, //hides all our adminverbs, //VOREStation Remove, // /client/proc/hide_most_verbs, //hides all our hideable adminverbs, //VOREStation Remove, // /client/proc/debug_variables, //allows us to -see- the variables of any instance in the game. +VAREDIT needed to modify, //VOREStation Remove, @@ -121,7 +123,9 @@ var/list/admin_verbs_admin = list( /datum/admins/proc/set_uplink, //VOREStation Add, /datum/admins/proc/sendFax, /client/proc/despawn_player, - /datum/admins/proc/view_feedback + /datum/admins/proc/view_feedback, + /client/proc/make_mentor, + /client/proc/unmake_mentor ) var/list/admin_verbs_ban = list( diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 57ce647a1a..0f6ef61ff4 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -24,6 +24,17 @@ else if(href_list["ahelp_tickets"]) GLOB.ahelp_tickets.BrowseTickets(text2num(href_list["ahelp_tickets"])) + if(href_list["mhelp"]) + var/mhelp_ref = href_list["mhelp"] + var/datum/mentor_help/MH = locate(mhelp_ref) + if (MH) + MH.Action(href_list["mhelp_action"]) + else + to_chat(usr, "Ticket [mhelp_ref] has been deleted!") + + if (href_list["mhelp_tickets"]) + GLOB.mhelp_tickets.BrowseTickets(text2num(href_list["mhelp_tickets"])) + if(href_list["dbsearchckey"] || href_list["dbsearchadmin"]) var/adminckey = href_list["dbsearchadmin"] diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index 0f0a5547ac..d805898b4f 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -60,6 +60,14 @@ cmd_admin_pm(C,null) return + if(href_list["mentorhelp_msg"]) + var/client/C = locate(href_list["mentorhelp_msg"]) + if(ismob(C)) + var/mob/M = C + C = M.client + cmd_mentor_pm(C, null) + return + if(href_list["irc_msg"]) if(!holder && received_irc_pm < world.time - 6000) //Worse they can do is spam IRC for 10 minutes to_chat(usr, "You are no longer able to use this, it's been more then 10 minutes since an admin on IRC has responded to you") @@ -121,6 +129,7 @@ switch(href_list["_src_"]) if("holder") hsrc = holder + if("mentorholder") hsrc = (check_rights(R_ADMIN, 0) ? holder : mentorholder) if("usr") hsrc = mob if("prefs") return prefs.process_link(usr,href_list) if("vars") return view_var_Topic(href,href_list,hsrc) @@ -183,6 +192,10 @@ GLOB.admins += src holder.owner = src + mentorholder = mentor_datums[ckey] + if (mentorholder) + mentorholder.associate(GLOB.directory[ckey]) + //preferences datum - also holds some persistant data for the client (because we may as well keep these datums to a minimum) prefs = preferences_datums[ckey] if(!prefs) @@ -260,6 +273,9 @@ if(holder) holder.owner = null GLOB.admins -= src + if (mentorholder) + mentorholder.owner = null + GLOB.mentors -= src GLOB.ahelp_tickets.ClientLogout(src) GLOB.directory -= ckey GLOB.clients -= src diff --git a/code/modules/client/verbs/who.dm b/code/modules/client/verbs/who.dm index 445acdc1c7..ab646448c2 100644 --- a/code/modules/client/verbs/who.dm +++ b/code/modules/client/verbs/who.dm @@ -83,7 +83,7 @@ else if(check_rights(R_STEALTH, FALSE, C)) // event managers //VOREStation Edit: Retired Staff category = R_EVENT num_event_managers_online++ - + temp += "\t[C] is a [C.holder.rank]" if(holder) if(C.holder.fakekey) @@ -121,6 +121,28 @@ if(config.show_event_managers) msg += "\n Current Miscellaneous ([num_event_managers_online]):\n" + eventMmsg + var/num_mentors_online = 0 + var/mmsg = "" + + for(var/client/C in GLOB.mentors) + num_mentors_online++ + mmsg += "\t[C] is a Mentor" + if(holder) + if(isobserver(C.mob)) + mmsg += " - Observing" + else if(istype(C.mob,/mob/new_player)) + mmsg += " - Lobby" + else + mmsg += " - Playing" + + if(C.is_afk()) + var/seconds = C.last_activity_seconds() + mmsg += " (AFK - [round(seconds / 60)] minutes, [seconds % 60] seconds)" + mmsg += "\n" + + if(config.show_mentors) + msg += "\n Current Mentors ([num_mentors_online]):\n" + mmsg + msg += "\nAdminhelps are also sent to Discord. If no admins are available in game try anyway and an admin on Discord may see it and respond." to_chat(src, msg) diff --git a/code/modules/mentor/mentor.dm b/code/modules/mentor/mentor.dm new file mode 100644 index 0000000000..fe7a11721f --- /dev/null +++ b/code/modules/mentor/mentor.dm @@ -0,0 +1,234 @@ +/client + var/datum/mentor/mentorholder = null + +var/list/mentor_datums = list() + +var/list/mentor_verbs_default = list( + /client/proc/cmd_mentor_ticket_panel, + /client/proc/cmd_mentor_say, + /client/proc/cmd_dementor +) + +/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_datums[ckey] = src + +/datum/mentor/proc/associate(client/C) + if(istype(C)) + owner = C + owner.mentorholder = src + owner.add_mentor_verbs() + GLOB.mentors |= C + +/datum/mentor/proc/disassociate() + if(owner) + GLOB.mentors -= owner + owner.remove_mentor_verbs() + owner.mentorholder = null + +/client/proc/add_mentor_verbs() + if(mentorholder) + verbs += mentor_verbs_default + +/client/proc/remove_mentor_verbs() + if(mentorholder) + verbs -= mentor_verbs_default + +/client/proc/make_mentor() + set category = "Special Verbs" + set name = "Make Mentor" + if(!holder) + to_chat(src, "Error: Only administrators may use this command.") + return + var/list/client/targets[0] + for(var/client/T in GLOB.clients) + targets["[T.key]"] = T + var/target = tgui_input_list(src,"Who do you want to make a mentor?","Make Mentor", sortList(targets)) + if(!target) + return + var/client/C = targets[target] + if(C.holder) + to_chat(src, "Error: You cannot make an admin a mentor.") + return + var/datum/mentor/M = new /datum/mentor(C.key) + M.associate(C) + to_chat(C, "You have been granted mentorship.") + to_chat(src, "You have made [C] a mentor.") + log_admin("[key_name(src)] made [key_name(C)] a mentor.") + feedback_add_details("admin_verb","Make Mentor") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/proc/unmake_mentor() + set category = "Special Verbs" + set name = "Unmake Mentor" + if(!holder) + to_chat(src, "Error: Only administrators may use this command.") + return + var/list/client/targets[0] + for(var/client/T in GLOB.mentors) + targets["[T.key]"] = T + var/target = tgui_input_list(src,"Which mentor do you want to unmake?","Unmake Mentor", sortList(targets)) + if(!target) + return + var/client/C = targets[target] + C.mentorholder.disassociate() + to_chat(C, "Your mentorship has been revoked.") + to_chat(src, "You have revoked [C]'s mentorship.") + log_admin("[key_name(src)] revoked [key_name(C)]'s mentorship.") + feedback_add_details("admin_verb","Unmake Mentor") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/proc/cmd_mentor_say(msg as text) + set category = "Admin" + set name ="Mentorsay" + + //check rights + if (!src.mentorholder && !src.holder) + return + + msg = sanitize(msg) + if (!msg) + return + + log_admin("Mentorsay: [key_name(src)]: [msg]") + + for(var/client/C in GLOB.mentors) + to_chat(C, create_text_tag("mentor", "MENTOR:", C) + " [src]: [msg]") + for(var/client/C in GLOB.admins) + to_chat(C, create_text_tag("mentor", "MENTOR:", C) + " [src]: [msg]") + +/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 + + if(href_list["mhelp"]) + var/mhelp_ref = href_list["mhelp"] + var/datum/mentor_help/MH = locate(mhelp_ref) + if (MH) + MH.Action(href_list["mhelp_action"]) + else + to_chat(usr, "Ticket [mhelp_ref] has been deleted!") + + if (href_list["mhelp_tickets"]) + GLOB.mhelp_tickets.BrowseTickets(text2num(href_list["mhelp_tickets"])) + +/client/proc/cmd_dementor() + set category = "Admin" + set name = "De-mentor" + src.mentorholder.disassociate() + +/client/proc/cmd_mhelp_reply(whom) + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, "Error: Mentor-PM: You are unable to use admin PM-s (muted).") + return + var/client/C + if(istext(whom)) + C = GLOB.directory[whom] + else if(istype(whom,/client)) + C = whom + if(!C) + if(holder) + 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 = tgui_input_text(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) + +/client/proc/cmd_mentor_pm(whom, msg, datum/mentor_help/MH) + set category = "Admin" + set name = "Mentor-PM" + set hidden = 1 + + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, "Error: Mentor-PM: You are unable to use admin PM-s (muted).") + return + + if(!(mentorholder || holder) && !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 = GLOB.directory[whom] + + else if(istype(whom,/client)) + recipient = whom + + if(!recipient) + if(mentorholder) + to_chat(src, "Error: Mentor-PM: Client not found.") + to_chat(src, msg) + else + log_admin("Mentorhelp: [key_name(src)] sent [msg]") + current_mentorhelp.MessageNoRecipient(msg) + return + + //get message text, limit it's length.and clean/escape html + if(!msg) + msg = tgui_input_text(src,"Message:", "Mentor-PM to [whom]") + + if(!msg) + return + + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, "Error: Mentor-PM: You are unable to use admin PM-s (muted).") + return + + if(!recipient) + if(holder) + to_chat(src, "Error:Mentor-PM: Client not found.") + else + log_admin("Mentorhelp: [key_name(src)] sent [msg]") + current_mentorhelp.MessageNoRecipient(msg) + return + + if (src.handle_spam_prevention(msg,MUTE_ADMINHELP)) + 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 && !(recipient.mentorholder || recipient.holder)) + recipient.current_mentorhelp.AddInteraction(interaction_message) + if (src.current_mentorhelp && !(src.mentorholder || src.holder)) + 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 ((recipient.mentorholder || recipient.holder) && (src.mentorholder || src.holder)) + 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)] sent [msg] to [key_name(recipient)]") + + if(recipient.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) + recipient << 'sound/effects/mentorhelp.mp3' + + for(var/client/C in GLOB.mentors) + if (C != recipient && C != src) + to_chat(C, interaction_message) + for(var/client/C in GLOB.admins) + if (C != recipient && C != src) + to_chat(C, interaction_message) \ No newline at end of file diff --git a/code/modules/mentor/mentorhelp.dm b/code/modules/mentor/mentorhelp.dm new file mode 100644 index 0000000000..2dfd049359 --- /dev/null +++ b/code/modules/mentor/mentorhelp.dm @@ -0,0 +1,407 @@ +/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/closed_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/cstatclick = new(null, null, AHELP_CLOSED) + var/obj/effect/statclick/mticket_list/rstatclick = new(null, null, AHELP_RESOLVED) + +/datum/mentor_help_tickets/Destroy() + QDEL_LIST(active_tickets) + QDEL_LIST(closed_tickets) + QDEL_LIST(resolved_tickets) + QDEL_NULL(astatclick) + QDEL_NULL(cstatclick) + QDEL_NULL(rstatclick) + return ..() + +//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_CLOSED) + mticket_list = closed_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("[title]") + dat += "Refresh

" + for(var/datum/mentor_help/MH as anything in l2b) + dat += "Ticket #[MH.id]: [MH.initiator_ckey]: [MH.name]
" + + usr << browse(dat.Join(), "window=mhelp_list[state];size=600x480") + +//Tickets statpanel +/datum/mentor_help_tickets/proc/stat_entry() + var/num_disconnected = 0 + 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 = initiator.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)] sent [msg]") + MessageNoRecipient(msg) + //show it to the person adminhelping too + to_chat(C, "PM to-Mentors: [name]") + + GLOB.mhelp_tickets.active_tickets += src + +/datum/mentor_help/Destroy() + RemoveActive() + GLOB.mhelp_tickets.closed_tickets -= src + 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]" + . = " (RSLVE)" + +//private +/datum/mentor_help/proc/LinkedReplyName(ref_src) + if(!ref_src) + ref_src = "\ref[src]" + return "[initiator_ckey]" + +//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 admins will see this +/datum/mentor_help/proc/MessageNoRecipient(msg) + var/ref_src = "\ref[src]" + var/chat_msg = "Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)]: [msg]" + AddInteraction("[LinkedReplyName(ref_src)]: [msg]") + for (var/client/C in GLOB.mentors) + if (C.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) + C << 'sound/effects/mentorhelp.mp3' + for (var/client/C in GLOB.admins) + if (C.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_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, "This ticket is already open.") + return + + if(GLOB.mhelp_tickets.CKey2ActiveTicket(initiator_ckey)) + to_chat(usr, "This user already has an active ticket, cannot reopen this one.") + return + + statclick = new(null, src) + GLOB.mhelp_tickets.active_tickets += src + GLOB.mhelp_tickets.closed_tickets -= src + GLOB.mhelp_tickets.resolved_tickets -= src + switch(state) + if(AHELP_CLOSED) + feedback_dec("mhelp_close") + if(AHELP_RESOLVED) + feedback_dec("mhelp_resolve") + state = AHELP_ACTIVE + closed_at = null + if(initiator) + initiator.current_mentorhelp = src + + AddInteraction("Reopened by [usr]") + if(initiator) + to_chat(initiator, "Ticket [TicketHref("#[id]")] was reopened by [usr].") + var/msg = "Ticket [TicketHref("#[id]")] reopened by [usr]." + 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("Resolved by [usr].") + if(initiator) + to_chat(initiator, "Ticket [TicketHref("#[id]")] was marked resolved by [usr].") + if(!silent) + feedback_inc("mhelp_resolve") + var/msg = "Ticket [TicketHref("#[id]")] resolved by [usr]" + message_mentors(msg) + log_admin(msg) + +//Show the ticket panel +/datum/mentor_help/proc/TicketPanel() + 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" + if(AHELP_CLOSED) + dat += "CLOSED" + else + dat += "UNKNOWN" + dat += "[GLOB.TAB][TicketHref("Refresh", ref_src)]" + if(state != AHELP_ACTIVE) + dat += "[GLOB.TAB][TicketHref("Reopen", ref_src, "reopen")]" + dat += "

Opened at: [gameTimestamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)" + if(closed_at) + dat += "
Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)" + dat += "

" + if(initiator) + dat += "Actions: [FullMonty(ref_src)]
" + else + dat += "DISCONNECTED[GLOB.TAB][ClosureLinks(ref_src)]
" + dat += "
Log:

" + for(var/I in _interactions) + dat += "[I]
" + + usr << browse(dat.Join(), "window=mhelp[id];size=620x480") + +/datum/mentor_help/proc/FullMonty(ref_src) + if(!ref_src) + ref_src = "\ref[src]" + if(state == AHELP_ACTIVE) + . += ClosureLinks(ref_src) + +//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() + +// +// 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, "Speech is currently admin-disabled.") + return + + //handle muting and automuting + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, "Error: Mentor-PM: You cannot send adminhelps (Muted).") + 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)] sent [msg]") + current_mentorhelp.MessageNoRecipient(msg) + to_chat(usr, "PM to-Mentors: [msg]") + return + else + to_chat(usr, "Ticket not found, creating new one...") + else + current_mentorhelp.AddInteraction("[usr] opened a new ticket.") + current_mentorhelp.Resolve() + + new /datum/mentor_help(msg, src, FALSE) + +//admin proc +/client/proc/cmd_mentor_ticket_panel() + set name = "Show 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 = "Mentor: [msg]" + + for(var/client/C in GLOB.mentors) + to_chat(C, msg) + for(var/client/C in GLOB.admins) + to_chat(C, msg) \ No newline at end of file diff --git a/code/modules/vchat/css/ss13styles.css b/code/modules/vchat/css/ss13styles.css index b32cfd4ae3..3b1405f523 100644 --- a/code/modules/vchat/css/ss13styles.css +++ b/code/modules/vchat/css/ss13styles.css @@ -90,6 +90,7 @@ body.inverted { .pm .other {color: #0000ff;} /* Admin: Channels */ +.mentor_channel {color: #ff9933; font-weight: bold;} .mod_channel {color: #735638; font-weight: bold;} .mod_channel .admin {color: #b82e00; font-weight: bold;} .admin_channel {color: #9611D4; font-weight: bold;} diff --git a/code/stylesheet.dm b/code/stylesheet.dm index 0aa8ef5f3c..de6c4dd8a7 100644 --- a/code/stylesheet.dm +++ b/code/stylesheet.dm @@ -34,6 +34,7 @@ em {font-style: normal;font-weight: bold;} .pm .other {color: #0000ff;} /* Admin: Channels */ +.mentor_channel {color: #ff9933; font-weight: bold;} .mod_channel {color: #735638; font-weight: bold;} .mod_channel .admin {color: #b82e00; font-weight: bold;} .admin_channel {color: #9611D4; font-weight: bold;} diff --git a/icons/chattags.dmi b/icons/chattags.dmi index a0b1d335d0f6fa4667b9abcdb2f9f0b78e2d7753..252e7ae78e5cbfe3d34b7da208acd4c287e8fb2a 100644 GIT binary patch literal 1869 zcmV-T2eSByP)V=-0C=2@(6J7JFc5&@xqXTY>;}}ei-8aZc!j3g(>9?kDJRCacQA4Ey4~>Q z?()}g>4$NKZW`r|dGjfPg7cw~;^i$lP;fd*;m|)Efr7S?8yP}SP^EOMq_943prDoF zo=w2-{4)U~rOdLWWS6nQNB&~6tg&c#bWQfnqBZD(pVpw$lM0zOOBp4Hd|8Lf%9kz4 zk2^;GG2Ahd{uj80e(s=~M)?7;w6bW%kt_uO00vP>L_t(|ob8)WXd6cq#=lh!hQzfJ z+cX$@=tU%Tz!cU{8mIJ7NE{h*h=K)yLJ77^N_9!FLVf9>Mg|w>(3Xa%4yE9W7dycQ z%WWXAqBN#pl^$x^p!U#1@x=*|DTYw6dXUDRot>GT)t?<5_IHuy?d*Qq_h#PA?2ZiE zwvqT`cMQM`fEa=!xiZ^l0l<4tZ;b;O2k>@)`V)oI^9ulw-P_Z0|I^*wmeKpp@)&@n z0EHbtV)w(@ZUA!tPHNDfxY+;c9yH#62da^mvil_ADEFOPQMP$71K>G`^HW<@Yi@}D zoi68waCnZWj;vf|96*og$341y85h>p9A%mdYir6j0W%#?>~_SNw3B5vMbx|ek<+Ts zK_JIPHc`2blt-LNJ6UG)AQtia0|LS6q$3;8-sp8T(|!RUEtqZbxgmM$_Iv#{q5T2? zw9ZNO2Cap304PBr7qbJbicMiWD4{wCd*@<$P$qae)Z5uVqdEu+@1Nv#UY=*`h-{Ma zpwuqY&~G&9YoCU`va$Hlw)J@bcK~kz=nYbQWwy`yyYJ#JZ#;YS4}enuUJp=zqHubC z;oZ*8?H^yh)N=pR-Q89P0DShxnE-WZb^!pB-<9?tnUA-Ky=&R(msdadFJS2!tAO$K z$=|f5pkLoS2c7!Qw!hV??^+4pcz)JV<8IHk!*;i4n_Ev*mv=o1&s8`8-218QvP}+0 zU3S(sIUM!6v{Zlj#g)VFS5G|P+UPRRM$QxIN5Gi@-0j(B9d&5q1%s|Dva#2z5##Ce zAkJi+IyoE_+IWHUtAF*x1FlnP$FgP7c|860TnDr{%P+2YIaZkto{^VcLXKctdn3W% z_5exB2;sm>L8>UPgh{XP3Jj@tys0}pqtYuPnz`q#2+R(iEpn|^w=x4II3eDD-2bJ-x@ha!=g z#z1Z5fA7g&kkW?~YM;wx_d~^C^lcc4=*rm2TvkgLOk}2$wVT(L0&1Vy^bOoIE25(n z>T#}7dkkD*9`PEvAy>UhYU{8*ImZw@#gl*v^V1B&Z6o&jkPpHE=yTTrj%!nn zC*OI&VC%7;66C3m+#G%5#1SHg5J;VRwHwj*)&_u>X8_n1uZp7cTMO-|An~E;s|RR7 z*1}cmHxh(;^?B)wgJ%G=Z!ZhF9WT)Pqn8 zr=eB!t%TF7y;dSKo%F8TToRe-WO}vND%iHRm1x1Xwd;2P!1dw#xW0O-b*X^I@O=Oo z06=^`ADAr7Z7r%!jP-b2>%+LO-v1an_4BvlTJ@#7qvPxAj&go<)MX>~IuUB;Z(ef# zmOC3R8?iTl{GBvCkn$E@yuns+`bqS;?}TacqoXbdld{b5yg+e~zLSmL*>E|Slx2>m z1H}uPs=g4zkUQ4vIHli`cf#@Y^+tL{L*#E>as)@zbJ_*Zb%0_^cGc&lFTeP89I4Mc zj}446qLnouZsk5!&z=GB>o_t_G@}Rjbr@;w?0I!N9}qu`Q#nV02wE`z6%E!LO3l`1 zQ;vu8>(j4PoB?6W>XgE1sOD*C9B#rGj0s0AOL}2W&3xqr7tvnEuFA7h(X6F95A6D2>We(adQPH;XAO z%sfhd|M|aK?tf&eOS}?JGlR;P)X2^|rfOrD0i4b^w^ELBaVQ6KD}~}v?(jIV@#@iK z_Ikv2L=KyauQ~2twA-299qngVv}~vJ=HkBmf5OCJtRqFoWHtdYUfOXxiVv?I8z?q_ zj=CmL{`0_f|Ek*zdG**pu>qth?;M1?qje(Cxvnj6Ltn1WdhR@^M6P_PR|?0ly+ zGS!ug_D2G}|BV=-0C=2@(6J7JFcgL1xqXTo*bS&_7Xu*-@Cr?}SKEZPq+Bt+y@QFPx7!)M z;pDI3(huVd-89M_^X5|o1?NK}#mifApx|_r!l8dS0tIa&H!_5vpi1dhNnw57KtU_T zJ)3~v{WD7mGB)_gs*o&eEE*nNlYO&j4Z7gd8gzP6A=73lqvSBN(BZQ3WlQqoj*&lx zJ4Vv~0@u*b9dy$uKif5@HFSgl(f|Mjlu1NERCt{2TTN&iM-={c5eOHqrK++_f>7?~*RSJR zt>(L@ajjNUmWlWiPz*a_3~gkXOc9Yoo)|+L877nQn^6aF(s1ek-2>2#1OOKbxyTkc z6`4XkaQmM^ev;R`JZE)8vXFY`y_M4F${-2H5$gB6}?NTzSjlYyk+6e&o>d(cP zxe1N|03Ut*nI8Ib4aWB${Gl}ke*gFqbn*u}!P~Dc_%wFs4t=uSxkIj=NSF6M3eqkV z03Q9?3CK2AQUN(D+gwS>UcF<2Q-OEq4%vYYWISikx*{3-zCIRs{5%k2Sfe&qQbHNe zaV-C7AHWmx!uzCx8v!99d*Yzn36iLgIfm%nesw{`iPBC`240>MnL-hCr+yhUm!2ML z>%^9x9^1FdHEsO2%C%l6_)GV%n7MmcLK*<@&*6`#-Y>_wABb$R1OPCysjAi#0D$WK zvKIbqv6M5isp?AziqQV+8-wec=bmt7^gT~To)f?gn|Uwa8eAKd<$#RHSv@b`8t9W} z=zAK{Jwe)Va?Khgat`|(2hy;o4JX$;XRvzgrwH6eNCsl4WOl~_cE(kqVmvUU16ae^ zG#DpDp+7E+<+s4ib5CN4hDku$3G%{SB?T1IMB0UtnP-cdik=Flei<|i2Wf4cSm7XT z7Y@>Pwphx=%#CcZlq(#hd!68ZC%C;G2byp&;NMjHQ8qqg(S!rT1GJ}rPJY7!gq%V*;?nL<2Q_r3HWe|?zzymfs;HB5|TU`G( z9LLelp8$YHCZomuGyTuZw+$`)+W?GuCZjb4zPtJbbn=Iu6U@}>zWc`ddO$|xLqf>i zxU=RT%Y$bD8Ig|w{$GcE16f}W7)b8J4CgtDgV4VYXC6EY7)b8J45uB%a~jP*kxnpE zua6QHBcX9;&F36ZdAUtr$zka`dAJ5?FWulzqoe2{H6 z&`t|SC^s1}Yl!H(Fqw$|^qinnL$t>SgzpBA0qo2dJWn>Gk)ggP$mYJvGb4fx>2Vtm zL^vnt0~-Odh!A7wbAr6OAd~Tm;M6aJX47%Bbz)7&u`g=mzvwu2zXo`FKm~0pK6a~} z-vgR90KC8RGhRG?i_TFuHvZMESpb0H!SmWyB{pIjRy&U$cAn>c`sQCPDX_XVn=?E- z^5~hjT6v#Rd$|Iul}CHIGWedzczX0b`#vHYaJ7NY7e916N8LdY6rDR{kKL)BPYgS= zm^yE|30~N<0~LX%$2y92pikEVI)8TqD}d^OE1n+fDAs{AI!E2pPLS&WtCbIg_$Yc& zvH$vp(%C~*y)bY)!BJ9R92g}9q9S3n5m^4%!bKN2+UT$Xrvj@Dq!vwI5d?yTi!N}y z^NOro6{+*4JN3(8qUkh^Pp{-X4UG_*PSaSx834eG$8UGoNSGv6w`Ow-m(SS$0Wmm( Uca0(W<^TWy07*qoM6N<$f)TYDUH||9 diff --git a/sound/effects/mentorhelp.mp3 b/sound/effects/mentorhelp.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a72cec612ac766b006c80d72c5e0e95c929754a1 GIT binary patch literal 7334 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 literal 0 HcmV?d00001 diff --git a/vorestation.dme b/vorestation.dme index efe1c49cf6..0ed257300d 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -2624,6 +2624,8 @@ #include "code\modules\media\media_tracks.dm" #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"