diff --git a/beestation.dme b/beestation.dme
index 992a7d69213ca..cb1420c2a7d17 100644
--- a/beestation.dme
+++ b/beestation.dme
@@ -3534,7 +3534,11 @@
#include "code\modules\tgui\states.dm"
#include "code\modules\tgui\status_composers.dm"
#include "code\modules\tgui\tgui.dm"
+#include "code\modules\tgui\tgui_alert.dm"
#include "code\modules\tgui\tgui_input_emoji.dm"
+#include "code\modules\tgui\tgui_input_list.dm"
+#include "code\modules\tgui\tgui_input_number.dm"
+#include "code\modules\tgui\tgui_input_text.dm"
#include "code\modules\tgui\tgui_select_picture.dm"
#include "code\modules\tgui\tgui_window.dm"
#include "code\modules\tgui\states\admin.dm"
diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm
index 3c4e20c717680..5a795a11f77d2 100644
--- a/code/datums/components/cult_ritual_item.dm
+++ b/code/datums/components/cult_ritual_item.dm
@@ -348,7 +348,7 @@
to_chat(cultist, "\"I am already here. There is no need to try to summon me now.\"")
return FALSE
var/confirm_final = alert(cultist, "This is the FINAL step to summon Nar'Sie; it is a long, painful ritual and the crew will be alerted to your presence.", "Are you prepared for the final battle?", "My life for Nar'Sie!", "No")
- if(confirm_final == "No")
+ if(confirm_final == "No" || !confirm_final)
to_chat(cultist, "You decide to prepare further before scribing the rune.")
return
if(!check_if_in_ritual_site(cultist, cult_team))
diff --git a/code/game/machinery/computer/launchpad_control.dm b/code/game/machinery/computer/launchpad_control.dm
index 6805bc354f44c..b82081e41e7c4 100644
--- a/code/game/machinery/computer/launchpad_control.dm
+++ b/code/game/machinery/computer/launchpad_control.dm
@@ -134,7 +134,7 @@
current_pad.display_name = new_name
. = TRUE
if("remove")
- if(usr && alert(usr, "Are you sure?", "Unlink Launchpad", "I'm Sure", "Abort") != "Abort")
+ if(usr && alert(usr, "Are you sure?", "Unlink Launchpad", "I'm Sure", "Abort") == "I'm Sure")
launchpads -= current_pad
selected_id = null
. = TRUE
diff --git a/code/game/machinery/computer/law.dm b/code/game/machinery/computer/law.dm
index 7039d59e679e8..8d6bbbad8331e 100644
--- a/code/game/machinery/computer/law.dm
+++ b/code/game/machinery/computer/law.dm
@@ -40,7 +40,7 @@
current = null
return
M.install(current.laws, user)
- if(alert("Do you wish to scramble the upload code?", "Scramble Code", "Yes", "No") == "No")
+ if(alert("Do you wish to scramble the upload code?", "Scramble Code", "Yes", "No") != "Yes")
return
message_admins("[ADMIN_LOOKUPFLW(usr)] has scrambled the upload code [GLOB.upload_code]!")
GLOB.upload_code = random_code(4)
diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm
index 86c6c8894e0c2..a6c53074c8fb3 100644
--- a/code/game/machinery/cryopod.dm
+++ b/code/game/machinery/cryopod.dm
@@ -362,7 +362,7 @@ GLOBAL_LIST_EMPTY(cryopod_computers)
to_chat(user, "You can't put [target] into [src]. They're conscious.")
return
else if(target.client)
- if(alert(target,"Would you like to enter cryosleep?",,"Yes","No") == "No")
+ if(alert(target,"Would you like to enter cryosleep?",,"Yes","No") != "Yes")
return
var/generic_plsnoleave_message = " Please adminhelp before leaving the round, even if there are no administrators online!"
diff --git a/code/game/machinery/launch_pad.dm b/code/game/machinery/launch_pad.dm
index e7e67ac9dc79f..c3fe413c9c803 100644
--- a/code/game/machinery/launch_pad.dm
+++ b/code/game/machinery/launch_pad.dm
@@ -391,7 +391,7 @@
. = TRUE
if("remove")
. = TRUE
- if(usr && alert(usr, "Are you sure?", "Unlink Launchpad", "I'm Sure", "Abort") != "Abort")
+ if(usr && alert(usr, "Are you sure?", "Unlink Launchpad", "I'm Sure", "Abort") == "I'm Sure")
pad = null
if("launch")
sending = TRUE
diff --git a/code/game/objects/items/implants/implant_explosive.dm b/code/game/objects/items/implants/implant_explosive.dm
index 3df869644276f..be032cd839c2d 100644
--- a/code/game/objects/items/implants/implant_explosive.dm
+++ b/code/game/objects/items/implants/implant_explosive.dm
@@ -29,13 +29,13 @@
/obj/item/implant/explosive/activate(cause)
. = ..()
if(!cause || !imp_in || active)
- return 0
+ return FALSE
if(cause == "action_button" && !popup)
popup = TRUE
var/response = alert(imp_in, "Are you sure you want to activate your [name]? This will cause you to explode!", "[name] Confirmation", "Yes", "No")
popup = FALSE
- if(response == "No")
- return 0
+ if(response != "Yes")
+ return FALSE
heavy = round(heavy)
medium = round(medium)
weak = round(weak)
@@ -49,7 +49,7 @@
if(imp_in)
imp_in.gib(1)
qdel(src)
- return
+ return TRUE
timed_explosion()
/obj/item/implant/explosive/implant(mob/living/target, mob/user, silent = FALSE, force = FALSE)
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index 5f1743ad63faa..2824c3ccfcc57 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -320,11 +320,10 @@
if (!usr.client.holder)
return
var/confirm = alert("End the round and restart the game world?", "End Round", "Yes", "Cancel")
- if(confirm == "Cancel")
+ if(confirm != "Yes")
return
- if(confirm == "Yes")
- SSticker.force_ending = 1
- SSblackbox.record_feedback("tally", "admin_verb", 1, "End Round") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+ SSticker.force_ending = 1
+ SSblackbox.record_feedback("tally", "admin_verb", 1, "End Round") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/datum/admins/proc/dynamic_mode_options(mob/user)
var/dat = {"
diff --git a/code/modules/admin/permissionedit.dm b/code/modules/admin/permissionedit.dm
index f7d252fdd94bb..c3c4625b25036 100644
--- a/code/modules/admin/permissionedit.dm
+++ b/code/modules/admin/permissionedit.dm
@@ -168,7 +168,7 @@
use_db = FALSE
else
use_db = alert("Permanent changes are saved to the database for future rounds, temporary changes will affect only the current round", "Permanent or Temporary?", "Permanent", "Temporary", "Cancel")
- if(use_db == "Cancel")
+ if(use_db == "Cancel" || !use_db)
return
if(use_db == "Permanent")
use_db = TRUE
diff --git a/code/modules/admin/secrets.dm b/code/modules/admin/secrets.dm
index 823cae8dd95ed..c9e4c76c456bb 100644
--- a/code/modules/admin/secrets.dm
+++ b/code/modules/admin/secrets.dm
@@ -134,7 +134,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new)
if(!check_rights(R_ADMIN))
return
var/delete_mobs = alert("Clear all mobs?","Confirm","Yes","No","Cancel")
- if(delete_mobs == "Cancel")
+ if(delete_mobs == "Cancel" || !delete_mobs)
return
log_admin("[key_name(usr)] reset the thunderdome to default with delete_mobs==[delete_mobs].", 1)
@@ -454,7 +454,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new)
if(animetype =="Yes")
droptype = alert("Make the uniforms undroppable?",,"Yes","No","Cancel")
- if(animetype == "Cancel" || droptype == "Cancel")
+ if(animetype == "Cancel" || droptype == "Cancel" || !animetype || (!droptype && animetype == "Yes"))
return
SSblackbox.record_feedback("nested tally", "admin_secrets_fun_used", 1, list("Chinese Cartoons"))
message_admins("[key_name_admin(usr)] made everything kawaii.")
@@ -695,7 +695,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new)
if("flipmovement")
if(!check_rights(R_FUN))
return
- if(alert("Flip all movement controls?","Confirm","Yes","Cancel") == "Cancel")
+ if(alert("Flip all movement controls?","Confirm","Yes","Cancel") != "Yes")
return
var/list/movement_keys = SSinput.movement_keys
for(var/i in 1 to movement_keys.len)
@@ -707,7 +707,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new)
if("randommovement")
if(!check_rights(R_FUN))
return
- if(alert("Randomize all movement controls?","Confirm","Yes","Cancel") == "Cancel")
+ if(alert("Randomize all movement controls?","Confirm","Yes","Cancel") != "Yes")
return
var/list/movement_keys = SSinput.movement_keys
for(var/i in 1 to movement_keys.len)
@@ -719,7 +719,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new)
if("custommovement")
if(!check_rights(R_FUN))
return
- if(alert("Are you sure you want to change every movement key?","Confirm","Yes","Cancel") == "Cancel")
+ if(alert("Are you sure you want to change every movement key?","Confirm","Yes","Cancel") != "Yes")
return
var/list/movement_keys = SSinput.movement_keys
var/list/new_movement = list()
@@ -740,7 +740,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new)
if("resetmovement")
if(!check_rights(R_FUN))
return
- if(alert("Are you sure you want to reset movement keys to default?","Confirm","Yes","Cancel") == "Cancel")
+ if(alert("Are you sure you want to reset movement keys to default?","Confirm","Yes","Cancel") != "Yes")
return
SSinput.setup_default_movement_keys()
message_admins("[key_name_admin(usr)] has reset all movement keys.")
@@ -820,7 +820,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new)
if(E)
E.processing = FALSE
if(E.announceWhen>0)
- if(alert(usr, "Would you like to alert the crew?", "Alert", "Yes", "No") == "No")
+ if(alert(usr, "Would you like to alert the crew?", "Alert", "Yes", "No") != "Yes")
E.announceChance = 0
E.processing = TRUE
if (usr)
diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm
index 489c49b7ccfed..fb7da927dc0dc 100644
--- a/code/modules/admin/sql_ban_system.dm
+++ b/code/modules/admin/sql_ban_system.dm
@@ -819,7 +819,7 @@
to_chat(usr, "Failed to establish database connection.")
return
var/target = ban_target_string(player_key, player_ip, player_cid)
- if(alert(usr, "Please confirm unban of [target] from [role].", "Unban confirmation", "Yes", "No") == "No")
+ if(alert(usr, "Please confirm unban of [target] from [role].", "Unban confirmation", "Yes", "No") != "Yes")
return
var/kn = key_name(usr)
var/kna = key_name_admin(usr)
diff --git a/code/modules/admin/stickyban.dm b/code/modules/admin/stickyban.dm
index c3cabea87f0bb..936b5af674c60 100644
--- a/code/modules/admin/stickyban.dm
+++ b/code/modules/admin/stickyban.dm
@@ -61,7 +61,7 @@
if (!ban)
to_chat(usr, "Error: No sticky ban for [ckey] found!")
return
- if (alert("Are you sure you want to remove the sticky ban on [ckey]?","Are you sure","Yes","No") == "No")
+ if (alert("Are you sure you want to remove the sticky ban on [ckey]?","Are you sure","Yes","No") != "Yes")
return
if (!get_stickyban_from_ckey(ckey))
to_chat(usr, "Error: The ban disappeared.")
@@ -98,7 +98,7 @@
to_chat(usr, "Error: [alt] is not linked to [ckey]'s sticky ban!")
return
- if (alert("Are you sure you want to disassociate [alt] from [ckey]'s sticky ban? \nNote: Nothing stops byond from re-linking them, Use \[E] to exempt them","Are you sure","Yes","No") == "No")
+ if (alert("Are you sure you want to disassociate [alt] from [ckey]'s sticky ban? \nNote: Nothing stops byond from re-linking them, Use \[E] to exempt them","Are you sure","Yes","No") != "Yes")
return
//we have to do this again incase something changes
@@ -180,7 +180,7 @@
to_chat(usr, "Error: [alt] is not linked to [ckey]'s sticky ban!")
return
- if (alert("Are you sure you want to exempt [alt] from [ckey]'s sticky ban?","Are you sure","Yes","No") == "No")
+ if (alert("Are you sure you want to exempt [alt] from [ckey]'s sticky ban?","Are you sure","Yes","No") != "Yes")
return
//we have to do this again incase something changes
@@ -230,7 +230,7 @@
to_chat(usr, "Error: [alt] is not exempt from [ckey]'s sticky ban!")
return
- if (alert("Are you sure you want to unexempt [alt] from [ckey]'s sticky ban?","Are you sure","Yes","No") == "No")
+ if (alert("Are you sure you want to unexempt [alt] from [ckey]'s sticky ban?","Are you sure","Yes","No") != "Yes")
return
//we have to do this again incase something changes
@@ -272,7 +272,7 @@
var/ckey = data["ckey"]
- if (alert("Are you sure you want to put [ckey]'s stickyban on timeout until next round (or removed)?","Are you sure","Yes","No") == "No")
+ if (alert("Are you sure you want to put [ckey]'s stickyban on timeout until next round (or removed)?","Are you sure","Yes","No") != "Yes")
return
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
@@ -298,7 +298,7 @@
return
var/ckey = data["ckey"]
- if (alert("Are you sure you want to lift the timeout on [ckey]'s stickyban?","Are you sure","Yes","No") == "No")
+ if (alert("Are you sure you want to lift the timeout on [ckey]'s stickyban?","Are you sure","Yes","No") != "Yes")
return
var/ban = get_stickyban_from_ckey(ckey)
@@ -323,7 +323,7 @@
if (!data["ckey"])
return
var/ckey = data["ckey"]
- if (alert("Are you sure you want to revert the sticky ban on [ckey] to its state at round start (or last edit)?","Are you sure","Yes","No") == "No")
+ if (alert("Are you sure you want to revert the sticky ban on [ckey] to its state at round start (or last edit)?","Are you sure","Yes","No") != "Yes")
return
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 6a634fcadf9bd..eddb7c1e82ba2 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1702,10 +1702,9 @@
if(!check_rights(R_ADMIN))
return
var/confirm = alert("Are you sure you want to reboot the server?", "Confirm Reboot", "Yes", "No")
- if(confirm == "No")
+ if(confirm != "Yes")
return
- if(confirm == "Yes")
- restart()
+ restart()
else if(href_list["check_teams"])
if(!check_rights(R_ADMIN))
diff --git a/code/modules/admin/verbs/_help.dm b/code/modules/admin/verbs/_help.dm
index 8e6a3f3de711c..89a7ddb110157 100644
--- a/code/modules/admin/verbs/_help.dm
+++ b/code/modules/admin/verbs/_help.dm
@@ -129,7 +129,7 @@
if("claim")
if(ticket.claimee)
var/confirm = alert("This ticket is already claimed, override claim?", null,"Yes", "No")
- if(confirm == "No")
+ if(confirm != "Yes")
return
claim_ticket = CLAIM_OVERRIDE
if("reject")
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index 8d968302b9615..39ca82623804f 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -506,7 +506,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that
else
H = M
if(H.l_store || H.r_store || H.s_store) //saves a lot of time for admins and coders alike
- if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") == "No")
+ if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") != "Yes")
delete_pocket = TRUE
SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
@@ -776,7 +776,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that
if (response == "Jump")
usr.forceMove(get_turf(exists[template]))
return
- else if (response == "Cancel")
+ else if (response != "Place Another")
return
var/len = GLOB.ruin_landmarks.len
@@ -800,7 +800,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that
if(ruin_size < 10 || ruin_size >= 200)
return
var/response = alert(src, "This will place the ruin at your current location.", "Spawn Ruin", "Spawn Ruin", "Cancel")
- if (response == "Cancel")
+ if (response != "Spawn Ruin")
return
var/border_size = (world.maxx - ruin_size) / 2
generate_space_ruin(mob.x, mob.y, mob.z, border_size, border_size)
diff --git a/code/modules/admin/verbs/healall.dm b/code/modules/admin/verbs/healall.dm
index ae4fac4e0fe26..9033e62386e97 100644
--- a/code/modules/admin/verbs/healall.dm
+++ b/code/modules/admin/verbs/healall.dm
@@ -8,7 +8,7 @@
if(!check_rights(R_FUN))
to_chat(src, "You need the fun permission to use this command.")
return
- if(alert(src, "Confirm Heal All?","Are you sure?","Yes","No") == "No")
+ if(alert(src, "Confirm Heal All?","Are you sure?","Yes","No") != "Yes")
return
message_admins("[key_name_admin(usr)] healed all living mobs")
log_admin("[key_name_admin(usr)] healed all living mobs")
diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm
index 0d9b014cb5b82..63a75cfc1a859 100644
--- a/code/modules/admin/verbs/mapping.dm
+++ b/code/modules/admin/verbs/mapping.dm
@@ -51,7 +51,8 @@ GLOBAL_LIST_INIT(admin_verbs_debug_mapping, list(
/client/proc/show_line_profiling,
/client/proc/create_mapping_job_icons,
/client/proc/debug_z_levels,
- /client/proc/place_ruin
+ /client/proc/place_ruin,
+ /client/proc/test_tgui_inputs,
))
GLOBAL_PROTECT(admin_verbs_debug_mapping)
@@ -375,3 +376,25 @@ GLOBAL_VAR_INIT(say_disabled, FALSE)
messages += ""
to_chat(src, messages.Join(""))
+
+/client/proc/test_tgui_inputs()
+ set name = "Test TGUI Inputs"
+ set category = "Debug"
+ var/response = tgui_alert(usr, "Message Here", "Title Here", list("Button 1", "Button 2", "Button 3"))
+ to_chat(usr, response)
+ response = tgui_alert(usr, "Message Here", "Title Here", list("Yes", "No"))
+ to_chat(usr, response)
+ var/list/L = list()
+ for (var/obj/machinery/camera/cam in GLOB.cameranet.cameras)
+ L["[cam.c_tag]"] = cam
+ response = tgui_input_list(usr, "Message Here", "Title Here", L)
+ to_chat(usr, response)
+ response = tgui_input_number(usr, "Message Here", "Title Here", 10, 500, 2)
+ to_chat(usr, response)
+ response = tgui_input_text(usr, "Message Here", "Title Here", "Default Text", 32, FALSE)
+ to_chat(usr, response)
+ response = tgui_input_text(usr, "Message Here", "Title Here", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore\
+ et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit\
+ in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id\
+ est laborum.", 1024, TRUE)
+ to_chat(usr, response)
diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index a7626fbc3fed6..392b34326c565 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -639,7 +639,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
if ((devastation != -1) || (heavy != -1) || (light != -1) || (flash != -1) || (flames != -1))
if ((devastation > 20) || (heavy > 20) || (light > 20) || (flames > 20))
- if (alert(src, "Are you sure you want to do this? It will laaag.", "Confirmation", "Yes", "No") == "No")
+ if (alert(src, "Are you sure you want to do this? It will laaag.", "Confirmation", "Yes", "No") != "Yes")
return
explosion(O, devastation, heavy, light, flash, null, null,flames)
@@ -683,7 +683,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
return
var/confirm = alert(src, "Drop a brain?", "Confirm", "Yes", "No","Cancel")
- if(confirm == "Cancel")
+ if(confirm == "Cancel" || !confirm)
return
//Due to the delay here its easy for something to have happened to the mob
if(!M)
@@ -800,7 +800,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
var/notifyplayers = alert(src, "Do you want to notify the players?", "Options", "Yes", "No", "Cancel")
- if(notifyplayers == "Cancel")
+ if(notifyplayers == "Cancel" || !notifyplayers)
return
log_admin("Admin [key_name(src)] has forced the players to have random appearances.")
diff --git a/code/modules/admin/view_variables/mass_edit_variables.dm b/code/modules/admin/view_variables/mass_edit_variables.dm
index e29580b3b27dc..91232299f9214 100644
--- a/code/modules/admin/view_variables/mass_edit_variables.dm
+++ b/code/modules/admin/view_variables/mass_edit_variables.dm
@@ -143,7 +143,7 @@
if (VV_NEW_TYPE)
var/many = alert(src, "Create only one [value["type"]] and assign each or a new one for each thing", "How Many", "One", "Many", "Cancel")
- if (many == "Cancel")
+ if (many == "Cancel" || !many)
return
if (many == "Many")
many = TRUE
diff --git a/code/modules/admin/view_variables/modify_variables.dm b/code/modules/admin/view_variables/modify_variables.dm
index dbc4af543f105..a74e588019b92 100644
--- a/code/modules/admin/view_variables/modify_variables.dm
+++ b/code/modules/admin/view_variables/modify_variables.dm
@@ -179,7 +179,7 @@ GLOBAL_PROTECT(VVpixelmovement)
return
var/assoc = 0
var/prompt = alert(src, "Do you want to edit the key or its assigned value?", "Associated List", "Key", "Assigned Value", "Cancel")
- if (prompt == "Cancel")
+ if (prompt == "Cancel" || !prompt)
return
if (prompt == "Assigned Value")
assoc = 1
diff --git a/code/modules/antagonists/brainwashing/brainwashing.dm b/code/modules/antagonists/brainwashing/brainwashing.dm
index f0ce4205bfd10..57d082ec819ad 100644
--- a/code/modules/antagonists/brainwashing/brainwashing.dm
+++ b/code/modules/antagonists/brainwashing/brainwashing.dm
@@ -88,7 +88,7 @@
log_objective(C, objective, admin)
while(alert(admin,"Add another objective?","More Brainwashing","Yes","No") == "Yes")
- if(alert(admin,"Confirm Brainwashing?","Are you sure?","Yes","No") == "No")
+ if(alert(admin,"Confirm Brainwashing?","Are you sure?","Yes","No") != "Yes")
return
if(!LAZYLEN(objectives))
diff --git a/code/modules/antagonists/changeling/powers/headcrab.dm b/code/modules/antagonists/changeling/powers/headcrab.dm
index 4bfb77c9906eb..85cf9dd85961a 100644
--- a/code/modules/antagonists/changeling/powers/headcrab.dm
+++ b/code/modules/antagonists/changeling/powers/headcrab.dm
@@ -11,7 +11,7 @@
/datum/action/changeling/headcrab/sting_action(mob/user)
set waitfor = FALSE
- if(alert("Are we sure we wish to kill ourself and create a headslug?",,"Yes", "No") == "No")
+ if(alert("Are we sure we wish to kill ourself and create a headslug?",,"Yes", "No") != "Yes")
return
if(isliving(user))
var/mob/living/L = user
diff --git a/code/modules/antagonists/clock_cult/mobs/cogscarab.dm b/code/modules/antagonists/clock_cult/mobs/cogscarab.dm
index b4115d7a98fa3..8d233a2d20db5 100644
--- a/code/modules/antagonists/clock_cult/mobs/cogscarab.dm
+++ b/code/modules/antagonists/clock_cult/mobs/cogscarab.dm
@@ -74,7 +74,7 @@ GLOBAL_LIST_INIT(cogscarabs, list())
to_chat(user, "Can't become a cogscarab before the game has started.")
return
var/be_drone = alert("Become a cogscarab? (Warning, You can no longer be cloned!)",,"Yes","No")
- if(be_drone == "No" || QDELETED(src) || !isobserver(user))
+ if(be_drone != "Yes" || QDELETED(src) || !isobserver(user))
return
var/mob/living/simple_animal/drone/D = new mob_type(get_turf(loc))
if(!D.default_hatmask && seasonal_hats && possible_seasonal_hats.len)
diff --git a/code/modules/antagonists/clock_cult/structure/eminence_beacon.dm b/code/modules/antagonists/clock_cult/structure/eminence_beacon.dm
index 6b9ae6d5d1b6e..45950cc4c9743 100644
--- a/code/modules/antagonists/clock_cult/structure/eminence_beacon.dm
+++ b/code/modules/antagonists/clock_cult/structure/eminence_beacon.dm
@@ -21,7 +21,7 @@
to_chat(user, "The Eminence has already been released.")
return
var/option = alert(user,"Who shall control the Eminence?",,"Yourself","A ghost", "Cancel")
- if(option == "Cancel")
+ if(option != "A ghost")
return
else if(option == "Yourself")
hierophant_message("[user] has elected themselves to become the Eminence. Interact with [src] to object.", span="")
diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm
index 1738534592f93..526ea0f510175 100644
--- a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm
+++ b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm
@@ -27,7 +27,7 @@
if(!check_allowed(user))
return
- if(are_you_sure == "No")
+ if(are_you_sure != "Yes")
to_chat(user, "On second thought, the element of surprise isn't so bad after all.")
return
diff --git a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm
index 415c6e270cc2b..6e04609ec07ca 100644
--- a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm
+++ b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm
@@ -666,7 +666,7 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list(
if(!owner_AI.can_place_transformer(src))
return
active = TRUE
- if(alert(owner, "Are you sure you want to place the machine here?", "Are you sure?", "Yes", "No") == "No")
+ if(alert(owner, "Are you sure you want to place the machine here?", "Are you sure?", "Yes", "No") != "Yes")
active = FALSE
return
if(!owner_AI.can_place_transformer(src))
diff --git a/code/modules/awaymissions/corpse.dm b/code/modules/awaymissions/corpse.dm
index 9216004a441ef..9e120c9403c56 100644
--- a/code/modules/awaymissions/corpse.dm
+++ b/code/modules/awaymissions/corpse.dm
@@ -51,7 +51,7 @@
to_chat(user, "You have died recently, you must wait [(user.client.next_ghost_role_tick - world.time)/10] seconds until you can use a ghost spawner.")
return
var/ghost_role = alert("Become [mob_name]? (Warning, You can no longer be cloned!)",,"Yes","No")
- if(ghost_role == "No" || !loc)
+ if(ghost_role != "Yes" || !loc)
return
log_game("[key_name(user)] became [mob_name]")
create(ckey = user.ckey)
@@ -598,7 +598,7 @@
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/effect/mob_spawn/human/alive/space_bar_patron/attack_hand(mob/user)
var/despawn = alert("Return to cryosleep? (Warning, Your mob will be deleted!)",,"Yes","No")
- if(despawn == "No" || !loc || !Adjacent(user))
+ if(despawn != "Yes" || !loc || !Adjacent(user))
return
user.visible_message("[user.name] climbs back into cryosleep...")
qdel(user)
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 1782c012af447..db56eab834537 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -1045,23 +1045,22 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
addtimer(CALLBACK(src, .proc/restore_account_identifier), 20) //Don't DoS DB queries, asshole
var/confirm = alert("Do NOT share the verification ID in the following popup. Understand?", "Important Warning", "Yes", "Cancel")
- if(confirm == "Cancel")
+ if(confirm != "Yes")
return
- if(confirm == "Yes")
- var/uuid = fetch_uuid()
- if(!uuid)
- alert("Failed to fetch your verification ID. Try again later. If problems persist, tell an admin.", "Account Verification", "Okay")
- log_sql("Failed to fetch UUID for [key_name(src)]")
- else
- var/dat
- dat += "
Account Identifier
"
- dat += " "
- dat += "
Do NOT share this id:
"
- dat += " "
- dat += "[uuid]"
-
- src << browse(dat, "window=accountidentifier;size=600x320")
- onclose(src, "accountidentifier")
+ var/uuid = fetch_uuid()
+ if(!uuid)
+ alert("Failed to fetch your verification ID. Try again later. If problems persist, tell an admin.", "Account Verification", "Okay")
+ log_sql("Failed to fetch UUID for [key_name(src)]")
+ else
+ var/dat
+ dat += "
Account Identifier
"
+ dat += " "
+ dat += "
Do NOT share this id:
"
+ dat += " "
+ dat += "[uuid]"
+
+ src << browse(dat, "window=accountidentifier;size=600x320")
+ onclose(src, "accountidentifier")
/client/proc/restore_account_identifier()
add_verb(/client/proc/show_account_identifier)
diff --git a/code/modules/events/pirates.dm b/code/modules/events/pirates.dm
index 71fd946ae5100..b00f8a3dea977 100644
--- a/code/modules/events/pirates.dm
+++ b/code/modules/events/pirates.dm
@@ -128,7 +128,7 @@
/obj/machinery/shuttle_scrambler/interact(mob/user)
if(!active)
- if(alert(user, "Turning the scrambler on will make the shuttle trackable by GPS. Are you sure you want to do it?", "Scrambler", "Yes", "Cancel") == "Cancel")
+ if(alert(user, "Turning the scrambler on will make the shuttle trackable by GPS. Are you sure you want to do it?", "Scrambler", "Yes", "Cancel") != "Yes")
return
if(active || !user.canUseTopic(src, BE_CLOSE))
return
diff --git a/code/modules/hydroponics/grown/replicapod.dm b/code/modules/hydroponics/grown/replicapod.dm
index dac6c141ede9b..69f38af8fb401 100644
--- a/code/modules/hydroponics/grown/replicapod.dm
+++ b/code/modules/hydroponics/grown/replicapod.dm
@@ -109,7 +109,7 @@
if(!make_podman)
// Prevent accidental harvesting. Make sure the user REALLY wants to do this if there's a chance of this coming from a living creature.
if(mind || ckey)
- if(alert("The pod is currently devoid of soul. There is a possibility that a soul could claim this creature, or you could harvest it for seeds.", "Harvest Seeds?", "Harvest Seeds", "Cancel") == "Cancel")
+ if(alert("The pod is currently devoid of soul. There is a possibility that a soul could claim this creature, or you could harvest it for seeds.", "Harvest Seeds?", "Harvest Seeds", "Cancel") != "Harvest Seeds")
return result
// If this plant has already been harvested, return early.
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 29ddac10a7598..999cd61a9be59 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -644,7 +644,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
return 0
if(can_reenter_corpse && mind && mind.current)
- if(alert(src, "Your soul is still tied to your former life as [mind.current.name], if you go forward there is no going back to that life. Are you sure you wish to continue?", "Move On", "Yes", "No") == "No")
+ if(alert(src, "Your soul is still tied to your former life as [mind.current.name], if you go forward there is no going back to that life. Are you sure you wish to continue?", "Move On", "Yes", "No") != "Yes")
return 0
if(target.key)
to_chat(src, "Someone has taken this body while you were choosing!")
diff --git a/code/modules/mob/living/brain/posibrain.dm b/code/modules/mob/living/brain/posibrain.dm
index 8109f1c9ff279..6a292d9634c36 100644
--- a/code/modules/mob/living/brain/posibrain.dm
+++ b/code/modules/mob/living/brain/posibrain.dm
@@ -100,7 +100,7 @@ GLOBAL_VAR(posibrain_notify_cooldown)
to_chat(user, "[src] fizzles slightly. Sadly it doesn't take those who suicided!")
return
var/posi_ask = alert("Become a [name]? (Warning, You can no longer be cloned, and all past lives will be forgotten!)","Are you positive?","Yes","No")
- if(posi_ask == "No" || QDELETED(src))
+ if(posi_ask != "Yes" || QDELETED(src))
return
if(brainmob.suiciding) //clear suicide status if the old occupant suicided.
brainmob.set_suicide(FALSE)
diff --git a/code/modules/mob/living/living_sentience.dm b/code/modules/mob/living/living_sentience.dm
index 293c8e920ae7a..2e349cbcd38c9 100644
--- a/code/modules/mob/living/living_sentience.dm
+++ b/code/modules/mob/living/living_sentience.dm
@@ -29,7 +29,7 @@
if(key || !playable || stat)
return 0
var/question = alert("Do you want to become [name]?", "[name]", "Yes", "No")
- if(question == "No" || !src || QDELETED(src))
+ if(question != "Yes" || !src || QDELETED(src))
return TRUE
if(key)
to_chat(user, "Someone else already took [name].")
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm b/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm
index e09b6fc4f21dd..b5dd3f01af466 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm
@@ -57,7 +57,7 @@
to_chat(user, "Can't become a drone before the game has started.")
return
var/be_drone = alert("Become a drone? (Warning, You can no longer be cloned!)",,"Yes","No")
- if(be_drone == "No" || QDELETED(src) || !isobserver(user))
+ if(be_drone != "Yes" || QDELETED(src) || !isobserver(user))
return
var/mob/living/simple_animal/drone/D = new mob_type(get_turf(loc))
if(!D.default_hatmask && seasonal_hats && possible_seasonal_hats.len)
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
index 66f3ce4c77a49..f5bbe1cf3477f 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
@@ -42,7 +42,7 @@
var/obj/structure/elite_tumor/T = target
if(T.mychild == src && T.activity == TUMOR_PASSIVE)
var/elite_remove = alert("Re-enter the tumor?", "Despawn yourself?", "Yes", "No")
- if(elite_remove == "No" || !src || QDELETED(src))
+ if(elite_remove != "Yes" || !src || QDELETED(src))
return
T.mychild = null
T.activity = TUMOR_INACTIVE
diff --git a/code/modules/reagents/chemistry/machinery/chem_dispenser.dm b/code/modules/reagents/chemistry/machinery/chem_dispenser.dm
index 5a512ec3385d7..7749795b7ff5d 100644
--- a/code/modules/reagents/chemistry/machinery/chem_dispenser.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_dispenser.dm
@@ -309,7 +309,7 @@
var/name = stripped_input(usr,"Name","What do you want to name this recipe?", "Recipe", MAX_NAME_LEN)
if(!usr.canUseTopic(src, !issilicon(usr)))
return
- if(saved_recipes[name] && alert("\"[name]\" already exists, do you want to overwrite it?",, "Yes", "No") == "No")
+ if(saved_recipes[name] && alert("\"[name]\" already exists, do you want to overwrite it?",, "Yes", "No") != "Yes")
return
if(name && recording_recipe)
for(var/reagent in recording_recipe)
diff --git a/code/modules/research/destructive_analyzer.dm b/code/modules/research/destructive_analyzer.dm
index 42700bfa9e92a..7acaeda553913 100644
--- a/code/modules/research/destructive_analyzer.dm
+++ b/code/modules/research/destructive_analyzer.dm
@@ -120,7 +120,7 @@ Note: Must be placed within 3 tiles of the R&D Console
if(length(worths) && !length(differences))
return FALSE
var/choice = input("Are you sure you want to destroy [loaded_item] to [!length(worths) ? "reveal [TN.display_name]" : "boost [TN.display_name] by [json_encode(differences)] point\s"]?") in list("Proceed", "Cancel")
- if(choice == "Cancel")
+ if(choice == "Cancel" || !choice)
return FALSE
if(QDELETED(loaded_item) || QDELETED(linked_console) || !user.Adjacent(linked_console) || QDELETED(src))
return FALSE
@@ -138,7 +138,7 @@ Note: Must be placed within 3 tiles of the R&D Console
else if(loaded_item.materials.len)
user_mode_string = " for material reclamation"
var/choice = input("Are you sure you want to destroy [loaded_item][user_mode_string]?") in list("Proceed", "Cancel")
- if(choice == "Cancel")
+ if(choice != "Proceed")
return FALSE
if(QDELETED(loaded_item) || QDELETED(linked_console) || !user.Adjacent(linked_console) || QDELETED(src))
return FALSE
diff --git a/code/modules/tgui/tgui_alert.dm b/code/modules/tgui/tgui_alert.dm
new file mode 100644
index 0000000000000..13006c4e5b6ca
--- /dev/null
+++ b/code/modules/tgui/tgui_alert.dm
@@ -0,0 +1,181 @@
+/**
+ * Creates a TGUI alert window and returns the user's response.
+ *
+ * This proc should be used to create alerts that the caller will wait for a response from.
+ * Arguments:
+ * * user - The user to show the alert to.
+ * * message - The content of the alert, shown in the body of the TGUI window.
+ * * title - The of the alert modal, shown on the top of the TGUI window.
+ * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * autofocus - The bool that controls if this alert should grab window focus.
+ */
+/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE)
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ // Client does NOT have tgui_input on: Returns regular input
+ /*if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ if(length(buttons) == 2)
+ return alert(user, message, title, buttons[1], buttons[2])
+ if(length(buttons) == 3)
+ return alert(user, message, title, buttons[1], buttons[2], buttons[3])*/
+ var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout, autofocus)
+ alert.ui_interact(user)
+ alert.wait()
+ if (alert)
+ . = alert.choice
+ qdel(alert)
+
+/**
+ * Creates an asynchronous TGUI alert window with an associated callback.
+ *
+ * This proc should be used to create alerts that invoke a callback with the user's chosen option.
+ * Arguments:
+ * * user - The user to show the alert to.
+ * * message - The content of the alert, shown in the body of the TGUI window.
+ * * title - The of the alert modal, shown on the top of the TGUI window.
+ * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise.
+ * * autofocus - The bool that controls if this alert should grab window focus.
+ */
+/proc/tgui_alert_async(mob/user, message = "", title, list/buttons = list("Ok"), datum/callback/callback, timeout = 0, autofocus = TRUE)
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ // Client does NOT have tgui_input on: Returns regular input
+ /*if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ if(length(buttons) == 2)
+ return alert(user, message, title, buttons[1], buttons[2])
+ if(length(buttons) == 3)
+ return alert(user, message, title, buttons[1], buttons[2], buttons[3])*/
+ var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout, autofocus)
+ alert.ui_interact(user)
+
+/**
+ * # tgui_modal
+ *
+ * Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
+ * a message and has buttons for responses.
+ */
+/datum/tgui_modal
+ /// The title of the TGUI window
+ var/title
+ /// The textual body of the TGUI window
+ var/message
+ /// The list of buttons (responses) provided on the TGUI window
+ var/list/buttons
+ /// The button that the user has pressed, null if no selection has been made
+ var/choice
+ /// The time at which the tgui_modal was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the tgui_modal, after which the window will close and delete itself.
+ var/timeout
+ /// The bool that controls if this modal should grab window focus
+ var/autofocus
+ /// Boolean field describing if the tgui_modal was closed by the user.
+ var/closed
+
+/datum/tgui_modal/New(mob/user, message, title, list/buttons, timeout, autofocus)
+ src.autofocus = autofocus
+ src.buttons = buttons.Copy()
+ src.message = message
+ src.title = title
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+
+/datum/tgui_modal/Destroy(force, ...)
+ SStgui.close_uis(src)
+ QDEL_NULL(buttons)
+ . = ..()
+
+/**
+ * Waits for a user's response to the tgui_modal's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_modal/proc/wait()
+ while (!choice && !closed && !QDELETED(src))
+ stoplag(1)
+
+/datum/tgui_modal/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "AlertModal")
+ ui.open()
+
+/datum/tgui_modal/ui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_modal/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/tgui_modal/ui_data(mob/user)
+ . = list()
+ .["autofocus"] = autofocus
+ .["buttons"] = buttons
+ .["message"] = message
+ .["preferences"] = list()
+ .["preferences"]["large_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ .["preferences"]["swapped_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ .["title"] = title
+ if(timeout)
+ .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+
+/datum/tgui_modal/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("choose")
+ if (!(params["choice"] in buttons))
+ CRASH("[usr] entered a non-existent button choice: [params["choice"]]")
+ set_choice(params["choice"])
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+/datum/tgui_modal/proc/set_choice(choice)
+ src.choice = choice
+
+/**
+ * # async tgui_modal
+ *
+ * An asynchronous version of tgui_modal to be used with callbacks instead of waiting on user responses.
+ */
+/datum/tgui_modal/async
+ /// The callback to be invoked by the tgui_modal upon having a choice made.
+ var/datum/callback/callback
+
+/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus)
+ ..(user, message, title, buttons, timeout, autofocus)
+ src.callback = callback
+
+/datum/tgui_modal/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_modal/async/set_choice(choice)
+ . = ..()
+ if(!isnull(src.choice))
+ callback?.InvokeAsync(src.choice)
+
+/datum/tgui_modal/async/wait()
+ return
diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui/tgui_input_list.dm
new file mode 100644
index 0000000000000..2eff761716b3b
--- /dev/null
+++ b/code/modules/tgui/tgui_input_list.dm
@@ -0,0 +1,202 @@
+/**
+ * Creates a TGUI input list window and returns the user's response.
+ *
+ * This proc should be used to create alerts that the caller will wait for a response from.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * items - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * default - If an option is already preselected on the UI. Current values, etc.
+ * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0)
+ if (!user)
+ user = usr
+ if(!length(items))
+ return
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ // Client does NOT have tgui_input on: Returns regular input
+ //if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ // return input(user, message, title) as null|anything in items
+ var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout)
+ input.ui_interact(user)
+ input.wait()
+ if (input)
+ . = input.choice
+ qdel(input)
+
+/**
+ * Creates an asynchronous TGUI input list window with an associated callback.
+ *
+ * This proc should be used to create inputs that invoke a callback with the user's chosen option.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * items - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * default - If an option is already preselected on the UI. Current values, etc.
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_list_async(mob/user, message, title = "Select", list/items, default, datum/callback/callback, timeout = 60 SECONDS)
+ if (!user)
+ user = usr
+ if(!length(items))
+ return
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ // Client does NOT have tgui_input on: Returns regular input
+ //if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ // return input(user, message, title) as null|anything in items
+ var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout)
+ input.ui_interact(user)
+
+/**
+ * # tgui_list_input
+ *
+ * Datum used for instantiating and using a TGUI-controlled list input that prompts the user with
+ * a message and shows a list of selectable options
+ */
+/datum/tgui_list_input
+ /// The title of the TGUI window
+ var/title
+ /// The textual body of the TGUI window
+ var/message
+ /// The list of items (responses) provided on the TGUI window
+ var/list/items
+ /// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb)
+ var/list/items_map
+ /// The button that the user has pressed, null if no selection has been made
+ var/choice
+ /// The default button to be selected
+ var/default
+ /// The time at which the tgui_list_input was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the tgui_list_input, after which the window will close and delete itself.
+ var/timeout
+ /// Boolean field describing if the tgui_list_input was closed by the user.
+ var/closed
+
+/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout)
+ src.title = title
+ src.message = message
+ src.items = list()
+ src.items_map = list()
+ src.default = default
+ var/list/repeat_items = list()
+
+ // Gets rid of illegal characters
+ var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
+
+ for(var/i in items)
+ if(!i)
+ continue
+
+ var/string_key = whitelistedWords.Replace("[i]", "")
+
+ //avoids duplicated keys E.g: when areas have the same name
+ string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
+
+ src.items += string_key
+ src.items_map[string_key] = i
+
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+
+/datum/tgui_list_input/Destroy(force, ...)
+ SStgui.close_uis(src)
+ QDEL_NULL(items)
+ . = ..()
+
+/**
+ * Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_list_input/proc/wait()
+ while (!choice && !closed)
+ stoplag(1)
+
+/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ListInputModal")
+ ui.open()
+
+/datum/tgui_list_input/ui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_list_input/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/tgui_list_input/ui_static_data(mob/user)
+ . = list()
+ .["items"] = items
+
+/datum/tgui_list_input/ui_data(mob/user)
+ . = list()
+ .["init_value"] = default || items[1]
+ .["message"] = message
+ .["preferences"] = list()
+ .["preferences"]["large_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ .["preferences"]["swapped_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ .["title"] = title
+ if(timeout)
+ .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
+
+/datum/tgui_list_input/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("submit")
+ if (!(params["entry"] in items))
+ return
+ set_choice(items_map[params["entry"]])
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+/datum/tgui_list_input/proc/set_choice(choice)
+ src.choice = choice
+
+/**
+ * # async tgui_list_input
+ *
+ * An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
+ */
+/datum/tgui_list_input/async
+ /// The callback to be invoked by the tgui_list_input upon having a choice made.
+ var/datum/callback/callback
+
+/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout)
+ ..(user, message, title, items, default, timeout)
+ src.callback = callback
+
+/datum/tgui_list_input/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_list_input/async/set_choice(choice)
+ . = ..()
+ if(!isnull(src.choice))
+ callback?.InvokeAsync(src.choice)
+
+/datum/tgui_list_input/async/wait()
+ return
diff --git a/code/modules/tgui/tgui_input_number.dm b/code/modules/tgui/tgui_input_number.dm
new file mode 100644
index 0000000000000..19fed34c3eaca
--- /dev/null
+++ b/code/modules/tgui/tgui_input_number.dm
@@ -0,0 +1,207 @@
+/**
+ * Creates a TGUI window with a number input. Returns the user's response as num | null.
+ *
+ * This proc should be used to create windows for number entry that the caller will wait for a response from.
+ * If tgui fancy chat is turned off: Will return a normal input. If a max or min value is specified, will
+ * validate the input inside the UI and ui_act.
+ *
+ * Arguments:
+ * * user - The user to show the number input to.
+ * * message - The content of the number input, shown in the body of the TGUI window.
+ * * title - The title of the number input modal, shown on the top of the TGUI window.
+ * * default - The default (or current) value, shown as a placeholder. Users can press refresh with this.
+ * * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000.
+ * * min_value - Specifies a minimum value. Often 0.
+ * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * round_value - whether the inputted number is rounded down into an integer.
+ */
+/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE)
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ // Client does NOT have tgui_input on: Returns regular input
+ //if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ // var/input_number = input(user, message, title, default) as null|num
+ // return clamp(round_value ? round(input_number) : input_number, min_value, max_value)
+ var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value)
+ number_input.ui_interact(user)
+ number_input.wait()
+ if (number_input)
+ . = number_input.entry
+ qdel(number_input)
+
+/**
+ * Creates an asynchronous TGUI number input window with an associated callback.
+ *
+ * This proc should be used to create number inputs that invoke a callback with the user's entry.
+ *
+ * Arguments:
+ * * user - The user to show the number input to.
+ * * message - The content of the number input, shown in the body of the TGUI window.
+ * * title - The title of the number input modal, shown on the top of the TGUI window.
+ * * default - The default (or current) value, shown as a placeholder. Users can press refresh with this.
+ * * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000.
+ * * min_value - Specifies a minimum value. Often 0.
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * round_value - whether the inputted number is rounded down into an integer.
+ */
+/proc/tgui_input_number_async(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, datum/callback/callback, timeout = 60 SECONDS, round_value = TRUE)
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ // Client does NOT have tgui_input on: Returns regular input
+ //if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ // var/input_number = input(user, message, title, default) as null|num
+ // return clamp(round_value ? round(input_number) : input_number, min_value, max_value)
+ var/datum/tgui_input_number/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout, round_value)
+ number_input.ui_interact(user)
+
+/**
+ * # tgui_input_number
+ *
+ * Datum used for instantiating and using a TGUI-controlled number input that prompts the user with
+ * a message and has an input for number entry.
+ */
+/datum/tgui_input_number
+ /// Boolean field describing if the tgui_input_number was closed by the user.
+ var/closed
+ /// The default (or current) value, shown as a default. Users can press reset with this.
+ var/default
+ /// The entry that the user has return_typed in.
+ var/entry
+ /// The maximum value that can be entered.
+ var/max_value
+ /// The prompt's body, if any, of the TGUI window.
+ var/message
+ /// The minimum value that can be entered.
+ var/min_value
+ /// The time at which the number input was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the number input, after which the window will close and delete itself.
+ var/timeout
+ /// The title of the TGUI window
+ var/title
+ /// Whether the submitted number is rounded down into an integer.
+ var/round_value
+
+
+/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value)
+ src.default = default
+ src.max_value = max_value
+ src.message = message
+ src.min_value = min_value
+ src.title = title
+ src.round_value = round_value
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+ /// Checks for empty numbers - bank accounts, etc.
+ if(max_value == 0)
+ src.min_value = 0
+ if(default)
+ src.default = 0
+ /// Sanity check
+ if(default < min_value)
+ src.default = min_value
+ if(default > max_value)
+ CRASH("Default value is greater than max value.")
+
+/datum/tgui_input_number/Destroy(force, ...)
+ SStgui.close_uis(src)
+ . = ..()
+
+/**
+ * Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_input_number/proc/wait()
+ while (!entry && !closed && !QDELETED(src))
+ stoplag(1)
+
+/datum/tgui_input_number/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "NumberInputModal")
+ ui.open()
+
+/datum/tgui_input_number/ui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_input_number/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/tgui_input_number/ui_data(mob/user)
+ . = list()
+ .["init_value"] = default // Default is a reserved keyword
+ .["max_value"] = max_value
+ .["message"] = message
+ .["min_value"] = min_value
+ .["preferences"] = list()
+ .["preferences"]["large_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ .["preferences"]["swapped_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ .["title"] = title
+ if(timeout)
+ .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+
+/datum/tgui_input_number/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("submit")
+ if(!isnum(params["entry"]))
+ CRASH("A non number was input into tgui input number by [usr]")
+ var/choice = round_value ? round(params["entry"]) : params["entry"]
+ if(choice > max_value)
+ CRASH("A number greater than the max value was input into tgui input number by [usr]")
+ if(choice < min_value)
+ CRASH("A number less than the min value was input into tgui input number by [usr]")
+ set_entry(choice)
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+/datum/tgui_input_number/proc/set_entry(entry)
+ src.entry = entry
+
+/**
+ * # async tgui_input_number
+ *
+ * An asynchronous version of tgui_input_number to be used with callbacks instead of waiting on user responses.
+ */
+/datum/tgui_input_number/async
+ /// The callback to be invoked by the tgui_input_number upon having a choice made.
+ var/datum/callback/callback
+
+/datum/tgui_input_number/async/New(mob/user, message, title, default, max_value, min_value, callback, timeout)
+ ..(user, message, title, default, max_value, min_value, timeout)
+ src.callback = callback
+
+/datum/tgui_input_number/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_input_number/async/set_entry(entry)
+ . = ..()
+ if(!isnull(src.entry))
+ callback?.InvokeAsync(src.entry)
+
+/datum/tgui_input_number/async/wait()
+ return
diff --git a/code/modules/tgui/tgui_input_text.dm b/code/modules/tgui/tgui_input_text.dm
new file mode 100644
index 0000000000000..3326f78a2cb83
--- /dev/null
+++ b/code/modules/tgui/tgui_input_text.dm
@@ -0,0 +1,211 @@
+/**
+ * Creates a TGUI window with a text input. Returns the user's response.
+ *
+ * This proc should be used to create windows for text entry that the caller will wait for a response from.
+ * If tgui fancy chat is turned off: Will return a normal input. If max_length is specified, will return
+ * stripped_multiline_input.
+ *
+ * Arguments:
+ * * user - The user to show the text input to.
+ * * message - The content of the text input, shown in the body of the TGUI window.
+ * * title - The title of the text input modal, shown on the top of the TGUI window.
+ * * default - The default (or current) value, shown as a placeholder.
+ * * max_length - Specifies a max length for input. MAX_MESSAGE_LEN is default (1024)
+ * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc.
+ * * encode - Toggling this determines if input is filtered via html_encode. Setting this to FALSE gives raw input.
+ * * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, timeout = 0)
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ // Client does NOT have tgui_input on: Returns regular input
+ /*if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ if(encode)
+ if(multiline)
+ return stripped_multiline_input(user, message, title, default, max_length)
+ else
+ return stripped_input(user, message, title, default, max_length)
+ else
+ if(multiline)
+ return input(user, message, title, default) as message|null
+ else
+ return input(user, message, title, default) as text|null*/
+ var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout)
+ text_input.ui_interact(user)
+ text_input.wait()
+ if (text_input)
+ . = text_input.entry
+ qdel(text_input)
+
+/**
+ * Creates an asynchronous TGUI text input window with an associated callback.
+ *
+ * This proc should be used to create text inputs that invoke a callback with the user's entry.
+ * Arguments:
+ * * user - The user to show the text input to.
+ * * message - The content of the text input, shown in the body of the TGUI window.
+ * * title - The title of the text input modal, shown on the top of the TGUI window.
+ * * default - The default (or current) value, shown as a placeholder.
+ * * max_length - Specifies a max length for input.
+ * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc.
+ * * encode - If toggled, input is filtered via html_encode. Setting this to FALSE gives raw input.
+ * * callback - The callback to be invoked when a choice is made.
+ */
+/proc/tgui_input_text_async(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, datum/callback/callback, timeout = 60 SECONDS)
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ // Client does NOT have tgui_input on: Returns regular input
+ /*if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ if(encode)
+ if(multiline)
+ return stripped_multiline_input(user, message, title, default, max_length)
+ else
+ return stripped_input(user, message, title, default, max_length)
+ else
+ if(multiline)
+ return input(user, message, title, default) as message|null
+ else
+ return input(user, message, title, default) as text|null*/
+ var/datum/tgui_input_text/async/text_input = new(user, message, title, default, max_length, multiline, encode, callback, timeout)
+ text_input.ui_interact(user)
+
+/**
+ * # tgui_input_text
+ *
+ * Datum used for instantiating and using a TGUI-controlled text input that prompts the user with
+ * a message and has an input for text entry.
+ */
+/datum/tgui_input_text
+ /// Boolean field describing if the tgui_input_text was closed by the user.
+ var/closed
+ /// The default (or current) value, shown as a default.
+ var/default
+ /// Whether the input should be stripped using html_encode
+ var/encode
+ /// The entry that the user has return_typed in.
+ var/entry
+ /// The maximum length for text entry
+ var/max_length
+ /// The prompt's body, if any, of the TGUI window.
+ var/message
+ /// Multiline input for larger input boxes.
+ var/multiline
+ /// The time at which the text input was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the text input, after which the window will close and delete itself.
+ var/timeout
+ /// The title of the TGUI window
+ var/title
+
+
+/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout)
+ src.default = default
+ src.encode = encode
+ src.max_length = max_length
+ src.message = message
+ src.multiline = multiline
+ src.title = title
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+
+/datum/tgui_input_text/Destroy(force, ...)
+ SStgui.close_uis(src)
+ . = ..()
+
+/**
+ * Waits for a user's response to the tgui_input_text's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_input_text/proc/wait()
+ while (!entry && !closed && !QDELETED(src))
+ stoplag(1)
+
+/datum/tgui_input_text/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "TextInputModal")
+ ui.open()
+
+/datum/tgui_input_text/ui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_input_text/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/tgui_input_text/ui_data(mob/user)
+ . = list()
+ .["max_length"] = max_length
+ .["message"] = message
+ .["multiline"] = multiline
+ .["placeholder"] = default // Default is a reserved keyword
+ .["preferences"] = list()
+ .["preferences"]["large_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ .["preferences"]["swapped_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ .["title"] = title
+ if(timeout)
+ .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+
+/datum/tgui_input_text/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("submit")
+ if(max_length)
+ if(length(params["entry"]) > max_length)
+ CRASH("[usr] typed a text string longer than the max length")
+ if(encode && (length(html_encode(params["entry"])) > max_length))
+ to_chat(usr, "Input uses special characters, thus reducing the maximum length.")
+ set_entry(params["entry"])
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+/datum/tgui_input_text/proc/set_entry(entry)
+ if(!isnull(entry))
+ var/converted_entry = encode ? html_encode(entry) : entry
+ src.entry = trim(converted_entry, max_length)
+
+/**
+ * # async tgui_input_text
+ *
+ * An asynchronous version of tgui_input_text to be used with callbacks instead of waiting on user responses.
+ */
+/datum/tgui_input_text/async
+ // The callback to be invoked by the tgui_input_text upon having a choice made.
+ var/datum/callback/callback
+
+/datum/tgui_input_text/async/New(mob/user, message, title, default, max_length, multiline, encode, callback, timeout)
+ ..(user, message, title, default, max_length, multiline, encode, timeout)
+ src.callback = callback
+
+/datum/tgui_input_text/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_input_text/async/set_entry(entry)
+ . = ..()
+ if(!isnull(src.entry))
+ callback?.InvokeAsync(src.entry)
+
+/datum/tgui_input_text/async/wait()
+ return
diff --git a/interface/interface.dm b/interface/interface.dm
index 58010814f4f0a..8ad6c9dbce6c5 100644
--- a/interface/interface.dm
+++ b/interface/interface.dm
@@ -63,6 +63,8 @@
if(GLOB.revdata.testmerge.len)
message += " The following experimental changes are active and are probably the cause of any new or sudden issues you may experience. If possible, please try to find a specific thread for your issue instead of posting to the general issue tracker: "
message += GLOB.revdata.GetTestMergeInfo(FALSE)
+ // We still use tgalert here because some people were concerned that if someone wanted to report that tgui wasn't working
+ // then the report issue button being tgui-based would be problematic.
if(tgalert(src, message, "Report Issue","Yes","No")!="Yes")
return
var/static/issue_template = rustg_file_read(".github/ISSUE_TEMPLATE.md")
diff --git a/tgui/packages/tgui/components/Autofocus.tsx b/tgui/packages/tgui/components/Autofocus.tsx
new file mode 100644
index 0000000000000..28945dd7aa481
--- /dev/null
+++ b/tgui/packages/tgui/components/Autofocus.tsx
@@ -0,0 +1,19 @@
+import { Component, createRef } from 'inferno';
+
+export class Autofocus extends Component {
+ ref = createRef();
+
+ componentDidMount() {
+ setTimeout(() => {
+ this.ref.current?.focus();
+ }, 1);
+ }
+
+ render() {
+ return (
+