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

Approximates chatmessage's height and width valueWe so back #11947

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions code/__DEFINES/text.dm
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
/// Macro from Lummox used to get height from a MeasureText proc
#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1))

#define WXH_TO_WIDTH(x) text2num(copytext(x, 1, findtextEx(x, "x") + 1))

#define CENTER(text) {"<center>[##text]</center>"}

#define SANITIZE_FILENAME(text) (GLOB.filename_forbidden_chars.Replace(text, ""))
Expand Down Expand Up @@ -37,3 +39,12 @@
/// BYOND's string procs don't support being used on datum references (as in it doesn't look for a name for stringification)
/// We just use this macro to ensure that we will only pass strings to this BYOND-level function without developers needing to really worry about it.
#define LOWER_TEXT(thing) lowertext(UNLINT("[thing]"))

/// Index of normal sized character size in runechat lists
#define NORMAL_FONT_INDEX 1

/// Index of small sized character size in runechat lists
#define SMALL_FONT_INDEX 2

/// Index of big sized character size in runechat lists
#define BIG_FONT_INDEX 3
65 changes: 65 additions & 0 deletions code/controllers/subsystem/runechat.dm
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
TIMER_SUBSYSTEM_DEF(runechat)
name = "Runechat"
priority = FIRE_PRIORITY_RUNECHAT
/// List of most characters in the font. Do not varedit it in game.
/// Format of it is as follows: character, size when normal, size when small, size when big.
var/list/letters = list()
flags = SS_TICKER

/datum/controller/subsystem/timer/runechat/Initialize()
load_character_list()
initialized = TRUE
return SS_INIT_SUCCESS

/datum/controller/subsystem/timer/runechat/proc/load_character_list()
var/json_file = file("config/runechat_cache.json")
if(!fexists(json_file))
log_world("Missing runechat cache config file!")
return
var loaded_values = json_decode(rustg_file_read(json_file))
for(var/values as() in loaded_values)
var/list/widths = values["values"]
letters[ascii2text(values["id"])] = list(widths[1], widths[2], widths[3])

// This is left to regenerate the file, if it ever gets lost
// /datum/myLetter
// var/list/values = list()
// var/id
// var/letter

// /datum/controller/subsystem/timer/runechat/proc/init_runechat_list(client/actor)
// var/ckey = actor.ckey
// var/list/list_letters = list()
// //This is the end of BMP plane of Unicode
// for(var/i = 0, i < 65535, i++)
// var/key = ascii2text(i)
// letters[key] = list(null, null, null)
// handle_single_letter(key, actor, NORMAL_FONT_INDEX)
// handle_single_letter(key, actor, SMALL_FONT_INDEX)
// handle_single_letter(key, actor, BIG_FONT_INDEX)
// var/datum/myLetter/letter = new()
// letter.id = i
// letter.letter = key
// letter.values = letters[key]
// list_letters.Add(letter)

// var/jsonpath = file("path")
// if(fexists(jsonpath))
// fdel(jsonpath)
// var/text = "\["
// for(var/datum/myLetter/L in list_letters)
// text+= "{ \"id\": [L.id], \"letter\": [json_encode(L.letter)], \"values\": [json_encode(L.values)] },"
// text += "]"
// WRITE_FILE(jsonpath, text)

// /datum/controller/subsystem/timer/runechat/proc/handle_single_letter(letter, client/measured_client, font_index)
// set waitfor = TRUE
// var/font_class
// if(font_index == NORMAL_FONT_INDEX)
// font_class = ""
// else if(font_index == SMALL_FONT_INDEX)
// font_class = "small"
// else
// font_class = "big"
// if(!measured_client)
// return FALSE
// var/response = WXH_TO_WIDTH(measured_client.MeasureText("<span class='[font_class]'>[letter]</span>"))
// letters[letter][font_index] = response
// return TRUE
109 changes: 88 additions & 21 deletions code/datums/chatmessage.dm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
#define CHAT_MESSAGE_ICON_SIZE 7
/// How much the message moves up before fading out.
#define MESSAGE_FADE_PIXEL_Y 10
/// Approximation of the height
#define APPROX_HEIGHT(font_size, lines) ((font_size * 1.7 * lines) + 2)
/// Default font size (defined in skin.dmf), those are 1 size bigger than in skin, to account 1px black outline
#define DEFAULT_FONT_SIZE 8
/// Big font size, used by megaphones and such
#define BIG_FONT_SIZE 9
/// Small font size, used mostly by whispering
#define WHISPER_FONT_SIZE 7

