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]: ") + for(var/client/C in GLOB.admins) + to_chat(C, create_text_tag("mentor", "MENTOR:", C) + " [src]: ") + +/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("