Files
Bubberstation/code/modules/interview/interview_manager.dm
Bobbahbrown 4e48e1379d Interview System / Soft Panic Bunker (#54465)
About The Pull Request

Ports and improves my interview system that has been previously used in the summer ball and toolbox tournament events. Allows for a 'softer' panic bunker, wherein players who fall below the required living time limit can still join the server and be restricted to filling out a questionnaire. Upon completing the questionnaire, the player may be allowed into the server by an administrator. If the application is approved, they get a notification that they will be reconnected and upon reconnecting will have all verbs as they usually would. If the application is denied the user is put on a cooldown after which they may submit a new questionnaire.

Players who are being interviewed (herein interviewees) have no verbs other than those required for the stat panel to function, as well as a verb to pull up the interview panel. Interviews do not persist through restarts, and the ability to join that is granted by an accepted interview is only valid for the duration of that round.

Open interviews are listed under a new 'interviews' tab for admins, which is VERY similar to the existing tickets tab.

Below is what a player who is flagged as an interviewee will see when they join the server. They can do nothing but respond to the questionnaire or leave.
image

This is what an administrator sees after an interview is submitted, they will also see a corresponding message within their chatbox, and an age-old BWOINK when an interview is submitted.
image

The interviews tab, which is similar to the tickets menu. You can open the interview manager panel to view all active (including non-submitted) interviews, queued (submitted) interviews, and closed interviews.

image
FAQ:

What happens if someone submits an interview when no admins are on? It's treated like adminhelps are, the message gets sent to TGS to be dispatched off to configured end-points (like Discord or IRC), and the user is notified that their interview was handled this way.

Can you configure the questions? Yes, in config/ there is now a interviews.txt file in which the welcome message and the individual questions can be set and modified.

Can this be turned on and off during a round? Yes, it can be toggled like the panic bunker. It requires the panic bunker to be raised in order to function.

Can interviewees have further questions asked to them? Yes, if you admin-pm them, which is possible using regular means or a conveniently placed button on the interview UI, they will be able to respond to the message.
Technical details

To use the interview system you must have the panic bunker enabled, this is an additional setting for the panic bunker. It can be set through the PANIC_BUNKER_INTERVIEW setting in config.txt, or alternatively enabled in-game as prompted during the panic bunker toggling process. It also can be toggled on its own using a verb added for this purpose, Toggle PB Interviews found under the server tab. These new actions are included in the logging for the panic bunker. I have also added a reporting stat to the world topic status keyword, which now reports if the interview system is on using the keyword interviews.

As mentioned above, for server operators, configure the questions and welcome message in config/interviews.txt.

Note to maintainers and those with big brains I had to add a call to init_verbs on the stat panel window being ready because seemingly a race condition exists wherein the add_verb of the 'view my interview' verb doesn't cause a refresh of the tabs (and therefore doesn't show the 'Interview' tab) when running in dream daemon but running it directly from visual studio code properly shows the tab. Adding a init_verbs call directly after adding the verb didn't seem to help.
A note for downstreams

If you don't use the HTML stat panel (which may not be a bad thing) then you will have to do some conversion from the HTML stat panel stuff used here to the old style stat panels. It's pretty trivial, but just be aware of that. You can see how I used to use the old stat panels in my PR from the summer ball, here, which should be helpful.
Why It's Good For The Game

This allows for a softer version of the panic bunker which impedes the flow of malicious players while allowing genuine players a chance to enter a round to gain enough time to not be affected by the panic bunker's restrictions.
Changelog

🆑 bobbahbrown
add: Added the interview system, a 'soft' panic bunker which lets players who would normally be blocked from joining be interviewed by admins to be selectively allowed to play.
/🆑
2020-10-25 14:10:06 +13:00

219 lines
6.9 KiB
Plaintext

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, "<span class='notice'>No active admins are online, your interview's submission was sent through TGS to admins who are available. This may use IRC or Discord.</span>")
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, "<span class='adminhelp'>Interview for [ckey] enqueued for review. Current position in queue: [to_queue.pos_in_queue]</span>", 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)