// Message types
#define CHATMESSAGE_CANNOT_HEAR 0
Expand Down Expand Up @@ -57,10 +65,6 @@
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
/// Color of the message
Expand All @@ -81,10 +85,9 @@
* * lifespan - The lifespan of the message in deciseconds
*/
/datum/chatmessage/New(text, atom/target, list/client/hearers, language_icon, list/extra_classes = list(), lifespan = CHAT_MESSAGE_LIFESPAN)
. = ..()
if (!istype(target))
CRASH("Invalid target given for chatmessage")
INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, hearers, language_icon, extra_classes, lifespan)
generate_image(text, target, hearers, language_icon, extra_classes, lifespan)

/datum/chatmessage/Destroy()
if (hearers)
Expand Down Expand Up @@ -191,41 +194,56 @@
LAZYADD(prefixes, "\icon[r_icon]")
tgt_color = COLOR_CHAT_EMOTE

// Determine the font size
var/bold_font = FALSE
var/font_size = DEFAULT_FONT_SIZE
if (extra_classes.Find("megaphone"))
font_size = BIG_FONT_SIZE
else if (extra_classes.Find("italics") || extra_classes.Find("emote"))
font_size = WHISPER_FONT_SIZE
if (extra_classes.Find("yell"))
bold_font = TRUE

// Append language icon if the language uses one
var/datum/language/language_instance = GLOB.language_datum_instances[language]
var/has_language_icon = FALSE
if (language_instance?.display_icon(first_hearer.mob))
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]")
has_language_icon = TRUE

// Approximate text height
var values = approx_str_width(text, font_size, bold_font, has_language_icon)
Archanial marked this conversation as resolved.
Show resolved Hide resolved
text = values[2]
approx_lines = CEILING(values[1] / CHAT_MESSAGE_WIDTH, 1)

//Add on the icons.
//Add on the icons. The icon isn't measured in str_width
text = "[prefixes?.Join("&nbsp;")][text]"

// Approximate text height
// Complete the text with rest of extra classes
var/complete_text = "<span class='center [extra_classes.Join(" ")]' style='color: [tgt_color]'>[target.say_emphasis(text)]</span>"
var/mheight = WXH_TO_HEIGHT(first_hearer.MeasureText(complete_text, 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 = get_atom_on_turf(target)
if (LAZYLEN(message_loc.chat_messages))
var/idx = 1
var/combined_height = approx_lines
for(var/datum/chatmessage/m as() in message_loc.chat_messages)
if(!m?.message)
if(!m.message)
continue
animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME)
animate(m.message, pixel_y = m.message.pixel_y + APPROX_HEIGHT(font_size, approx_lines), 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 = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** CEILING(combined_height, 1))
if (remaining_time)
if (remaining_time > 0)
deltimer(m.fadertimer, SSrunechat)
m.fadertimer = addtimer(CALLBACK(m, PROC_REF(end_of_life)), remaining_time, TIMER_STOPPABLE|TIMER_DELETE_ME, SSrunechat)
else
Expand All @@ -248,7 +266,7 @@
message.alpha = 0
message.pixel_y = bound_height - MESSAGE_FADE_PIXEL_Y
message.maptext_width = CHAT_MESSAGE_WIDTH
message.maptext_height = mheight
message.maptext_height = APPROX_HEIGHT(font_size, approx_lines)
message.maptext_x = (CHAT_MESSAGE_WIDTH - bound_width) * -0.5
if(extra_classes.Find("italics"))
message.color = "#CCCCCC"
Expand Down Expand Up @@ -492,7 +510,7 @@
//handle color
if(color)
tgt_color = color
INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner)
generate_image(text, target, owner)

/datum/chatmessage/balloon_alert/Destroy()
if(!QDELETED(message_loc))
Expand Down Expand Up @@ -521,19 +539,25 @@

