Files
Yogstation/code/datums/chatmessage.dm
Oblisk234 cd1aca6432 [Admin] Makes the lag switch subsystem work (#22958)
* Lag

* Moves the subsystem file

* Fixes that one line
2025-01-14 08:02:54 -05:00

329 lines
13 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 96
/// 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 messsage
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
/// Contains the reference to the next chatmessage in the bucket, used by runechat subsystem
var/datum/chatmessage/next
/// Contains the reference to the previous chatmessage in the bucket, used by runechat subsystem
var/datum/chatmessage/prev
/// 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
/**
* 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
if(!SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(owner, TRAIT_BYPASS_MEASURES))
INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, language, extra_classes, lifespan)
/datum/chatmessage/Destroy()
if (owned_by)
if (owned_by.seen_messages)
LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src)
owned_by.images.Remove(message)
owned_by = null
message_loc = null
message = null
leave_subsystem()
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), src)
// 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
// Calculate target color if not already present
if (!target.chat_color || target.chat_color_name != target.name)
target.chat_color = colorize_string(target.name)
target.chat_color_darkened = colorize_string(target.name, 0.85, 0.85)
target.chat_color_name = target.name
// 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"
var/list/prefixes
// Append radio icon if from a virtual speaker
if (extra_classes.Find("virtual-speaker"))
var/image/r_icon = image('icons/UI_Icons/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_Icons/chat/chat_icons.dmi', icon_state = "emote")
LAZYADD(prefixes, "\icon[r_icon]")
// 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("&nbsp;")][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 class='center maptext [extra_classes.Join(" ")]' style='color: [tgt_color]'>[text]</span>"
var/mheight
WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH), mheight)
approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
// Translate any existing messages upwards, apply exponential decay factors to timers
message_loc = get_atom_on_turf(target)
if (owned_by.seen_messages)
var/idx = 1
var/combined_height = approx_lines
for(var/msg in owned_by.seen_messages[message_loc])
var/datum/chatmessage/m = msg
animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME)
combined_height += m.approx_lines
// When choosing to update the remaining time we have to be careful not to update the
// scheduled time once the EOL completion time has been set.
var/sched_remaining = m.scheduled_destruction - world.time
if (!m.eol_complete)
var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
m.enter_subsystem(world.time + remaining_time) // push updated time to runechat SS
// 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_y = owner.bound_height * 0.95
message.maptext_width = CHAT_MESSAGE_WIDTH
message.maptext_height = mheight
message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5
message.maptext = complete_text
// View the message
LAZYADDASSOCLIST(owned_by.seen_messages, message_loc, src)
owned_by.images |= message
animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME)
// Register with the runechat SS to handle EOL and destruction
scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE)
RegisterSignal(message_loc, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(loc_z_changed))
enter_subsystem()
/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)
/**
* Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion,
* sets time for scheduling deletion and re-enters the runechat SS for qdeling
*
* Arguments:
* * fadetime - The amount of time to animate the message's fadeout for
*/
/datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE)
eol_complete = scheduled_destruction + fadetime
animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL)
enter_subsystem(eol_complete) // re-enter the runechat SS with the EOL completion time to QDEL self
/**
* 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)
// 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"
//NTSL doesn't pass a speaker when you do broadcast() since technically nothing is actually speaking.
if(!speaker)
return
// Ignore virtual speaker (most often radio messages) from ourself
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(lang_treat(speaker, message_language, raw_message, spans, null, TRUE), speaker, src, message_language, spans)
// Tweak these defines to change the available color ranges
#define CM_COLOR_SAT_MIN 0.6
#define CM_COLOR_SAT_MAX 0.7
#define CM_COLOR_LUM_MIN 0.65
#define CM_COLOR_LUM_MAX 0.75
/**
* Gets a color for a name, will return the same color for a given string consistently within a round.atom
*
* Note that this proc aims to produce pastel-ish colors using the HSL colorspace. These seem to be favorable for displaying on the map.
*
* Arguments:
* * name - The name to generate a color for
* * sat_shift - A value between 0 and 1 that will be multiplied against the saturation
* * lum_shift - A value between 0 and 1 that will be multiplied against the luminescence
*/
/datum/chatmessage/proc/colorize_string(name, sat_shift = 1, lum_shift = 1)
// seed to help randomness
var/static/rseed = rand(1,26)
// get hsl using the selected 6 characters of the md5 hash
var/hash = copytext(md5(name + GLOB.round_id), rseed, rseed + 6)
var/h = hex2num(copytext(hash, 1, 3)) * (360 / 255)
var/s = (hex2num(copytext(hash, 3, 5)) >> 2) * ((CM_COLOR_SAT_MAX - CM_COLOR_SAT_MIN) / 63) + CM_COLOR_SAT_MIN
var/l = (hex2num(copytext(hash, 5, 7)) >> 2) * ((CM_COLOR_LUM_MAX - CM_COLOR_LUM_MIN) / 63) + CM_COLOR_LUM_MIN
// adjust for shifts
s *= clamp(sat_shift, 0, 1)
l *= clamp(lum_shift, 0, 1)
// convert to rgb
var/h_int = round(h/60) // mapping each section of H to 60 degree sections
var/c = (1 - abs(2 * l - 1)) * s
var/x = c * (1 - abs((h / 60) % 2 - 1))
var/m = l - c * 0.5
x = (x + m) * 255
c = (c + m) * 255
m *= 255
switch(h_int)
if(0)
return "#[num2hex(c, 2)][num2hex(x, 2)][num2hex(m, 2)]"
if(1)
return "#[num2hex(x, 2)][num2hex(c, 2)][num2hex(m, 2)]"
if(2)
return "#[num2hex(m, 2)][num2hex(c, 2)][num2hex(x, 2)]"
if(3)
return "#[num2hex(m, 2)][num2hex(x, 2)][num2hex(c, 2)]"
if(4)
return "#[num2hex(x, 2)][num2hex(m, 2)][num2hex(c, 2)]"
if(5)
return "#[num2hex(c, 2)][num2hex(m, 2)][num2hex(x, 2)]"
#undef CHAT_MESSAGE_SPAWN_TIME
#undef CHAT_MESSAGE_LIFESPAN
#undef CHAT_MESSAGE_EOL_FADE
#undef CHAT_MESSAGE_EXP_DECAY
#undef CHAT_MESSAGE_HEIGHT_DECAY
#undef CHAT_MESSAGE_APPROX_LHEIGHT
#undef CHAT_MESSAGE_WIDTH
#undef CHAT_LAYER_Z_STEP
#undef CHAT_LAYER_MAX_Z
#undef CHAT_MESSAGE_ICON_SIZE