Files
Bubberstation/code/datums/chatmessage.dm
san7890 ccef887efe Lints Against Unmanaged Local Defines (#74333)
# MAINTAINER - USE THE BUTTON THAT SAYS "MERGE MASTER" THEN SET THE PR
TO AUTO-MERGE! IT'S MUCH EASIER FOR ME TO FIX THINGS BEFORE THEY SKEW
RATHER THAN AFTER THE FACT.

## About The Pull Request

Hey there,

This took a while to do, but here's the gist:

Python file now regexes every file in `/code` except for those that have
some valid reason to be tacking on more global defines. Some of those
reasons are simply just that I don't have the time right now (doing what
you see in this PR took a few hours) to refactor and parse what should
belong and what should be thrown out. For the time being though, this PR
will at least _halt_ people making the mistake of not `#undef`ing any
files they `#define` "locally", or within the scope of a file.

Most people forget to do this and this leads to a lot of mess later on
due to how many variables can be unmanaged on the global level. I've
made this mistake, you've made this mistake, it's a common thing. Let's
automatically check for it so it can be fixed no-stress.

Scenarios this PR corrects:

* Forgetting to undef a define but undeffing others.
* Not undeffing any defines in your file.
* Earmarking a define as a "file local" define, but not defining it.
* Having a define be a "file local" define, but having it be used
elsewhere.
* Having a "local" define not even be in the file that it only shows up
in.
* Having a completely unused define*

(* I kept some of these because they seemed important... Others were
junked.)
## Why It's Good For The Game

If you wanna use it across multiple files, no reason to not make it a
global define (maybe there's a few reasons but let's assume that this is
the 95% case).

Let me know if you don't like how I re-arranged some of the defines and
how you'd rather see it be implemented, and I'd be happy to do that.
This was mostly just "eh does it need it or not" sorta stuff.

I used a pretty cool way to detect if we should use the standardized
GitHub "error" output, you can see the results of that here
https://github.com/san7890/bruhstation/actions/runs/4549766579/jobs/8022186846#step:7:792
## Changelog
Nothing that really concerns players.

(I fixed up all this stuff using vscode, no regexes beyond what you see
in the python script. sorry downstreams)
2023-03-29 10:17:03 -07:00

350 lines
14 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)
/// 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 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
/// 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
/// Contains ID of assigned timer for end_of_life fading event
var/fadertimer = null
/// States if end_of_life is being executed
var/isFading = FALSE
/**
* 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 (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
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_PARENT_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
// 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 [extra_classes.Join(" ")]' style='color: [tgt_color]'>[owner.say_emphasis(text)]</span>"
var/mheight
WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH), mheight)
if(!TICK_CHECK)
return finish_image_generation(mheight, target, owner, complete_text, lifespan)
var/datum/callback/our_callback = CALLBACK(src, PROC_REF(finish_image_generation), mheight, target, owner, complete_text, lifespan)
SSrunechat.message_queue += our_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)
approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
// 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])
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 has been executed.
if (!m.isFading)
var/sched_remaining = timeleft(m.fadertimer, SSrunechat)
var/remaining_time = max(0, (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height))
if (remaining_time)
deltimer(m.fadertimer, SSrunechat)
m.fadertimer = addtimer(CALLBACK(m, PROC_REF(end_of_life)), remaining_time, TIMER_STOPPABLE|TIMER_DELETE_ME, SSrunechat)
else
m.end_of_life()
// 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 = target.maptext_height
message.pixel_x = -target.base_pixel_x
message.maptext_width = CHAT_MESSAGE_WIDTH
message.maptext_height = mheight
message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5
message.maptext = 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)
RegisterSignal(message_loc, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(loc_z_changed))
// Register with the runechat SS to handle EOL and destruction
var/duration = lifespan - CHAT_MESSAGE_EOL_FADE
fadertimer = addtimer(CALLBACK(src, PROC_REF(end_of_life)), duration, TIMER_STOPPABLE|TIMER_DELETE_ME, SSrunechat)
/**
* Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion,
* sets timer for scheduling deletion
*
* Arguments:
* * fadetime - The amount of time to animate the message's fadeout for
*/
/datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE)
isFading = TRUE
animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src), fadetime, TIMER_DELETE_ME, SSrunechat)
/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 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(raw_message, 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_LAYER_MAX_Z
#undef CHAT_LAYER_Z_STEP
#undef CHAT_MESSAGE_APPROX_LHEIGHT
#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
#undef CM_COLOR_LUM_MAX
#undef CM_COLOR_LUM_MIN
#undef CM_COLOR_SAT_MAX
#undef CM_COLOR_SAT_MIN