[MIRROR] Interview System / Soft Panic Bunker (#1458)

* Interview System / Soft Panic Bunker

* a

Co-authored-by: Bobbahbrown <bobbahbrown@gmail.com>
Co-authored-by: Azarak <azarak10@gmail.com>
This commit is contained in:
SkyratBot
2020-10-26 08:32:37 +01:00
committed by GitHub
co-authored by Bobbahbrown Azarak
parent 6c58c5bad7
commit 91773c46da
30 changed files with 810 additions and 39 deletions
+162
View File
@@ -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("<span class='adminnotice'>[key_name(approved_by)] has approved interview #[id] for [owner_ckey][!owner ? "(DC)": ""].</span>")
if (owner)
SEND_SOUND(owner, sound('sound/effects/adminhelp.ogg'))
to_chat(owner, "<font color='red' size='4'><b>-- Interview Update --</b></font>" \
+ "\n<span class='adminsay'>Your interview was approved, you will now be reconnected in 5 seconds.</span>", 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("<span class='adminnotice'>[key_name(denied_by)] has denied interview #[id] for [owner_ckey][!owner ? "(DC)": ""].</span>")
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, "<font color='red' size='4'><b>-- Interview Update --</b></font>" \
+ "\n<span class='adminsay'>Unfortunately your interview was denied. Please try submitting another questionnaire." \
+ " You may do this in three minutes.</span>", 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, "<span class='adminsay'>You are on cooldown for interviews. Please" \
+ " wait at least 3 minutes before starting a new questionnaire.</span>", 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)
+218
View File
@@ -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, "<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)