Skip to content

Commit

Permalink
Port Runechat
Browse files Browse the repository at this point in the history
  • Loading branch information
Arokha committed May 30, 2021
1 parent e018309 commit ea42ee2
Show file tree
Hide file tree
Showing 17 changed files with 414 additions and 31 deletions.
2 changes: 2 additions & 0 deletions code/__defines/_planes+layers.dm
Expand Up @@ -93,6 +93,8 @@ What is the naming convention for planes or layers?
#define BELOW_MOB_LAYER 3.9 // Should be converted to plane swaps
#define ABOVE_MOB_LAYER 4.1 // Should be converted to plane swaps

#define ABOVE_MOB_PLANE -24

// Invisible things plane
#define CLOAKED_PLANE -15

Expand Down
2 changes: 2 additions & 0 deletions code/_helpers/time.dm
Expand Up @@ -20,6 +20,8 @@
#define TICKS2DS(T) ((T) TICKS) // Convert ticks to deciseconds
#define DS2NEARESTTICK(DS) TICKS2DS(-round(-(DS2TICKS(DS))))

var/world_startup_time

/proc/get_game_time()
var/global/time_offset = 0
var/global/last_time = 0
Expand Down
16 changes: 16 additions & 0 deletions code/_helpers/unsorted.dm
Expand Up @@ -1691,3 +1691,19 @@ GLOBAL_REAL_VAR(list/stack_trace_storage)
return "CLIENT: [D]"
else
return "Unknown data type: [D]"

/*
is_holder_of(): Returns 1 if A is a holder of B, meaning, A is B.loc or B.loc.loc or B.loc.loc.loc etc.
This is essentially the same as calling (locate(B) in A), but a little clearer as to what you're doing, and locate() has been known to bug out or be extremely slow in the past.
*/
/proc/is_holder_of(const/atom/movable/A, const/atom/movable/B)
if(istype(A, /turf) || istype(B, /turf)) //Clicking on turfs is a common thing and turfs are also not /atom/movable, so it was causing the assertion to fail.
return 0
ASSERT(istype(A) && istype(B))
var/atom/O = B
while(O && !isturf(O))
if(O == A)
return 1
O = O.loc
return 0

293 changes: 293 additions & 0 deletions code/datums/chat_message.dm
@@ -0,0 +1,293 @@
#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.8 // Messages decay at pow(factor, idx in stack)
#define CHAT_MESSAGE_HEIGHT_DECAY 0.7 // 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_NORM_LENGTH 68 // characters
#define CHAT_MESSAGE_EXT_LENGTH 150 // characters
#define CHAT_MESSAGE_MOB 1
#define CHAT_MESSAGE_OBJ 2
#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
* Ported from TGStation; https://github.com/tgstation/tgstation/pull/50608/, author: bobbahbrown
*/

// Cached runechat icon
var/runechat_icon = null

/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(!istype(owner) || QDELETED(owner) || !owner.client)
stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner")
qdel(src)
return
generate_image(text, target, owner, extra_classes, lifespan)

/datum/chatmessage/Destroy()
if (owned_by)
owned_by.seen_messages.Remove(src)
owned_by.images.Remove(message)
UnregisterSignal(owned_by, COMSIG_PARENT_QDELETING)
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)
set waitfor = FALSE
// Register client who owns this message
owned_by = owner.client
RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, .proc/qdel_self)

// Clip message
var/maxlen = owned_by.is_preference_enabled(/datum/client_preference/runechat_long_messages) ? CHAT_MESSAGE_EXT_LENGTH : CHAT_MESSAGE_NORM_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"

// If we heard our name, it's important
var/list/names = splittext(owner.name, " ")
for (var/word in names)
text = replacetext(text, word, "<b>[word]</b>")

// Append radio icon if comes from a radio
if (extra_classes.Find("spoken_into_radio"))
if (!runechat_icon)
var/image/r_icon = image('icons/UI_Icons/chat/chat_icons.dmi', icon_state = "radio")
runechat_icon = "\icon[r_icon]&nbsp;"
text = runechat_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
message_loc = target
if (owned_by.seen_messages)
var/idx = 1
var/combined_height = approx_lines
for(var/msg in owned_by.seen_messages)
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
spawn(remaining_time)
m.end_of_life()

