Skip to content

Commit

Permalink
Merge pull request #3408 from X0-11/chat-update-2
Browse files Browse the repository at this point in the history
Runechat, too
  • Loading branch information
BDpuffy420 authored Jun 22, 2023
2 parents 01f7b8a + 5049df8 commit aa82122
Show file tree
Hide file tree
Showing 14 changed files with 325 additions and 19 deletions.
1 change: 1 addition & 0 deletions baystation12.dme
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@
#include "code\datums\browser.dm"
#include "code\datums\callbacks.dm"
#include "code\datums\category.dm"
#include "code\datums\chatmessage.dm"
#include "code\datums\datacore.dm"
#include "code\datums\datum.dm"
#include "code\datums\hierarchy.dm"
Expand Down
1 change: 1 addition & 0 deletions code/__defines/_planes+layers.dm
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ What is the naming convention for planes or layers?
#define ABOVE_PROJECTILE_LAYER 5
#define SINGULARITY_LAYER 6
#define POINTER_LAYER 7
#define CHAT_LAYER 7

#define OBSERVER_PLANE -3 // For observers and ghosts

Expand Down
2 changes: 1 addition & 1 deletion code/controllers/subsystems/garbage.dm
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(garbage)
flags = SS_POST_FIRE_TIMING|SS_BACKGROUND|SS_NO_INIT
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY

var/collection_timeout = 3000 // deciseconds to wait to let running procs finish before we just say fuck it and force del() the object
var/collection_timeout = 30 SECONDS // deciseconds to wait to let running procs finish before we just say fuck it and force del() the object
var/delslasttick = 0 // number of del()'s we've done this tick
var/gcedlasttick = 0 // number of things that gc'ed last tick
var/totaldels = 0
Expand Down
262 changes: 262 additions & 0 deletions code/datums/chatmessage.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
#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 92 // pixels
#define CHAT_MESSAGE_MAX_LENGTH 110 // characters
#define WXH_TO_HEIGHT(x) text2num(copytext((x), findtextEx((x), "x") + 1)) // thanks lummox
/atom
var/chat_color = null
var/chat_color_name = null
var/chat_color_darkened = null

/client
var/list/seen_messages = list()


/**
* # 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
* * messageloc_override - Put the message above this item, instead.
* * lifespan - The lifespan of the message in deciseconds
*/
/datum/chatmessage/New(text, atom/target, mob/owner, list/extra_classes = null, messageloc_override = 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
if(messageloc_override)
message_loc = messageloc_override
INVOKE_ASYNC(src, .proc/generate_image, text, target, owner, extra_classes, lifespan)

/datum/chatmessage/Destroy()
if (owned_by)
var/list/msgloclist = owned_by.seen_messages[message_loc]
if (owned_by.seen_messages)
msgloclist -= src
if(msgloclist.len == 0)
owned_by.seen_messages -= message_loc
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

// Clip message
var/maxlen = owned_by.prefs.max_chat_length
var/textlen = length_char(text)
if (textlen > maxlen)
textlen = 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/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 = "<span class='center maptext [extra_classes != null ? extra_classes.Join(" ") : ""]' style='color: [tgt_color]'>[text]</span>"
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
if(!message_loc) //For overriding line in-vehicles and whatnot.
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
var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
m.scheduled_destruction = world.time + remaining_time
//See below as to why this is like this instead of the original method.

// Build message image
message = image(loc = message_loc, layer = CHAT_LAYER)
message.plane = EFFECTS_BELOW_LIGHTING_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
if(!(message_loc in owned_by.seen_messages))
owned_by.seen_messages[message_loc] = list()
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)
GLOB.processing_objects += src
//I wish we didn't have to do this, but we don't have a timer subsystem. More load for processing.

/datum/chatmessage/proc/process()
if(world.time >= scheduled_destruction)
GLOB.processing_objects -= src
end_of_life()
return PROCESS_KILL

