mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-28 15:47:15 +01:00
[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:
co-authored by
Bobbahbrown
Azarak
parent
6c58c5bad7
commit
91773c46da
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user