diff --git a/code/game/world.dm b/code/game/world.dm index efe50f333128..bee7c5d8fc59 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -159,6 +159,7 @@ GLOBAL_VAR(restart_counter) SetupLogs() load_admins(initial = TRUE) + load_mentors() // MASSMETA ADDITION (mentors) load_poll_data() diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 627f1f9399d8..381b6deffed0 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -642,6 +642,10 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) Retitle() if("reject") Reject() + // MASSMETA ADDITION START (mentors) + if("mhelp") + MHelpThis() + // MASSMETA ADDITION END if("reply") usr.client.cmd_ahelp_reply(initiator) if("icissue") diff --git a/modular_meta/_defines/_main_modular_defines_include.dm b/modular_meta/_defines/_main_modular_defines_include.dm index 19456344b402..c9f5fa861812 100644 --- a/modular_meta/_defines/_main_modular_defines_include.dm +++ b/modular_meta/_defines/_main_modular_defines_include.dm @@ -6,3 +6,4 @@ #include "re_hooch_heals_assistants.dm" #include "justice_mecha.dm" #include "techweb_nodes.dm" +#include "mentors.dm" diff --git a/modular_meta/_defines/mentors.dm b/modular_meta/_defines/mentors.dm new file mode 100644 index 000000000000..c3ba2d617290 --- /dev/null +++ b/modular_meta/_defines/mentors.dm @@ -0,0 +1,3 @@ +#define COMSIG_KB_ADMIN_MSAY_DOWN "keybinding_mentor_msay_down" + +#define REQUEST_MENTORHELP "request_mentorhelp" diff --git a/modular_meta/features/mentors/code/ahelp_reject.dm b/modular_meta/features/mentors/code/ahelp_reject.dm new file mode 100644 index 000000000000..2424d8c61857 --- /dev/null +++ b/modular_meta/features/mentors/code/ahelp_reject.dm @@ -0,0 +1,33 @@ +/datum/admin_help/ClosureLinks(ref_src) + . = ..() + . += " (MHELP)" + +/** + * We're overwriting /datum/admin_help/proc/Action(action) + * This is to add the "Mhelp" button to the admin_help's Action + */ + +/datum/admin_help/Action(action) + . = ..() + switch(action) + if("mhelp") + MHelpThis() + +/datum/admin_help/proc/MHelpThis(key_name = key_name_admin(usr)) + if(state != AHELP_ACTIVE) + return + + if(initiator) + initiator.giveadminhelpverb() + + SEND_SOUND(initiator, sound('sound/effects/adminhelp.ogg')) + + to_chat(initiator, "- MentorHelp Question! -") + to_chat(initiator, "This question is about game mechanics, so should be asked in Mentorhelp instead. To do so, use the Mentorhelp verb under the Mentor tab on the upper right of your screen.") + + SSblackbox.record_feedback("tally", "ahelp_stats", 1, "mhelp this") + var/msg = "Ticket [TicketHref("#[id]")] told to mentorhelp by [key_name]" + message_admins(msg) + log_admin_private(msg) + AddInteraction("Told to mentorhelp by [key_name].") + Close(silent = TRUE) diff --git a/modular_meta/features/mentors/code/follow.dm b/modular_meta/features/mentors/code/follow.dm new file mode 100644 index 000000000000..09f8e7a058de --- /dev/null +++ b/modular_meta/features/mentors/code/follow.dm @@ -0,0 +1,24 @@ +/client/proc/mentor_follow(mob/living/followed_guy) + if(!is_mentor()) + return + if(isnull(followed_guy)) + return + if(!ismob(usr)) + return + mentor_datum.following = followed_guy + usr.reset_perspective(followed_guy) + add_verb(src, /client/proc/mentor_unfollow) + to_chat(GLOB.admins, span_adminooc("MENTOR: [key_name(usr)] is now following [key_name(followed_guy)]")) + to_chat(usr, span_info("Click the \"Stop Following\" button in the Mentor tab to stop following [key_name(followed_guy)].")) + log_mentor("[key_name(usr)] began following [key_name(followed_guy)]") + +/client/proc/mentor_unfollow() + set category = "Mentor" + set name = "Stop Following" + set desc = "Stop following the followed." + + remove_verb(src, /client/proc/mentor_unfollow) + usr.reset_perspective() + to_chat(GLOB.admins, span_adminooc("MENTOR: [key_name(usr)] is no longer following [key_name(mentor_datum.following)]")) + log_mentor("[key_name(usr)] stopped following [key_name(mentor_datum.following)]") + mentor_datum.following = null diff --git a/modular_meta/features/mentors/code/mentor.dm b/modular_meta/features/mentors/code/mentor.dm new file mode 100644 index 000000000000..a893ea75be4b --- /dev/null +++ b/modular_meta/features/mentors/code/mentor.dm @@ -0,0 +1,107 @@ +GLOBAL_LIST_EMPTY(mentor_datums) +GLOBAL_PROTECT(mentor_datums) + +GLOBAL_VAR_INIT(mentor_href_token, GenerateToken()) +GLOBAL_PROTECT(mentor_href_token) + +/datum/mentors + var/name = "someone's mentor datum" + /// The Mentor's Client + var/client/owner + /// the Mentor's Ckey + var/target + /// href token for Mentor commands, uses the same token used by Admins. + var/href_token + ///The mob currently being followed with mfollow. + var/mob/following + /// Are we a Contributor? + var/is_contributor = FALSE + ///List of all contributors for special MSAY text. + var/static/list/contributor_list = world.file2list("[global.config.directory]/contributors.txt") + +/datum/mentors/New(ckey) + if(!ckey) + QDEL_IN(src, 0) + throw EXCEPTION("Mentor datum created without a ckey") + return + link_mentor_datum(ckey) + +/datum/mentors/proc/link_mentor_datum(ckey) + target = ckey(ckey) + name = "[ckey]'s mentor datum" + href_token = GenerateToken() + GLOB.mentor_datums[target] = src + /// Set the owner var and load commands + owner = GLOB.directory[ckey] + if(owner) + owner.mentor_datum = src + owner.add_mentor_verbs() + GLOB.mentors += owner + if(ckey in contributor_list) + is_contributor = TRUE + +/proc/RawMentorHrefToken(forceGlobal = FALSE) + var/tok = GLOB.mentor_href_token + if(!forceGlobal && usr) + var/client/all_clients = usr.client + to_chat(world, all_clients) + to_chat(world, usr) + if(!all_clients) + CRASH("No client for HrefToken()!") + var/datum/mentors/holder = all_clients.mentor_datum + if(holder) + tok = holder.href_token + return tok + +/proc/MentorHrefToken(forceGlobal = FALSE) + return "mentor_token=[RawMentorHrefToken(forceGlobal)]" + +///Loads all mentors from the mentors.txt file, setting admins as mentors as well. +/proc/load_mentors() + GLOB.mentor_datums.Cut() + for(var/client/mentor_clients in GLOB.mentors) + mentor_clients.remove_mentor_verbs() + mentor_clients.mentor_datum = null + GLOB.mentors.Cut() + var/list/lines = world.file2list("[global.config.directory]/mentors.txt") + for(var/line in lines) + if(!length(line)) + continue + if(findtextEx(line, "#", 1, 2)) + continue + new /datum/mentors(line) + for(var/client/admin in GLOB.admins) + //not a mentor, let's add them. + if(!GLOB.mentor_datums[admin.ckey]) + new /datum/mentors(admin.ckey) + +ADMIN_VERB(reload_mentors, R_ADMIN, "Reload Mentors", "Reload all mentors", "Mentor") + if(!user) + return + + var/confirm = tgui_alert(usr, "Are you sure you want to reload all mentors?", "Confirm", list("Yes", "No")) + if(confirm != "Yes") + return + + load_mentors() + SSblackbox.record_feedback("tally", "admin_verb", 1, "Reload All Mentors") // If you are copy-pasting this, ensure the 4th parameter is unique to the new proc! + message_admins("[key_name_admin(usr)] manually reloaded mentors") + +ADMIN_VERB(add_mentor, R_PERMISSIONS, "Add Mentor", "Add a new mentor", "Mentor" ) + if(!user) + return + + var/path = "[global.config.directory]/mentors.txt" + var/list/lines = world.file2list(path) + var/ckey = input("Enter ckey.", "Ckey") as text|null + + if(ckey) + if(ckey in lines) + return + if(!GLOB.mentor_datums[ckey]) + new /datum/mentors(ckey) + var/F = file(path) + WRITE_FILE(F, ckey) + SSblackbox.record_feedback("tally", "admin_verb", 1, "Add a new mentor") + message_admins("[key_name_admin(usr)] has made [ckey] a mentor.") + log_admin("[key_name(usr)] has made [ckey] a mentor.") diff --git a/modular_meta/features/mentors/code/mentor_clientprocs.dm b/modular_meta/features/mentors/code/mentor_clientprocs.dm new file mode 100644 index 000000000000..c4f7863b14c4 --- /dev/null +++ b/modular_meta/features/mentors/code/mentor_clientprocs.dm @@ -0,0 +1,36 @@ +/client + ///If this is set, this person is a Mentor. + var/datum/mentors/mentor_datum + +/client/New() + . = ..() + mentor_datum_set() + +// Overwrites /client/Topic to return for mentor client procs +/client/Topic(href, href_list, hsrc) + //Replying to a mentorhelp + if(href_list["mentor_msg"]) + cmd_mentor_pm(href_list["mentor_msg"], null) + return TRUE + //Following someone through a mentorhelp. + if(href_list["mentor_follow"]) + var/mob/living/followed_guy = locate(href_list["mentor_follow"]) + if(istype(followed_guy)) + mentor_follow(followed_guy) + return TRUE + return ..() + +///Sets the person to a mentor datum, linking if it exists, otherwise we'll create a new one if it's an admin. +///Anyone that isn't set to be a mentor will get nothing from this. +/client/proc/mentor_datum_set() + mentor_datum = GLOB.mentor_datums[ckey] + if(mentor_datum) + mentor_datum.link_mentor_datum(ckey) + else if(holder) + new /datum/mentors(ckey) + +///Returns whether or not this client is a Mentor (Or Admin, cause they are also Mentors). +/client/proc/is_mentor() + if(mentor_datum || holder) + return TRUE + return FALSE diff --git a/modular_meta/features/mentors/code/mentor_config.dm b/modular_meta/features/mentors/code/mentor_config.dm new file mode 100644 index 000000000000..5536066955a7 --- /dev/null +++ b/modular_meta/features/mentors/code/mentor_config.dm @@ -0,0 +1 @@ +/datum/config_entry/string/headofpseudostaff diff --git a/modular_meta/features/mentors/code/mentor_globalvars.dm b/modular_meta/features/mentors/code/mentor_globalvars.dm new file mode 100644 index 000000000000..5c9cf4a60efe --- /dev/null +++ b/modular_meta/features/mentors/code/mentor_globalvars.dm @@ -0,0 +1,11 @@ +GLOBAL_LIST_EMPTY(mentorlog) +GLOBAL_PROTECT(mentorlog) +GLOBAL_LIST_EMPTY(mentors) +GLOBAL_PROTECT(mentors) + +GLOBAL_PROTECT(mentor_verbs) + +GLOBAL_LIST_INIT(mentor_verbs, list( + /client/proc/cmd_mentor_say, + /client/proc/mentor_requests, +)) diff --git a/modular_meta/features/mentors/code/mentor_keybinds.dm b/modular_meta/features/mentors/code/mentor_keybinds.dm new file mode 100644 index 000000000000..fc5885bb368a --- /dev/null +++ b/modular_meta/features/mentors/code/mentor_keybinds.dm @@ -0,0 +1,20 @@ +/datum/keybinding/mentor + category = CATEGORY_ADMIN + weight = WEIGHT_ADMIN + +/datum/keybinding/mentor/can_use(client/user) + return user.mentor_datum ? TRUE : FALSE + +/datum/keybinding/mentor/mentor_say + hotkey_keys = list("F4") + name = "mentor_say" + full_name = "Mentor say" + description = "Talk with fellow mentors and admins." + keybind_signal = COMSIG_KB_ADMIN_MSAY_DOWN + +/datum/keybinding/mentor/mentor_say/down(client/user) + . = ..() + if(.) + return + user.get_mentor_say() + return TRUE diff --git a/modular_meta/features/mentors/code/mentor_logging.dm b/modular_meta/features/mentors/code/mentor_logging.dm new file mode 100644 index 000000000000..1c89db2b00f2 --- /dev/null +++ b/modular_meta/features/mentors/code/mentor_logging.dm @@ -0,0 +1,11 @@ +#define LOG_CATEGORY_MENTOR "mentor" + +/proc/log_mentor(text, list/data) + GLOB.mentorlog.Add(text) + logger.Log(LOG_CATEGORY_MENTOR, text, data) + logger.Log(LOG_CATEGORY_COMPAT_GAME, "MENTOR: [text]") + +/datum/log_category/mentorhelp + category = LOG_CATEGORY_MENTOR + master_category = /datum/log_category/admin + config_flag = /datum/config_entry/flag/log_admin diff --git a/modular_meta/features/mentors/code/mentor_manager.dm b/modular_meta/features/mentors/code/mentor_manager.dm new file mode 100644 index 000000000000..acc960f217c4 --- /dev/null +++ b/modular_meta/features/mentors/code/mentor_manager.dm @@ -0,0 +1,90 @@ +/// Verb for opening the requests manager panel +/client/proc/mentor_requests() + set name = "Mentor Manager" + set desc = "Open the mentor manager panel to view all requests during this round" + set category = "Mentor" + + SSblackbox.record_feedback("tally", "mentor_verb", 1, "Mentor Manager") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + GLOB.mentor_requests.ui_interact(usr) + +GLOBAL_DATUM_INIT(mentor_requests, /datum/request_manager/mentor, new) + +/datum/request_manager/mentor/ui_state(mob/user) + return GLOB.mentor_state + +/datum/request_manager/mentor/pray(client/C, message, is_chaplain) + return + +/datum/request_manager/mentor/message_centcom(client/C, message) + return + +/datum/request_manager/mentor/message_syndicate(client/C, message) + return + +/datum/request_manager/mentor/nuke_request(client/C, message) + return + +/datum/request_manager/mentor/fax_request(client/requester, message, additional_info) + return + +/datum/request_manager/mentor/music_request(client/requester, message) + return + +/datum/request_manager/mentor/proc/mentorhelp(client/requester, message) + var/sanitizied_message = copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN) + request_for_client(requester, REQUEST_MENTORHELP, sanitizied_message) + +/datum/request_manager/mentor/ui_interact(mob/user, datum/tgui/ui = null) + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "RequestManagerMentor") + ui.open() + +/datum/request_manager/mentor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + // Only admins should be sending actions + var/client/mentor_client = usr.client + if(!mentor_client || !mentor_client.is_mentor()) + to_chat(mentor_client, "You are not allowed to be using this mentor-only proc. Please report it.", confidential = TRUE) + return + + // Get the request this relates to + var/id = params["id"] != null ? text2num(params["id"]) : null + if (!id) + to_chat(mentor_client, "Failed to find a request ID in your action, please report this.", confidential = TRUE) + CRASH("Received an action without a request ID, this shouldn't happen!") + var/datum/request/request = !id ? null : requests_by_id[id] + if(isnull(request)) + to_chat(mentor_client, "Failed to find a request to reply to, please report this.", confidential = TRUE) + return + + switch(action) + if ("reply") + var/mob/M = request.owner?.mob + mentor_client.cmd_mentor_pm(M) + return TRUE + if ("follow") + var/mob/M = request.owner?.mob + mentor_client.mentor_follow(M) + return TRUE + return ..() + +/datum/request_manager/mentor/ui_data(mob/user) + . = list( + "requests" = list(), + ) + for (var/ckey in requests) + for (var/datum/request/request as anything in requests[ckey]) + if(request.req_type != REQUEST_MENTORHELP) + continue + var/list/data = list( + "id" = request.id, + "req_type" = request.req_type, + "owner" = request.owner ? "[REF(request.owner)]" : null, + "owner_ckey" = request.owner_ckey, + "owner_name" = request.owner_name, + "message" = request.message, + "additional_info" = request.additional_information, + "timestamp" = request.timestamp, + "timestamp_str" = round_timestamp(wtime = request.timestamp) + ) + .["requests"] += list(data) diff --git a/modular_meta/features/mentors/code/mentor_state.dm b/modular_meta/features/mentors/code/mentor_state.dm new file mode 100644 index 000000000000..5a2ee061e865 --- /dev/null +++ b/modular_meta/features/mentors/code/mentor_state.dm @@ -0,0 +1,12 @@ +/** + * tgui state: mentor_state + * + * Checks that the user is a mentor, end-of-story. + */ + +GLOBAL_DATUM_INIT(mentor_state, /datum/ui_state/mentor_state, new) + +/datum/ui_state/mentor_state/can_use_topic(src_object, mob/user) + if(user.client.is_mentor()) + return UI_INTERACTIVE + return UI_CLOSE diff --git a/modular_meta/features/mentors/code/mentorhelp.dm b/modular_meta/features/mentors/code/mentorhelp.dm new file mode 100644 index 000000000000..7a3d77bb43d0 --- /dev/null +++ b/modular_meta/features/mentors/code/mentorhelp.dm @@ -0,0 +1,90 @@ +/client/verb/mentorhelp(msg as text) + set category = "Mentor" + set name = "Mentorhelp" + + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, + type = MESSAGE_TYPE_MODCHAT, + html = "Error: MentorPM: You are muted from Mentorhelps. (muted).", + confidential = TRUE) + return + //Cleans the input message + if(!msg) + return + //This shouldn't happen, but just in case. + if(!mob) + return + + msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN)) + var/mentor_msg = "MENTORHELP: [key_name_mentor(src, TRUE, FALSE)]: [msg]" + log_mentor("MENTORHELP: [key_name_mentor(src, null, FALSE, FALSE)]: [msg]") + + //Send the Mhelp to all Mentors/Admins + for(var/client/honked_clients in GLOB.mentors | GLOB.admins) + SEND_SOUND(honked_clients, 'sound/items/bikehorn.ogg') + to_chat(honked_clients, + type = MESSAGE_TYPE_MODCHAT, + html = mentor_msg, + confidential = TRUE) + + //Also show it to person Mhelping + to_chat(usr, + type = MESSAGE_TYPE_MODCHAT, + html = "PM to-Mentors: [msg]", + confidential = TRUE) + + GLOB.mentor_requests.mentorhelp(src, msg) + +/proc/key_name_mentor(whom, include_link = null, include_name = TRUE, include_follow = TRUE, char_name_only = TRUE) + var/mob/user + var/client/chosen_client + var/key + var/ckey + + if(!whom) + return "*null*" + + if(istype(whom, /client)) + chosen_client = whom + user = chosen_client.mob + key = chosen_client.key + ckey = chosen_client.ckey + else if(ismob(whom)) + user = whom + chosen_client = user.client + key = user.key + ckey = user.ckey + else if(istext(whom)) + key = whom + ckey = ckey(whom) + chosen_client = GLOB.directory[ckey] + if(chosen_client) + user = chosen_client.mob + else + return "*invalid*" + + . = "" + + if(!ckey) + include_link = null + + if(key) + if(include_link != null) + . += "" + + if(chosen_client && chosen_client.holder && chosen_client.holder.fakekey) + . += "Administrator" + else + . += key + if(!chosen_client) + . += "\[DC\]" + + if(include_link != null) + . += "" + else + . += "*no key*" + + if(include_follow) + . += " (F)" + + return . diff --git a/modular_meta/features/mentors/code/mentorpm.dm b/modular_meta/features/mentors/code/mentorpm.dm new file mode 100644 index 000000000000..969032e6c5e0 --- /dev/null +++ b/modular_meta/features/mentors/code/mentorpm.dm @@ -0,0 +1,101 @@ +/// Takes input from /client/Topic and sends them a PM, fetching messages if needed. src is the sender and chosen_client is the target client +/client/proc/cmd_mentor_pm(whom, msg) + var/client/chosen_client + if(ismob(whom)) + var/mob/potential_mobs = whom + chosen_client = potential_mobs.client + else if(istext(whom)) + chosen_client = GLOB.directory[whom] + else if(istype(whom, /client)) + chosen_client = whom + if(chosen_client.prefs.muted & MUTE_ADMINHELP) + to_chat(src, + type = MESSAGE_TYPE_MODCHAT, + html = "Error: MentorPM: You are muted from Mentorhelps. (muted).", + confidential = TRUE) + return + if(!chosen_client) + if(is_mentor()) + to_chat(src, + type = MESSAGE_TYPE_MODCHAT, + html = "Error: Mentor-PM: Client not found.", + confidential = TRUE) + else + /// Mentor we are replying to left. Mentorhelp instead. + mentorhelp(msg) + return + + //Get message text, limit it's length.and clean/escape html + if(!msg) + msg = input(src, "Message:", "Private message") as text|null + + if(!msg) + return + + if(!chosen_client) + if(is_mentor()) + to_chat(src, + type = MESSAGE_TYPE_MODCHAT, + html = "Error: Mentor-PM: Client not found.", + confidential = TRUE) + else + //Mentor we are replying to has vanished, Mentorhelp instead + mentorhelp(msg) + return + + //Neither party is a mentor, they shouldn't be PMing! + if(!chosen_client.is_mentor() && !is_mentor()) + return + + msg = sanitize(copytext(msg, 1, MAX_MESSAGE_LEN)) + if(!msg) + return + + log_mentor("Mentor PM: [key_name(src)]->[key_name(chosen_client)]: [msg]") + + msg = emoji_parse(msg) + SEND_SOUND(chosen_client, 'sound/items/bikehorn.ogg') + if(chosen_client.is_mentor()) + if(is_mentor()) + //Both are Mentors + to_chat(chosen_client, + type = MESSAGE_TYPE_MODCHAT, + html = "Mentor PM from-[key_name_mentor(src, chosen_client, TRUE, FALSE)]: [msg]", + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_MODCHAT, + html = "Mentor PM to-[key_name_mentor(chosen_client, chosen_client, TRUE, FALSE)]: [msg]", + confidential = TRUE) + else + //Sender is a Non-Mentor + to_chat(chosen_client, + type = MESSAGE_TYPE_MODCHAT, + html = "Reply PM from-[key_name_mentor(src, chosen_client, TRUE, FALSE)]: [msg]", + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_MODCHAT, + html = "Mentor PM to-[key_name_mentor(chosen_client, chosen_client, TRUE, FALSE)]: [msg]", + confidential = TRUE) + + else + if(is_mentor()) + //Receiver is a Non-Mentor - Left unsorted so people that Mentorhelp with Mod chat off will still get it, otherwise they'll complain. + to_chat(chosen_client, "Mentor PM from-[key_name_mentor(src, chosen_client, TRUE, FALSE, FALSE)]: [msg]") + to_chat(src, + type = MESSAGE_TYPE_MODCHAT, + html = "Mentor PM to-[key_name_mentor(chosen_client, chosen_client, TRUE, FALSE)]: [msg]", + confidential = TRUE) + + //We don't use message_Mentors here because the sender/receiver might get it too + for(var/client/honked_clients in GLOB.mentors | GLOB.admins) + //Check client/honked_clients is an Mentor and isn't the Sender/Recipient + if(honked_clients.key != key && honked_clients.key != chosen_client.key) + to_chat(honked_clients, + type = MESSAGE_TYPE_MODCHAT, + html = "Mentor PM: [key_name_mentor(src, honked_clients, FALSE, FALSE)]->[key_name_mentor(chosen_client, honked_clients, FALSE, FALSE)]: [msg]", + confidential = TRUE) + + for(var/datum/request/request as anything in GLOB.mentor_requests.requests[chosen_client.ckey]) + if(request.req_type != REQUEST_MENTORHELP) + continue + request.additional_information = "Player was last replied to in mentorhelps by [src]" diff --git a/modular_meta/features/mentors/code/mentorsay.dm b/modular_meta/features/mentors/code/mentorsay.dm new file mode 100644 index 000000000000..500c3181282e --- /dev/null +++ b/modular_meta/features/mentors/code/mentorsay.dm @@ -0,0 +1,80 @@ +/// for [/proc/check_mentor_pings], if there are any admin pings in the msay message, this index in the return list contains a list of mentors to ping +/// This is a copy paste of ASAY_LINK_PINGED_ADMINS_INDEX +#define MSAY_LINK_PINGED_MENTORS_INDEX "!pinged_mentors" + +/client/proc/cmd_mentor_say(msg as text) + set category = "Mentor" + set name = "Mentorsay" + + if(!is_mentor()) + to_chat(src, span_danger("Error: Only mentors and administrators may use this command."), confidential = TRUE) + return + + msg = emoji_parse(copytext(sanitize(msg), 1, MAX_MESSAGE_LEN)) + if(!msg) + return + + var/list/pinged_mentor_clients = check_mentor_pings(msg) + if(length(pinged_mentor_clients) && pinged_mentor_clients[MSAY_LINK_PINGED_MENTORS_INDEX]) + msg = pinged_mentor_clients[MSAY_LINK_PINGED_MENTORS_INDEX] + pinged_mentor_clients -= MSAY_LINK_PINGED_MENTORS_INDEX + + for(var/iter_ckey in pinged_mentor_clients) + var/client/iter_mentor_client = pinged_mentor_clients[iter_ckey] + if(!iter_mentor_client || !iter_mentor_client.is_mentor()) + continue + window_flash(iter_mentor_client) + SEND_SOUND(iter_mentor_client.mob, sound('sound/misc/bloop.ogg')) + + log_mentor("MSAY: [key_name(src)] : [msg]") + msg = keywords_lookup(msg) + if(src.key == "[CONFIG_GET(string/headofpseudostaff)]") + msg = "HOP: [key_name(src, include_link = FALSE, include_name = FALSE)]: [msg]" + else if(mentor_datum?.is_contributor) + msg = "CONTRIB: [key_name(src, include_link = FALSE, include_name = FALSE)]: [msg]" + else if(holder) + msg = "STAFF: [key_name(src, include_link = FALSE, include_name = FALSE)]: [msg]" + else + msg = "MENTOR: [key_name(src, include_link = FALSE, include_name = FALSE)]: [msg]" + to_chat(GLOB.admins | GLOB.mentors, + type = MESSAGE_TYPE_MODCHAT, + html = msg, + confidential = TRUE) + + SSblackbox.record_feedback("tally", "mentor_verb", 1, "Msay") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/proc/get_mentor_say() + var/msg = input(src, null, "msay \"text\"") as text|null + cmd_mentor_say(msg) + +// see /proc/check_asay_links(msg) we just check for mentor_datum instead of holder +/proc/check_mentor_pings(msg) + var/list/msglist = splittext(msg, " ") + var/list/mentors_to_ping = list() + + var/i = 0 + for(var/word in msglist) + i++ + if(!length(word)) + continue + if(word[1] != "@") + continue + var/ckey_check = lowertext(copytext(word, 2)) + var/client/client_check = GLOB.directory[ckey_check] + if(client_check?.mentor_datum) + msglist[i] = "[word]" + mentors_to_ping[ckey_check] = client_check + + if(length(mentors_to_ping)) + mentors_to_ping[MSAY_LINK_PINGED_MENTORS_INDEX] = jointext(msglist, " ") + return mentors_to_ping + +///Gives both Mentors & Admins all Mentor verb +/client/proc/add_mentor_verbs() + if(mentor_datum || holder) + add_verb(src, GLOB.mentor_verbs) + +/client/proc/remove_mentor_verbs() + remove_verb(src, GLOB.mentor_verbs) + +#undef MSAY_LINK_PINGED_MENTORS_INDEX diff --git a/modular_meta/features/mentors/code/mentorwho.dm b/modular_meta/features/mentors/code/mentorwho.dm new file mode 100644 index 000000000000..9cdce9adcefc --- /dev/null +++ b/modular_meta/features/mentors/code/mentorwho.dm @@ -0,0 +1,96 @@ +/** + * Basically all of this is copied from adminwho with everything + * renamed to mentor instead. + * Please keep parity with that if possible. + */ + +/client/verb/mentorwho() + set category = "Mentor" + set name = "Mentorwho" + + var/list/lines = list() + var/payload_string = generate_mentorwho_string() + var/header = "Current Mentors:" + + lines += span_bold(header) + lines += payload_string + + var/finalized_string = boxed_message(jointext(lines, "\n")) + to_chat(src, finalized_string) + +/// Proc that returns a list of cliented mentors. Remember that this list can contain nulls! +/// Also, will return null if we don't have any mentors. +/proc/get_list_of_mentors() + var/returnable_list = list() + + for(var/client/mentor_clients in GLOB.mentors) + returnable_list += mentor_clients + + if(length(returnable_list) == 0) + return null + + return returnable_list + + +/// Proc that generates the applicable string to dispatch to the client for mentorwho. +/client/proc/generate_mentorwho_string() + var/list/list_of_mentors = get_list_of_mentors() + if(isnull(list_of_mentors)) + return + + var/list/message_strings = list() + if(isnull(holder)) + message_strings += get_general_mentorwho_information(list_of_mentors) + else + message_strings += get_sensitive_mentorwho_information(list_of_mentors) + + return jointext(message_strings, "\n") + +/// Proc that gathers mentorwho information for a general player, which will only give information if an admin isn't AFK, and handles potential fakekeying. +/// Will return a list of strings. +/proc/get_general_mentorwho_information(list/checkable_mentors) + var/returnable_list = list() + + for(var/client/mentor_client in checkable_mentors) + //AFK people don't show up + if(mentor_client.is_afk()) + continue + //Deadmins don't show up unless it's the pseudostaff cause they are generally expected to be. + if(GLOB.deadmins[mentor_client.ckey] && !(mentor_client.key == "[CONFIG_GET(string/headofpseudostaff)]")) + continue + + if(mentor_client.mentor_datum.is_contributor) + returnable_list += "• [mentor_client] is a Contributor" + else + returnable_list += "• [mentor_client] is a Mentor" + + return returnable_list + +/// Proc that gathers mentorwho information for mentors, which will contain information on if the admin is AFK, readied to join, etc. Only arg is a list of clients to use. +/// Will return a list of strings. +/proc/get_sensitive_mentorwho_information(list/checkable_mentors) + var/returnable_list = list() + + for(var/client/mentor_client in checkable_mentors) + var/list/mentor_strings = list() + + if(GLOB.deadmins[mentor_client.ckey]) + mentor_strings += "\t[mentor_client] is a Deadmin" + else if(mentor_client.mentor_datum.is_contributor) + mentor_strings += "\t[mentor_client] is a Contributor" + else + mentor_strings += "\t[mentor_client] is a Mentor" + + if(isobserver(mentor_client.mob)) + mentor_strings += "- Observing" + else if(isnewplayer(mentor_client.mob)) + mentor_strings += "- Lobby" + else + mentor_strings += "- Playing" + + if(mentor_client.is_afk()) + mentor_strings += "(AFK)" + + returnable_list += jointext(mentor_strings, " ") + + return returnable_list diff --git a/modular_meta/features/mentors/includes.dm b/modular_meta/features/mentors/includes.dm new file mode 100644 index 000000000000..2f84200b8ead --- /dev/null +++ b/modular_meta/features/mentors/includes.dm @@ -0,0 +1,21 @@ +#include "code\ahelp_reject.dm" +#include "code\follow.dm" +#include "code\mentor_clientprocs.dm" +#include "code\mentor_config.dm" +#include "code\mentor_globalvars.dm" +#include "code\mentor_keybinds.dm" +#include "code\mentor_logging.dm" +#include "code\mentor.dm" +#include "code\mentorhelp.dm" +#include "code\mentorpm.dm" +#include "code\mentorsay.dm" +#include "code\mentorwho.dm" +#include "code\mentor_manager.dm" +#include "code\mentor_state.dm" + +/datum/modpack/mentors + id = "mentors" + name = "Ментор Хелп" + group = "Features" + desc = "Менторы, помогающие игрокам с внутреигровыми фишками." + author = "Glamyr" diff --git a/modular_meta/main_modular_include.dm b/modular_meta/main_modular_include.dm index 7cc209f52401..0d0d45403312 100644 --- a/modular_meta/main_modular_include.dm +++ b/modular_meta/main_modular_include.dm @@ -53,6 +53,7 @@ #include "features\ntts-nd-tg-tts\includes.dm" #include "features\meta_redesign\includes.dm" #include "features\holidays\includes.dm" +#include "features\mentors\includes.dm" /* --- Reverts --- */ diff --git a/tgui/packages/tgui/interfaces/RequestManagerMentor.jsx b/tgui/packages/tgui/interfaces/RequestManagerMentor.jsx new file mode 100644 index 000000000000..25e6c2b818f8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RequestManagerMentor.jsx @@ -0,0 +1,163 @@ +/** + * @file + * @copyright 2021 bobbahbrown (https://github.com/bobbahbrown) + * @license MIT + */ +import { useState } from 'react'; +import { Button, Input, Popper, Section, Table } from 'tgui-core/components'; +import { decodeHtmlEntities } from 'tgui-core/string'; + +import { useBackend, useLocalState } from '../backend'; +import { Window } from '../layouts'; + +export const RequestManagerMentor = (props) => { + const { act, data } = useBackend(); + const { requests } = data; + const [filteredTypes, _] = useLocalState( + 'filteredTypes', + Object.fromEntries( + Object.entries(displayTypeMap).map(([type, _]) => [type, true]), + ), + ); + const [searchText, setSearchText] = useState(''); + + // Handle filtering + let displayedRequests = requests.filter( + (request) => filteredTypes[request.req_type], + ); + if (searchText) { + const filterText = searchText.toLowerCase(); + displayedRequests = displayedRequests.filter( + (request) => + decodeHtmlEntities(request.message) + .toLowerCase() + .includes(filterText) || + request.owner_name.toLowerCase().includes(filterText), + ); + } + + return ( + + + + setSearchText(value)} + placeholder={'Search...'} + mr={1} + /> + + > + } + > + {displayedRequests.map((request) => ( + + + + + {request.owner_name} + {request.owner === null && ' [DC]'} + + + {request.timestamp_str} + + + + + {decodeHtmlEntities(request.message)} + + {request.additional_info && ( + + {request.additional_info} + + )} + + {request.owner !== null && } + + ))} + + + + ); +}; + +const displayTypeMap = { + request_mentorhelp: 'MENTORHELP', +}; + +const RequestType = (props) => { + const { requestType } = props; + + return ( + + {displayTypeMap[requestType]}: + + ); +}; + +const RequestControls = (props) => { + const { act, _ } = useBackend(); + const { request } = props; + + return ( + + act('reply', { id: request.id })}>REPLY + act('follow', { id: request.id })}>FOLLOW + + ); +}; + +const FilterPanel = (props) => { + const [filterVisible, setFilterVisible] = useState(false); + const [filteredTypes, setFilteredTypes] = useLocalState( + 'filteredTypes', + Object.fromEntries( + Object.entries(displayTypeMap).map(([type, _]) => [type, true]), + ), + ); + + return ( + + + {Object.keys(displayTypeMap).map((type) => { + return ( + + + + + + { + filteredTypes[type] = !filteredTypes[type]; + setFilteredTypes(filteredTypes); + }} + my={0.25} + /> + + + ); + })} + + + } + > + + setFilterVisible(!filterVisible)}> + Type Filter + + + + ); +};