From 24c1fb35f17ff928ce9906e575e3ab2a502e3439 Mon Sep 17 00:00:00 2001
From: SuhEugene <32931701+SuhEugene@users.noreply.github.com>
Date: Mon, 17 Jul 2023 01:36:29 +0300
Subject: [PATCH] Add Runechat
---
baystation12.dme | 1 +
code/__defines/__renderer.dm | 2 +
code/__defines/_renderer.dm | 5 +
code/__defines/lists.dm | 7 +
code/datums/chat_message.dm | 382 ++++++++++++++++++
code/game/atoms.dm | 12 +-
code/modules/client/client_defines.dm | 3 +
.../preference_setup/global/preferences.dm | 18 +
code/modules/emotes/emote_define.dm | 8 +-
code/modules/emotes/emote_mob.dm | 4 +-
code/modules/mob/hear_say.dm | 7 +
code/modules/mob/mob.dm | 9 +-
icons/chaticons.dmi | Bin 0 -> 337 bytes
interface/skin.dmf | 1 +
14 files changed, 452 insertions(+), 7 deletions(-)
create mode 100644 code/datums/chat_message.dm
create mode 100644 icons/chaticons.dmi
diff --git a/baystation12.dme b/baystation12.dme
index 1e39fe61d1f54..ff1ac4da790c9 100644
--- a/baystation12.dme
+++ b/baystation12.dme
@@ -266,6 +266,7 @@
#include "code\datums\browser.dm"
#include "code\datums\callbacks.dm"
#include "code\datums\category.dm"
+#include "code\datums\chat_message.dm"
#include "code\datums\cinematic.dm"
#include "code\datums\datum.dm"
#include "code\datums\footsteps.dm"
diff --git a/code/__defines/__renderer.dm b/code/__defines/__renderer.dm
index 58f9737e378de..0928096cac28b 100644
--- a/code/__defines/__renderer.dm
+++ b/code/__defines/__renderer.dm
@@ -159,6 +159,8 @@
#define HUD_ABOVE_ITEM_LAYER 4
#define HUD_ABOVE_HUD_LAYER 5
+#define RUNECHAT_PLANE 7
+
/// This plane masks out lighting, to create an "emissive" effect for e.g glowing screens in otherwise dark areas.
#define EMISSIVE_PLANE 10
#define EMISSIVE_TARGET "*emissive"
diff --git a/code/__defines/_renderer.dm b/code/__defines/_renderer.dm
index 1e2f3aec47c51..ff9410d24046c 100644
--- a/code/__defines/_renderer.dm
+++ b/code/__defines/_renderer.dm
@@ -220,6 +220,11 @@ GLOBAL_LIST_EMPTY(zmimic_renderers)
group = RENDER_GROUP_SCREEN
plane = HUD_PLANE
+/atom/movable/renderer/runechat
+ name = "Runechat"
+ group = RENDER_GROUP_SCREEN
+ plane = RUNECHAT_PLANE
+
/* *
* Group renderers
diff --git a/code/__defines/lists.dm b/code/__defines/lists.dm
index a32c0b6c74795..9405c6561d57e 100644
--- a/code/__defines/lists.dm
+++ b/code/__defines/lists.dm
@@ -9,8 +9,15 @@
#define UNSETEMPTY(L) if (L && !length(L)) L = null
// Removes I from list L, and sets I to null if it is now empty
#define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = null; } }
+// Removes the value V from the item K, if the item K is empty will remove it from the list, if the list is empty will set the list to null
+#define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; }
// Adds I to L, initalizing L if necessary
#define LAZYADD(L, I) if(!L) { L = list(); } L += I;
+// Adds to the item K the value V, if the list is null it will initialize it
+#define LAZYADDASSOC(L, K, V) if(!L) { L = list(); } L[K] += V;
+// This is used to add onto lazy assoc list when the value you're adding is a /list/.
+// This one has extra safety over lazyaddassoc because the value could be null (and thus cant be used to += objects)
+#define LAZYADDASSOCLIST(L, K, V) if(!L) { L = list(); } L[K] += list(V);
// Insert I into L at position X, initalizing L if necessary
#define LAZYINSERT(L, I, X) if(!L) { L = list(); } L.Insert(X, I);
// Adds I to L, initalizing L if necessary, if I is not already in L
diff --git a/code/datums/chat_message.dm b/code/datums/chat_message.dm
new file mode 100644
index 0000000000000..b385bcb13b190
--- /dev/null
+++ b/code/datums/chat_message.dm
@@ -0,0 +1,382 @@
+/// 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 default runechat message length in characters
+#define CHAT_MESSAGE_LENGTH 68
+/// Max extended runechat message length in characters
+#define CHAT_MESSAGE_EXT_LENGTH 150
+/// Max default runechat message width in pixels
+#define CHAT_MESSAGE_WIDTH 96
+/// Max extended runechat message width in pixels
+#define CHAT_MESSAGE_EXT_WIDTH 128
+
+// 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.8
+
+/// Macro from Lummox used to get height from a MeasureText proc.
+/// resolves the MeasureText() return value once, then resolves the height, then sets return_var to that.
+#define WXH_TO_HEIGHT(measurement, return_var) \
+ do { \
+ var/_measurement = measurement; \
+ return_var = text2num(copytext(_measurement, findtextEx(_measurement, "x") + 1)); \
+ } while(FALSE);
+
+// Cached runechat icon
+GLOBAL_LIST_EMPTY(runechat_image_cache)
+
+/hook/startup/proc/runechat_images()
+ var/image/radio_image = image('icons/chaticons.dmi', icon_state = "radio")
+ GLOB.runechat_image_cache["radio"] = radio_image
+
+ var/image/emote_image = image('icons/chaticons.dmi', icon_state = "emote")
+ GLOB.runechat_image_cache["emote"] = emote_image
+
+ return TRUE
+
+/**
+ * # Chat Message Overlay
+ *
+ * Datum for generating a message overlay on the map
+ * Ported from TGStation; https://github.com/tgstation/tgstation/pull/50608, author: bobbahbrown
+ */
+/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
+ /// When we started animating the message
+ var/animate_start = 0
+ /// Our animation lifespan, how long this message will last
+ var/animate_lifespan = 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
+ * * 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 = list(), lifespan = CHAT_MESSAGE_LIFESPAN)
+ . = ..()
+ if (!istype(target))
+ CRASH("Invalid target given for chatmessage")
+ if(QDELETED(owner) || !istype(owner) || !owner.client)
+ stack_trace("[src.type] 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 (!QDELING(owned_by))
+ if(world.timeofday < animate_start + animate_lifespan)
+ stack_trace("Del'd before we finished fading, with [(animate_start + animate_lifespan) - world.timeofday] time left")
+
+ 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 ..()
+
+/**
+ * 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
+ GLOB.destroyed_event.register(owned_by, src, .proc/qdel_self)
+
+ // 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/extra_length = owned_by.get_preference_value(/datum/client_preference/runechat_messages_length) == GLOB.PREF_LONG
+ var/maxlen = extra_length ? CHAT_MESSAGE_EXT_LENGTH : CHAT_MESSAGE_LENGTH
+ var/msgwidth = extra_length ? CHAT_MESSAGE_EXT_WIDTH : CHAT_MESSAGE_WIDTH
+ 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"
+
+ // Why are you yelling?
+ if(copytext_char(text, -2) == "!!")
+ extra_classes |= "yell"
+
+ // Append radio icon if from a virtual speaker
+ if (extra_classes.Find("virtual-speaker"))
+ var/image/r_icon = image('icons/chaticons.dmi', icon_state = "radio")
+ text = "\icon[r_icon] " + text
+ else if (extra_classes.Find("emote"))
+ var/image/r_icon = image('icons/chaticons.dmi', icon_state = "emote")
+ text = "\icon[r_icon] " + text
+
+ // We dim italicized text to make it more distinguishable from regular text
+ var/tgt_color = target.chat_color
+ if (extra_classes.Find("italics") || extra_classes.Find("emote"))
+ tgt_color = target.chat_color_darkened
+
+ // 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, msgwidth), mheight)
+
+ invoke_async(src, .proc/finish_image_generation, mheight, target, owner, complete_text, lifespan)
+
+/// 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)
+ var/rough_time = world.timeofday
+ 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])
+ 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.
+ if (time_spent >= time_before_fade)
+ animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
+ 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)
+ // Stay faded in for a while, then
+ animate(m.message, alpha = 255, remaining_time)
+ // Fade out
+ animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
+ m.animate_lifespan = remaining_time + CHAT_MESSAGE_EOL_FADE
+ else
+ // Your time has come my son
+ animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
+ // 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
+ animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
+
+ // Build message image
+ message = image(loc = message_loc, layer = ABOVE_HUMAN_LAYER)
+ message.plane = RUNECHAT_PLANE
+ message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
+ message.alpha = 0
+ message.pixel_y = target.maptext_height
+ message.maptext_width = CHAT_MESSAGE_WIDTH
+ message.maptext_height = mheight * 1.25
+ 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
+
+ // 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)
+
+ // Desctruct yourself
+ addtimer(new Callback(src, .proc/qdel_self), lifespan + CHAT_MESSAGE_GRACE_PERIOD, TIMER_UNIQUE|TIMER_OVERRIDE)
+
+/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
+
+/**
+ * Creates a message overlay at a defined location for a given speaker
+ *
+ * Arguments:
+ * * speaker - The atom who is saying this message
+ * * message - The text content of the message
+ * * italics - Decides if this should be small or not, as generally italics text are for whisper/radio overhear
+ * * existing_extra_classes - Additional classes to add to the message
+ */
+/mob/proc/create_chat_message(atom/movable/speaker, message, italics, list/existing_extra_classes, audible = TRUE)
+ if(!client)
+ return
+
+ // Doesn't want to hear
+ if(ismob(speaker) && client.get_preference_value(/datum/client_preference/runechat_mob) != GLOB.PREF_YES)
+ return
+ if(isobj(speaker) && client.get_preference_value(/datum/client_preference/runechat_obj) != GLOB.PREF_YES)
+ return
+
+ // Incapable of receiving
+ if((audible && is_deaf()) || (!audible && is_blind()))
+ return
+
+ // Check for virtual speakers (aka hearing a message through a radio)
+ if(existing_extra_classes.Find("radio"))
+ return
+
+ /* Not currently necessary
+ message = strip_html_properly(message)
+ if(!message)
+ return
+ */
+
+ var/list/extra_classes = list()
+ extra_classes += existing_extra_classes
+
+ if(italics)
+ extra_classes |= "italics"
+
+ // Display visual above source
+ new /datum/chatmessage(message, speaker, src, extra_classes)
+
+/**
+ * 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), rseed, rseed + 6)
+ var/h = hex2num(copytext(hash, 1, 3)) * (360 / 255)
+ var/s = SHIFTR(hex2num(copytext(hash, 3, 5)), 2) * ((CM_COLOR_SAT_MAX - CM_COLOR_SAT_MIN) / 63) + CM_COLOR_SAT_MIN
+ var/l = SHIFTR(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 rgba
+ 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 rgb(c,x,m)
+ if(1)
+ return rgb(x,c,m)
+ if(2)
+ return rgb(m,c,x)
+ if(3)
+ return rgb(m,x,c)
+ if(4)
+ return rgb(x,m,c)
+ if(5)
+ return rgb(c,m,x)
+
+/atom/proc/runechat_message(message, range = world.view, italics, list/classes = list(), audible = TRUE)
+ var/list/hearing_mobs = list()
+ var/list/objs = list()
+ get_mobs_and_objs_in_view_fast(get_turf(src), range, hearing_mobs, objs, checkghosts = FALSE)
+
+ for(var/mob in hearing_mobs)
+ var/mob/M = mob
+ if(!M.client)
+ continue
+ M.create_chat_message(src, message, italics, classes, audible)
+
+
+#undef CHAT_MESSAGE_SPAWN_TIME
+#undef CHAT_MESSAGE_LIFESPAN
+#undef CHAT_MESSAGE_EOL_FADE
+#undef CHAT_MESSAGE_GRACE_PERIOD
+#undef CHAT_MESSAGE_EXP_DECAY
+#undef CHAT_MESSAGE_HEIGHT_DECAY
+#undef CHAT_MESSAGE_APPROX_LHEIGHT
+#undef CHAT_MESSAGE_LENGTH
+#undef CHAT_MESSAGE_EXT_LENGTH
+#undef CHAT_MESSAGE_WIDTH
+#undef CHAT_MESSAGE_EXT_WIDTH
+
+#undef CM_COLOR_SAT_MIN
+#undef CM_COLOR_SAT_MAX
+#undef CM_COLOR_LUM_MIN
+#undef CM_COLOR_LUM_MAX
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index d01709522c080..5780e774561fc 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -36,6 +36,14 @@
/// This atom's cache of overlays that can only be removed explicitly, like C4. Do not manipulate directly- See SSoverlays.
var/list/atom_protected_overlay_cache
+ /// 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
+ /// The chat color var, without alpha.
+ var/chat_color_hover
/atom/New(loc, ...)
SHOULD_CALL_PARENT(TRUE)
@@ -741,7 +749,7 @@
* - `exclude_objs` - List of objects to not display the message to.
* - `exclude_mobs` - List of mobs to not display the message to.
*/
-/atom/proc/audible_message(message, deaf_message, hearing_distance = world.view, checkghosts = null, list/exclude_objs = null, list/exclude_mobs = null)
+/atom/proc/audible_message(message, deaf_message, hearing_distance = world.view, checkghosts = null, list/exclude_objs = null, list/exclude_mobs = null, runemessage = -1)
var/turf/T = get_turf(src)
var/list/mobs = list()
var/list/objs = list()
@@ -753,6 +761,8 @@
exclude_mobs -= M
continue
M.show_message(message,2,deaf_message,1)
+ if(runemessage != -1)
+ M.create_chat_message(src, "[runemessage]", FALSE, list("emote"))
for(var/o in objs)
var/obj/O = o
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index bbea8dae39e03..7afa107358e43 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -15,6 +15,9 @@
var/datum/preferences/prefs = null
var/adminobs = null
+ // Runechat messages
+ var/list/seen_messages
+
///datum that controls the displaying and hiding of tooltips
var/datum/tooltip/tooltips
diff --git a/code/modules/client/preference_setup/global/preferences.dm b/code/modules/client/preference_setup/global/preferences.dm
index 21fea4f93dc16..730b4c3af1aac 100644
--- a/code/modules/client/preference_setup/global/preferences.dm
+++ b/code/modules/client/preference_setup/global/preferences.dm
@@ -303,6 +303,24 @@ var/global/list/_client_preferences_by_type
options = list(GLOB.PREF_YES, GLOB.PREF_NO)
default_value = GLOB.PREF_NO
+/datum/client_preference/runechat_mob
+ description = "Enable mob runechat"
+ key = "RUNECHAT_MOB"
+ options = list(GLOB.PREF_YES, GLOB.PREF_NO)
+ default_value = GLOB.PREF_YES
+
+/datum/client_preference/runechat_obj
+ description = "Enable obj runechat"
+ key = "RUNECHAT_OBJ"
+ options = list(GLOB.PREF_YES, GLOB.PREF_NO)
+ default_value = GLOB.PREF_YES
+
+/datum/client_preference/runechat_messages_length
+ description = "Length of runechat messages"
+ key = "RUNECHAT_MESSAGES_LENGTH"
+ options = list(GLOB.PREF_SHORT, GLOB.PREF_LONG)
+ default_value = GLOB.PREF_SHORT
+
/********************
* General Staff Preferences *
diff --git a/code/modules/emotes/emote_define.dm b/code/modules/emotes/emote_define.dm
index 05bcc80be61d6..0d7b4626aba5c 100644
--- a/code/modules/emotes/emote_define.dm
+++ b/code/modules/emotes/emote_define.dm
@@ -63,6 +63,7 @@
var/use_3p
var/use_1p
+ var/runemessage = -1
if(emote_message_1p)
if(target && emote_message_1p_target)
use_1p = get_emote_message_1p(user, target, extra_params)
@@ -86,6 +87,9 @@
use_3p = replacetext(use_3p, "USER_THEM", user_pronouns.him)
use_3p = replacetext(use_3p, "USER_THEIR", user_pronouns.his)
use_3p = replacetext(use_3p, "USER_SELF", user_pronouns.self)
+
+ runemessage = replacetext(use_3p, "USER", "")
+
use_3p = replacetext(use_3p, "USER", "\the [user]")
use_3p = capitalize(use_3p)
@@ -96,9 +100,9 @@
if(ismob(user))
var/mob/M = user
if(message_type == AUDIBLE_MESSAGE)
- M.audible_message(message = use_3p, self_message = use_1p, deaf_message = emote_message_impaired, hearing_distance = use_range, checkghosts = /datum/client_preference/ghost_sight)
+ M.audible_message(message = use_3p, self_message = use_1p, deaf_message = emote_message_impaired, hearing_distance = use_range, checkghosts = /datum/client_preference/ghost_sight, runemessage = runemessage)
else
- M.visible_message(message = use_3p, self_message = use_1p, blind_message = emote_message_impaired, range = use_range, checkghosts = /datum/client_preference/ghost_sight)
+ M.visible_message(message = use_3p, self_message = use_1p, blind_message = emote_message_impaired, range = use_range, checkghosts = /datum/client_preference/ghost_sight, runemessage = runemessage)
do_extra(user, target)
diff --git a/code/modules/emotes/emote_mob.dm b/code/modules/emotes/emote_mob.dm
index 2e45a8a25fd88..11f760a59e9c3 100644
--- a/code/modules/emotes/emote_mob.dm
+++ b/code/modules/emotes/emote_mob.dm
@@ -137,9 +137,9 @@
//do not show NPC animal emotes to ghosts, it turns into hellscape
var/check_ghosts = client ? /datum/client_preference/ghost_sight : null
if(m_type == VISIBLE_MESSAGE)
- visible_message(message, checkghosts = check_ghosts)
+ visible_message(message, checkghosts = check_ghosts, runemessage = input)
else
- audible_message(message, checkghosts = check_ghosts)
+ audible_message(message, checkghosts = check_ghosts, runemessage = input)
// Specific mob type exceptions below.
/mob/living/silicon/ai/emote(act, type, message)
diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm
index b3920f09e2929..3336eabcc68a1 100644
--- a/code/modules/mob/hear_say.dm
+++ b/code/modules/mob/hear_say.dm
@@ -75,6 +75,8 @@
if (italics)
display_message = "[display_message]"
+ var/runechat_message = display_message
+
var/display_controls
if (is_ghost)
if (display_name != speaker.real_name && speaker.real_name)
@@ -110,6 +112,11 @@
on_hear_say({"[SPAN_CLASS("game say", "[display_controls][SPAN_CLASS("name", display_name)][alt_name] [display_message]")]"})
+ if (istype(language, /datum/language/noise))
+ create_chat_message(speaker, runechat_message, italics, list("emote"))
+ else
+ create_chat_message(speaker, capitalize(runechat_message), italics, list())
+
/mob/proc/on_hear_say(message)
to_chat(src, message)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index dfc0f452b63c1..ff74966aef232 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -83,7 +83,7 @@
// message is the message output to anyone who can see e.g. "[src] does something!"
// self_message (optional) is what the src mob sees e.g. "You do something!"
// blind_message (optional) is what blind people will hear e.g. "You hear something!"
-/mob/visible_message(message, self_message, blind_message, range = world.view, checkghosts = null, narrate = FALSE, list/exclude_objs = null, list/exclude_mobs = null)
+/mob/visible_message(message, self_message, blind_message, range = world.view, checkghosts = null, narrate = FALSE, list/exclude_objs = null, list/exclude_mobs = null, runemessage = -1)
set waitfor = FALSE
var/turf/T = get_turf(src)
var/list/mobs = list()
@@ -122,6 +122,8 @@
if((!M.is_blind() && M.see_invisible >= src.invisibility) || narrate)
M.show_message(mob_message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE)
+ if(runemessage != -1)
+ M.create_chat_message(src, "[runemessage]", FALSE, list("emote"), audible = FALSE)
continue
if(blind_message)
@@ -137,7 +139,7 @@
// self_message (optional) is what the src mob hears.
// deaf_message (optional) is what deaf people will see.
// hearing_distance (optional) is the range, how many tiles away the message can be heard.
-/mob/audible_message(message, self_message, deaf_message, hearing_distance = world.view, checkghosts = null, narrate = FALSE, list/exclude_objs = null, list/exclude_mobs = null)
+/mob/audible_message(message, self_message, deaf_message, hearing_distance = world.view, checkghosts = null, narrate = FALSE, list/exclude_objs = null, list/exclude_mobs = null, runemessage = -1)
var/turf/T = get_turf(src)
var/list/mobs = list()
var/list/objs = list()
@@ -168,6 +170,9 @@
else
M.show_message(mob_message, AUDIBLE_MESSAGE)
+ if(runemessage != -1)
+ M.create_chat_message(src, "[runemessage]", FALSE, list("emote"), audible = TRUE)
+
for(var/o in objs)
var/obj/O = o
if (length(exclude_objs) && (O in exclude_objs))
diff --git a/icons/chaticons.dmi b/icons/chaticons.dmi
new file mode 100644
index 0000000000000000000000000000000000000000..8cc4b2c5598fa8818a6b80684a33cc19c4475a2f
GIT binary patch
literal 337
zcmV-X0j~auP)V=-0C=2r%CQQAFc1ddIrkJlx|bGQ9BwI*l0n}gkh8hL29iTx-$Cf+_6=W;
z_w)%1{2XYP+O+(@7#>e@hbGHljO4QDHyaouHH#)-bTuX|9l4~nf$<`@*0{;#|B|hk
z+^rtd;^F5&C*08_dM&1E;{X5vlSxEDR49?HlCcfLFbo8rf_BLOZqkLl2k`)zBLjF4
z_wO!^2kZs*w~NREdynk}vPz!=MOp)!uLh
zRx`*p32LABKi9{583f&E@bh(>?