From 929d3fffeb87079f294b8747cc4ba9503f2e5fd5 Mon Sep 17 00:00:00 2001 From: Huz2e Date: Wed, 19 Feb 2025 20:25:38 +0300 Subject: [PATCH 1/4] Adds mentorhelp --- code/__DEFINES/admin.dm | 1 + code/__DEFINES/keybinding.dm | 1 + code/game/world.dm | 1 + code/modules/admin/verbs/admin.dm | 5 + code/modules/admin/verbs/admingame.dm | 1 + code/modules/admin/verbs/adminhelp.dm | 4 + .../_defines/_main_modular_defines_include.dm | 1 + modular_meta/_defines/mhelp.dm | 1 + .../features/mhelp/code/ahelp_reject.dm | 33 ++++ modular_meta/features/mhelp/code/follow.dm | 24 +++ modular_meta/features/mhelp/code/mentor.dm | 88 ++++++++++ .../features/mhelp/code/mentor_clientprocs.dm | 36 ++++ .../features/mhelp/code/mentor_config.dm | 6 + .../features/mhelp/code/mentor_globalvars.dm | 11 ++ .../features/mhelp/code/mentor_keybinds.dm | 20 +++ .../features/mhelp/code/mentor_logging.dm | 11 ++ .../features/mhelp/code/mentor_manager.dm | 90 ++++++++++ .../features/mhelp/code/mentor_state.dm | 12 ++ .../features/mhelp/code/mentorhelp.dm | 90 ++++++++++ modular_meta/features/mhelp/code/mentorpm.dm | 101 +++++++++++ modular_meta/features/mhelp/code/mentorsay.dm | 80 +++++++++ modular_meta/features/mhelp/code/mentorwho.dm | 96 +++++++++++ modular_meta/features/mhelp/includes.dm | 21 +++ modular_meta/main_modular_include.dm | 1 + .../tgui/interfaces/RequestManagerFulp.jsx | 163 ++++++++++++++++++ 25 files changed, 898 insertions(+) create mode 100644 modular_meta/_defines/mhelp.dm create mode 100644 modular_meta/features/mhelp/code/ahelp_reject.dm create mode 100644 modular_meta/features/mhelp/code/follow.dm create mode 100644 modular_meta/features/mhelp/code/mentor.dm create mode 100644 modular_meta/features/mhelp/code/mentor_clientprocs.dm create mode 100644 modular_meta/features/mhelp/code/mentor_config.dm create mode 100644 modular_meta/features/mhelp/code/mentor_globalvars.dm create mode 100644 modular_meta/features/mhelp/code/mentor_keybinds.dm create mode 100644 modular_meta/features/mhelp/code/mentor_logging.dm create mode 100644 modular_meta/features/mhelp/code/mentor_manager.dm create mode 100644 modular_meta/features/mhelp/code/mentor_state.dm create mode 100644 modular_meta/features/mhelp/code/mentorhelp.dm create mode 100644 modular_meta/features/mhelp/code/mentorpm.dm create mode 100644 modular_meta/features/mhelp/code/mentorsay.dm create mode 100644 modular_meta/features/mhelp/code/mentorwho.dm create mode 100644 modular_meta/features/mhelp/includes.dm create mode 100644 tgui/packages/tgui/interfaces/RequestManagerFulp.jsx diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm index ea5c52bf36f0f..2106acde89d3b 100644 --- a/code/__DEFINES/admin.dm +++ b/code/__DEFINES/admin.dm @@ -7,6 +7,7 @@ #define MUTE_ADMINHELP (1<<3) #define MUTE_DEADCHAT (1<<4) #define MUTE_INTERNET_REQUEST (1<<5) +#define MUTE_MENTORHELP (1<<6) // MASSMETA ADDITION: mentors #define MUTE_ALL ALL //Some constants for DB_Ban diff --git a/code/__DEFINES/keybinding.dm b/code/__DEFINES/keybinding.dm index 8ae95933e646b..997bb031ed334 100644 --- a/code/__DEFINES/keybinding.dm +++ b/code/__DEFINES/keybinding.dm @@ -10,6 +10,7 @@ //Admin #define COMSIG_KB_ADMIN_ASAY_DOWN "keybinding_admin_asay_down" #define COMSIG_KB_ADMIN_DSAY_DOWN "keybinding_admin_dsay_down" +#define COMSIG_KB_ADMIN_MSAY_DOWN "keybinding_mentor_msay_down" // MASSMETA ADDITION: mentors #define COMSIG_KB_ADMIN_TOGGLEBUILDMODE_DOWN "keybinding_admin_togglebuildmode_down" #define COMSIG_KB_ADMIN_AGHOST_DOWN "keybinding_admin_aghost_down" #define COMSIG_KB_ADMIN_PLAYERPANELNEW_DOWN "keybinding_admin_playerpanelnew_down" diff --git a/code/game/world.dm b/code/game/world.dm index 1f705d1328194..a96feab0b4894 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -178,6 +178,7 @@ GLOBAL_PROTECT(tracy_init_reason) SetupLogs() load_admins() + load_mentors() // MASSMETA ADDITION: mentors load_poll_data() diff --git a/code/modules/admin/verbs/admin.dm b/code/modules/admin/verbs/admin.dm index 1688f34d2024f..b7128ac880dd2 100644 --- a/code/modules/admin/verbs/admin.dm +++ b/code/modules/admin/verbs/admin.dm @@ -184,6 +184,11 @@ ADMIN_VERB(drop_everything, R_ADMIN, "Drop Everything", ADMIN_VERB_NO_DESCRIPTIO if(MUTE_INTERNET_REQUEST) mute_string = "internet sound requests" feedback_string = "Internet Sound Requests" + // MASSMETA ADDITION START: mentors + if(MUTE_MENTORHELP) + mute_string = "mhelp" + feedback_string = "Mentor Help" + // MASSMETA ADDIITON END: mentors if(MUTE_ALL) mute_string = "everything" feedback_string = "Everything" diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm index 8e7fd97cc1f8b..6eb20ca97f895 100644 --- a/code/modules/admin/verbs/admingame.dm +++ b/code/modules/admin/verbs/admingame.dm @@ -88,6 +88,7 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_ADMIN, "Show Player Panel", mo body += "PRAY | " body += "ADMINHELP | " body += "WEBREQ | " + body += "MHELP | " // MASSMETA ADDITION: mentors body += "DEADCHAT\]" body += "(toggle all)" diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index da09e19d0debe..a4c9aac43ce26 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -641,6 +641,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 fa53e9d04e1c2..bfcee9ff691c9 100644 --- a/modular_meta/_defines/_main_modular_defines_include.dm +++ b/modular_meta/_defines/_main_modular_defines_include.dm @@ -2,3 +2,4 @@ // So you can easily use your defines in TG code folder, not only in our modular folder #include "additional_circuit.dm" +#include "mhelp.dm" diff --git a/modular_meta/_defines/mhelp.dm b/modular_meta/_defines/mhelp.dm new file mode 100644 index 0000000000000..d252fa0260644 --- /dev/null +++ b/modular_meta/_defines/mhelp.dm @@ -0,0 +1 @@ +#define REQUEST_MENTORHELP "request_mentorhelp" diff --git a/modular_meta/features/mhelp/code/ahelp_reject.dm b/modular_meta/features/mhelp/code/ahelp_reject.dm new file mode 100644 index 0000000000000..2424d8c61857a --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/follow.dm b/modular_meta/features/mhelp/code/follow.dm new file mode 100644 index 0000000000000..09f8e7a058de5 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentor.dm b/modular_meta/features/mhelp/code/mentor.dm new file mode 100644 index 0000000000000..a84edd6cfd7ce --- /dev/null +++ b/modular_meta/features/mhelp/code/mentor.dm @@ -0,0 +1,88 @@ +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") diff --git a/modular_meta/features/mhelp/code/mentor_clientprocs.dm b/modular_meta/features/mhelp/code/mentor_clientprocs.dm new file mode 100644 index 0000000000000..c4f7863b14c4d --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentor_config.dm b/modular_meta/features/mhelp/code/mentor_config.dm new file mode 100644 index 0000000000000..3453a6d1e1923 --- /dev/null +++ b/modular_meta/features/mhelp/code/mentor_config.dm @@ -0,0 +1,6 @@ +/datum/config_entry/flag/mentors_mobname_only + +/datum/config_entry/flag/mentor_legacy_system //Defines whether the server uses the legacy mentor system with mentors.txt or the SQL system + protection = CONFIG_ENTRY_LOCKED + +/datum/config_entry/string/headofpseudostaff diff --git a/modular_meta/features/mhelp/code/mentor_globalvars.dm b/modular_meta/features/mhelp/code/mentor_globalvars.dm new file mode 100644 index 0000000000000..5c9cf4a60efe3 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentor_keybinds.dm b/modular_meta/features/mhelp/code/mentor_keybinds.dm new file mode 100644 index 0000000000000..fc5885bb368a2 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentor_logging.dm b/modular_meta/features/mhelp/code/mentor_logging.dm new file mode 100644 index 0000000000000..1c89db2b00f29 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentor_manager.dm b/modular_meta/features/mhelp/code/mentor_manager.dm new file mode 100644 index 0000000000000..4f202f77d5f7b --- /dev/null +++ b/modular_meta/features/mhelp/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, "RequestManagerFulp") + 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" = gameTimestamp(wtime = request.timestamp) + ) + .["requests"] += list(data) diff --git a/modular_meta/features/mhelp/code/mentor_state.dm b/modular_meta/features/mhelp/code/mentor_state.dm new file mode 100644 index 0000000000000..5a2ee061e8650 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentorhelp.dm b/modular_meta/features/mhelp/code/mentorhelp.dm new file mode 100644 index 0000000000000..7a3d77bb43d03 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentorpm.dm b/modular_meta/features/mhelp/code/mentorpm.dm new file mode 100644 index 0000000000000..969032e6c5e07 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentorsay.dm b/modular_meta/features/mhelp/code/mentorsay.dm new file mode 100644 index 0000000000000..500c3181282e8 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/code/mentorwho.dm b/modular_meta/features/mhelp/code/mentorwho.dm new file mode 100644 index 0000000000000..9cdce9adcefc3 --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp/includes.dm b/modular_meta/features/mhelp/includes.dm new file mode 100644 index 0000000000000..4069539496f0e --- /dev/null +++ b/modular_meta/features/mhelp/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/mhelp + id = "mhelp" + name = "Менторхелп" + group = "Features" + desc = "Менторхелп для помощи с вопросами относительно игровых механик." + author = "Huz2e" diff --git a/modular_meta/main_modular_include.dm b/modular_meta/main_modular_include.dm index cfb4211d06794..cd304f09cde58 100644 --- a/modular_meta/main_modular_include.dm +++ b/modular_meta/main_modular_include.dm @@ -13,6 +13,7 @@ #include "features\cheburek_car\includes.dm" #endif #include "features\venom_knife\includes.dm" +#include "features\mhelp\includes.dm" /* -- REVERTS -- */ #include "reverts\revert_glasses_protect_welding\includes.dm" diff --git a/tgui/packages/tgui/interfaces/RequestManagerFulp.jsx b/tgui/packages/tgui/interfaces/RequestManagerFulp.jsx new file mode 100644 index 0000000000000..4b847dee5a022 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RequestManagerFulp.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 RequestManagerFulp = (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 ( +
+ + +
+ ); +}; + +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} + /> + + + ); + })} +
+ + } + > +
+ +
+
+ ); +}; From 3e78c295318cf88359d9055ef5335256a3e77e4b Mon Sep 17 00:00:00 2001 From: Huz2e Date: Thu, 20 Feb 2025 15:33:58 +0300 Subject: [PATCH 2/4] remove mhelp mute --- code/__DEFINES/admin.dm | 1 - code/modules/admin/verbs/admin.dm | 5 ----- code/modules/admin/verbs/admingame.dm | 1 - 3 files changed, 7 deletions(-) diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm index 2106acde89d3b..ea5c52bf36f0f 100644 --- a/code/__DEFINES/admin.dm +++ b/code/__DEFINES/admin.dm @@ -7,7 +7,6 @@ #define MUTE_ADMINHELP (1<<3) #define MUTE_DEADCHAT (1<<4) #define MUTE_INTERNET_REQUEST (1<<5) -#define MUTE_MENTORHELP (1<<6) // MASSMETA ADDITION: mentors #define MUTE_ALL ALL //Some constants for DB_Ban diff --git a/code/modules/admin/verbs/admin.dm b/code/modules/admin/verbs/admin.dm index b7128ac880dd2..1688f34d2024f 100644 --- a/code/modules/admin/verbs/admin.dm +++ b/code/modules/admin/verbs/admin.dm @@ -184,11 +184,6 @@ ADMIN_VERB(drop_everything, R_ADMIN, "Drop Everything", ADMIN_VERB_NO_DESCRIPTIO if(MUTE_INTERNET_REQUEST) mute_string = "internet sound requests" feedback_string = "Internet Sound Requests" - // MASSMETA ADDITION START: mentors - if(MUTE_MENTORHELP) - mute_string = "mhelp" - feedback_string = "Mentor Help" - // MASSMETA ADDIITON END: mentors if(MUTE_ALL) mute_string = "everything" feedback_string = "Everything" diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm index 6eb20ca97f895..8e7fd97cc1f8b 100644 --- a/code/modules/admin/verbs/admingame.dm +++ b/code/modules/admin/verbs/admingame.dm @@ -88,7 +88,6 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_ADMIN, "Show Player Panel", mo body += "PRAY | " body += "ADMINHELP | " body += "WEBREQ | " - body += "MHELP | " // MASSMETA ADDITION: mentors body += "DEADCHAT\]" body += "(toggle all)" From 2ad31d003f3adcfa77dc8af925acc591e50d7f20 Mon Sep 17 00:00:00 2001 From: Huz2e Date: Thu, 20 Feb 2025 15:46:28 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=BB=D0=B8=D1=88=D0=BD=D0=B5=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modular_meta/features/mhelp/code/mentor_config.dm | 3 --- 1 file changed, 3 deletions(-) diff --git a/modular_meta/features/mhelp/code/mentor_config.dm b/modular_meta/features/mhelp/code/mentor_config.dm index 3453a6d1e1923..004631c2b4e1d 100644 --- a/modular_meta/features/mhelp/code/mentor_config.dm +++ b/modular_meta/features/mhelp/code/mentor_config.dm @@ -1,6 +1,3 @@ /datum/config_entry/flag/mentors_mobname_only -/datum/config_entry/flag/mentor_legacy_system //Defines whether the server uses the legacy mentor system with mentors.txt or the SQL system - protection = CONFIG_ENTRY_LOCKED - /datum/config_entry/string/headofpseudostaff From f8dd431b62a200fec91d8b3b14b4fc02136456a1 Mon Sep 17 00:00:00 2001 From: Huz2e <102353096+Huz2e@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:47:42 +0300 Subject: [PATCH 4/4] Update mentor_config.dm --- modular_meta/features/mhelp/code/mentor_config.dm | 2 -- 1 file changed, 2 deletions(-) diff --git a/modular_meta/features/mhelp/code/mentor_config.dm b/modular_meta/features/mhelp/code/mentor_config.dm index 004631c2b4e1d..5536066955a77 100644 --- a/modular_meta/features/mhelp/code/mentor_config.dm +++ b/modular_meta/features/mhelp/code/mentor_config.dm @@ -1,3 +1 @@ -/datum/config_entry/flag/mentors_mobname_only - /datum/config_entry/string/headofpseudostaff