diff --git a/code/__DEFINES/statpanel.dm b/code/__DEFINES/statpanel.dm new file mode 100644 index 00000000000..7988b9b5c83 --- /dev/null +++ b/code/__DEFINES/statpanel.dm @@ -0,0 +1,8 @@ +/// Bare minimum required verbs for stat panel operation +GLOBAL_LIST_INIT(stat_panel_verbs, list( + /client/verb/set_tab, + /client/verb/send_tabs, + /client/verb/remove_tabs, + /client/verb/reset_tabs, + /client/verb/panel_ready +)) diff --git a/code/controllers/configuration/config_entry.dm b/code/controllers/configuration/config_entry.dm index ac1c768a2d1..41417a2ed30 100644 --- a/code/controllers/configuration/config_entry.dm +++ b/code/controllers/configuration/config_entry.dm @@ -118,6 +118,19 @@ config_entry_value = text2num(trim(str_val)) != 0 return TRUE +/// List config entry, used for configuring a list of strings +/datum/config_entry/str_list + abstract_type = /datum/config_entry/str_list + config_entry_value = list() + dupes_allowed = TRUE + +/datum/config_entry/str_list/ValidateAndSet(str_val) + if (!VASProcCallGuard(str_val)) + return FALSE + str_val = trim(str_val) + if (str_val != "") + config_entry_value += str_val + /datum/config_entry/number_list abstract_type = /datum/config_entry/number_list config_entry_value = list() diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 3d0646da4c7..02915377581 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -314,6 +314,9 @@ /datum/config_entry/number/panic_bunker_living // living time in minutes that a player needs to pass the panic bunker +/// Flag for requiring players who would otherwise be denied access by the panic bunker to complete a written interview +/datum/config_entry/flag/panic_bunker_interview + /datum/config_entry/string/panic_bunker_message config_entry_value = "Sorry but the server is currently not accepting connections from never before seen players." @@ -496,4 +499,3 @@ /datum/config_entry/string/centcom_ban_db // URL for the CentCom Galactic Ban DB API /datum/config_entry/string/centcom_source_whitelist - diff --git a/code/controllers/configuration/entries/interview.dm b/code/controllers/configuration/entries/interview.dm new file mode 100644 index 00000000000..b18f64d724d --- /dev/null +++ b/code/controllers/configuration/entries/interview.dm @@ -0,0 +1,3 @@ +/datum/config_entry/str_list/interview_questions + +/datum/config_entry/string/interview_welcome_msg diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index 09bed51c271..ddc5cb30931 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -54,6 +54,35 @@ SUBSYSTEM_DEF(statpanels) if(target.stat_tab == "Tickets") var/list/ahelp_tickets = GLOB.ahelp_tickets.stat_entry() target << output("[url_encode(json_encode(ahelp_tickets))];", "statbrowser:update_tickets") + if(target.stat_tab == "Interviews") + var/datum/interview_manager/m = GLOB.interviews + + // get open interview count + var/dc = 0 + for (var/ckey in m.open_interviews) + var/datum/interview/I = m.open_interviews[ckey] + if (I && !I.owner) + dc++ + var/stat_string = "([m.open_interviews.len - dc] online / [dc] disconnected)" + + // Prepare each queued interview + var/list/queued = list() + for (var/datum/interview/I in m.interview_queue) + queued += list(list( + "ref" = REF(I), + "status" = "\[[I.pos_in_queue]\]: [I.owner_ckey][!I.owner ? " (DC)": ""] \[INT-[I.id]\]" + )) + + var/list/data = list( + "status" = list( + "Active:" = "[m.open_interviews.len] [stat_string]", + "Queued:" = "[m.interview_queue.len]", + "Closed:" = "[m.closed_interviews.len]"), + "interviews" = queued + ) + + // Push update + target << output("[url_encode(json_encode(data))];", "statbrowser:update_interviews") if(!length(GLOB.sdql2_queries) && ("SDQL2" in target.panel_tabs)) target << output("", "statbrowser:remove_sdql2") else if(length(GLOB.sdql2_queries) && (target.stat_tab == "SDQL2" || !("SDQL2" in target.panel_tabs))) @@ -170,3 +199,4 @@ SUBSYSTEM_DEF(statpanels) set hidden = TRUE statbrowser_ready = TRUE + init_verbs() diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm index 7bde5574b71..baae7c6bba1 100644 --- a/code/datums/world_topic.dm +++ b/code/datums/world_topic.dm @@ -183,6 +183,7 @@ .["extreme_popcap"] = CONFIG_GET(number/extreme_popcap) || 0 .["popcap"] = max(CONFIG_GET(number/soft_popcap), CONFIG_GET(number/hard_popcap), CONFIG_GET(number/extreme_popcap)) //generalized field for this concept for use across ss13 codebases .["bunkered"] = CONFIG_GET(flag/panic_bunker) || FALSE + .["interviews"] = CONFIG_GET(flag/panic_bunker_interview) || FALSE if(SSshuttle?.emergency) .["shuttle_mode"] = SSshuttle.emergency.mode // Shuttle status, see /__DEFINES/stat.dm diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 55328ef4cc6..d5349bf05fd 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -127,6 +127,7 @@ GLOBAL_PROTECT(admin_verbs_server) /client/proc/forcerandomrotate, /client/proc/adminchangemap, /client/proc/panicbunker, + /client/proc/toggle_interviews, /client/proc/toggle_hub, /client/proc/toggle_cdn ) @@ -250,6 +251,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list( /proc/release, /client/proc/reload_admins, /client/proc/panicbunker, + /client/proc/toggle_interviews, /client/proc/admin_change_sec_level, /client/proc/toggle_nuke, /client/proc/cmd_display_del_log, diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index b471aba555a..16cd23326a5 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -2308,6 +2308,18 @@ var/obj/item/nuclear_challenge/button = locate(href_list["force_war"]) button.force_war() + else if (href_list["interview"]) + if(!check_rights(R_ADMIN)) + return + var/datum/interview/I = locate(href_list["interview"]) + if (I) + I.ui_interact(usr) + + else if (href_list["interview_man"]) + if(!check_rights(R_ADMIN)) + return + GLOB.interviews.ui_interact(usr) + /datum/admins/proc/HandleCMode() if(!check_rights(R_ADMIN)) return diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index 200ccff0da5..234f10e0897 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -274,7 +274,6 @@ if(!already_logged) //Reply to an existing ticket SSblackbox.LogAhelp(recipient.current_ticket.id, "Reply", msg, recipient.ckey, src.ckey) - //always play non-admin recipients the adminhelp sound SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) diff --git a/code/modules/admin/verbs/panicbunker.dm b/code/modules/admin/verbs/panicbunker.dm index 846e5f9530a..c0e229ef6e7 100644 --- a/code/modules/admin/verbs/panicbunker.dm +++ b/code/modules/admin/verbs/panicbunker.dm @@ -6,6 +6,7 @@ return var/new_pb = !CONFIG_GET(flag/panic_bunker) + var/interview = CONFIG_GET(flag/panic_bunker_interview) var/time_rec = 0 var/message = "" if(new_pb) @@ -14,10 +15,23 @@ message = replacetext(message, "%minutes%", time_rec) CONFIG_SET(number/panic_bunker_living, time_rec) CONFIG_SET(string/panic_bunker_message, message) - + + var/interview_sys = alert(src, "Should the interview system be enabled? (Allows players to connect under the hour limit and force them to be manually approved to play)", "Enable interviews?", "Enable", "Disable") + interview = interview_sys == "Enable" + CONFIG_SET(flag/panic_bunker_interview, interview) CONFIG_SET(flag/panic_bunker, new_pb) - log_admin("[key_name(usr)] has toggled the Panic Bunker, it is now [new_pb ? "on and set to [time_rec] with a message of [message]" : "off"]") - message_admins("[key_name_admin(usr)] has toggled the Panic Bunker, it is now [new_pb ? "enabled with a living minutes requirement of [time_rec]" : "disabled"].") + log_admin("[key_name(usr)] has toggled the Panic Bunker, it is now [new_pb ? "on and set to [time_rec] with a message of [message]. The interview system is [interview ? "enabled" : "disabled"]" : "off"].") + message_admins("[key_name_admin(usr)] has toggled the Panic Bunker, it is now [new_pb ? "enabled with a living minutes requirement of [time_rec]. The interview system is [interview ? "enabled" : "disabled"]" : "disabled"].") if (new_pb && !SSdbcore.Connect()) message_admins("The Database is not connected! Panic bunker will not work until the connection is reestablished.") SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Panic Bunker", "[new_pb ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/proc/toggle_interviews() + set category = "Server" + set name = "Toggle PB Interviews" + if (!CONFIG_GET(flag/panic_bunker)) + to_chat(usr, "NOTE: The panic bunker is not enabled, so this change will not effect anything until it is enabled.", confidential = TRUE) + var/new_interview = !CONFIG_GET(flag/panic_bunker_interview) + CONFIG_SET(flag/panic_bunker_interview, new_interview) + log_admin("[key_name(usr)] has toggled the Panic Bunker's interview system, it is now [new_interview ? "enabled" : "disabled"].") + message_admins("[key_name(usr)] has toggled the Panic Bunker's interview system, it is now [new_interview ? "enabled" : "disabled"].") diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index fe9c1c4277c..4b3de98c4b7 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -204,3 +204,6 @@ var/next_move_dir_add /// On next move, subtract this dir from the move that would otherwise be done var/next_move_dir_sub + + /// If the client is currently under the restrictions of the interview system + var/interviewee = FALSE diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index a742d5a996d..3a8654bcdbc 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -212,6 +212,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( tgui_panel = new(src) GLOB.ahelp_tickets.ClientLogin(src) + GLOB.interviews.client_login(src) var/connecting_admin = FALSE //because de-admined admins connecting should be treated like admins. //Admin Authorisation holder = GLOB.admin_datums[ckey] @@ -426,26 +427,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(!tooltips) tooltips = new /datum/tooltip(src) - var/list/topmenus = GLOB.menulist[/datum/verbs/menu] - for (var/thing in topmenus) - var/datum/verbs/menu/topmenu = thing - var/topmenuname = "[topmenu]" - if (topmenuname == "[topmenu.type]") - var/list/tree = splittext(topmenuname, "/") - topmenuname = tree[tree.len] - winset(src, "[topmenu.type]", "parent=menu;name=[url_encode(topmenuname)]") - var/list/entries = topmenu.Generate_list(src) - for (var/child in entries) - winset(src, "[child]", "[entries[child]]") - if (!ispath(child, /datum/verbs/menu)) - var/procpath/verbpath = child - if (verbpath.name[1] != "@") - new child(src) - - for (var/thing in prefs.menuoptions) - var/datum/verbs/menu/menuitem = GLOB.menulist[thing] - if (menuitem) - menuitem.Load_checked(src) + if (!interviewee) + initialize_menus() view_size = new(src, getScreenSize(prefs.widescreenpref)) view_size.resetFormat() @@ -467,6 +450,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( GLOB.directory -= ckey log_access("Logout: [key_name(src)]") GLOB.ahelp_tickets.ClientLogout(src) + GLOB.interviews.client_logout(src) SSserver_maint.UpdateHubStatus() if(credits) QDEL_LIST(credits) @@ -552,7 +536,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( var/living_recs = CONFIG_GET(number/panic_bunker_living) //Relies on pref existing, but this proc is only called after that occurs, so we're fine. var/minutes = get_exp_living(pure_numeric = TRUE) - if(minutes <= living_recs) + if(minutes <= living_recs && !CONFIG_GET(flag/panic_bunker_interview)) var/reject_message = "Failed Login: [key] - Account attempting to connect during panic bunker, but they do not have the required living time [minutes]/[living_recs]" log_access(reject_message) message_admins("[reject_message]") @@ -868,6 +852,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( ..() /client/proc/add_verbs_from_config() + if (interviewee) + return if(CONFIG_GET(flag/see_own_notes)) add_verb(src, /client/proc/self_notes) if(CONFIG_GET(flag/use_exp_tracking)) @@ -1058,3 +1044,28 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(statbrowser_ready) return to_chat(src, "Statpanel failed to load, click here to reload the panel ") + +/** + * Initializes dropdown menus on client + */ +/client/proc/initialize_menus() + var/list/topmenus = GLOB.menulist[/datum/verbs/menu] + for (var/thing in topmenus) + var/datum/verbs/menu/topmenu = thing + var/topmenuname = "[topmenu]" + if (topmenuname == "[topmenu.type]") + var/list/tree = splittext(topmenuname, "/") + topmenuname = tree[tree.len] + winset(src, "[topmenu.type]", "parent=menu;name=[url_encode(topmenuname)]") + var/list/entries = topmenu.Generate_list(src) + for (var/child in entries) + winset(src, "[child]", "[entries[child]]") + if (!ispath(child, /datum/verbs/menu)) + var/procpath/verbpath = child + if (verbpath.name[1] != "@") + new child(src) + + for (var/thing in prefs.menuoptions) + var/datum/verbs/menu/menuitem = GLOB.menulist[thing] + if (menuitem) + menuitem.Load_checked(src) diff --git a/code/modules/interview/interview.dm b/code/modules/interview/interview.dm new file mode 100644 index 00000000000..59076d057ec --- /dev/null +++ b/code/modules/interview/interview.dm @@ -0,0 +1,162 @@ +/// State when an interview has been approved +#define INTERVIEW_APPROVED "interview_approved" +/// State when an interview as been denied +#define INTERVIEW_DENIED "interview_denied" +/// State when an interview has had no action on it yet +#define INTERVIEW_PENDING "interview_pending" + +/** + * Represents a new-player interview form + * + * Represents a new-player interview form, enabled by configuration to require + * players with low playtime to request access to the server. To do so, they will + * out a brief questionnaire, and are otherwise unable to do anything while they + * wait for a response. + */ +/datum/interview + /// Unique ID of the interview + var/id + /// Atomic ID for incrementing unique IDs + var/static/atomic_id = 0 + /// The /client who owns this interview, the intiator + var/client/owner + /// The Ckey of the owner, used for when a client could disconnect + var/owner_ckey + /// The welcome message shown at the top of the interview panel + var/welcome_message + /// The questions to display on the questionnaire of the interview + var/list/questions + /// The stored responses, will be filled as the questionnaire is answered + var/list/responses = list() + /// Boolean operator controlling if the questionnaire's contents can be edited + var/read_only = FALSE + /// Integer that contains the current position in the interview queue, used for rendering + var/pos_in_queue + /// Contains the state of the form, used for rendering and sanity checking + var/status = INTERVIEW_PENDING + +/datum/interview/New(client/interviewee) + if(!interviewee) + qdel(src) + return + id = ++atomic_id + owner = interviewee + owner_ckey = owner.ckey + questions = CONFIG_GET(str_list/interview_questions) + responses.len = questions.len + welcome_message = CONFIG_GET(string/interview_welcome_msg) + +/** + * Approves the interview, forces reconnect of owner if relevant. + * + * Approves the interview, and if relevant will force the owner to reconnect so that they have the proper + * verbs returned to them. + * Arguments: + * * approved_by - The user who approved the interview, used for logging + */ +/datum/interview/proc/approve(client/approved_by) + status = INTERVIEW_APPROVED + read_only = TRUE + GLOB.interviews.approved_ckeys |= owner_ckey + GLOB.interviews.close_interview(src) + log_admin_private("[key_name(approved_by)] has approved interview #[id] for [owner_ckey][!owner ? "(DC)": ""].") + message_admins("[key_name(approved_by)] has approved interview #[id] for [owner_ckey][!owner ? "(DC)": ""].") + if (owner) + SEND_SOUND(owner, sound('sound/effects/adminhelp.ogg')) + to_chat(owner, "-- Interview Update --" \ + + "\nYour interview was approved, you will now be reconnected in 5 seconds.", confidential = TRUE) + addtimer(CALLBACK(src, .proc/reconnect_owner), 50) + +/** + * Denies the interview and adds the owner to the cooldown for new interviews. + * + * Arguments: + * * denied_by - The user who denied the interview, used for logging + */ +/datum/interview/proc/deny(client/denied_by) + status = INTERVIEW_DENIED + read_only = TRUE + GLOB.interviews.close_interview(src) + GLOB.interviews.cooldown_ckeys |= owner_ckey + log_admin_private("[key_name(denied_by)] has denied interview #[id] for [owner_ckey][!owner ? "(DC)": ""].") + message_admins("[key_name(denied_by)] has denied interview #[id] for [owner_ckey][!owner ? "(DC)": ""].") + addtimer(CALLBACK(GLOB.interviews, /datum/interview_manager.proc/release_from_cooldown, owner_ckey), 180) + if (owner) + SEND_SOUND(owner, sound('sound/effects/adminhelp.ogg')) + to_chat(owner, "-- Interview Update --" \ + + "\nUnfortunately your interview was denied. Please try submitting another questionnaire." \ + + " You may do this in three minutes.", confidential = TRUE) + +/** + * Forces client to reconnect, used in the callback from approval + */ +/datum/interview/proc/reconnect_owner() + if (!owner) + return + winset(owner, null, "command=.reconnect") + +/** + * Verb for opening the existing interview, or if relevant creating a new interview if possible. + */ +/mob/dead/new_player/proc/open_interview() + set name = "Open Interview" + set category = "Interview" + var/mob/dead/new_player/M = usr + if (M?.client?.interviewee) + var/datum/interview/I = GLOB.interviews.interview_for_client(M.client) + if (I) // we can be returned nothing if the user is on cooldown + I.ui_interact(M) + else + to_chat(usr, "You are on cooldown for interviews. Please" \ + + " wait at least 3 minutes before starting a new questionnaire.", confidential = TRUE) + +/datum/interview/ui_interact(mob/user, datum/tgui/ui = null) + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "Interview") + ui.open() + +/datum/interview/ui_state(mob/user) + return GLOB.new_player_state + +/datum/interview/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if (..()) + return + switch(action) + if ("update_answer") + if (!read_only) + responses[text2num(params["qidx"])] = copytext_char(params["answer"], 1, 501) // byond indexing moment + . = TRUE + if ("submit") + if (!read_only) + read_only = TRUE + GLOB.interviews.enqueue(src) + . = TRUE + if ("approve") + if (usr.client?.holder && status == INTERVIEW_PENDING) + src.approve(usr) + . = TRUE + if ("deny") + if (usr.client?.holder && status == INTERVIEW_PENDING) + src.deny(usr) + . = TRUE + if ("adminpm") + if (usr.client?.holder && owner) + usr.client.cmd_admin_pm(owner, null) + +/datum/interview/ui_data(mob/user) + . = list( + "welcome_message" = welcome_message, + "questions" = list(), + "read_only" = read_only, + "queue_pos" = pos_in_queue, + "is_admin" = !!(user?.client && user.client.holder), + "status" = status, + "connected" = !!owner) + for (var/i in 1 to questions.len) + var/list/data = list( + "qidx" = i, + "question" = questions[i], + "response" = responses.len < i ? null : responses[i] + ) + .["questions"] += list(data) diff --git a/code/modules/interview/interview_manager.dm b/code/modules/interview/interview_manager.dm new file mode 100644 index 00000000000..2aabe91f960 --- /dev/null +++ b/code/modules/interview/interview_manager.dm @@ -0,0 +1,218 @@ +GLOBAL_DATUM_INIT(interviews, /datum/interview_manager, new) + +/** + * # Interview Manager + * + * Handles all interviews in the duration of a round, includes the primary functionality for + * handling the interview queue. + */ +/datum/interview_manager + /// The interviews that are currently "open", those that are not submitted as well as those that are waiting review + var/list/open_interviews = list() + /// The queue of interviews to be processed (submitted interviews) + var/list/interview_queue = list() + /// All closed interviews + var/list/closed_interviews = list() + /// Ckeys which are allowed to bypass the time-based allowlist + var/list/approved_ckeys = list() + /// Ckeys which are currently in the cooldown system, they will be unable to create new interviews + var/list/cooldown_ckeys = list() + +/datum/interview_manager/Destroy(force, ...) + QDEL_LIST(open_interviews) + QDEL_LIST(interview_queue) + QDEL_LIST(closed_interviews) + QDEL_LIST(approved_ckeys) + QDEL_LIST(cooldown_ckeys) + return ..() + +/** + * Used in the new client pipeline to catch when clients are reconnecting and need to have their + * reference re-assigned to the 'owner' variable of an interview + * + * Arguments: + * * C - The client who is logging in + */ +/datum/interview_manager/proc/client_login(client/C) + for(var/ckey in open_interviews) + var/datum/interview/I = open_interviews[ckey] + if (I && !I.owner && C.ckey == I.owner_ckey) + I.owner = C + +/** + * Used in the destroy client pipeline to catch when clients are disconnecting and need to have their + * reference nulled on the 'owner' variable of an interview + * + * Arguments: + * * C - The client who is logging out + */ +/datum/interview_manager/proc/client_logout(client/C) + for(var/ckey in open_interviews) + var/datum/interview/I = open_interviews[ckey] + if (I?.owner && C.ckey == I.owner_ckey) + I.owner = null + +/** + * Attempts to return an interview for a given client, using an existing interview if found, otherwise + * a new interview is created; if the user is on cooldown then it will return null. + * + * Arguments: + * * C - The client to get the interview for + */ +/datum/interview_manager/proc/interview_for_client(client/C) + if (!C) + return + if (open_interviews[C.ckey]) + return open_interviews[C.ckey] + else if (!(C.ckey in cooldown_ckeys)) + log_admin_private("New interview created for [key_name(C)].") + open_interviews[C.ckey] = new /datum/interview(C) + return open_interviews[C.ckey] + +/** + * Attempts to return an interview for a provided ID, will return null if no matching interview is found + * + * Arguments: + * * id - The ID of the interview to find + */ +/datum/interview_manager/proc/interview_by_id(id) + if (!id) + return + for (var/ckey in open_interviews) + var/datum/interview/I = open_interviews[ckey] + if (I?.id == id) + return I + for (var/datum/interview/I in closed_interviews) + if (I.id == id) + return I + +/** + * Enqueues an interview in the interview queue, and notifies admins of the new interview to be + * reviewed. + * + * Arguments: + * * to_queue - The interview to enqueue + */ +/datum/interview_manager/proc/enqueue(datum/interview/to_queue) + if (!to_queue || (to_queue in interview_queue)) + return + to_queue.pos_in_queue = interview_queue.len + 1 + interview_queue |= to_queue + + // Notify admins + var/ckey = to_queue.owner_ckey + log_admin_private("Interview for [ckey] has been enqueued for review. Current position in queue: [to_queue.pos_in_queue]") + var/admins_present = send2tgs_adminless_only("panic-bunker-interview", "Interview for [ckey] enqueued for review. Current position in queue: [to_queue.pos_in_queue]") + if (admins_present <= 0 && to_queue.owner) + to_chat(to_queue.owner, "No active admins are online, your interview's submission was sent through TGS to admins who are available. This may use IRC or Discord.") + for(var/client/X in GLOB.admins) + if(X.prefs.toggles & SOUND_ADMINHELP) + SEND_SOUND(X, sound('sound/effects/adminhelp.ogg')) + window_flash(X, ignorepref = TRUE) + to_chat(X, "Interview for [ckey] enqueued for review. Current position in queue: [to_queue.pos_in_queue]", confidential = TRUE) + +/** + * Removes a ckey from the cooldown list, used for enforcing cooldown after an interview is denied. + * + * Arguments: + * * ckey - The ckey to remove from the cooldown list + */ +/datum/interview_manager/proc/release_from_cooldown(ckey) + cooldown_ckeys -= ckey + +/** + * Dequeues the first interview from the interview queue, and updates the queue positions of any relevant + * interviews that follow it. + */ +/datum/interview_manager/proc/dequeue() + if (interview_queue.len == 0) + return + + // Get the first interview off the front of the queue + var/datum/interview/to_return = interview_queue[1] + interview_queue -= to_return + + // Decrement any remaining interview queue positions + for(var/datum/interview/i in interview_queue) + i.pos_in_queue-- + + return to_return + +/** + * Dequeues an interview from the interview queue if present, and updates the queue positions of + * any relevant interviews that follow it. + * + * Arguments: + * * to_dequeue - The interview to dequeue + */ +/datum/interview_manager/proc/dequeue_specific(datum/interview/to_dequeue) + if (!to_dequeue) + return + + // Decrement all interviews in queue past the interview being removed + var/found = FALSE + for (var/datum/interview/i in interview_queue) + if (found) + i.pos_in_queue-- + if (i == to_dequeue) + found = TRUE + + interview_queue -= to_dequeue + +/** + * Closes an interview, removing it from the queued interviews as well as adding it to the closed + * interviews list. + * + * Arguments: + * * to_close - The interview to dequeue + */ +/datum/interview_manager/proc/close_interview(datum/interview/to_close) + if (!to_close) + return + dequeue_specific(to_close) + if (open_interviews[to_close.owner_ckey]) + open_interviews -= to_close.owner_ckey + closed_interviews += to_close + +/datum/interview_manager/ui_interact(mob/user, datum/tgui/ui = null) + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "InterviewManager") + ui.open() + +/datum/interview_manager/ui_state(mob/user) + return GLOB.admin_state + +/datum/interview_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if (..()) + return + switch(action) + if ("open") + var/datum/interview/I = interview_by_id(text2num(params["id"])) + if (I) + I.ui_interact(usr) + + +/datum/interview_manager/ui_data(mob/user) + . = list( + "open_interviews" = list(), + "closed_interviews" = list()) + for (var/ckey in open_interviews) + var/datum/interview/I = open_interviews[ckey] + if (I) + var/list/data = list( + "id" = I.id, + "ckey" = I.owner_ckey, + "status" = I.status, + "queued" = I.pos_in_queue && I.status == INTERVIEW_PENDING, + "disconnected" = !I.owner + ) + .["open_interviews"] += list(data) + for (var/datum/interview/I in closed_interviews) + var/list/data = list( + "id" = I.id, + "ckey" = I.owner_ckey, + "status" = I.status, + "disconnected" = !I.owner + ) + .["closed_interviews"] += list(data) diff --git a/code/modules/mob/dead/new_player/login.dm b/code/modules/mob/dead/new_player/login.dm index 2cc7ae74708..19aa1525057 100644 --- a/code/modules/mob/dead/new_player/login.dm +++ b/code/modules/mob/dead/new_player/login.dm @@ -26,13 +26,18 @@ sight |= SEE_TURFS - new_player_panel() client.playtitlemusic() + + // Check if user should be added to interview queue + if (!client.holder && CONFIG_GET(flag/panic_bunker) && CONFIG_GET(flag/panic_bunker_interview) && !(client.ckey in GLOB.interviews.approved_ckeys)) + var/required_living_minutes = CONFIG_GET(number/panic_bunker_living) + var/living_minutes = client.get_exp_living(TRUE) + if (required_living_minutes > living_minutes) + client.interviewee = TRUE + register_for_interview() + return + + new_player_panel() if(SSticker.current_state < GAME_STATE_SETTING_UP) var/tl = SSticker.GetTimeLeft() - var/postfix - if(tl > 0) - postfix = "in about [DisplayTimeText(tl)]" - else - postfix = "soon" - to_chat(src, "Please set up your character and select \"Ready\". The game will start [postfix].") + to_chat(src, "Please set up your character and select \"Ready\". The game will start [tl > 0 ? "in about [DisplayTimeText(tl)]" : "soon"].") diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 9b00008a9d0..f4cb367b301 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -44,6 +44,9 @@ * This proc generates the panel that opens to all newly joining players, allowing them to join, observe, view polls, view the current crew manifest, and open the character customization menu. */ /mob/dead/new_player/proc/new_player_panel() + if (client?.interviewee) + return + var/datum/asset/asset_datum = get_asset_datum(/datum/asset/simple/lobby) asset_datum.send(client) var/list/output = list("
+ {welcome_message} +
+'+(n?e:H(e,!0))+"\n":""+(n?e:H(e,!0))+"\n"},t.blockquote=function(e){return"\n"+e+"\n"},t.html=function(e){return e},t.heading=function(e,t,n,o){return this.options.headerIds?"
"+e+"
\n"},t.table=function(e,t){return t&&(t=""+t+""),""+e+""},t.br=function(){return this.options.xhtml?""+Z(l.message+"",!0)+"";throw l}}return ne.options=ne.setOptions=function(e){return X(ne.defaults,e),ee(ne.defaults),ne},ne.getDefaults=J,ne.defaults=te,ne.use=function(e){var t=X({},e);if(e.renderer&&function(){var n=ne.defaults.renderer||new U,o=function(t){var o=n[t];n[t]=function(){for(var r=arguments.length,a=new Array(r),i=0;i
'+(n?e:ee(e,!0))+"\n":""+(n?e:ee(e,!0))+"\n"}return e}(),t.blockquote=function(){function e(e){return"\n"+e+"\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
"+e+"
\n"}return e}(),t.table=function(){function e(e,t){return t&&(t=""+t+""),""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?""+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a