Escape menu improvements (#91397)

## About The Pull Request

Removes the blur of the escape menu so players can sorta see what's
going on in their game while going through, so they can react in case
something's happening while they're in menu.
Changes the font of the escape menu so it's no longer the LISA font
Makes the title only show up on the home menu, so being on the suicide
panel wont show you "another day on space station 13".
Moves the 'back' button on the suicide menu to the top left.


![image](https://github.com/user-attachments/assets/4d527059-38ab-4330-9e8c-f373e0942777)

![image](https://github.com/user-attachments/assets/23f0f230-f901-480d-9426-9ca8f33493f9)

## Why It's Good For The Game

This hopes to make the escape menu a little more easy to go through,
removing some obstruction of your game's screen so you can see what's
happening around you, and give a little generic part of submenus in case
we add more (which I do have at least one planned).

## Changelog

🆑
qol: The escape menu no longer blurs the game in the background.
qol: The escape menu now has a more readable font, and the escape menu's
back button is now smaller and in the corner.
/🆑
This commit is contained in:
John Willard
2025-05-31 19:12:05 -07:00
committed by GitHub
parent 620f1f9331
commit 7d7f284d82
12 changed files with 285 additions and 283 deletions
+8 -4
View File
@@ -27,7 +27,8 @@
client?.screen += screen_object
return screen_object
/// Gives the screen object to the client, but does not qdel it when it's cleared
/// Gives the screen object to the client, but does not qdel it when it's cleared,
/// this is used for screen object instances you plan on giving to multiple mobs.
/datum/screen_object_holder/proc/give_protected_screen_object(atom/screen_object)
ASSERT(istype(screen_object))
@@ -39,10 +40,13 @@
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
qdel(screen_object)
screen_objects -= screen_object
//protected objects don't get qdel'ed
if(screen_object in protected_screen_objects)
protected_screen_objects -= screen_object
else
qdel(screen_object)
/datum/screen_object_holder/proc/clear()
client?.screen -= screen_objects
+1 -1
View File
@@ -167,7 +167,7 @@ ADMIN_VERB(reset_ooc_color, R_FUN, "Reset Player OOC Color", "Returns player OOC
/client/verb/admin_notice()
set name = "Adminnotice"
set category = "Admin"
set desc ="Check the admin notice if it has been set"
set desc = "Check the admin notice if it has been set"
if(GLOB.admin_notice)
to_chat(src, "[span_boldnotice("Admin Notice:")]\n \t [GLOB.admin_notice]")
+5 -15
View File
@@ -25,6 +25,8 @@ GLOBAL_LIST_EMPTY(escape_menus)
VAR_PRIVATE
ckey
static/atom/movable/screen/fullscreen/dimmer/dim_screen
datum/screen_object_holder/base_holder
datum/screen_object_holder/page_holder
@@ -41,6 +43,8 @@ GLOBAL_LIST_EMPTY(escape_menus)
src.client = client
base_holder = new(client)
if(isnull(dim_screen))
dim_screen = new()
populate_base_ui()
page_holder = new(client)
@@ -61,7 +65,6 @@ GLOBAL_LIST_EMPTY(escape_menus)
QDEL_NULL(page_holder)
GLOB.escape_menus -= ckey
plane_master_controller.remove_filter("escape_menu_blur")
var/sound/esc_clear = sound(null, repeat = FALSE, channel = CHANNEL_ESCAPEMENU) //yes, I'm doing it like this with a null, no its absolutely intentional, cuts off the sound right as needed.
SEND_SOUND(client, esc_clear)
@@ -98,10 +101,7 @@ GLOBAL_LIST_EMPTY(escape_menus)
/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(dim_screen)
base_holder.give_protected_screen_object(give_escape_menu_details())
/datum/escape_menu/proc/open_home_page()
@@ -116,16 +116,6 @@ GLOBAL_LIST_EMPTY(escape_menus)
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
+1 -242
View File
@@ -1,4 +1,5 @@
/datum/escape_menu/proc/show_home_page()
page_holder.give_protected_screen_object(give_escape_menu_title())
page_holder.give_screen_object(
new /atom/movable/screen/escape_menu/home_button(
null,
@@ -178,245 +179,3 @@
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/hud/hud_owner,
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,
/* hud_owner = */ 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
on_click_callback = null
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, datum/hud/hud_owner, 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("<span style='font-size: 24px; color: [istype(escape_menu_loc) ? escape_menu_loc.text_color() : "white"]'>[button_text]</span>")
if (hovered)
maptext = "<u>[maptext]</u>"
/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/hud/hud_owner,
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(seconds_per_tick)
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/hud/hud_owner,
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()
+14 -14
View File
@@ -1,6 +1,18 @@
/datum/escape_menu/proc/show_leave_body_page()
PRIVATE_PROC(TRUE)
page_holder.give_screen_object(
new /atom/movable/screen/escape_menu/lobby_button/small(
null,
/* hud_owner = */ null,
"Back",
/* tooltip_text = */ null,
/* pixel_offset = */ list(-260, 190),
CALLBACK(src, PROC_REF(open_home_page)),
/* button_overlay = */ "back",
)
)
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
@@ -13,7 +25,7 @@
/* hud_owner = */ null,
"Suicide",
"Perform a dramatic suicide in game",
/* pixel_offset = */ list(-105, -1),
/* pixel_offset = */ list(-55, -1),
CALLBACK(src, PROC_REF(leave_suicide)),
/* button_overlay = */ dead_clown,
))
@@ -24,24 +36,12 @@
/* hud_owner = */ null,
"Ghost",
"Exit quietly, leaving your body",
/* pixel_offset = */ list(0, -1),
/* pixel_offset = */ list(55, -1),
CALLBACK(src, PROC_REF(leave_ghost)),
/* button_overlay = */ "ghost",
)
)
page_holder.give_screen_object(
new /atom/movable/screen/escape_menu/lobby_button(
null,
/* hud_owner = */ null,
"Back",
/* tooltip_text = */ null,
/* pixel_offset = */ list(105, -1),
CALLBACK(src, PROC_REF(open_home_page)),
/* button_overlay = */ "back",
)
)
/datum/escape_menu/proc/create_dead_clown()
PRIVATE_PROC(TRUE)
@@ -0,0 +1,129 @@
/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/hud/hud_owner,
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(seconds_per_tick)
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)
@@ -0,0 +1,87 @@
/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/hud/hud_owner,
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,
/* hud_owner = */ 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
on_click_callback = null
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 = 300
maptext_height = 50
pixel_x = -80
VAR_PRIVATE
button_text
hovered = FALSE
/atom/movable/screen/escape_menu/home_button_text/Initialize(mapload, datum/hud/hud_owner, 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_PIXELLARI("<span style='font-size: 24px; color: [istype(escape_menu_loc) ? escape_menu_loc.text_color() : "white"]'>[button_text]</span>")
if (hovered)
maptext = "<u>[maptext]</u>"
@@ -0,0 +1,24 @@
/atom/movable/screen/escape_menu/home_button/leave_body
/atom/movable/screen/escape_menu/home_button/leave_body/Initialize(
mapload,
datum/hud/hud_owner,
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()
@@ -56,25 +56,31 @@
closeToolTip(usr)
/atom/movable/screen/escape_menu/lobby_button/proc/add_maptext(button_text)
SHOULD_CALL_PARENT(TRUE)
animate(src,
maptext = MAPTEXT_VCR_OSD_MONO("<b style='font-size: [font_size]px; text-align: center'>[button_text]</b>"),
maptext = MAPTEXT_PIXELLARI("<b style='font-size: [font_size]px; text-align: center'>[button_text]</b>"),
flags = ANIMATION_CONTINUE,
)
/atom/movable/screen/escape_menu/lobby_button/small
icon = 'icons/hud/escape_menu_icons.dmi'
font_size = 9
font_size = 6
maptext_width = 80
maptext_x = -20
maptext_y = -14
/atom/movable/screen/escape_menu/lobby_button/small/add_maptext(button_text)
//overriding parent for a different font here.
animate(src,
maptext = MAPTEXT_GRAND9K("<b style='font-size: [font_size]px; text-align: center'>[button_text]</b>"),
flags = ANIMATION_CONTINUE,
)
///Amount of time between animations when we fade in and out.
#define COLLAPSIBLE_BUTTON_DURATION (0.4 SECONDS)
/atom/movable/screen/escape_menu/lobby_button/small/collapsible
maptext_width = 48
maptext_x = -5
maptext_x = -4
maptext_y = -44 //we change this during animation to bring it up
layer = parent_type::layer - 0.01
@@ -97,7 +103,7 @@
/atom/movable/screen/escape_menu/lobby_button/small/collapsible/add_maptext(button_text)
//more than 6 characters, lets bump the maptext down a bit, because we're smaller buttons we would be overlaying over the icon itself otherwise.
if(length(button_text) > 6)
maptext_y -= 12
maptext_y -= 10
//let's take the icons out
animate(src,
transform = transform.Translate(x = end_point, y = 0),
+1 -1
View File
@@ -35,7 +35,7 @@ GLOBAL_DATUM(escape_menu_title, /atom/movable/screen/escape_menu/title)
</span>
"}
maptext = "<font align='top'>" + subtitle_text + MAPTEXT_VCR_OSD_MONO(title_text) + "</font>"
maptext = "<font align='top'>" + subtitle_text + MAPTEXT_PIXELLARI(title_text) + "</font>"
/atom/movable/screen/escape_menu/title/proc/on_station_name_changed()
SIGNAL_HANDLER
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+4 -1
View File
@@ -4157,9 +4157,12 @@
#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\lobby_buttons.dm"
#include "code\modules\escape_menu\subsystem.dm"
#include "code\modules\escape_menu\title.dm"
#include "code\modules\escape_menu\screen_objects\admin_text.dm"
#include "code\modules\escape_menu\screen_objects\home_text.dm"
#include "code\modules\escape_menu\screen_objects\leave_body_text.dm"
#include "code\modules\escape_menu\screen_objects\lobby_buttons.dm"
#include "code\modules\events\_event.dm"
#include "code\modules\events\_event_admin_setup.dm"
#include "code\modules\events\aurora_caelus.dm"