/**
* 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)
spawn(fadetime)
qdel(src)

/**
* 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()

var/atom/movable/originalSpeaker = speaker
var/messageloc_override = null

// Ignore virtual speaker (most often radio messages) from ourself
if (originalSpeaker != src && speaker == src)
return
if(speaker.z != src.z) //We'll assume that speech from people we can't see is radio-speech.
return //They don't want to see non-z (radio) messages.

if(istype(originalSpeaker.loc,/obj/vehicles) || istype(originalSpeaker.loc,/obj/structure/closet))
messageloc_override = originalSpeaker.loc

// Display visual above source
new /datum/chatmessage(capitalize(raw_message), speaker, src, spans, messageloc_override)


// 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 + game_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)]"
21 changes: 21 additions & 0 deletions code/modules/client/preference_setup/global/01_ui.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
var/clientfps = 0
var/ooccolor = "#010000" //Whatever this is set to acts as 'reset' color and is thus unusable as an actual custom color

var/chat_on_map = 1
var/max_chat_length = CHAT_MESSAGE_MAX_LENGTH

var/UI_style = "Midnight"
var/UI_style_color = "#ffffff"
var/UI_style_alpha = 255
Expand All @@ -14,20 +17,26 @@
S["UI_style"] >> pref.UI_style
S["UI_style_color"] >> pref.UI_style_color
S["UI_style_alpha"] >> pref.UI_style_alpha
S["chat_on_map"] >> pref.chat_on_map
S["max_chat_length"]>> pref.max_chat_length
S["ooccolor"] >> pref.ooccolor
S["clientfps"] >> pref.clientfps

/datum/category_item/player_setup_item/player_global/ui/save_preferences(var/savefile/S)
S["UI_style"] << pref.UI_style
S["UI_style_color"] << pref.UI_style_color
S["UI_style_alpha"] << pref.UI_style_alpha
S["chat_on_map"] << pref.chat_on_map
S["max_chat_length"]<< pref.max_chat_length
S["ooccolor"] << pref.ooccolor
S["clientfps"] << pref.clientfps

/datum/category_item/player_setup_item/player_global/ui/sanitize_preferences()
pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style))
pref.UI_style_color = sanitize_hexcolor(pref.UI_style_color, initial(pref.UI_style_color))
pref.UI_style_alpha = sanitize_integer(pref.UI_style_alpha, 0, 255, initial(pref.UI_style_alpha))
pref.chat_on_map = sanitize_integer(pref.chat_on_map, 0, 1, initial(pref.chat_on_map))
pref.max_chat_length= sanitize_integer(pref.max_chat_length, 1, CHAT_MESSAGE_MAX_LENGTH, initial(pref.max_chat_length))
pref.ooccolor = sanitize_hexcolor(pref.ooccolor, initial(pref.ooccolor))
pref.clientfps = sanitize_integer(pref.clientfps, CLIENT_MIN_FPS, CLIENT_MAX_FPS, initial(pref.clientfps))

Expand All @@ -43,6 +52,8 @@
. += "<a href='?src=\ref[src];select_ooc_color=1'><b>Using Default</b></a><br>"
else
. += "<a href='?src=\ref[src];select_ooc_color=1'><b>[pref.ooccolor]</b></a> <table style='display:inline;' bgcolor='[pref.ooccolor]'><tr><td>__</td></tr></table> <a href='?src=\ref[src];reset=ooc'>reset</a><br>"
. += "<b>Show Runechat Chat Bubbles:</b> <a href='?src=\ref[src];chat_on_map=1'>[pref.chat_on_map ? "Enabled" : "Disabled"]</a><br>"
. += "<b>Runechat message char limit:</b> <a href='?src=\ref[src];max_chat_length=1;task=input'>[pref.max_chat_length]</a><br>"
. += "<b>Client FPS:</b> <a href='?src=\ref[src];select_fps=1'><b>[pref.clientfps]</b></a><br>"

/datum/category_item/player_setup_item/player_global/ui/OnTopic(var/href,var/list/href_list, var/mob/user)
Expand Down Expand Up @@ -85,6 +96,16 @@
target_mob.client.apply_fps(pref.clientfps)
return TOPIC_REFRESH

else if (href_list["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(pref.max_chat_length)]))", "Character Preference", pref.max_chat_length) as null|num
if (!isnull(desiredlength))
pref.max_chat_length = clamp(desiredlength, 1, CHAT_MESSAGE_MAX_LENGTH)
return TOPIC_REFRESH

else if(href_list["chat_on_map"])
pref.chat_on_map = !pref.chat_on_map
return TOPIC_REFRESH

else if(href_list["reset"])
switch(href_list["reset"])
if("ui")
Expand Down
1 change: 1 addition & 0 deletions code/modules/halo/covenant/species/lekgolo/lekgolo.dm
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
for(var/language in languages)
languages -= language
add_language(language)
default_language = languages[1]

//create our actions
for(var/action_type in typesof(/obj/item/hunter_action) - /obj/item/hunter_action)
Expand Down
1 change: 1 addition & 0 deletions code/modules/halo/flood/flood.dm
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ GLOBAL_LIST_EMPTY(live_flood_simplemobs)
. = ..()
GLOB.live_flood_simplemobs.Add(src)
add_language(LANGUAGE_FLOODMIND)
default_language = languages[1]
sm_radio = new(src)
/*if(prob(50))
wander = 1
Expand Down
3 changes: 3 additions & 0 deletions code/modules/mob/hear_say.dm
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@
else
if(language)
on_hear_say("<span class='game say'><span class='name'>[speaker_name]</span>[alt_name] [track][language.format_message(message, verb)]</span>")
if(client && client.prefs && !(client.prefs.chat_on_map))
return
create_chat_message(speaker, language, message, list(), null)
else
on_hear_say("<span class='game say'><span class='name'>[speaker_name]</span>[alt_name] [track][verb], <span class='message'><span class='body'>\"[message]\"</span></span></span>")
if (speech_sound && (get_dist(speaker, src) <= world.view && src.z == speaker.z))
Expand Down
Loading

0 comments on commit aa82122

Please sign in to comment.