diff --git a/code/modules/admin/secrets.dm b/code/modules/admin/secrets.dm
index 1dbab89d88e..614a4c10f16 100644
--- a/code/modules/admin/secrets.dm
+++ b/code/modules/admin/secrets.dm
@@ -9,8 +9,9 @@
Admin Log
Show Admin List
+ Mentor Log
- "}
+ "}//SKYRAT EDIT ADDITION - MENTOR
if(check_rights(R_ADMIN,0))
dat += {"
@@ -97,6 +98,11 @@
var/datum/round_event/E
var/ok = FALSE
switch(item)
+ //SKYRAT EDIT ADDITION BEGIN - MENTOR
+ if("mentor_log")
+ MentorLogSecret()
+ //SKYRAT EDIT ADDITION END
+
if("admin_log")
var/dat = "Admin Log
"
for(var/l in GLOB.admin_log)
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index f761a7b57a2..bf644814151 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -36,6 +36,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
/client/Topic(href, href_list, hsrc)
if(!usr || usr != mob) //stops us calling Topic for somebody else's client. Also helps prevent usr=null
return
+ //SKYRAT EDIT ADDITION BEGIN - MENTOR
+ if(mentor_client_procs(href_list))
+ return
+ //SKYRAT EDIT ADDITION END
// asset_cache
var/asset_cache_job
diff --git a/config/skyrat/mentors.txt b/config/skyrat/mentors.txt
new file mode 100644
index 00000000000..928da766c44
--- /dev/null
+++ b/config/skyrat/mentors.txt
@@ -0,0 +1,2 @@
+thattraitorgeorgemellons
+Azalis
diff --git a/config/skyrat/skyrat_config.txt b/config/skyrat/skyrat_config.txt
index af58770f756..10cc57cdce1 100644
--- a/config/skyrat/skyrat_config.txt
+++ b/config/skyrat/skyrat_config.txt
@@ -1,3 +1,9 @@
+## Comment this out if you want to use the SQL based mentor system, the legacy system uses mentors.txt.
+## You need to set up your database to use the SQL based system.
+## This flag is automatically enabled if SQL_ENABLED isn't
+## But currently the SQL system is not implemented and it'll read from mentors.txt nonetheless
+MENTOR_LEGACY_SYSTEM
+
ALERT_AMBER_UPTO A major security emergency has developed. Security staff may have weapons unholstered at all times. Random searches are allowed and advised.
ALERT_AMBER_DOWNTO A major security emergency is still underway. Non-security personnel are required to obey all relevant instructions from security staff.
ALERT_ORANGE_UPTO A major engineering emergency has developed. Non-engineering personnel are required to evacuate any affected areas and obey relevant instructions from engineering staff.
@@ -6,4 +12,4 @@ ALERT_VIOLET_UPTO A major medical emergency has developed. Non-medical personnel
ALERT_VIOLET_DOWNTO A major medical emergency has developed. Non-medical personnel are required to obey all relevant instructions from medical staff.
## How long until someone can be put in cryo if they are SSD
-CRYO_MIN_SSD_TIME 15
\ No newline at end of file
+CRYO_MIN_SSD_TIME 15
diff --git a/modular_skyrat/modules/mentor/code/_globalvars.dm b/modular_skyrat/modules/mentor/code/_globalvars.dm
new file mode 100644
index 00000000000..6a1ce0c19d9
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/_globalvars.dm
@@ -0,0 +1,3 @@
+//all clients whom are mentors
+GLOBAL_LIST_EMPTY(mentors)
+GLOBAL_PROTECT(mentors)
diff --git a/modular_skyrat/modules/mentor/code/client_procs.dm b/modular_skyrat/modules/mentor/code/client_procs.dm
new file mode 100644
index 00000000000..18a12bb73a5
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/client_procs.dm
@@ -0,0 +1,45 @@
+/client/New()
+ . = ..()
+ mentor_datum_set()
+
+/client/Destroy()
+ if(GLOB.mentors[src])
+ GLOB.mentors -= src
+ return ..()
+
+/client/proc/mentor_client_procs(href_list)
+ if(href_list["mentor_msg"])
+ if(CONFIG_GET(flag/mentors_mobname_only))
+ var/mob/M = locate(href_list["mentor_msg"])
+ cmd_mentor_pm(M,null)
+ else
+ cmd_mentor_pm(href_list["mentor_msg"],null)
+ return TRUE
+
+ //Mentor Follow
+ if(href_list["mentor_follow"])
+ var/mob/living/M = locate(href_list["mentor_follow"])
+
+ if(istype(M))
+ mentor_follow(M)
+ return TRUE
+
+ if(href_list["mentor_unfollow"])
+ if(mentor_datum.following)
+ mentor_unfollow()
+ return TRUE
+
+/client/proc/mentor_datum_set(admin)
+ mentor_datum = GLOB.mentor_datums[ckey]
+ if(!mentor_datum && check_rights_for(src, R_ADMIN,0)) // admin with no mentor datum?let's fix that
+ new /datum/mentors(ckey)
+ if(mentor_datum)
+ mentor_datum.owner = src
+ GLOB.mentors[src] = TRUE
+ add_mentor_verbs()
+ if(check_rights_for(src, R_ADMIN,0))
+ cmd_mentor_dementor()
+
+/client/proc/is_mentor() // admins are mentors too.
+ if(mentor_datum || check_rights_for(src, R_ADMIN,0))
+ return TRUE
diff --git a/modular_skyrat/modules/mentor/code/config.dm b/modular_skyrat/modules/mentor/code/config.dm
new file mode 100644
index 00000000000..a1d1053796a
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/config.dm
@@ -0,0 +1,8 @@
+/datum/configuration
+ var/mentors_mobname_only = FALSE // Only display mob name to mentors in mentorhelps
+ var/mentor_legacy_system = FALSE // Whether to use the legacy mentor system (flat file) instead of SQL
+
+/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
diff --git a/modular_skyrat/modules/mentor/code/dementor.dm b/modular_skyrat/modules/mentor/code/dementor.dm
new file mode 100644
index 00000000000..d0ab511de36
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/dementor.dm
@@ -0,0 +1,19 @@
+/client/proc/cmd_mentor_dementor()
+ set category = "Mentor"
+ set name = "dementor"
+ if(!is_mentor())
+ return
+ remove_mentor_verbs()
+ if (/client/proc/mentor_unfollow in verbs)
+ mentor_unfollow()
+ GLOB.mentors -= src
+ add_verb(src,/client/proc/cmd_mentor_rementor)
+
+/client/proc/cmd_mentor_rementor()
+ set category = "Mentor"
+ set name = "rementor"
+ if(!is_mentor())
+ return
+ add_mentor_verbs()
+ GLOB.mentors[src] = TRUE
+ remove_verb(src,/client/proc/cmd_mentor_rementor)
diff --git a/modular_skyrat/modules/mentor/code/follow.dm b/modular_skyrat/modules/mentor/code/follow.dm
new file mode 100644
index 00000000000..49760a34078
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/follow.dm
@@ -0,0 +1,28 @@
+/client/proc/mentor_follow(mob/living/M)
+ if(!is_mentor())
+ return
+ var/orbiting = TRUE
+ if(!isobserver(usr))
+ mentor_datum.following = M
+ usr.reset_perspective(M)
+ add_verb(src,/client/proc/mentor_unfollow)
+ to_chat(usr, "Click the \"Stop Following\" button here or in the Mentor tab to stop following [key_name(M)].")
+ orbiting = FALSE
+ else
+ var/mob/dead/observer/O = usr
+ O.ManualFollow(M)
+ to_chat(GLOB.admins, "MENTOR: [key_name(usr)] is now [orbiting ? "orbiting" : "following"] [key_name(M)][key_name(M)][orbiting ? " as a ghost" : ""].")
+ log_mentor("[key_name(usr)] [orbiting ? "is now orbiting" : "began following"][key_name(M)][orbiting ? " as a ghost" : ""].")
+
+/client/proc/mentor_unfollow()
+ set category = "Mentor"
+ set name = "Stop Following"
+ set desc = "Stop following the followed."
+
+ if(!is_mentor())
+ return
+ usr.reset_perspective()
+ remove_verb(src,/client/proc/mentor_unfollow)
+ to_chat(GLOB.admins, "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_skyrat/modules/mentor/code/logging.dm b/modular_skyrat/modules/mentor/code/logging.dm
new file mode 100644
index 00000000000..b34be3d2137
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/logging.dm
@@ -0,0 +1,15 @@
+GLOBAL_LIST_EMPTY(mentorlog)
+GLOBAL_PROTECT(mentorlog)
+
+/proc/log_mentor(text)
+ GLOB.mentorlog.Add(text)
+ WRITE_FILE(GLOB.world_game_log, "MENTOR: [text]")
+
+/datum/admins/proc/MentorLogSecret()
+ var/dat = "Mentor Log
"
+ for(var/l in GLOB.mentorlog)
+ dat += "
[l]"
+
+ if(!GLOB.mentorlog.len)
+ dat += "No mentors have done anything this round!"
+ usr << browse(dat, "window=mentor_log")
diff --git a/modular_skyrat/modules/mentor/code/mentor.dm b/modular_skyrat/modules/mentor/code/mentor.dm
new file mode 100644
index 00000000000..90e390a7a2c
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/mentor.dm
@@ -0,0 +1,91 @@
+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"
+ var/client/owner // the actual mentor, client type
+ var/target // the mentor's ckey
+ var/href_token // href token for mentor commands, uses the same token used by admins.
+ var/mob/following
+
+/datum/mentors/New(ckey)
+ if(!ckey)
+ QDEL_IN(src, 0)
+ CRASH("Mentor datum created without a 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()
+ if(!check_rights_for(owner, R_ADMIN,0)) // don't add admins to mentor list.
+ GLOB.mentors[owner] = TRUE
+
+/datum/mentors/proc/CheckMentorHREF(href, href_list)
+ var/auth = href_list["mentor_token"]
+ . = auth && (auth == href_token || auth == GLOB.mentor_href_token)
+ if(.)
+ return
+ var/msg = !auth ? "no" : "a bad"
+ message_admins("[key_name_admin(usr)] clicked an href with [msg] authorization key!")
+ if(CONFIG_GET(flag/debug_admin_hrefs))
+ message_admins("Debug mode enabled, call not blocked. Please ask your coders to review this round's logs.")
+ log_world("UAH: [href]")
+ return TRUE
+ log_admin_private("[key_name(usr)] clicked an href with [msg] authorization key! [href]")
+
+/proc/RawMentorHrefToken(forceGlobal = FALSE)
+ var/tok = GLOB.mentor_href_token
+ if(!forceGlobal && usr)
+ var/client/C = usr.client
+ to_chat(world, C)
+ to_chat(world, usr)
+ if(!C)
+ CRASH("No client for HrefToken()!")
+ var/datum/mentors/holder = C.mentor_datum
+ if(holder)
+ tok = holder.href_token
+ return tok
+
+/proc/MentorHrefToken(forceGlobal = FALSE)
+ return "mentor_token=[RawMentorHrefToken(forceGlobal)]"
+
+/proc/load_mentors()
+ usr = null
+ GLOB.mentor_datums.Cut()
+ for(var/it in GLOB.mentors)
+ var/client/C = it
+ C.remove_mentor_verbs()
+ C.mentor_datum = null
+ GLOB.mentors.Cut()
+ //if(CONFIG_GET(flag/mentor_legacy_system))//legacy
+ var/list/lines = world.file2list("config/skyrat/mentors.txt")
+ for(var/line in lines)
+ if(!length(line))
+ continue
+ if(findtextEx(line, "#", 1, 2))
+ continue
+ new /datum/mentors(line)
+ /*else//Database
+ if(!SSdbcore.Connect())
+ log_world("Failed to connect to database in load_mentors(). Reverting to legacy system.")
+ WRITE_FILE(GLOB.world_game_log, "Failed to connect to database in load_mentors(). Reverting to legacy system.")
+ CONFIG_SET(flag/mentor_legacy_system, TRUE)
+ load_mentors()
+ return
+ var/datum/DBQuery/query_load_mentors = SSdbcore.NewQuery("SELECT ckey FROM [format_table_name("mentor")]")
+ if(!query_load_mentors.Execute())
+ return
+ while(query_load_mentors.NextRow())
+ var/ckey = ckey(query_load_mentors.item[1])
+ new /datum/mentors(ckey)*/
+
+// new client var: mentor_datum. Acts the same way holder does towards admin: it holds the mentor datum. if set, the guy's a mentor.
+/client
+ var/datum/mentors/mentor_datum
diff --git a/modular_skyrat/modules/mentor/code/mentor_verbs.dm b/modular_skyrat/modules/mentor/code/mentor_verbs.dm
new file mode 100644
index 00000000000..5989c3b12e6
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/mentor_verbs.dm
@@ -0,0 +1,12 @@
+GLOBAL_LIST_INIT(mentor_verbs, list(
+ /client/proc/cmd_mentor_say,
+ /client/proc/cmd_mentor_dementor
+ ))
+GLOBAL_PROTECT(mentor_verbs)
+
+/client/proc/add_mentor_verbs()
+ if(mentor_datum)
+ add_verb(src,GLOB.mentor_verbs)
+
+/client/proc/remove_mentor_verbs()
+ remove_verb(src,GLOB.mentor_verbs)
diff --git a/modular_skyrat/modules/mentor/code/mentorhelp.dm b/modular_skyrat/modules/mentor/code/mentorhelp.dm
new file mode 100644
index 00000000000..9e1d3e8a01d
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/mentorhelp.dm
@@ -0,0 +1,99 @@
+/client/verb/mentorhelp(msg as text)
+ set category = "Mentor"
+ set name = "Mentorhelp"
+
+ //clean the input msg
+ if(!msg) return
+
+ //remove out mentorhelp verb temporarily to prevent spamming of mentors.
+ remove_verb(src,/client/verb/mentorhelp)
+ spawn(300)
+ add_verb(src,/client/verb/mentorhelp) // 30 second cool-down for mentorhelp
+
+ msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN))
+ if(!msg || !mob)
+ return
+
+ var/show_char = CONFIG_GET(flag/mentors_mobname_only)
+ var/mentor_msg = "MENTORHELP: [key_name_mentor(src, 1, 0, 1, show_char)]: [msg]"
+ log_mentor("MENTORHELP: [key_name_mentor(src, 0, 0, 0, 0)]: [msg]")
+
+ for(var/it in GLOB.mentors)
+ var/client/X = it
+ SEND_SOUND(X, 'sound/items/bikehorn.ogg')
+ to_chat(X, mentor_msg)
+
+ to_chat(src, "PM to-Mentors: [msg]")
+ return
+
+/proc/get_mentor_counts()
+ . = list("total" = 0, "afk" = 0, "present" = 0)
+ for(var/X in GLOB.mentors)
+ var/client/C = X
+ .["total"]++
+ if(C.is_afk())
+ .["afk"]++
+ else
+ .["present"]++
+
+/proc/key_name_mentor(var/whom, var/include_link = null, var/include_name = 0, var/include_follow = 0, var/char_name_only = 0)
+ var/mob/M
+ var/client/C
+ var/key
+ var/ckey
+
+ if(!whom) return "*null*"
+ if(istype(whom, /client))
+ C = whom
+ M = C.mob
+ key = C.key
+ ckey = C.ckey
+ else if(ismob(whom))
+ M = whom
+ C = M.client
+ key = M.key
+ ckey = M.ckey
+ else if(istext(whom))
+ key = whom
+ ckey = ckey(whom)
+ C = GLOB.directory[ckey]
+ if(C)
+ M = C.mob
+ else
+ return "*invalid*"
+
+ . = ""
+
+ if(!ckey)
+ include_link = 0
+
+ if(key)
+ if(include_link)
+ if(CONFIG_GET(flag/mentors_mobname_only))
+ . += ""
+ else
+ . += ""
+
+ if(C && C.holder && C.holder.fakekey)
+ . += "Administrator"
+ else if (char_name_only && CONFIG_GET(flag/mentors_mobname_only))
+ if(istype(C.mob,/mob/dead/new_player) || istype(C.mob, /mob/dead/observer)) //If they're in the lobby or observing, display their ckey
+ . += key
+ else if(C && C.mob) //If they're playing/in the round, only show the mob name
+ . += C.mob.name
+ else //If for some reason neither of those are applicable and they're mentorhelping, show ckey
+ . += key
+ else
+ . += key
+ if(!C)
+ . += "\[DC\]"
+
+ if(include_link)
+ . += ""
+ else
+ . += "*no key*"
+
+ if(include_follow)
+ . += " (F)"
+
+ return .
diff --git a/modular_skyrat/modules/mentor/code/mentorpm.dm b/modular_skyrat/modules/mentor/code/mentorpm.dm
new file mode 100644
index 00000000000..fce7c475287
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/mentorpm.dm
@@ -0,0 +1,87 @@
+//shows a list of clients we could send PMs to, then forwards our choice to cmd_Mentor_pm
+/client/proc/cmd_mentor_pm_panel()
+ set category = "Mentor"
+ set name = "Mentor PM"
+ if(!is_mentor())
+ to_chat(src, "Error: Mentor-PM-Panel: Only Mentors and Admins may use this command.")
+ return
+ var/list/client/targets[0]
+ for(var/client/T)
+ targets["[T]"] = T
+
+ var/list/sorted = sortList(targets)
+ var/target = input(src,"To whom shall we send a message?","Mentor PM",null) in sorted|null
+ cmd_mentor_pm(targets[target],null)
+ SSblackbox.record_feedback("tally", "Mentor_verb", 1, "APM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+
+
+//takes input from cmd_mentor_pm_context, cmd_Mentor_pm_panel or /client/Topic and sends them a PM.
+//Fetching a message if needed. src is the sender and C is the target client
+/client/proc/cmd_mentor_pm(whom, msg)
+ var/client/C
+ if(ismob(whom))
+ var/mob/M = whom
+ C = M.client
+ else if(istext(whom))
+ C = GLOB.directory[whom]
+ else if(istype(whom,/client))
+ C = whom
+ if(!C)
+ if(is_mentor()) to_chat(src, "Error: Mentor-PM: Client not found.")
+ else mentorhelp(msg) //Mentor we are replying to left. Mentorhelp instead(check below)
+ return
+
+ if(is_mentor(whom))
+ to_chat(GLOB.mentors, "[src] has started replying to [whom]'s mhelp.")
+
+ //get message text, limit it's length.and clean/escape html
+ if(!msg)
+ msg = input(src,"Message:", "Private message") as text|null
+
+ if(!msg)
+ if (is_mentor(whom))
+ to_chat(GLOB.mentors, "[src] has stopped their reply to [whom]'s mhelp.")
+ return
+
+ if(!C)
+ if(is_mentor())
+ to_chat(src, "Error: Mentor-PM: Client not found.")
+ else
+ mentorhelp(msg) //Mentor we are replying to has vanished, Mentorhelp instead (how the fuck does this work?let's hope it works,shrug)
+ return
+
+ // Neither party is a mentor, they shouldn't be PMing!
+ if (!C.is_mentor() && !is_mentor())
+ return
+
+ msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN))
+ if(!msg)
+ if (is_mentor(whom))
+ to_chat(GLOB.mentors, "[src] has stopped their reply to [whom]'s mhelp.")
+ return
+ log_mentor("Mentor PM: [key_name(src)]->[key_name(C)]: [msg]")
+
+ msg = emoji_parse(msg)
+ C << 'sound/items/bikehorn.ogg'
+ var/show_char = CONFIG_GET(flag/mentors_mobname_only)
+ if(C.is_mentor())
+ if(is_mentor())//both are mentors
+ to_chat(C, "Mentor PM from-[key_name_mentor(src, C, 1, 0, 0)]: [msg]")
+ to_chat(src, "Mentor PM to-[key_name_mentor(C, C, 1, 0, 0)]: [msg]")
+
+ else //recipient is a mentor but sender is not
+ to_chat(C, "Reply PM from-[key_name_mentor(src, C, 1, 0, show_char)]: [msg]")
+ to_chat(src, "Mentor PM to-[key_name_mentor(C, C, 1, 0, 0)]: [msg]")
+
+ else
+ if(is_mentor()) //sender is a mentor but recipient is not.
+ to_chat(C, "Mentor PM from-[key_name_mentor(src, C, 1, 0, 0)]: [msg]")
+ to_chat(src, "Mentor PM to-[key_name_mentor(C, C, 1, 0, show_char)]: [msg]")
+
+ //we don't use message_Mentors here because the sender/receiver might get it too
+ var/show_char_sender = !is_mentor() && CONFIG_GET(flag/mentors_mobname_only)
+ var/show_char_recip = !C.is_mentor() && CONFIG_GET(flag/mentors_mobname_only)
+ for(var/it in GLOB.mentors)
+ var/client/X = it
+ if(X.key!=key && X.key!=C.key) //check client/X is an Mentor and isn't the sender or recipient
+ to_chat(X, "Mentor PM: [key_name_mentor(src, X, 0, 0, show_char_sender)]->[key_name_mentor(C, X, 0, 0, show_char_recip)]: [msg]") //inform X
diff --git a/modular_skyrat/modules/mentor/code/mentorsay.dm b/modular_skyrat/modules/mentor/code/mentorsay.dm
new file mode 100644
index 00000000000..b69ce29047d
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/mentorsay.dm
@@ -0,0 +1,19 @@
+/client/proc/cmd_mentor_say(msg as text)
+ set category = "Mentor"
+ set name = "Msay" //Gave this shit a shorter name so you only have to time out "msay" rather than "mentor say" to use it --NeoFite
+ set hidden = 1
+ if(!is_mentor())
+ return
+
+ msg = copytext_char(sanitize(msg), 1, MAX_MESSAGE_LEN)
+ if(!msg)
+ return
+
+ msg = emoji_parse(msg)
+ log_mentor("MSAY: [key_name(src)] : [msg]")
+
+ if(check_rights_for(src, R_ADMIN,0))
+ msg = "MENTOR: [key_name(src, 0, 0)]: [msg]"
+ else
+ msg = "MENTOR: [key_name(src, 0, 0)]: [msg]"
+ to_chat(GLOB.admins | GLOB.mentors, msg)
diff --git a/modular_skyrat/modules/mentor/code/mentorwho.dm b/modular_skyrat/modules/mentor/code/mentorwho.dm
new file mode 100644
index 00000000000..b374a078232
--- /dev/null
+++ b/modular_skyrat/modules/mentor/code/mentorwho.dm
@@ -0,0 +1,22 @@
+/client/verb/mentorwho()
+ set category = "Mentor"
+ set name = "Mentorwho"
+ var/msg = "Current Mentors:\n"
+ for(var/X in GLOB.mentors)
+ var/client/C = X
+ if(!C)
+ GLOB.mentors -= C
+ continue // weird runtime that happens randomly
+ var/suffix = ""
+ if(holder)
+ if(isobserver(C.mob))
+ suffix += " - Observing"
+ else if(istype(C.mob,/mob/dead/new_player))
+ suffix += " - Lobby"
+ else
+ suffix += " - Playing"
+
+ if(C.is_afk())
+ suffix += " (AFK)"
+ msg += "\t[C][suffix]\n"
+ to_chat(src, msg)
diff --git a/modular_skyrat/modules/mentor/readme.md b/modular_skyrat/modules/mentor/readme.md
new file mode 100644
index 00000000000..7b2ce2ad6cf
--- /dev/null
+++ b/modular_skyrat/modules/mentor/readme.md
@@ -0,0 +1,29 @@
+## Title: Mentor
+
+MODULE ID: MENTOR
+
+### Description:
+
+Adds a mentor system, allowing for players to send a-help like messages to mentors, asking them about game mechanics and help.
+
+### TG Proc Changes:
+
+- code/modules/client/client_procs.dm > client/Topic()
+- /code/modules/admin/secrets.dm > /datum/admins/proc/Secrets_topic(), /datum/admins/proc/Secrets()
+
+### Defines:
+
+- N/A
+
+### Master file additions
+
+- N/A
+
+### Included files that are not contained in this module:
+
+- N/A
+
+### Credits:
+
+Azarak - Porting, tweaks
+Poojawa - Implementation
diff --git a/tgstation.dme b/tgstation.dme
index 5cd1f94148c..b50e62fcec1 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3463,6 +3463,18 @@
#include "modular_skyrat\modules\icspawning\code\game\objects\items\cards_ids.dm"
#include "modular_skyrat\modules\icspawning\code\modules\clothing\outfits\standard.dm"
#include "modular_skyrat\modules\icspawning\code\modules\spells\spell.dm"
+#include "modular_skyrat\modules\mentor\code\_globalvars.dm"
+#include "modular_skyrat\modules\mentor\code\client_procs.dm"
+#include "modular_skyrat\modules\mentor\code\config.dm"
+#include "modular_skyrat\modules\mentor\code\dementor.dm"
+#include "modular_skyrat\modules\mentor\code\follow.dm"
+#include "modular_skyrat\modules\mentor\code\logging.dm"
+#include "modular_skyrat\modules\mentor\code\mentor.dm"
+#include "modular_skyrat\modules\mentor\code\mentor_verbs.dm"
+#include "modular_skyrat\modules\mentor\code\mentorhelp.dm"
+#include "modular_skyrat\modules\mentor\code\mentorpm.dm"
+#include "modular_skyrat\modules\mentor\code\mentorsay.dm"
+#include "modular_skyrat\modules\mentor\code\mentorwho.dm"
#include "modular_skyrat\modules\inflatables\code\inflatable.dm"
#include "modular_skyrat\modules\jukebox\code\controllers\subsystem\jukeboxes.dm"
#include "modular_skyrat\modules\jukebox\code\controllers\subsystem\game\machinery\dance_machine.dm"