diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index 8891c06f..45fb2819 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -75,6 +75,7 @@
#define AREA_LAYER 10
#define MASSIVE_OBJ_LAYER 11
#define POINT_LAYER 12
+#define CHAT_LAYER 12.1
#define LIGHTING_PLANE 15
#define LIGHTING_LAYER 15
diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm
index 1bc7d8e3..912dd0dd 100644
--- a/code/datums/brain_damage/imaginary_friend.dm
+++ b/code/datums/brain_damage/imaginary_friend.dm
@@ -138,6 +138,8 @@
friend_talk(message)
/mob/camera/imaginary_friend/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, message_mode)
+ if (client?.prefs.chat_on_map && (client.prefs.see_chat_non_mob || ismob(speaker)))
+ create_chat_message(speaker, message_language, raw_message, spans, message_mode)
to_chat(src, compose_message(speaker, message_language, raw_message, radio_freq, spans, message_mode))
/mob/camera/imaginary_friend/proc/friend_talk(message)
diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm
new file mode 100644
index 00000000..2ec275f1
--- /dev/null
+++ b/code/datums/chatmessage.dm
@@ -0,0 +1,236 @@
+#define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS
+#define CHAT_MESSAGE_LIFESPAN 5 SECONDS
+#define CHAT_MESSAGE_EOL_FADE 0.7 SECONDS
+#define CHAT_MESSAGE_EXP_DECAY 0.7 // Messages decay at pow(factor, idx in stack)
+#define CHAT_MESSAGE_HEIGHT_DECAY 0.9 // Increase message decay based on the height of the message
+#define CHAT_MESSAGE_APPROX_LHEIGHT 11 // Approximate height in pixels of an 'average' line, used for height decay
+#define CHAT_MESSAGE_WIDTH 96 // pixels
+#define CHAT_MESSAGE_MAX_LENGTH 110 // characters
+#define WXH_TO_HEIGHT(x) text2num(copytext((x), findtextEx((x), "x") + 1)) // thanks lummox
+
+/**
+ * # 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
+ var/scheduled_destruction
+ /// Contains the approximate amount of lines for height decay
+ var/approx_lines
+
+/**
+ * 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
+ * * 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, list/extra_classes = null, 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/generate_image, text, target, owner, extra_classes, lifespan)
+
+/datum/chatmessage/Destroy()
+ if (owned_by)
+ //if (owned_by.seen_messages) //Im not sure what this is. the prog isnt in our code, works without it, hope it dont break shit. QuoteFox
+ //LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src)
+ owned_by.images.Remove(message)
+ owned_by = null
+ message_loc = null
+ message = null
+ return ..()
+
+/**
+ * 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
+ * * 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, list/extra_classes, lifespan)
+ // Register client who owns this message
+ owned_by = owner.client
+ //RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, .proc/qdel, src)
+
+ // Clip message
+ var/maxlen = owned_by.prefs.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"
+
+ // 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")
+ text = "\icon[r_icon] " + 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
+ // Note we have to replace HTML encoded metacharacters otherwise MeasureText will return a zero height
+ // BYOND Bug #2563917
+ // Construct text
+ var/static/regex/html_metachars = new(@"&[A-Za-z]{1,7};", "g")
+ var/complete_text = ""
+ var/mheight = WXH_TO_HEIGHT(owned_by.MeasureText(replacetext(complete_text, html_metachars, "m"), null, CHAT_MESSAGE_WIDTH))
+ approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
+
+ // Translate any existing messages upwards, apply exponential decay factors to timers
+ message_loc = 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
+ var/sched_remaining = m.scheduled_destruction - world.time
+ if (sched_remaining > CHAT_MESSAGE_SPAWN_TIME)
+ var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
+ m.scheduled_destruction = world.time + remaining_time
+ addtimer(CALLBACK(m, .proc/end_of_life), remaining_time, TIMER_UNIQUE|TIMER_OVERRIDE)
+
+ // Build message image
+ message = image(loc = message_loc, layer = CHAT_LAYER)
+ message.plane = GAME_PLANE
+ 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
+ //LAZYADDASSOC(owned_by.seen_messages, message_loc, src)
+ owned_by.images |= message
+ animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME)
+
+ // Prepare for destruction
+ scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE)
+ addtimer(CALLBACK(src, .proc/end_of_life), lifespan - CHAT_MESSAGE_EOL_FADE, TIMER_UNIQUE|TIMER_OVERRIDE)
+
+/**
+ * Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion
+ */
+/datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE)
+ animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL)
+ QDEL_IN(src, fadetime)
+
+/**
+ * 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
+ * * message_mode - Bitflags relating to the mode of the message
+ */
+/mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, message_mode)
+ // Ensure the list we are using, if present, is a copy so we don't modify the list provided to us
+ spans = spans?.Copy()
+
+ // 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
+ new /datum/chatmessage(lang_treat(speaker, message_language, raw_message, spans, null, TRUE), speaker, src, 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)]"
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 9e8756bb..a3cbda60 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -13,6 +13,13 @@
//HUD images that this atom can provide.
var/list/hud_possible
+ /// Last name used to calculate a color for the chatmessage overlays
+ var/chat_color_name
+ /// Last color calculated for the the chatmessage overlays
+ var/chat_color
+ /// A luminescence-shifted value of the last color calculated for chatmessage overlays
+ var/chat_color_darkened
+
//Value used to increment ex_act() if reactionary_explosions is on
var/explosion_block = 0
diff --git a/code/game/say.dm b/code/game/say.dm
index 4ce1d3c7..2bb67dfc 100644
--- a/code/game/say.dm
+++ b/code/game/say.dm
@@ -94,21 +94,21 @@ GLOBAL_LIST_INIT(freqtospan, list(
return "[say_mod(input, message_mode)][spanned ? ", \"[spanned]\"" : ""]"
// Citadel edit [spanned ? ", \"[spanned]\"" : ""]"
-/atom/movable/proc/lang_treat(atom/movable/speaker, datum/language/language, raw_message, list/spans, message_mode)
+/atom/movable/proc/lang_treat(atom/movable/speaker, datum/language/language, raw_message, list/spans, message_mode, no_quote = FALSE)
if(has_language(language))
var/atom/movable/AM = speaker.GetSource()
if(AM) //Basically means "if the speaker is virtual"
- return AM.say_quote(raw_message, spans, message_mode)
+ return no_quote ? raw_message : AM.say_quote(raw_message, spans, message_mode)
else
- return speaker.say_quote(raw_message, spans, message_mode)
+ return no_quote ? raw_message : speaker.say_quote(raw_message, spans, message_mode)
else if(language)
var/atom/movable/AM = speaker.GetSource()
var/datum/language/D = GLOB.language_datum_instances[language]
raw_message = D.scramble(raw_message)
if(AM)
- return AM.say_quote(raw_message, spans, message_mode)
+ return no_quote ? raw_message : AM.say_quote(raw_message, spans, message_mode)
else
- return speaker.say_quote(raw_message, spans, message_mode)
+ return no_quote ? raw_message : speaker.say_quote(raw_message, spans, message_mode)
else
return "makes a strange sound."
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 4e7ec45b..6251a84e 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -15,6 +15,8 @@
var/last_message = "" //Contains the last message sent by this client - used to protect against copy-paste spamming.
var/last_message_count = 0 //contins a number of how many times a message identical to last_message was sent.
var/ircreplyamount = 0
+ /// Messages currently seen by this client
+ var/list/seen_messages
/////////
//OTHER//
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 1713df90..94c9798d 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -458,6 +458,7 @@ GLOBAL_LIST_EMPTY(external_rsc_urls)
if(movingmob != null)
movingmob.client_mobs_in_contents -= mob
UNSETEMPTY(movingmob.client_mobs_in_contents)
+ seen_messages = null
Master.UpdateTickRate()
return ..()
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index fbc30413..b4484ed8 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -45,6 +45,9 @@ GLOBAL_LIST_EMPTY(preferences_datums)
var/UI_style = null
var/buttons_locked = FALSE
var/hotkeys = FALSE
+ var/chat_on_map = TRUE
+ var/max_chat_length = CHAT_MESSAGE_MAX_LENGTH
+ var/see_chat_non_mob = TRUE
var/tgui_fancy = TRUE
var/tgui_lock = TRUE
var/windowflashing = TRUE
@@ -819,6 +822,9 @@ GLOBAL_LIST_EMPTY(preferences_datums)
dat += "UI Style: [UI_style]
"
dat += "tgui Monitors: [(tgui_lock) ? "Primary" : "All"]
"
dat += "tgui Style: [(tgui_fancy) ? "Fancy" : "No Frills"]
"
+ dat += "Show Runechat Chat Bubbles: [chat_on_map ? "Enabled" : "Disabled"]
"
+ dat += "Runechat message char limit: [max_chat_length]
"
+ dat += "See Runechat for non-mobs: [see_chat_non_mob ? "Enabled" : "Disabled"]
"
dat += "
"
dat += "Action Buttons: [(buttons_locked) ? "Locked In Place" : "Unlocked"]
"
dat += "Keybindings: [(hotkeys) ? "Hotkeys" : "Default"]
"
@@ -2151,6 +2157,10 @@ GLOBAL_LIST_EMPTY(preferences_datums)
var/pickedPDASkin = input(user, "Choose your PDA reskin.", "Character Preference", pda_skin) as null|anything in GLOB.pda_reskins
if(pickedPDASkin)
pda_skin = pickedPDASkin
+ if ("max_chat_length")
+ var/desiredlength = input(user, "Choose the max character length of shown Runechat messages. Valid range is 1 to [CHAT_MESSAGE_MAX_LENGTH] (default: [initial(max_chat_length)]))", "Character Preference", max_chat_length) as null|num
+ if (!isnull(desiredlength))
+ max_chat_length = clamp(desiredlength, 1, CHAT_MESSAGE_MAX_LENGTH)
else
switch(href_list["preference"])
@@ -2236,6 +2246,10 @@ GLOBAL_LIST_EMPTY(preferences_datums)
winset(user, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED] mainwindow.macro=old_default")
if("action_buttons")
buttons_locked = !buttons_locked
+ if("chat_on_map")
+ chat_on_map = !chat_on_map
+ if("see_chat_non_mob")
+ see_chat_non_mob = !see_chat_non_mob
if("tgui_fancy")
tgui_fancy = !tgui_fancy
if("tgui_lock")
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index d9ec2c66..fc619515 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -100,6 +100,10 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
S["inquisitive_ghost"] >> inquisitive_ghost
S["uses_glasses_colour"]>> uses_glasses_colour
S["clientfps"] >> clientfps
+ S["chat_on_map"] >> chat_on_map
+ S["max_chat_length"] >> max_chat_length
+ S["see_chat_non_mob"] >> see_chat_non_mob
+
S["parallax"] >> parallax
S["ambientocclusion"] >> ambientocclusion
@@ -129,6 +133,9 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog))
UI_style = sanitize_inlist(UI_style, GLOB.available_ui_styles, GLOB.available_ui_styles[1])
hotkeys = sanitize_integer(hotkeys, 0, 1, initial(hotkeys))
+ chat_on_map = sanitize_integer(chat_on_map, 0, 1, initial(chat_on_map))
+ max_chat_length = sanitize_integer(max_chat_length, 1, CHAT_MESSAGE_MAX_LENGTH, initial(max_chat_length))
+ see_chat_non_mob = sanitize_integer(see_chat_non_mob, 0, 1, initial(see_chat_non_mob))
tgui_fancy = sanitize_integer(tgui_fancy, 0, 1, initial(tgui_fancy))
tgui_lock = sanitize_integer(tgui_lock, 0, 1, initial(tgui_lock))
buttons_locked = sanitize_integer(buttons_locked, 0, 1, initial(buttons_locked))
@@ -180,6 +187,9 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
WRITE_FILE(S["lastchangelog"], lastchangelog)
WRITE_FILE(S["UI_style"], UI_style)
WRITE_FILE(S["hotkeys"], hotkeys)
+ WRITE_FILE(S["chat_on_map"], chat_on_map)
+ WRITE_FILE(S["max_chat_length"], max_chat_length)
+ WRITE_FILE(S["see_chat_non_mob"], see_chat_non_mob)
WRITE_FILE(S["tgui_fancy"], tgui_fancy)
WRITE_FILE(S["tgui_lock"], tgui_lock)
WRITE_FILE(S["buttons_locked"], buttons_locked)
diff --git a/code/modules/flufftext/Hallucination.dm b/code/modules/flufftext/Hallucination.dm
index d035db82..54ca1808 100644
--- a/code/modules/flufftext/Hallucination.dm
+++ b/code/modules/flufftext/Hallucination.dm
@@ -683,31 +683,27 @@ GLOBAL_LIST_INIT(hallucination_list, list(
if(get_dist(target,H)"
@@ -821,9 +821,11 @@
else
jobpart = "Unknown"
- var/rendered = "[start][hrefpart][namepart] ([jobpart]) [raw_message]"
+ var/rendered = "[start][hrefpart][namepart] ([jobpart]) [treated_message]"
+ if (client?.prefs.chat_on_map && (client.prefs.see_chat_non_mob || ismob(speaker)))
+ create_chat_message(speaker, message_language, raw_message, spans, message_mode)
- show_message(rendered, 2)
+ show_message(rendered, 2)
/mob/living/silicon/ai/fully_replace_character_name(oldname,newname)
..()
diff --git a/icons/UI_Icons/chat/chat_icons.dmi b/icons/UI_Icons/chat/chat_icons.dmi
new file mode 100644
index 00000000..7040b396
Binary files /dev/null and b/icons/UI_Icons/chat/chat_icons.dmi differ
diff --git a/modular_citadel/interface/skin.dmf b/modular_citadel/interface/skin.dmf
index 7dddfa2e..b822aa46 100644
--- a/modular_citadel/interface/skin.dmf
+++ b/modular_citadel/interface/skin.dmf
@@ -111,6 +111,7 @@ window "mapwindow"
is-default = true
saved-params = "icon-size"
zoom-mode = distort
+ style=".center { text-align: center; } .maptext { font-family: 'Small Fonts'; font-size: 7px; -dm-text-outline: 1px black; color: white; line-height: 1.1; } .command_headset { font-weight: bold; font-size: 8px; } .small { font-size: 6px; } .big { font-size: 8px; } .reallybig { font-size: 8px; } .extremelybig { font-size: 8px; } .greentext { color: #00FF00; font-size: 7px; } .redtext { color: #FF0000; font-size: 7px; } .clown { color: #FF69Bf; font-size: 7px; font-weight: bold; } .his_grace { color: #15D512; } .hypnophrase { color: #0d0d0d; font-weight: bold; } .yell { font-weight: bold; } .italics { font-size: 6px; }"
window "infowindow"
elem "infowindow"
diff --git a/tgstation.dme b/tgstation.dme
index 2d0cb018..a0d513b8 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -297,6 +297,7 @@
#include "code\datums\beam.dm"
#include "code\datums\browser.dm"
#include "code\datums\callback.dm"
+#include "code\datums\chatmessage.dm"
#include "code\datums\cinematic.dm"
#include "code\datums\dash_weapon.dm"
#include "code\datums\datacore.dm"