if(LAZYLEN(message_loc.balloon_alerts))
for(var/datum/chatmessage/balloon_alert/m as() in message_loc.balloon_alerts) //We get rid of old alerts so it doesn't clutter up the screen
if (!m.isFading)
var/sched_remaining = timeleft(m.fadertimer, SSrunechat)
if (sched_remaining)
deltimer(m.fadertimer, SSrunechat)
m.end_of_life()
if (m.isFading)
continue
var/sched_remaining = timeleft(m.fadertimer, SSrunechat)
if (sched_remaining)
deltimer(m.fadertimer, SSrunechat)
m.end_of_life()

// Approximate text height
var values = approx_str_width(text, DEFAULT_FONT_SIZE, FALSE, FALSE)
Archanial marked this conversation as resolved.
Show resolved Hide resolved
text = values[2]
approx_lines = CEILING(values[1] / CHAT_MESSAGE_WIDTH, 1)

// Build message image
message = image(loc = message_loc, layer = CHAT_LAYER)
message.plane = BALLOON_CHAT_PLANE
message.alpha = 0
message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
message.maptext_width = BALLOON_TEXT_WIDTH
message.maptext_height = WXH_TO_HEIGHT(owned_by?.MeasureText(text, null, BALLOON_TEXT_WIDTH))
message.maptext_height = APPROX_HEIGHT(DEFAULT_FONT_SIZE, approx_lines)
message.maptext_x = (BALLOON_TEXT_WIDTH - bound_width) * -0.5
message.maptext = MAPTEXT("<span style='text-align: center; -dm-text-outline: 1px #0005; color: [tgt_color]'>[text]</span>")

Expand All @@ -555,6 +579,45 @@
var/duration = BALLOON_TEXT_TOTAL_LIFETIME(duration_mult)
fadertimer = addtimer(CALLBACK(src, PROC_REF(end_of_life)), duration, TIMER_STOPPABLE|TIMER_DELETE_ME, SSrunechat)

/**
* Approximates the chatmesseges width based on cached widths of each char.
* If the character is not found in this cache we assume the worst and add the highest possible value.
*
* Arguments:
* * string - string to measure width
* * font size - font size that the displayed string will be in, used to calculate font size multiplier
* * is_bold - passed if the font is bold, the approximation takes into account additional width of the font
* * has_icon - text has an icon, which adds extra 8 pixels
*/
/datum/chatmessage/proc/approx_str_width(string, font_size = DEFAULT_FONT_SIZE, is_bold = FALSE, has_icon = FALSE)
var/value = 0
var/index = NORMAL_FONT_INDEX
if(font_size == WHISPER_FONT_SIZE)
index = SMALL_FONT_INDEX
else if(font_size == BIG_FONT_SIZE)
index = BIG_FONT_INDEX

var/i = 1
while(i <= length(string))
var/list/letters = SSrunechat.letters[string[i]]
Copy link
Member

@PowerfulBacon PowerfulBacon Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For how many times this gets called, using an assoc lookup is going to be slow. If you have a sequence of characters you can just use an int lookup with base offset

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are sure converting char to int will be faster than accessing this list I can change it

//List wasnt initialized or was tampered with
if(letters == null)
//Replace with question mark
var/char_len = length(string[i])
string = splicetext(string, i, i += char_len, "?")
value += SSrunechat.letters["?"][index]
i -= char_len
else
value += letters[index]

i += length(string[i])

if(is_bold)
value += length(string)

if(has_icon)
value += CHAT_MESSAGE_ICON_SIZE
return list(value, string)
Archanial marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allocating lists in the return value is a bad idea, since its on a datum you don't need a pointer because you can just modify the message as a class scoped variable.


#undef BALLOON_TEXT_CHAR_LIFETIME_INCREASE_MIN
#undef BALLOON_TEXT_CHAR_LIFETIME_INCREASE_MULT
Expand Down Expand Up @@ -585,3 +648,7 @@
#undef CM_COLOR_SAT_MAX
#undef CM_COLOR_LUM_MIN
#undef CM_COLOR_LUM_MAX
#undef APPROX_HEIGHT
#undef DEFAULT_FONT_SIZE
#undef BIG_FONT_SIZE
#undef WHISPER_FONT_SIZE
Loading
Loading