diff --git a/code/__DEFINES/dcs/signals/signals_admin.dm b/code/__DEFINES/dcs/signals/signals_admin.dm
index 9b4b06ad5d4..dafd135ead7 100644
--- a/code/__DEFINES/dcs/signals/signals_admin.dm
+++ b/code/__DEFINES/dcs/signals/signals_admin.dm
@@ -3,5 +3,8 @@
/// Fired when an adminhelp is made inactive either due to closing or resolving.
#define COMSIG_ADMIN_HELP_MADE_INACTIVE "admin_help_made_inactive"
-/// Called when the player replies. From /client/proc/cmd_admin_pm().
+/// Called on the /datum/admin_help when the player replies. From /client/proc/cmd_admin_pm().
#define COMSIG_ADMIN_HELP_REPLIED "admin_help_replied"
+
+/// Called on a client when a player receives an adminhelp. From /client/proc/receive_ahelp(message)
+#define COMSIG_ADMIN_HELP_RECEIVED "admin_help_received"
diff --git a/code/__DEFINES/dcs/signals/signals_client.dm b/code/__DEFINES/dcs/signals/signals_client.dm
index e20946d925a..07e5251cad3 100644
--- a/code/__DEFINES/dcs/signals/signals_client.dm
+++ b/code/__DEFINES/dcs/signals/signals_client.dm
@@ -1,7 +1,14 @@
-// Yes, they do support this
-
// from /client/proc/change_view() : (new_size)
#define COMSIG_VIEW_SET "view_set"
// from /client/proc/handle_popup_close() : (window_id)
#define COMSIG_POPUP_CLEARED "popup_cleared"
+
+/// Called after one or more verbs are added: (list of verbs added)
+#define COMSIG_CLIENT_VERB_ADDED "client_verb_added"
+
+/// Called after one or more verbs are added: (list of verbs added)
+#define COMSIG_CLIENT_VERB_REMOVED "client_verb_removed"
+
+/// Called after a client logs into a mob: (mob)
+#define COMSIG_CLIENT_MOB_LOGIN "client_mob_changed"
diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm
index 79e35ec3fee..bafb53f1ffd 100644
--- a/code/__DEFINES/dcs/signals/signals_global.dm
+++ b/code/__DEFINES/dcs/signals/signals_global.dm
@@ -74,3 +74,7 @@
#define COMSIG_GLOB_LIGHT_MECHANISM_COMPLETED "!light_mechanism_completed"
/// Global Signal sent when the crew wins the revolution (No arguments).
#define COMSIG_GLOB_REVOLUTION_VICTORY "!revolution_victory"
+
+/// Global signal called after the station changes its name.
+/// (new_name, old_name)
+#define COMSIG_GLOB_STATION_NAME_CHANGED "!station_name_changed"
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index 9bf9988160f..d526b9c87b0 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -85,13 +85,22 @@
#define HUD_PLANE 40
#define ABOVE_HUD_PLANE 41
-///Plane of the "splash" icon used that shows on the lobby screen. only render plate planes should be above this
+///Plane of the "splash" icon used that shows on the lobby screen
#define SPLASHSCREEN_PLANE 50
+// The largest plane here must still be less than RENDER_PLANE_GAME
+
//-------------------- Rendering ---------------------
#define RENDER_PLANE_GAME 100
#define RENDER_PLANE_NON_GAME 101
-#define RENDER_PLANE_MASTER 102
+
+// Only VERY special planes should be here, as they are above not just the game, but the UI planes as well.
+
+/// Plane related to the menu when pressing Escape.
+/// Needed so that we can apply a blur effect to EVERYTHING, and guarantee we are above all UI.
+#define ESCAPE_MENU_PLANE 105
+
+#define RENDER_PLANE_MASTER 110
// Lummox I swear to god I will find you
// NOTE! You can only ever have planes greater then -10000, if you add too many with large offsets you will brick multiz
@@ -252,6 +261,7 @@
///Plane master controller keys
#define PLANE_MASTERS_GAME "plane_masters_game"
+#define PLANE_MASTERS_NON_MASTER "plane_masters_non_master"
#define PLANE_MASTERS_COLORBLIND "plane_masters_colorblind"
//Plane master critical flags
diff --git a/code/__DEFINES/text.dm b/code/__DEFINES/text.dm
index 740223a3e5f..df891b4c5a8 100644
--- a/code/__DEFINES/text.dm
+++ b/code/__DEFINES/text.dm
@@ -4,6 +4,9 @@
/// Prepares a text to be used for maptext. Use this so it doesn't look hideous.
#define MAPTEXT(text) {"[##text]"}
+/// Prepares a text to be used for maptext, using a font that can handle larger text better.
+#define MAPTEXT_VCR_OSD_MONO(text) {"[##text]"}
+
/// Macro from Lummox used to get height from a MeasureText proc.
/// resolves the MeasureText() return value once, then resolves the height, then sets return_var to that.
#define WXH_TO_HEIGHT(measurement, return_var) \
diff --git a/code/__DEFINES/time.dm b/code/__DEFINES/time.dm
index 847eddcf25c..64955e4a569 100644
--- a/code/__DEFINES/time.dm
+++ b/code/__DEFINES/time.dm
@@ -2,7 +2,7 @@
#define MIDNIGHT_ROLLOVER 864000
///displays the current time into the round, with a lot of extra code just there for ensuring it looks okay after an entire day passes
-#define ROUND_TIME ( "[world.time - SSticker.round_start_time > MIDNIGHT_ROLLOVER ? "[round((world.time - SSticker.round_start_time)/MIDNIGHT_ROLLOVER)]:[worldtime2text()]" : worldtime2text()]" )
+#define ROUND_TIME(...) ( "[world.time - SSticker.round_start_time > MIDNIGHT_ROLLOVER ? "[round((world.time - SSticker.round_start_time)/MIDNIGHT_ROLLOVER)]:[worldtime2text()]" : worldtime2text()]" )
/// Define that just has the current in-universe year for use in whatever context you might want to display that in. (For example, 2022 -> 2562 given a 540 year offset)
#define CURRENT_STATION_YEAR (GLOB.year_integer + STATION_YEAR_OFFSET)
diff --git a/code/__HELPERS/names.dm b/code/__HELPERS/names.dm
index d5356227613..82d9ea1ffab 100644
--- a/code/__HELPERS/names.dm
+++ b/code/__HELPERS/names.dm
@@ -45,8 +45,9 @@ GLOBAL_VAR(command_name)
return GLOB.station_name
-/proc/set_station_name(newname)
- GLOB.station_name = newname
+/proc/set_station_name(new_name)
+ var/old_name = GLOB.station_name
+ GLOB.station_name = new_name
var/config_server_name = CONFIG_GET(string/servername)
if(config_server_name)
@@ -54,6 +55,7 @@ GLOBAL_VAR(command_name)
else
world.name = html_decode(GLOB.station_name)
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_STATION_NAME_CHANGED, new_name, old_name)
/proc/new_station_name()
var/random = rand(1,5)
diff --git a/code/__HELPERS/verbs.dm b/code/__HELPERS/verbs.dm
index d042929f118..badd3c6727a 100644
--- a/code/__HELPERS/verbs.dm
+++ b/code/__HELPERS/verbs.dm
@@ -46,6 +46,8 @@
target.stat_panel.send_message("add_verb_list", output_list)
+ SEND_SIGNAL(target, COMSIG_CLIENT_VERB_ADDED, verbs_list)
+
/**
* handles removing verb and sending it to browser to update, use this for removing verbs
*
@@ -92,3 +94,5 @@
output_list[++output_list.len] = list(verb_to_remove.category, verb_to_remove.name)
target.stat_panel.send_message("remove_verb_list", output_list)
+
+ SEND_SIGNAL(target, COMSIG_CLIENT_VERB_REMOVED, verbs_list)
diff --git a/code/_onclick/hud/generic_dextrous.dm b/code/_onclick/hud/generic_dextrous.dm
index 9358ef18384..c0fa5e481c9 100644
--- a/code/_onclick/hud/generic_dextrous.dm
+++ b/code/_onclick/hud/generic_dextrous.dm
@@ -50,7 +50,7 @@
using.hud = src
static_inventory += using
- mymob.client.screen = list()
+ mymob.client.clear_screen()
for(var/atom/movable/screen/inventory/inv in (static_inventory + toggleable_inventory))
if(inv.slot_id)
diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm
index a8462025d5c..6d4e399732e 100644
--- a/code/_onclick/hud/hud.dm
+++ b/code/_onclick/hud/hud.dm
@@ -318,7 +318,7 @@ GLOBAL_LIST_INIT(available_erp_ui_styles, list(
// This code is the absolute fucking worst, I want it to go die in a fire
// Seriously, why
- screenmob.client.screen = list()
+ screenmob.client.clear_screen()
screenmob.client.apply_clickcatcher()
var/display_hud_version = version
diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm
index 5edf9ee99dc..10ab271c19c 100644
--- a/code/_onclick/hud/rendering/plane_master.dm
+++ b/code/_onclick/hud/rendering/plane_master.dm
@@ -615,8 +615,16 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/plane_master)
/atom/movable/screen/plane_master/splashscreen
name = "Splashscreen"
- documentation = "Anything that's drawn above LITERALLY everything else. Think cinimatics and the well, spashscreen."
+ documentation = "Cinematics and the splash screen."
plane = SPLASHSCREEN_PLANE
appearance_flags = PLANE_MASTER|NO_CLIENT_COLOR
render_relay_planes = list(RENDER_PLANE_NON_GAME)
allows_offsetting = FALSE
+
+/atom/movable/screen/plane_master/escape_menu
+ name = "Escape Menu"
+ documentation = "Anything relating to the escape menu."
+ plane = ESCAPE_MENU_PLANE
+ appearance_flags = PLANE_MASTER|NO_CLIENT_COLOR
+ render_relay_planes = list(RENDER_PLANE_MASTER)
+ allows_offsetting = FALSE
diff --git a/code/_onclick/hud/rendering/plane_master_controller.dm b/code/_onclick/hud/rendering/plane_master_controller.dm
index 6e7676a5c71..f4b71611716 100644
--- a/code/_onclick/hud/rendering/plane_master_controller.dm
+++ b/code/_onclick/hud/rendering/plane_master_controller.dm
@@ -86,6 +86,15 @@ INITIALIZE_IMMEDIATE(/atom/movable/plane_master_controller)
RENDER_PLANE_GAME
)
+/// Exists for convienience when referencing all non-master render plates.
+/// This is the whole game and the UI, but not the escape menu.
+/atom/movable/plane_master_controller/non_master
+ name = PLANE_MASTERS_NON_MASTER
+ controlled_planes = list(
+ RENDER_PLANE_GAME,
+ RENDER_PLANE_NON_GAME,
+ )
+
/// Exists for convienience when referencing all game render plates
/atom/movable/plane_master_controller/colorblind
name = PLANE_MASTERS_COLORBLIND
diff --git a/code/_onclick/hud/screen_object_holder.dm b/code/_onclick/hud/screen_object_holder.dm
new file mode 100644
index 00000000000..ba6a9b96172
--- /dev/null
+++ b/code/_onclick/hud/screen_object_holder.dm
@@ -0,0 +1,59 @@
+/// A helper instance that will handle adding objects from the client's screen
+/// to easily remove from later.
+/datum/screen_object_holder
+ VAR_PRIVATE
+ client/client
+ list/screen_objects = list()
+ list/protected_screen_objects = list()
+
+/datum/screen_object_holder/New(client/client)
+ ASSERT(istype(client))
+
+ src.client = client
+
+ RegisterSignal(client, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_qdel))
+
+/datum/screen_object_holder/Destroy()
+ clear()
+ client = null
+
+ return ..()
+
+/// Gives the screen object to the client, qdel'ing it when it's cleared
+/datum/screen_object_holder/proc/give_screen_object(atom/screen_object)
+ ASSERT(istype(screen_object))
+
+ screen_objects += screen_object
+ client?.screen += screen_object
+
+/// Gives the screen object to the client, but does not qdel it when it's cleared
+/datum/screen_object_holder/proc/give_protected_screen_object(atom/screen_object)
+ ASSERT(istype(screen_object))
+
+ protected_screen_objects += screen_object
+ client?.screen += screen_object
+
+/datum/screen_object_holder/proc/remove_screen_object(atom/screen_object)
+ ASSERT(istype(screen_object))
+ ASSERT((screen_object in screen_objects) || (screen_object in protected_screen_objects))
+
+ screen_objects -= screen_object
+ protected_screen_objects -= screen_object
+ client?.screen -= screen_object
+
+/datum/screen_object_holder/proc/clear()
+ client?.screen -= screen_objects
+ client?.screen -= protected_screen_objects
+
+ QDEL_LIST(screen_objects)
+ protected_screen_objects.Cut()
+
+// We don't qdel here, as clients leaving should not be a concern for consumers
+// Consumers ought to be qdel'ing this on their own Destroy, but we shouldn't
+// hard del because they aren't watching for the client, that's our job.
+/datum/screen_object_holder/proc/on_parent_qdel()
+ PRIVATE_PROC(TRUE)
+ SIGNAL_HANDLER
+
+ clear()
+ client = null
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 95175edd60a..326cb4db9a0 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -33,6 +33,9 @@
*/
var/del_on_map_removal = TRUE
+ /// If FALSE, this will not be cleared when calling /client/clear_screen()
+ var/clear_with_screen = TRUE
+
/atom/movable/screen/Destroy()
master = null
hud = null
diff --git a/code/controllers/subsystem/input.dm b/code/controllers/subsystem/input.dm
index c39712771be..65dc1e31a1e 100644
--- a/code/controllers/subsystem/input.dm
+++ b/code/controllers/subsystem/input.dm
@@ -38,7 +38,7 @@ VERB_MANAGER_SUBSYSTEM_DEF(input)
"Any+UP" = "\"KeyUp \[\[*\]\]\"",
"Back" = "\".winset \\\"input.text=\\\"\\\"\\\"\"",
"Tab" = "\".winset \\\"input.focus=true?map.focus=true:input.focus=true\\\"\"",
- "Escape" = "Reset-Held-Keys",
+ "Escape" = "Open-Escape-Menu",
)
// Badmins just wanna have fun ♪
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index 6417858d133..b0e64940f44 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -29,7 +29,7 @@ SUBSYSTEM_DEF(statpanels)
cached ? "Next Map: [cached.map_name]" : null,
"Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]",
"Server Time: [time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")]",
- "Round Time: [ROUND_TIME]",
+ "Round Time: [ROUND_TIME()]",
"Station Time: [station_time_timestamp()]",
"Time Dilation: [round(SStime_track.time_dilation_current,1)]% AVG:([round(SStime_track.time_dilation_avg_fast,1)]%, [round(SStime_track.time_dilation_avg,1)]%, [round(SStime_track.time_dilation_avg_slow,1)]%)"
)
diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm
index 02d2f3fe430..39bd15caec7 100644
--- a/code/modules/admin/verbs/adminhelp.dm
+++ b/code/modules/admin/verbs/adminhelp.dm
@@ -202,6 +202,10 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
var/webhook_sent = WEBHOOK_NONE
/// List of player interactions
var/list/player_interactions
+ /// List of admin ckeys that are involved, like through responding
+ var/list/admins_involved = list()
+ /// Has the player replied to this ticket yet?
+ var/player_replied = FALSE
//SKYRAT EDIT START
/// Have we requested this ticket to stop being part of the Ticket Ping subsystem?
@@ -294,7 +298,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
"PLAYERS" = player_count,
"ROUND STATE" = round_state,
"ROUND ID" = GLOB.round_id,
- "ROUND TIME" = ROUND_TIME,
+ "ROUND TIME" = ROUND_TIME(),
"MESSAGE" = message,
"ADMINS" = admin_text,
)
@@ -366,9 +370,12 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
return ..()
/datum/admin_help/proc/AddInteraction(formatted_message, player_message)
- if(heard_by_no_admins && usr && usr.ckey != initiator_ckey)
- heard_by_no_admins = FALSE
- send2adminchat(initiator_ckey, "Ticket #[id]: Answered by [key_name(usr)]")
+ if (!isnull(usr) && usr.ckey != initiator_ckey)
+ admins_involved |= usr.ckey
+ if(heard_by_no_admins)
+ heard_by_no_admins = FALSE
+ send2adminchat(initiator_ckey, "Ticket #[id]: Answered by [key_name(usr)]")
+
ticket_interactions += "[time_stamp()]: [formatted_message]"
if (!isnull(player_message))
player_interactions += "[time_stamp()]: [player_message]"
@@ -434,12 +441,20 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
confidential = TRUE)
//show it to the person adminhelping too
- to_chat(initiator,
- type = MESSAGE_TYPE_ADMINPM,
- html = span_adminnotice("PM to-Admins: [span_linkify(msg)]"),
- confidential = TRUE)
+ reply_to_admins_notification(msg)
SSblackbox.LogAhelp(id, "Ticket Opened", msg, null, initiator.ckey, urgent = urgent)
+/// Sends a message to the player that they are replying to admins.
+/datum/admin_help/proc/reply_to_admins_notification(message)
+ to_chat(
+ initiator,
+ type = MESSAGE_TYPE_ADMINPM,
+ html = span_notice("PM to-Admins: [span_linkify(message)]"),
+ confidential = TRUE,
+ )
+
+ player_replied = TRUE
+
//Reopen a closed ticket
/datum/admin_help/proc/Reopen()
if(state == AHELP_ACTIVE)
diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm
index 78931c1ee7a..8293c9e3187 100644
--- a/code/modules/admin/verbs/adminpm.dm
+++ b/code/modules/admin/verbs/adminpm.dm
@@ -326,13 +326,12 @@
var/name_key_with_link = key_name(src, TRUE, TRUE)
if(ambiguious_recipient == EXTERNAL_PM_USER)
- to_chat(src,
- type = MESSAGE_TYPE_ADMINPM,
- html = span_notice("PM to-Admins: [span_linkify(raw_message)]"),
- confidential = TRUE)
var/datum/admin_help/new_admin_help = admin_ticket_log(src,
"Reply PM from-[name_key_with_link] to External: [keyword_parsed_msg]",
player_message = "Reply PM from-[name_key_with_link] to External: [send_message]")
+
+ new_admin_help.reply_to_admins_notification(raw_message)
+
var/new_help_id = new_admin_help?.id
externalreplyamount--
@@ -409,10 +408,12 @@
type = MESSAGE_TYPE_ADMINPM,
html = "-- Administrator private message --",
confidential = TRUE)
- to_chat(recipient,
- type = MESSAGE_TYPE_ADMINPM,
- html = span_adminsay("Admin PM from-[link_to_us]: [span_linkify(send_message)]"),
- confidential = TRUE)
+
+ recipient.receive_ahelp(
+ link_to_us,
+ span_linkify(send_message),
+ )
+
to_chat(recipient,
type = MESSAGE_TYPE_ADMINPM,
html = span_adminsay("Click on the administrator's name to reply."),
@@ -480,10 +481,12 @@
// Admin on admin violence first
if(our_holder)
- to_chat(recipient,
- type = MESSAGE_TYPE_ADMINPM,
- html = span_danger("Admin PM from-[name_key_with_link]: [span_linkify(keyword_parsed_msg)]"),
- confidential = TRUE)
+ recipient.receive_ahelp(
+ name_key_with_link,
+ span_linkify(keyword_parsed_msg),
+ "danger",
+ )
+
to_chat(src,
type = MESSAGE_TYPE_ADMINPM,
html = span_notice("Admin PM to-[their_name_with_link]: [span_linkify(keyword_parsed_msg)]"),
@@ -516,11 +519,10 @@
type = MESSAGE_TYPE_ADMINPM,
html = span_danger("[replymsg]"),
confidential = TRUE)
- to_chat(src,
- type = MESSAGE_TYPE_ADMINPM,
- html = span_notice("PM to-Admins: [span_linkify(send_message)]"),
- confidential = TRUE)
+
+ ticket.reply_to_admins_notification(send_message)
SSblackbox.LogAhelp(ticket_id, "Reply", send_message, recip_ckey, our_ckey)
+
return TRUE
/// Notifies all admins about the existance of an admin pm, then logs the pm
@@ -732,10 +734,12 @@
type = MESSAGE_TYPE_ADMINPM,
html = "-- Administrator private message --",
confidential = TRUE)
- to_chat(recipient,
- type = MESSAGE_TYPE_ADMINPM,
- html = span_adminsay("Admin PM from-[adminname]: [message]"),
- confidential = TRUE)
+
+ recipient.receive_ahelp(
+ "[adminname]",
+ message,
+ )
+
to_chat(recipient,
type = MESSAGE_TYPE_ADMINPM,
html = span_adminsay("Click on the administrator's name to reply."),
@@ -781,6 +785,17 @@
return GLOB.directory[searching_ckey]
+/client/proc/receive_ahelp(reply_to, message, span_class = "adminsay")
+ to_chat(
+ src,
+ type = MESSAGE_TYPE_ADMINPM,
+ html = "Admin PM from-[reply_to]: [message]",
+ confidential = TRUE,
+ )
+
+ current_ticket?.player_replied = FALSE
+
+ SEND_SIGNAL(src, COMSIG_ADMIN_HELP_RECEIVED, message)
#undef EXTERNAL_PM_USER
#undef EXTERNALREPLYCOUNT
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 72cd26e43a3..3f68c238007 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -1333,3 +1333,13 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
winset(usr, "mapwindow.status_bar", "is-visible=true")
else
winset(usr, "mapwindow.status_bar", "is-visible=false")
+
+/// Clears the client's screen, aside from ones that opt out
+/client/proc/clear_screen()
+ for (var/object in screen)
+ if (istype(object, /atom/movable/screen))
+ var/atom/movable/screen/screen_object = object
+ if (!screen_object.clear_with_screen)
+ continue
+
+ screen -= object
diff --git a/code/modules/client/verbs/reset_held_keys.dm b/code/modules/client/verbs/reset_held_keys.dm
deleted file mode 100644
index d5a25044090..00000000000
--- a/code/modules/client/verbs/reset_held_keys.dm
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Manually clears any held keys, in case due to lag or other undefined behavior a key gets stuck.
- *
- * Hardcoded to the ESC key.
- */
-/client/verb/reset_held_keys()
- set name = "Reset Held Keys"
- set hidden = TRUE
-
- for(var/key in keys_held)
- keyUp(key)
-
- //In case one got stuck and the previous loop didn't clean it, somehow.
- for(var/key in key_combos_held)
- keyUp(key_combos_held[key])
-
- winset(src, "input", "text=[null]") //SKYRAT EDIT ADDITION - QOL
diff --git a/code/modules/escape_menu/details.dm b/code/modules/escape_menu/details.dm
new file mode 100644
index 00000000000..5a9a755a1cd
--- /dev/null
+++ b/code/modules/escape_menu/details.dm
@@ -0,0 +1,43 @@
+// This doesn't instantiate right away, since we rely on other GLOBs
+GLOBAL_DATUM(escape_menu_details, /atom/movable/screen/escape_menu/details)
+
+/// Provides a singleton for the escape menu details screen.
+/proc/give_escape_menu_details()
+ if (isnull(GLOB.escape_menu_details))
+ GLOB.escape_menu_details = new
+
+ return GLOB.escape_menu_details
+
+/atom/movable/screen/escape_menu/details
+ screen_loc = "EAST:-180,NORTH:-25"
+ maptext_height = 100
+ maptext_width = 200
+
+/atom/movable/screen/escape_menu/details/Initialize(mapload)
+ . = ..()
+
+ update_text()
+ START_PROCESSING(SSescape_menu, src)
+
+/atom/movable/screen/escape_menu/details/Destroy()
+ if (GLOB.escape_menu_details == src)
+ stack_trace("Something tried to delete the escape menu details screen")
+ return QDEL_HINT_LETMELIVE
+
+ STOP_PROCESSING(SSescape_menu, src)
+ return ..()
+
+/atom/movable/screen/escape_menu/details/process(delta_time)
+ update_text()
+
+/atom/movable/screen/escape_menu/details/proc/update_text()
+ var/new_maptext = {"
+
+ Round ID: [GLOB.round_id || "Unset"]
+ Round Time: [ROUND_TIME()]
+ Map: [SSmapping.config?.map_name || "Loading..."]
+ Time Dilation: [round(SStime_track.time_dilation_current,1)]%
+
+ "}
+
+ maptext = MAPTEXT(new_maptext)
diff --git a/code/modules/escape_menu/dimmer.dm b/code/modules/escape_menu/dimmer.dm
new file mode 100644
index 00000000000..6bdf87846b6
--- /dev/null
+++ b/code/modules/escape_menu/dimmer.dm
@@ -0,0 +1,8 @@
+/atom/movable/screen/fullscreen/dimmer
+ icon = 'icons/hud/screen_gen.dmi'
+ screen_loc = "WEST,SOUTH to EAST,NORTH"
+ icon_state = "black"
+ alpha = 200
+ plane = ESCAPE_MENU_PLANE
+ mouse_opacity = MOUSE_OPACITY_OPAQUE
+ clear_with_screen = FALSE
diff --git a/code/modules/escape_menu/escape_menu.dm b/code/modules/escape_menu/escape_menu.dm
new file mode 100644
index 00000000000..aebd636e5e9
--- /dev/null
+++ b/code/modules/escape_menu/escape_menu.dm
@@ -0,0 +1,127 @@
+GLOBAL_LIST_EMPTY(escape_menus)
+
+/// Opens the escape menu.
+/// Verb, hardcoded to Escape, set in the client skin.
+/client/verb/open_escape_menu()
+ set name = "Open Escape Menu"
+ set hidden = TRUE
+
+ var/current_escape_menu = GLOB.escape_menus[ckey]
+ if (!isnull(current_escape_menu))
+ qdel(current_escape_menu)
+ return
+
+ reset_held_keys()
+
+ new /datum/escape_menu(src)
+
+#define PAGE_HOME "PAGE_HOME"
+#define PAGE_LEAVE_BODY "PAGE_LEAVE_BODY"
+
+/datum/escape_menu
+ /// The client that owns this escape menu
+ var/client/client
+
+ VAR_PRIVATE
+ ckey
+
+ datum/screen_object_holder/base_holder
+ datum/screen_object_holder/page_holder
+
+ atom/movable/plane_master_controller/plane_master_controller
+
+ menu_page = PAGE_HOME
+
+/datum/escape_menu/New(client/client)
+ ASSERT(!(client.ckey in GLOB.escape_menus))
+
+ ckey = client?.ckey
+ src.client = client
+
+ base_holder = new(client)
+ populate_base_ui()
+
+ page_holder = new(client)
+ show_page()
+
+ RegisterSignal(client, COMSIG_PARENT_QDELETING, PROC_REF(on_client_qdel))
+ RegisterSignal(client, COMSIG_CLIENT_MOB_LOGIN, PROC_REF(on_client_mob_login))
+
+ if (!isnull(ckey))
+ GLOB.escape_menus[ckey] = src
+
+/datum/escape_menu/Destroy(force, ...)
+ QDEL_NULL(base_holder)
+ QDEL_NULL(page_holder)
+
+ GLOB.escape_menus -= ckey
+ plane_master_controller.remove_filter("escape_menu_blur")
+
+ return ..()
+
+/datum/escape_menu/proc/on_client_qdel()
+ SIGNAL_HANDLER
+ PRIVATE_PROC(TRUE)
+
+ qdel(src)
+
+/datum/escape_menu/proc/on_client_mob_login()
+ SIGNAL_HANDLER
+ PRIVATE_PROC(TRUE)
+
+ if (menu_page == PAGE_LEAVE_BODY)
+ qdel(src)
+
+/datum/escape_menu/proc/show_page()
+ PRIVATE_PROC(TRUE)
+
+ page_holder.clear()
+
+ switch (menu_page)
+ if (PAGE_HOME)
+ show_home_page()
+ if (PAGE_LEAVE_BODY)
+ show_leave_body_page()
+ else
+ CRASH("Unknown escape menu page: [menu_page]")
+
+/datum/escape_menu/proc/populate_base_ui()
+ PRIVATE_PROC(TRUE)
+
+ base_holder.give_screen_object(new /atom/movable/screen/fullscreen/dimmer)
+ add_blur()
+
+ base_holder.give_protected_screen_object(give_escape_menu_title())
+ base_holder.give_protected_screen_object(give_escape_menu_details())
+
+/datum/escape_menu/proc/open_home_page()
+ PRIVATE_PROC(TRUE)
+
+ menu_page = PAGE_HOME
+ show_page()
+
+/datum/escape_menu/proc/open_leave_body()
+ PRIVATE_PROC(TRUE)
+
+ menu_page = PAGE_LEAVE_BODY
+ show_page()
+
+/datum/escape_menu/proc/add_blur()
+ PRIVATE_PROC(TRUE)
+
+ var/list/plane_master_controllers = client?.mob.hud_used.plane_master_controllers
+ if (isnull(plane_master_controllers))
+ return
+
+ plane_master_controller = plane_master_controllers[PLANE_MASTERS_NON_MASTER]
+ plane_master_controller.add_filter("escape_menu_blur", 1, list("type" = "blur", "size" = 2))
+
+/atom/movable/screen/escape_menu
+ plane = ESCAPE_MENU_PLANE
+ clear_with_screen = FALSE
+
+// The escape menu can be opened before SSatoms
+INITIALIZE_IMMEDIATE(/atom/movable/screen/escape_menu)
+
+#undef PAGE_HOME
+#undef PAGE_LEAVE_BODY
diff --git a/code/modules/escape_menu/home_page.dm b/code/modules/escape_menu/home_page.dm
new file mode 100644
index 00000000000..6616de821fd
--- /dev/null
+++ b/code/modules/escape_menu/home_page.dm
@@ -0,0 +1,285 @@
+/datum/escape_menu/proc/show_home_page()
+ page_holder.give_screen_object(
+ new /atom/movable/screen/escape_menu/home_button(
+ null,
+ src,
+ "Resume",
+ /* offset = */ 0,
+ CALLBACK(src, PROC_REF(home_resume)),
+ )
+ )
+
+ page_holder.give_screen_object(
+ new /atom/movable/screen/escape_menu/home_button(
+ null,
+ src,
+ "Settings",
+ /* offset = */ 1,
+ CALLBACK(src, PROC_REF(home_open_settings)),
+ )
+ )
+
+ page_holder.give_screen_object(
+ new /atom/movable/screen/escape_menu/home_button/admin_help(
+ null,
+ src,
+ "Admin Help",
+ /* offset = */ 2,
+ )
+ )
+
+ page_holder.give_screen_object(
+ new /atom/movable/screen/escape_menu/home_button/leave_body(
+ null,
+ src,
+ "Leave Body",
+ /* offset = */ 3,
+ CALLBACK(src, PROC_REF(open_leave_body)),
+ )
+ )
+
+/datum/escape_menu/proc/home_resume()
+ qdel(src)
+
+/datum/escape_menu/proc/home_open_settings()
+ client?.prefs.ui_interact(client?.mob)
+ qdel(src)
+
+/atom/movable/screen/escape_menu/home_button
+ mouse_opacity = MOUSE_OPACITY_OPAQUE
+
+ VAR_PRIVATE
+ atom/movable/screen/escape_menu/home_button_text/home_button_text
+ datum/escape_menu/escape_menu
+ datum/callback/on_click_callback
+
+/atom/movable/screen/escape_menu/home_button/Initialize(
+ mapload,
+ datum/escape_menu/escape_menu,
+ button_text,
+ offset,
+ on_click_callback,
+)
+ . = ..()
+
+ src.escape_menu = escape_menu
+ src.on_click_callback = on_click_callback
+
+ home_button_text = new /atom/movable/screen/escape_menu/home_button_text(
+ src,
+ button_text,
+ )
+
+ vis_contents += home_button_text
+
+ screen_loc = "NORTH:-[100 + (32 * offset)],WEST:110"
+ transform = transform.Scale(6, 1)
+
+/atom/movable/screen/escape_menu/home_button/Destroy()
+ escape_menu = null
+ QDEL_NULL(on_click_callback)
+
+ return ..()
+
+/atom/movable/screen/escape_menu/home_button/Click(location, control, params)
+ if (!enabled())
+ return
+
+ on_click_callback.InvokeAsync()
+
+/atom/movable/screen/escape_menu/home_button/MouseEntered(location, control, params)
+ home_button_text.set_hovered(TRUE)
+
+/atom/movable/screen/escape_menu/home_button/MouseExited(location, control, params)
+ home_button_text.set_hovered(FALSE)
+
+/atom/movable/screen/escape_menu/home_button/proc/text_color()
+ return enabled() ? "white" : "gray"
+
+/atom/movable/screen/escape_menu/home_button/proc/enabled()
+ return TRUE
+
+// Needs to be separated so it doesn't scale
+/atom/movable/screen/escape_menu/home_button_text
+ maptext_width = 200
+ maptext_height = 50
+ pixel_x = -80
+
+ VAR_PRIVATE
+ button_text
+ hovered = FALSE
+
+/atom/movable/screen/escape_menu/home_button_text/Initialize(mapload, button_text)
+ . = ..()
+
+ src.button_text = button_text
+ update_text()
+
+/// Sets the hovered state of the button, and updates the text
+/atom/movable/screen/escape_menu/home_button_text/proc/set_hovered(hovered)
+ if (src.hovered == hovered)
+ return
+
+ src.hovered = hovered
+ update_text()
+
+/atom/movable/screen/escape_menu/home_button_text/proc/update_text()
+ var/atom/movable/screen/escape_menu/home_button/escape_menu_loc = loc
+
+ maptext = MAPTEXT_VCR_OSD_MONO("[button_text]")
+
+ if (hovered)
+ maptext = "[maptext]"
+
+/atom/movable/screen/escape_menu/home_button/admin_help
+ VAR_PRIVATE
+ current_blink = FALSE
+ is_blinking = FALSE
+ last_blink_time = 0
+
+ blink_interval = 0.4 SECONDS
+
+/atom/movable/screen/escape_menu/home_button/admin_help/Initialize(
+ mapload,
+ datum/escape_menu/escape_menu,
+ button_text,
+ offset,
+ on_click_callback,
+)
+ . = ..()
+
+ RegisterSignal(escape_menu.client, COMSIG_ADMIN_HELP_RECEIVED, PROC_REF(on_admin_help_received))
+ RegisterSignals(escape_menu.client, list(COMSIG_CLIENT_VERB_ADDED, COMSIG_CLIENT_VERB_REMOVED), PROC_REF(on_client_verb_changed))
+
+ var/datum/admin_help/current_ticket = escape_menu.client?.current_ticket
+ if (!isnull(current_ticket))
+ connect_ticket(current_ticket)
+ if (!current_ticket?.player_replied)
+ begin_processing()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/Click(location, control, params)
+ if (!enabled())
+ return
+
+ QDEL_IN(escape_menu, 0)
+
+ var/client/client = escape_menu.client
+
+ if (has_open_adminhelp())
+ client?.view_latest_ticket()
+ else
+ client?.adminhelp()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/proc/has_open_adminhelp()
+ var/client/client = escape_menu.client
+
+ var/datum/admin_help/current_ticket = client?.current_ticket
+
+ // This is null with a closed ticket.
+ // This is okay since the View Latest Ticket panel already tells you if your ticket is closed, intentionally.
+ if (isnull(current_ticket))
+ return FALSE
+
+ // If we sent a ticket, but nobody has responded, send another one instead.
+ // Not worth opening a menu when there's nothing to read, you're only going to want to send.
+ if (length(current_ticket.admins_involved - client?.ckey) == 0)
+ return FALSE
+
+ return TRUE
+
+/atom/movable/screen/escape_menu/home_button/admin_help/proc/on_admin_help_received()
+ SIGNAL_HANDLER
+
+ begin_processing()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/proc/on_client_verb_changed(client/source, list/verbs_changed)
+ SIGNAL_HANDLER
+
+ if (/client/verb/adminhelp in verbs_changed)
+ home_button_text.update_text()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/proc/begin_processing()
+ if (is_blinking)
+ return
+
+ is_blinking = TRUE
+ current_blink = TRUE
+ START_PROCESSING(SSescape_menu, src)
+ home_button_text.update_text()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/proc/end_processing()
+ if (!is_blinking)
+ return
+
+ is_blinking = FALSE
+ current_blink = FALSE
+ STOP_PROCESSING(SSescape_menu, src)
+ home_button_text.update_text()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/proc/connect_ticket(datum/admin_help/admin_help)
+ ASSERT(istype(admin_help))
+
+ RegisterSignal(admin_help, COMSIG_ADMIN_HELP_REPLIED, PROC_REF(on_admin_help_replied))
+
+/atom/movable/screen/escape_menu/home_button/admin_help/proc/on_admin_help_replied()
+ SIGNAL_HANDLER
+
+ end_processing()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/enabled()
+ if (!..())
+ return FALSE
+
+ if (!has_open_adminhelp())
+ return /client/verb/adminhelp in escape_menu.client?.verbs
+
+ return TRUE
+
+/atom/movable/screen/escape_menu/home_button/admin_help/process(delta_time)
+ if (world.time - last_blink_time < blink_interval)
+ return
+
+ current_blink = !current_blink
+ last_blink_time = world.time
+ home_button_text.update_text()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/text_color()
+ if (!enabled())
+ return ..()
+
+ return current_blink ? "red" : ..()
+
+/atom/movable/screen/escape_menu/home_button/admin_help/MouseEntered(location, control, params)
+ . = ..()
+
+ if (is_blinking)
+ openToolTip(usr, src, params, content = "An admin is trying to talk to you!")
+
+/atom/movable/screen/escape_menu/home_button/admin_help/MouseExited(location, control, params)
+ . = ..()
+
+ closeToolTip(usr)
+
+/atom/movable/screen/escape_menu/home_button/leave_body
+
+/atom/movable/screen/escape_menu/home_button/leave_body/Initialize(
+ mapload,
+ datum/escape_menu/escape_menu,
+ button_text,
+ offset,
+ on_click_callback,
+)
+ . = ..()
+
+ RegisterSignal(escape_menu.client, COMSIG_CLIENT_MOB_LOGIN, PROC_REF(on_client_mob_login))
+
+/atom/movable/screen/escape_menu/home_button/leave_body/enabled()
+ if (!..())
+ return FALSE
+
+ return isliving(escape_menu.client?.mob)
+
+/atom/movable/screen/escape_menu/home_button/leave_body/proc/on_client_mob_login()
+ SIGNAL_HANDLER
+
+ home_button_text.update_text()
diff --git a/code/modules/escape_menu/leave_body.dm b/code/modules/escape_menu/leave_body.dm
new file mode 100644
index 00000000000..fdba3b6ad9d
--- /dev/null
+++ b/code/modules/escape_menu/leave_body.dm
@@ -0,0 +1,134 @@
+/datum/escape_menu/proc/show_leave_body_page()
+ PRIVATE_PROC(TRUE)
+
+ var/static/dead_clown
+ if (isnull(dead_clown))
+ if (MC_RUNNING(SSatoms.init_stage)) // We're about to create a bunch of atoms for a human
+ dead_clown = create_dead_clown()
+ else
+ stack_trace("The leave body menu was opened before the atoms SS. This shouldn't be possible, as the leave body menu should only be accessible when you have a body.")
+
+ //SKYRAT EDIT REMOVAL BEGIN - Suicide Disabled
+ /*
+ page_holder.give_screen_object(new /atom/movable/screen/escape_menu/leave_body_button(
+ src,
+ "Suicide",
+ "Perform a dramatic suicide in game",
+ /* pixel_offset = */ -105,
+ CALLBACK(src, PROC_REF(leave_suicide)),
+ /* button_overlay = */ dead_clown,
+ ))
+ */
+ //SKYRAT EDIT REMOVAL END
+
+ page_holder.give_screen_object(
+ new /atom/movable/screen/escape_menu/leave_body_button(
+ src,
+ "Ghost",
+ "Exit quietly, leaving your body",
+ /* pixel_offset = */ 0,
+ CALLBACK(src, PROC_REF(leave_ghost)),
+ /* button_overlay = */ "ghost",
+ )
+ )
+
+ page_holder.give_screen_object(
+ new /atom/movable/screen/escape_menu/leave_body_button(
+ src,
+ "Back",
+ /* tooltip_text = */ null,
+ /* pixel_offset = */ 105,
+ CALLBACK(src, PROC_REF(open_home_page)),
+ /* button_overlay = */ "back",
+ )
+ )
+
+/datum/escape_menu/proc/create_dead_clown()
+ PRIVATE_PROC(TRUE)
+
+ var/mob/living/carbon/human/consistent/human = new
+ human.equipOutfit(/datum/outfit/job/clown)
+
+ var/mutable_appearance/appearance = new(human.appearance)
+ appearance.plane = ESCAPE_MENU_PLANE
+
+ // SpacemanDMM bug prevents us from just chain applying these :(
+ appearance.transform = appearance.transform.Scale(2.5, 2.5)
+ appearance.transform = appearance.transform.Turn(90)
+ appearance.transform = appearance.transform.Translate(34, 24)
+
+ qdel(human)
+
+ return appearance
+
+/datum/escape_menu/proc/leave_ghost()
+ PRIVATE_PROC(TRUE)
+
+ // Not guaranteed to be living. Everything defines verb/ghost separately. Fuck you.
+ var/mob/living/living_user = client?.mob
+ living_user?.ghost()
+
+//SKYRAT EDIT REMOVAL BEGIN - Suicide Disabled
+/*
+/datum/escape_menu/proc/leave_suicide()
+ PRIVATE_PROC(TRUE)
+
+ // Not guaranteed to be human. Everything defines verb/suicide separately. Fuck you, still.
+ var/mob/living/carbon/human/human_user = client?.mob
+ human_user?.suicide()
+*/
+//SKYRAT EDIT REMOVAL END
+
+/atom/movable/screen/escape_menu/leave_body_button
+ icon = 'icons/hud/escape_menu_leave_body.dmi'
+ icon_state = "template"
+ maptext_width = 96
+ maptext_y = -32
+
+ VAR_PRIVATE
+ datum/callback/on_click_callback
+ hovered = FALSE
+ tooltip_text
+
+/atom/movable/screen/escape_menu/leave_body_button/Initialize(
+ mapload,
+ button_text,
+ tooltip_text,
+ pixel_offset,
+ on_click_callback,
+ button_overlay,
+)
+ . = ..()
+
+ src.on_click_callback = on_click_callback
+ src.tooltip_text = tooltip_text
+
+ add_overlay(button_overlay)
+
+ maptext = MAPTEXT_VCR_OSD_MONO("[button_text]")
+ screen_loc = "CENTER:[pixel_offset],CENTER-1"
+
+/atom/movable/screen/escape_menu/leave_body_button/Destroy()
+ QDEL_NULL(on_click_callback)
+
+ return ..()
+
+/atom/movable/screen/escape_menu/leave_body_button/Click(location, control, params)
+ on_click_callback?.InvokeAsync()
+
+/atom/movable/screen/escape_menu/leave_body_button/MouseEntered(location, control, params)
+ if (hovered)
+ return
+
+ hovered = TRUE
+
+ // The UX on this is pretty shit, but it's okay enough for now.
+ // Regularly goes way too far from your cursor. Not designed for large icons.
+ openToolTip(usr, src, params, content = tooltip_text)
+
+/atom/movable/screen/escape_menu/leave_body_button/MouseExited(location, control, params)
+ if (!hovered)
+ return
+
+ hovered = FALSE
+ closeToolTip(usr)
diff --git a/code/modules/escape_menu/readme.md b/code/modules/escape_menu/readme.md
new file mode 100644
index 00000000000..c10aab5535a
--- /dev/null
+++ b/code/modules/escape_menu/readme.md
@@ -0,0 +1,2 @@
+Skyrat Edit:
+leave_body.dm - removed suicide menu option
diff --git a/code/modules/escape_menu/subsystem.dm b/code/modules/escape_menu/subsystem.dm
new file mode 100644
index 00000000000..f75726514ec
--- /dev/null
+++ b/code/modules/escape_menu/subsystem.dm
@@ -0,0 +1,6 @@
+/// Subsystem for controlling anything related to the escape menu
+PROCESSING_SUBSYSTEM_DEF(escape_menu)
+ name = "Escape Menu"
+ flags = SS_NO_INIT
+ runlevels = ALL
+ wait = 2 SECONDS
diff --git a/code/modules/escape_menu/title.dm b/code/modules/escape_menu/title.dm
new file mode 100644
index 00000000000..1a82967a6eb
--- /dev/null
+++ b/code/modules/escape_menu/title.dm
@@ -0,0 +1,43 @@
+// This doesn't instantiate right away, since we rely on other GLOBs
+GLOBAL_DATUM(escape_menu_title, /atom/movable/screen/escape_menu/title)
+
+/// Provides a singleton for the escape menu details screen.
+/proc/give_escape_menu_title()
+ if (isnull(GLOB.escape_menu_title))
+ GLOB.escape_menu_title = new
+
+ return GLOB.escape_menu_title
+
+/atom/movable/screen/escape_menu/title
+ screen_loc = "NORTH:-100,WEST:32"
+ maptext_height = 100
+ maptext_width = 500
+
+/atom/movable/screen/escape_menu/title/Initialize(mapload)
+ . = ..()
+
+ update_text()
+
+ RegisterSignal(SSdcs, COMSIG_GLOB_STATION_NAME_CHANGED, PROC_REF(on_station_name_changed))
+
+/atom/movable/screen/escape_menu/title/Destroy()
+ if (GLOB.escape_menu_title == src)
+ stack_trace("Something tried to delete the escape menu details screen")
+ return QDEL_HINT_LETMELIVE
+
+ return ..()
+
+/atom/movable/screen/escape_menu/title/proc/update_text()
+ var/subtitle_text = MAPTEXT("Another day on...")
+ var/title_text = {"
+
+ [station_name()]
+
+ "}
+
+ maptext = "" + subtitle_text + MAPTEXT_VCR_OSD_MONO(title_text) + ""
+
+/atom/movable/screen/escape_menu/title/proc/on_station_name_changed()
+ SIGNAL_HANDLER
+
+ update_text()
diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm
index bb2757761e0..ef87e12d901 100644
--- a/code/modules/keybindings/setup.dm
+++ b/code/modules/keybindings/setup.dm
@@ -39,3 +39,12 @@
window.set_mouse_macro()
update_special_keybinds()
+
+/// Manually clears any held keys, in case due to lag or other undefined behavior a key gets stuck.
+/client/proc/reset_held_keys()
+ for(var/key in keys_held)
+ keyUp(key)
+
+ //In case one got stuck and the previous loop didn't clean it, somehow.
+ for(var/key in key_combos_held)
+ keyUp(key_combos_held[key])
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 54108e0c2db..36c985963e4 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -883,7 +883,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
cleanup_observe()
if(..())
if(hud_used)
- client.screen = list()
+ client.clear_screen()
hud_used.show_hud(hud_used.hud_version)
@@ -939,7 +939,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
set_sight(null) //we dont want ghosts to see through walls in secret areas
RegisterSignal(mob_eye, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_observing_z_changed))
if(mob_eye.hud_used)
- client.screen = list()
+ client.clear_screen()
LAZYOR(mob_eye.observers, src)
mob_eye.hud_used.show_hud(mob_eye.hud_used.hud_version, src)
observetarget = mob_eye
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index a40c29459bc..cda4e179fcc 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -35,7 +35,7 @@
computer_id = client.computer_id
log_access("Mob Login: [key_name(src)] was assigned to a [type]")
world.update_status()
- client.screen = list() //remove hud items just in case
+ client.clear_screen() //remove hud items just in case
client.images = list()
client.set_right_click_menu_mode(shift_to_open_context_menu)
@@ -112,6 +112,7 @@
log_message("Client [key_name(src)] has taken ownership of mob [src]([src.type])", LOG_OWNERSHIP)
log_mob_tag("NEW OWNER: [key_name(src)]")
SEND_SIGNAL(src, COMSIG_MOB_CLIENT_LOGIN, client)
+ SEND_SIGNAL(client, COMSIG_CLIENT_MOB_LOGIN, src)
client.init_verbs()
AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm
index c551a130a7f..a2fbd42ec84 100644
--- a/code/modules/tooltip/tooltip.dm
+++ b/code/modules/tooltip/tooltip.dm
@@ -1,16 +1,4 @@
/*
-Tooltips v1.1 - 22/10/15
-Developed by Wire (#goonstation on irc.synirc.net)
-- Added support for screen_loc pixel offsets. Should work. Maybe.
-- Added init function to more efficiently send base vars
-
-Configuration:
-- Set control to the correct skin element (remember to actually place the skin element)
-- Set file to the correct path for the .html file (remember to actually place the html file)
-- Attach the datum to the user client on login, e.g.
- /client/New()
- src.tooltips = new /datum/tooltip(src)
-
Usage:
- Define mouse event procs on your (probably HUD) object and simply call the show and hide procs respectively:
/atom/movable/screen/hud
@@ -26,8 +14,6 @@ Customization:
Notes:
- You may have noticed 90% of the work is done via javascript on the client. Gotta save those cycles man.
-- This is entirely untested in any other codebase besides goonstation so I have no idea if it will port nicely. Good luck!
- - After testing and discussion (Wire, Remie, MrPerson, AnturK) ToolTips are ok and work for /tg/station13
*/
@@ -37,6 +23,7 @@ Notes:
var/showing = 0
var/queueHide = 0
var/init = 0
+ var/atom/last_target
/datum/tooltip/New(client/C)
@@ -52,6 +39,14 @@ Notes:
/datum/tooltip/proc/show(atom/movable/thing, params = null, title = null, content = null, theme = "default", special = "none")
if (!thing || !params || (!title && !content) || !owner || !isnum(world.icon_size))
return FALSE
+
+ if (!isnull(last_target))
+ UnregisterSignal(last_target, COMSIG_PARENT_QDELETING)
+
+ RegisterSignal(thing, COMSIG_PARENT_QDELETING, PROC_REF(on_target_qdel))
+
+ last_target = thing
+
if (!init)
//Initialize some vars
init = 1
@@ -96,11 +91,18 @@ Notes:
return TRUE
+/datum/tooltip/proc/on_target_qdel()
+ SIGNAL_HANDLER
+
+ INVOKE_ASYNC(src, PROC_REF(hide))
+ last_target = null
+
/datum/tooltip/proc/do_hide()
winshow(owner, control, FALSE)
-/* TG SPECIFIC CODE */
-
+/datum/tooltip/Destroy(force, ...)
+ last_target = null
+ return ..()
//Open a tooltip for user, at a location based on params
//Theme is a CSS class in tooltip.html, by default this wrapper chooses a CSS class based on the user's UI_style (Midnight, Plasmafire, Retro, etc)
diff --git a/code/modules/tutorials/tutorial_instruction.dm b/code/modules/tutorials/tutorial_instruction.dm
index 488b3558eef..7d8bda0fab7 100644
--- a/code/modules/tutorials/tutorial_instruction.dm
+++ b/code/modules/tutorials/tutorial_instruction.dm
@@ -46,8 +46,7 @@
change_message(message)
/atom/movable/screen/tutorial_instruction_text/proc/change_message(message)
- // We don't use MAPTEXT macro here because it doesn't handle big text
- message = "[message]"
+ message = MAPTEXT_VCR_OSD_MONO("[message]")
animate(src, alpha = 0, time = (maptext ? 0.5 SECONDS : 0), easing = SINE_EASING)
animate(alpha = 255, time = 0.5 SECONDS, maptext = message)
diff --git a/code/modules/unit_tests/create_and_destroy.dm b/code/modules/unit_tests/create_and_destroy.dm
index ba21ece1315..76a489b1de6 100644
--- a/code/modules/unit_tests/create_and_destroy.dm
+++ b/code/modules/unit_tests/create_and_destroy.dm
@@ -111,6 +111,8 @@ GLOBAL_VAR_INIT(running_create_and_destroy, FALSE)
ignore += typesof(/obj/structure/industrial_lift)
// Runtimes if the associated machinery does not exist, but not the base type
ignore += subtypesof(/obj/machinery/airlock_controller)
+ // Always ought to have an associated escape menu. Any references it could possibly hold would need one regardless.
+ ignore += subtypesof(/atom/movable/screen/escape_menu)
var/list/cached_contents = spawn_at.contents.Copy()
var/original_turf_type = spawn_at.type
diff --git a/icons/effects/96x96.dmi b/icons/effects/96x96.dmi
index e803dbcb259..7256ed50185 100644
Binary files a/icons/effects/96x96.dmi and b/icons/effects/96x96.dmi differ
diff --git a/icons/hud/escape_menu_leave_body.dmi b/icons/hud/escape_menu_leave_body.dmi
new file mode 100644
index 00000000000..2923e1376ac
Binary files /dev/null and b/icons/hud/escape_menu_leave_body.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 26577a70d66..4ae248786a1 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -566,6 +566,7 @@
#include "code\_onclick\hud\radial_persistent.dm"
#include "code\_onclick\hud\revenanthud.dm"
#include "code\_onclick\hud\robot.dm"
+#include "code\_onclick\hud\screen_object_holder.dm"
#include "code\_onclick\hud\screen_objects.dm"
#include "code\_onclick\hud\screentip.dm"
#include "code\_onclick\hud\rendering\plane_master.dm"
@@ -3031,7 +3032,6 @@
#include "code\modules\client\preferences\species_features\vampire.dm"
#include "code\modules\client\verbs\ooc.dm"
#include "code\modules\client\verbs\ping.dm"
-#include "code\modules\client\verbs\reset_held_keys.dm"
#include "code\modules\client\verbs\suicide.dm"
#include "code\modules\client\verbs\who.dm"
#include "code\modules\clothing\chameleon.dm"
@@ -3187,6 +3187,13 @@
#include "code\modules\emoji\emoji_parse.dm"
#include "code\modules\error_handler\error_handler.dm"
#include "code\modules\error_handler\error_viewer.dm"
+#include "code\modules\escape_menu\details.dm"
+#include "code\modules\escape_menu\dimmer.dm"
+#include "code\modules\escape_menu\escape_menu.dm"
+#include "code\modules\escape_menu\home_page.dm"
+#include "code\modules\escape_menu\leave_body.dm"
+#include "code\modules\escape_menu\subsystem.dm"
+#include "code\modules\escape_menu\title.dm"
#include "code\modules\events\_event.dm"
#include "code\modules\events\aurora_caelus.dm"
#include "code\modules\events\brain_trauma.dm"