// Build message image
message = image(loc = message_loc, layer = ABOVE_MOB_LAYER)
message.plane = PLANE_LIGHTING_ABOVE
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

if (is_holder_of(owner, target)) // Special case, holding an atom speaking (pAI, recorder...)
message.plane = PLANE_PLAYER_HUD_ABOVE

// View the message
owned_by.seen_messages.Add(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)
spawn(lifespan - CHAT_MESSAGE_EOL_FADE)
end_of_life()

/**
* 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)
if (gc_destroyed)
return
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 - 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.is_preference_enabled(/datum/client_preference/runechat_mob))
return
else if(isobj(speaker) && !client.is_preference_enabled(/datum/client_preference/runechat_obj))
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"

if (client.is_preference_enabled(/datum/client_preference/runechat_border))
extra_classes |= "black_outline"

var/dist = get_dist(src, speaker)
switch (dist)
if (4 to 5)
extra_classes |= "small"
if (5 to 16)
extra_classes |= "very_small"

// Display visual above source
new /datum/chatmessage(message, speaker, src, extra_classes)

// Tweak these defines to change the available color ranges
#define CM_COLOR_SAT_MIN 0.6
#define CM_COLOR_SAT_MAX 0.95
#define CM_COLOR_LUM_MIN 0.70
#define CM_COLOR_LUM_MAX 0.90

/**
* 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 + "[world_startup_time]"), 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 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/hear = get_mobs_and_objs_in_view_fast(get_turf(src), range, remote_ghosts = FALSE)

var/list/hearing_mobs = hear["mobs"]

for(var/mob in hearing_mobs)
var/mob/M = mob
if(!M.client)
continue
M.create_chat_message(src, message, italics, classes, audible)
17 changes: 15 additions & 2 deletions code/game/atoms.dm
Expand Up @@ -34,6 +34,15 @@
// Track if we are already had initialize() called to prevent double-initialization.
var/initialized = FALSE

/// 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, ...)
// Don't call ..() unless /datum/New() ever exists

Expand Down Expand Up @@ -490,7 +499,7 @@
// Use for objects performing visible actions
// message is output to anyone who can see, e.g. "The [src] does something!"
// blind_message (optional) is what blind people will hear e.g. "You hear something!"
/atom/proc/visible_message(var/message, var/blind_message, var/list/exclude_mobs, var/range = world.view)
/atom/proc/visible_message(var/message, var/blind_message, var/list/exclude_mobs, var/range = world.view, var/runemessage = "<span style='font-size: 1.5em'>👁</span>")

//VOREStation Edit
var/list/see
Expand All @@ -513,6 +522,8 @@
var/mob/M = mob
if(M.see_invisible >= invisibility && MOB_CAN_SEE_PLANE(M, plane))
M.show_message(message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE)
if(runemessage != -1)
M.create_chat_message(src, "* [runemessage || message] *", FALSE, list("emote"), audible = FALSE)
else if(blind_message)
M.show_message(blind_message, AUDIBLE_MESSAGE)

Expand All @@ -521,7 +532,7 @@
// message is the message output to anyone who can hear.
// deaf_message (optional) is what deaf people will see.
// hearing_distance (optional) is the range, how many tiles away the message can be heard.
/atom/proc/audible_message(var/message, var/deaf_message, var/hearing_distance, var/radio_message)
/atom/proc/audible_message(var/message, var/deaf_message, var/hearing_distance, var/radio_message, var/runemessage)

var/range = hearing_distance || world.view
var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src),range,remote_ghosts = FALSE)
Expand All @@ -542,6 +553,8 @@
var/mob/M = mob
var/msg = message
M.show_message(msg, AUDIBLE_MESSAGE, deaf_message, VISIBLE_MESSAGE)
if(runemessage != -1)
M.create_chat_message(src, "* [runemessage || message] *", FALSE, list("emote"))

/atom/movable/proc/dropInto(var/atom/destination)
while(istype(destination))
Expand Down
1 change: 1 addition & 0 deletions code/game/world.dm
@@ -1,5 +1,6 @@
#define RECOMMENDED_VERSION 501
/world/New()
world_startup_time = world.timeofday
to_world_log("Map Loading Complete")
//logs
//VOREStation Edit Start
Expand Down

0 comments on commit ea42ee2

Please sign in to comment.