Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runechat, too #3408

Merged
merged 13 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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