mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 09:42:29 +00:00
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may not be viewable. --> <!-- You can view Contributing.MD for a detailed description of the pull request process. --> ## About The Pull Request So I was running into the bit where custom emotes actually don't get the `+|_` emphasis formatting applied to them, _except_ for the runechat portion which *does*. This felt annoying, especially given I've seen a lot of people try it and have it not work. Add to that that your own emotes would keep getting highlighted, blotting out other people mentioning your highlighted messages, and here's this pr. In this pr we add a few flags to audible/visible messages, `WITH_EMPHASIS_MESSAGE` and `BLOCK_SELF_HIGHLIGHT_MESSAGE`, which respectively apply emphasis formatting and block highlighting the message to oneself. We're doing this with flags because I felt always applying this would be unnecessary. Most audible/visible messages won't need to check for formatting, and quite a lot we *do* want to be highlighted. As such, we apply these flags as need be. For emotes we do this by having `get_message_flags(intentional)`, which applies `BLOCK_SELF_HIGHLIGHT_MESSAGE` based on whether the message is intentional, and on the custom emote subtype applies `WITH_EMPHASIS_MESSAGE`. Because it's not just for _say_ anymore, and already was also used for emote runechats, we rename `say_emphasis(input)` into `apply_message_emphasis(input)`. We additionally move it down to `/atom` from `/atom/movable`, such that visible/audible messages can in fact call it. That resolves our issues. We also apply `BLOCK_SELF_HIGHLIGHT_MESSAGE` to sign language tone messages, as they're essentially a part of speech. <!-- Describe The Pull Request. Please be sure every change is documented or this can delay review and even discourage maintainers from merging your PR! --> ## Why It's Good For The Game Being able to do `+|_` emphasis formatting on your emotes is nice, I've seen a lot of people try it and have it not work. Especially weird given it DOES apply to the runechat message, just not the text chat message. It's annoying when your own emotes trip your own highlights! Like if you have a name highlight, your own emotes getting constantly highlighted would blot out other people talking to you. So having your own emotes not trip it just like your own talking makes that less of a pain. But sometimes emotes are forced, and in that case I think it's better to keep the highlight because it's just like other people's messages information the player might want to be notified of. Generally, I think if it's the player's input it probably shouldn't be highlighted, while if it isn't the player's input it probably should. There's no need for us to ever highlight our own sign language tone messages, because they're essentially a part of our talking. <!-- Argue for the merits of your changes and how they benefit the game, especially if they are controversial and/or far reaching. If you can't actually explain WHY what you are doing will improve the game, then it probably isn't good for the game in the first place. --> ## Changelog <!-- If your PR modifies aspects of the game that can be concretely observed by players or admins you should add a changelog. If your change does NOT meet this description, remove this section. Be sure to properly mark your PRs to prevent unnecessary GBP loss. You can read up on GBP and its effects on PRs in the tgstation guides for contributors. Please note that maintainers freely reserve the right to remove and add tags should they deem it appropriate. You can attempt to finagle the system all you want, but it's best to shoot for clear communication right off the bat. --> 🆑 add: When performing a custom emote, `+|_` emphasis formatting applies to the text chat message instead of just the runechat message. qol: Intentional emotes don't trip your own highlights. qol: Sign language tone messages don't trip your own highlights. /🆑 <!-- Both 🆑's are required for the changelog to work! You can put your name to the right of the first 🆑 if you want to overwrite your GitHub username as author ingame. --> <!-- You can use multiple of the same prefix (they're only used for the icon ingame) and delete the unneeded ones. Despite some of the tags, changelogs should generally represent how a player might be affected by the changes rather than a summary of the PR's contents. -->
357 lines
15 KiB
Plaintext
357 lines
15 KiB
Plaintext
/// How long the chat message's spawn-in animation will occur for
|
|
#define CHAT_MESSAGE_SPAWN_TIME (0.2 SECONDS)
|
|
/// How long the chat message will exist prior to any exponential decay
|
|
#define CHAT_MESSAGE_LIFESPAN (5 SECONDS)
|
|
/// How long the chat message's end of life fading animation will occur for
|
|
#define CHAT_MESSAGE_EOL_FADE (0.7 SECONDS)
|
|
/// Grace period for fade before we actually delete the chat message
|
|
#define CHAT_MESSAGE_GRACE_PERIOD (0.2 SECONDS)
|
|
/// Factor of how much the message index (number of messages) will account to exponential decay
|
|
#define CHAT_MESSAGE_EXP_DECAY 0.7
|
|
/// Factor of how much height will account to exponential decay
|
|
#define CHAT_MESSAGE_HEIGHT_DECAY 0.9
|
|
/// Approximate height in pixels of an 'average' line, used for height decay
|
|
#define CHAT_MESSAGE_APPROX_LHEIGHT 11
|
|
/// Max width of chat message in pixels
|
|
#define CHAT_MESSAGE_WIDTH 112
|
|
/// The dimensions of the chat message icons
|
|
#define CHAT_MESSAGE_ICON_SIZE 9
|
|
|
|
///Base layer of chat elements
|
|
#define CHAT_LAYER 1
|
|
///Highest possible layer of chat elements
|
|
#define CHAT_LAYER_MAX 2
|
|
/// Maximum precision of float before rounding errors occur (in this context)
|
|
#define CHAT_LAYER_Z_STEP 0.0001
|
|
/// The number of z-layer 'slices' usable by the chat message layering
|
|
#define CHAT_LAYER_MAX_Z (CHAT_LAYER_MAX - CHAT_LAYER) / CHAT_LAYER_Z_STEP
|
|
|
|
/**
|
|
* # Chat Message Overlay
|
|
*
|
|
* Datum for generating a message overlay on the map
|
|
*/
|
|
/datum/chatmessage
|
|
/// The visual element of the chat message
|
|
var/image/message
|
|
/// The location in which the message is appearing
|
|
var/atom/message_loc
|
|
/// The client who heard this message
|
|
var/client/owned_by
|
|
/// Contains the scheduled destruction time, used for scheduling EOL
|
|
var/scheduled_destruction
|
|
/// Contains the time that the EOL for the message will be complete, used for qdel scheduling
|
|
var/eol_complete
|
|
/// Contains the approximate amount of lines for height decay
|
|
var/approx_lines
|
|
/// The current index used for adjusting the layer of each sequential chat message such that recent messages will overlay older ones
|
|
var/static/current_z_idx = 0
|
|
/// When we started animating the message
|
|
var/animate_start = 0
|
|
/// Our animation lifespan, how long this message will last
|
|
var/animate_lifespan = 0
|
|
/// Callback to finish_image_generation passed to SSrunechat
|
|
var/datum/callback/finish_callback
|
|
|
|
/**
|
|
* Constructs a chat message overlay
|
|
*
|
|
* Arguments:
|
|
* * text - The text content of the overlay
|
|
* * target - The target atom to display the overlay at
|
|
* * owner - The mob that owns this overlay, only this mob will be able to view it
|
|
* * language - The language this message was spoken in
|
|
* * extra_classes - Extra classes to apply to the span that holds the text
|
|
* * lifespan - The lifespan of the message in deciseconds
|
|
*/
|
|
/datum/chatmessage/New(text, atom/target, mob/owner, datum/language/language, list/extra_classes = list(), lifespan = CHAT_MESSAGE_LIFESPAN)
|
|
. = ..()
|
|
if (!istype(target))
|
|
CRASH("Invalid target given for chatmessage")
|
|
if(QDELETED(owner) || !istype(owner) || !owner.client)
|
|
stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner")
|
|
qdel(src)
|
|
return
|
|
INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, language, extra_classes, lifespan)
|
|
|
|
/datum/chatmessage/Destroy()
|
|
if (!QDELING(owned_by))
|
|
if(REALTIMEOFDAY < animate_start + animate_lifespan)
|
|
stack_trace("Del'd before we finished fading, with [(animate_start + animate_lifespan) - REALTIMEOFDAY] time left")
|
|
|
|
if (owned_by.seen_messages)
|
|
LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src)
|
|
owned_by.images.Remove(message)
|
|
|
|
if (finish_callback)
|
|
SSrunechat.message_queue -= finish_callback
|
|
finish_callback = null
|
|
|
|
owned_by = null
|
|
message_loc = null
|
|
message = null
|
|
return ..()
|
|
|
|
/**
|
|
* Calls qdel on the chatmessage when its parent is deleted, used to register qdel signal
|
|
*/
|
|
/datum/chatmessage/proc/on_parent_qdel()
|
|
SIGNAL_HANDLER
|
|
qdel(src)
|
|
|
|
/**
|
|
* Generates a chat message image representation
|
|
*
|
|
* Arguments:
|
|
* * text - The text content of the overlay
|
|
* * target - The target atom to display the overlay at
|
|
* * owner - The mob that owns this overlay, only this mob will be able to view it
|
|
* * language - The language this message was spoken in
|
|
* * extra_classes - Extra classes to apply to the span that holds the text
|
|
* * lifespan - The lifespan of the message in deciseconds
|
|
*/
|
|
/datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, datum/language/language, list/extra_classes, lifespan)
|
|
/// Cached icons to show what language the user is speaking
|
|
var/static/list/language_icons
|
|
|
|
// Register client who owns this message
|
|
owned_by = owner.client
|
|
RegisterSignal(owned_by, COMSIG_QDELETING, PROC_REF(on_parent_qdel))
|
|
|
|
// Remove spans in the message from things like the recorder
|
|
var/static/regex/span_check = new(@"<\/?span[^>]*>", "gi")
|
|
text = replacetext(text, span_check, "")
|
|
|
|
// Clip message
|
|
var/maxlen = owned_by.prefs.read_preference(/datum/preference/numeric/max_chat_length)
|
|
if (length_char(text) > maxlen)
|
|
text = copytext_char(text, 1, maxlen + 1) + "..." // BYOND index moment
|
|
|
|
// Get rid of any URL schemes that might cause BYOND to automatically wrap something in an anchor tag
|
|
var/static/regex/url_scheme = new(@"[A-Za-z][A-Za-z0-9+-\.]*:\/\/", "g")
|
|
text = replacetext(text, url_scheme, "")
|
|
|
|
// Reject whitespace
|
|
var/static/regex/whitespace = new(@"^\s*$")
|
|
if (whitespace.Find(text))
|
|
qdel(src)
|
|
return
|
|
|
|
// Non mobs speakers can be small
|
|
if (!ismob(target))
|
|
extra_classes |= "small"
|
|
|
|
// Why are you yelling?
|
|
if(copytext_char(text, -2) == "!!")
|
|
extra_classes |= SPAN_YELL
|
|
|
|
var/list/prefixes
|
|
var/chat_color_name_to_use
|
|
|
|
// Append radio icon if from a virtual speaker
|
|
if (extra_classes.Find("virtual-speaker"))
|
|
var/image/r_icon = image('icons/ui/chat/chat_icons.dmi', icon_state = "radio")
|
|
LAZYADD(prefixes, "\icon[r_icon]")
|
|
else if (extra_classes.Find("emote"))
|
|
var/image/r_icon = image('icons/ui/chat/chat_icons.dmi', icon_state = "emote")
|
|
LAZYADD(prefixes, "\icon[r_icon]")
|
|
chat_color_name_to_use = target.get_visible_name(add_id_name = FALSE) // use face name for nonverbal messages
|
|
|
|
if(isnull(chat_color_name_to_use))
|
|
if(HAS_TRAIT(target, TRAIT_SIGN_LANG))
|
|
chat_color_name_to_use = target.get_visible_name(add_id_name = FALSE) // use face name for signers too
|
|
else
|
|
chat_color_name_to_use = target.GetVoice() // for everything else, use the target's voice name
|
|
|
|
// Calculate target color if not already present
|
|
if (!target.chat_color || target.chat_color_name != chat_color_name_to_use)
|
|
target.chat_color = colorize_string(chat_color_name_to_use)
|
|
target.chat_color_darkened = colorize_string(chat_color_name_to_use, 0.85, 0.85)
|
|
target.chat_color_name = chat_color_name_to_use
|
|
|
|
// Append language icon if the language uses one
|
|
var/datum/language/language_instance = GLOB.language_datum_instances[language]
|
|
if (language_instance?.display_icon(owner))
|
|
var/icon/language_icon = LAZYACCESS(language_icons, language)
|
|
if (isnull(language_icon))
|
|
language_icon = icon(language_instance.icon, icon_state = language_instance.icon_state)
|
|
language_icon.Scale(CHAT_MESSAGE_ICON_SIZE, CHAT_MESSAGE_ICON_SIZE)
|
|
LAZYSET(language_icons, language, language_icon)
|
|
LAZYADD(prefixes, "\icon[language_icon]")
|
|
|
|
text = "[prefixes?.Join(" ")][text]"
|
|
|
|
// We dim italicized text to make it more distinguishable from regular text
|
|
var/tgt_color = extra_classes.Find("italics") ? target.chat_color_darkened : target.chat_color
|
|
|
|
// Approximate text height
|
|
var/complete_text = "<span style='color: [tgt_color]'><span class='center [extra_classes.Join(" ")]'>[owner.apply_message_emphasis(text)]</span></span>"
|
|
|
|
var/mheight
|
|
WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH), mheight)
|
|
|
|
|
|
if(!VERB_SHOULD_YIELD)
|
|
return finish_image_generation(mheight, target, owner, complete_text, lifespan)
|
|
|
|
finish_callback = CALLBACK(src, PROC_REF(finish_image_generation), mheight, target, owner, complete_text, lifespan)
|
|
SSrunechat.message_queue += finish_callback
|
|
return
|
|
|
|
///finishes the image generation after the MeasureText() call in generate_image().
|
|
///necessary because after that call the proc can resume at the end of the tick and cause overtime.
|
|
/datum/chatmessage/proc/finish_image_generation(mheight, atom/target, mob/owner, complete_text, lifespan)
|
|
finish_callback = null
|
|
var/rough_time = REALTIMEOFDAY
|
|
approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
|
|
var/starting_height = target.maptext_height
|
|
// Translate any existing messages upwards, apply exponential decay factors to timers
|
|
message_loc = isturf(target) ? target : get_atom_on_turf(target)
|
|
if (owned_by.seen_messages)
|
|
var/idx = 1
|
|
var/combined_height = approx_lines
|
|
for(var/datum/chatmessage/m as anything in owned_by.seen_messages[message_loc])
|
|
combined_height += m.approx_lines
|
|
|
|
var/time_spent = rough_time - m.animate_start
|
|
var/time_before_fade = m.animate_lifespan - CHAT_MESSAGE_EOL_FADE
|
|
|
|
// When choosing to update the remaining time we have to be careful not to update the
|
|
// scheduled time once the EOL has been executed.
|
|
var/continuing = 0
|
|
if (time_spent >= time_before_fade)
|
|
if(m.message.pixel_z < starting_height)
|
|
var/max_height = m.message.pixel_z + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height
|
|
if(max_height > 0)
|
|
animate(m.message, pixel_z = m.message.pixel_z + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = continuing | ANIMATION_PARALLEL)
|
|
continuing |= ANIMATION_CONTINUE
|
|
else if(mheight + starting_height >= m.message.pixel_z)
|
|
animate(m.message, pixel_z = m.message.pixel_z + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = continuing | ANIMATION_PARALLEL)
|
|
continuing |= ANIMATION_CONTINUE
|
|
continue
|
|
|
|
var/remaining_time = time_before_fade * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
|
|
// Ensure we don't accidentially spike alpha up or something silly like that
|
|
m.message.alpha = m.get_current_alpha(time_spent)
|
|
if(remaining_time > 0)
|
|
if(time_spent < CHAT_MESSAGE_SPAWN_TIME)
|
|
// We haven't even had the time to fade in yet!
|
|
animate(m.message, alpha = 255, CHAT_MESSAGE_SPAWN_TIME - time_spent, flags=continuing)
|
|
continuing |= ANIMATION_CONTINUE
|
|
// Stay faded in for a while, then
|
|
animate(m.message, alpha = 255, time = remaining_time, flags=continuing)
|
|
continuing |= ANIMATION_CONTINUE
|
|
// Fade out
|
|
animate(m.message, alpha = 0, time = CHAT_MESSAGE_EOL_FADE, flags=continuing)
|
|
continuing |= ANIMATION_CONTINUE
|
|
m.animate_lifespan = remaining_time + CHAT_MESSAGE_EOL_FADE
|
|
else
|
|
// Your time has come my son
|
|
animate(m.message, alpha = 0, time = CHAT_MESSAGE_EOL_FADE, flags=continuing)
|
|
continuing |= ANIMATION_CONTINUE
|
|
// We run this after the alpha animate, because we don't want to interrup it, but also don't want to block it by running first
|
|
// Sooo instead we do this. bit messy but it fuckin works
|
|
if(m.message.pixel_z < starting_height)
|
|
var/max_height = m.message.pixel_z + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height
|
|
if(max_height > 0)
|
|
animate(m.message, pixel_z = m.message.pixel_z + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = continuing | ANIMATION_PARALLEL)
|
|
continuing |= ANIMATION_CONTINUE
|
|
else if(mheight + starting_height >= m.message.pixel_z)
|
|
animate(m.message, pixel_z = m.message.pixel_z + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = continuing | ANIMATION_PARALLEL)
|
|
continuing |= ANIMATION_CONTINUE
|
|
|
|
// Reset z index if relevant
|
|
if (current_z_idx >= CHAT_LAYER_MAX_Z)
|
|
current_z_idx = 0
|
|
|
|
// Build message image
|
|
message = image(loc = message_loc, layer = CHAT_LAYER + CHAT_LAYER_Z_STEP * current_z_idx++)
|
|
SET_PLANE_EXPLICIT(message, RUNECHAT_PLANE, message_loc)
|
|
message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
|
|
message.alpha = 0
|
|
message.pixel_z = starting_height
|
|
message.pixel_w = -target.base_pixel_w
|
|
message.maptext_width = CHAT_MESSAGE_WIDTH
|
|
message.maptext_height = mheight * 1.25 // We add extra because some characters are superscript, like actions
|
|
message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5
|
|
message.maptext = MAPTEXT(complete_text)
|
|
|
|
animate_start = rough_time
|
|
animate_lifespan = lifespan
|
|
|
|
// View the message
|
|
LAZYADDASSOCLIST(owned_by.seen_messages, message_loc, src)
|
|
owned_by.images |= message
|
|
|
|
// Fade in
|
|
animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME)
|
|
var/time_before_fade = lifespan - CHAT_MESSAGE_SPAWN_TIME - CHAT_MESSAGE_EOL_FADE
|
|
// Stay faded in
|
|
animate(alpha = 255, time = time_before_fade)
|
|
// Fade out
|
|
animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
|
|
RegisterSignal(message_loc, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(loc_z_changed))
|
|
|
|
// Register with the runechat SS to handle destruction
|
|
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src), lifespan + CHAT_MESSAGE_GRACE_PERIOD, TIMER_DELETE_ME, SSrunechat)
|
|
|
|
/datum/chatmessage/proc/get_current_alpha(time_spent)
|
|
if(time_spent < CHAT_MESSAGE_SPAWN_TIME)
|
|
return (time_spent / CHAT_MESSAGE_SPAWN_TIME) * 255
|
|
|
|
var/time_before_fade = animate_lifespan - CHAT_MESSAGE_EOL_FADE
|
|
if(time_spent <= time_before_fade)
|
|
return 255
|
|
|
|
return (1 - ((time_spent - time_before_fade) / CHAT_MESSAGE_EOL_FADE)) * 255
|
|
|
|
/datum/chatmessage/proc/loc_z_changed(datum/source, turf/old_turf, turf/new_turf, same_z_layer)
|
|
SIGNAL_HANDLER
|
|
SET_PLANE(message, RUNECHAT_PLANE, new_turf)
|
|
|
|
/**
|
|
* Creates a message overlay at a defined location for a given speaker
|
|
*
|
|
* Arguments:
|
|
* * speaker - The atom who is saying this message
|
|
* * message_language - The language that the message is said in
|
|
* * raw_message - The text content of the message
|
|
* * spans - Additional classes to be added to the message
|
|
*/
|
|
/mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, runechat_flags = NONE)
|
|
if(SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(speaker, TRAIT_BYPASS_MEASURES))
|
|
return
|
|
if(HAS_TRAIT(speaker, TRAIT_RUNECHAT_HIDDEN))
|
|
return
|
|
// Ensure the list we are using, if present, is a copy so we don't modify the list provided to us
|
|
spans = spans ? spans.Copy() : list()
|
|
|
|
// Check for virtual speakers (aka hearing a message through a radio)
|
|
var/atom/movable/originalSpeaker = speaker
|
|
if (istype(speaker, /atom/movable/virtualspeaker))
|
|
var/atom/movable/virtualspeaker/v = speaker
|
|
speaker = v.source
|
|
spans |= "virtual-speaker"
|
|
|
|
// Ignore virtual speaker (most often radio messages) from ourselves
|
|
if (originalSpeaker != src && speaker == src)
|
|
return
|
|
|
|
// Display visual above source
|
|
if(runechat_flags & EMOTE_MESSAGE)
|
|
new /datum/chatmessage(raw_message, speaker, src, message_language, list("emote", "italics"))
|
|
else
|
|
new /datum/chatmessage(raw_message, speaker, src, message_language, spans)
|
|
|
|
#undef CHAT_LAYER_MAX_Z
|
|
#undef CHAT_LAYER_Z_STEP
|
|
#undef CHAT_MESSAGE_APPROX_LHEIGHT
|
|
#undef CHAT_MESSAGE_GRACE_PERIOD
|
|
#undef CHAT_MESSAGE_EOL_FADE
|
|
#undef CHAT_MESSAGE_EXP_DECAY
|
|
#undef CHAT_MESSAGE_HEIGHT_DECAY
|
|
#undef CHAT_MESSAGE_ICON_SIZE
|
|
#undef CHAT_MESSAGE_LIFESPAN
|
|
#undef CHAT_MESSAGE_SPAWN_TIME
|
|
#undef CHAT_MESSAGE_WIDTH
|