Runechat - stop bugging me for gods sake (#14141)

* Runechat - stop bugging me for gods sake

* Hotfix

* V2

* Patch 1

* Removes Radio

* Colour Sanity

* Fixes loc issues

* 2020-08-28

* LF --> CRLF

* Forgot this

* Fixes holopad stuffs

* Preference toggle existing!

* Drask + Kidan
This commit is contained in:
AffectedArc07
2020-10-26 14:10:09 +00:00
committed by GitHub
parent 4b1bf3d32a
commit ca29f5340e
22 changed files with 673 additions and 21 deletions
+3
View File
@@ -80,6 +80,9 @@
#define MASSIVE_OBJ_LAYER 11
#define POINT_LAYER 12
#define CHAT_LAYER 12.0001 // Do not insert layers between these two values
#define CHAT_LAYER_MAX 12.9999
#define LIGHTING_PLANE 15
#define LIGHTING_LAYER 15
+3 -2
View File
@@ -48,10 +48,11 @@
#define PREFTOGGLE_2_WINDOWFLASHING 8
#define PREFTOGGLE_2_ANONDCHAT 16
#define PREFTOGGLE_2_AFKWATCH 32
#define PREFTOGGLE_2_RUNECHAT 64
#define TOGGLES_2_TOTAL 63 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined.
#define TOGGLES_2_TOTAL 127 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined.
#define TOGGLES_2_DEFAULT (PREFTOGGLE_2_FANCYUI|PREFTOGGLE_2_ITEMATTACK|PREFTOGGLE_2_WINDOWFLASHING)
#define TOGGLES_2_DEFAULT (PREFTOGGLE_2_FANCYUI|PREFTOGGLE_2_ITEMATTACK|PREFTOGGLE_2_WINDOWFLASHING|PREFTOGGLE_2_RUNECHAT)
// Sanity checks
#if TOGGLES_TOTAL > 16777215
+1
View File
@@ -114,6 +114,7 @@
#define FIRE_PRIORITY_MOBS 100
#define FIRE_PRIORITY_NANOUI 110
#define FIRE_PRIORITY_TICKER 200
#define FIRE_PRIORITY_RUNECHAT 410 // I hate how high the fire priority on this is -aa
#define FIRE_PRIORITY_OVERLAYS 500
#define FIRE_PRIORITY_INPUT 1000 // This must always always be the max highest priority. Player input must never be lost.
+3
View File
@@ -690,6 +690,9 @@ proc/dd_sortedObjectList(list/incoming)
// Lazying Episode 3
#define LAZYSET(L, K, V) LAZYINITLIST(L); L[K] = V;
#define LAZYADDASSOC(L, K, V) if(!L) { L = list(); } L[K] += list(V);
#define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; }
/// Returns whether a numerical index is within a given list's bounds. Faster than isnull(LAZYACCESS(L, I)).
#define ISINDEXSAFE(L, I) (I >= 1 && I <= length(L))
+58
View File
@@ -615,3 +615,61 @@
text = replacetext(text, "<td>", "\[cell\]")
text = replacetext(text, "<img src = ntlogo.png>", "\[logo\]")
return text
/datum/html/split_holder
var/list/opening
var/inner_text
var/list/closing
/datum/html/split_holder/New()
opening = list()
inner_text = ""
closing = list()
/proc/split_html(raw_text="")
// gently borrowed and re-purposed from code/modules/pda/utilities.dm
// define a datum to hold our result
var/datum/html/split_holder/s = new()
// copy the raw_text to get started
var/text = copytext_char(raw_text, 1)
// search for tag brackets
var/tag_start = findtext_char(text, "<")
var/tag_stop = findtext_char(text, ">")
// until we run out of opening tags
while((tag_start != 0) && (tag_stop != 0))
// if the tag isn't at the beginning of the string
if(tag_start > 1)
// we've found our text, so copy it out
s.inner_text = copytext_char(text, 1, tag_start)
// and chop the text for the next round
text = copytext_char(text, tag_start)
break
// otherwise, we found an opening tag, so add it to the list
var/tag = copytext_char(text, tag_start, tag_stop+1)
s.opening.Add(tag)
// and chop the text for the next round
text = copytext_char(text, tag_stop+1)
// look for the next tag in what's left
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")
// search for tag brackets
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")
// until we run out of closing tags
while((tag_start != 0) && (tag_stop != 0))
// we found a closing tag, so add it to the list
var/tag = copytext_char(text, tag_start, tag_stop+1)
s.closing.Add(tag)
// and chop the text for the next round
text = copytext_char(text, tag_stop+1)
// look for the next tag in what's left
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")
// return the split html object to the caller
return s
+233
View File
@@ -0,0 +1,233 @@
/// Controls how many buckets should be kept, each representing a tick. (30 seconds worth)
#define BUCKET_LEN (world.fps * 1 * 30)
/// Helper for getting the correct bucket for a given chatmessage
#define BUCKET_POS(scheduled_destruction) (((round((scheduled_destruction - SSrunechat.head_offset) / world.tick_lag) + 1) % BUCKET_LEN) || BUCKET_LEN)
/// Gets the maximum time at which messages will be handled in buckets, used for deferring to secondary queue
#define BUCKET_LIMIT (world.time + TICKS2DS(min(BUCKET_LEN - (SSrunechat.practical_offset - DS2TICKS(world.time - SSrunechat.head_offset)) - 1, BUCKET_LEN - 1)))
/**
* # Runechat Subsystem
*
* Maintains a timer-like system to handle destruction of runechat messages. Much of this code is modeled
* after or adapted from the timer subsystem. Made by Bobbahbrown of /tg/station13
*
* Note that this has the same structure for storing and queueing messages as the timer subsystem does
* for handling timers: the bucket_list is a list of chatmessage datums, each of which are the head
* of a circularly linked list. Any given index in bucket_list could be null, representing an empty bucket.
*
* AA Note:
* One of the primary reasons for this is because each chatmessage has a timer attached to it, which is extra load on the GC
* At 150 population, the GC literally cannot keep up with processing 368,000 runechats and 368,000 extra timers in a 1 hour 30 minute round
* This also makes performance profiling a lot easier.
*
*/
SUBSYSTEM_DEF(runechat)
name = "Runechat"
flags = SS_TICKER | SS_NO_INIT
wait = 1
priority = FIRE_PRIORITY_RUNECHAT
offline_implications = "Runechat messages will no longer clear. Shuttle call recommended."
/// world.time of the first entry in the bucket list, effectively the 'start time' of the current buckets
var/head_offset = 0
/// Index of the first non-empty bucket
var/practical_offset = 1
/// world.tick_lag the bucket was designed for
var/bucket_resolution = 0
/// How many messages are in the buckets
var/bucket_count = 0
/// List of buckets, each bucket holds every message that has to be killed that byond tick
var/list/bucket_list = list()
/// Queue used for storing messages that are scheduled for deletion too far in the future for the buckets
var/list/datum/chatmessage/second_queue = list()
/datum/controller/subsystem/runechat/PreInit()
bucket_list.len = BUCKET_LEN
head_offset = world.time
bucket_resolution = world.tick_lag
/datum/controller/subsystem/runechat/stat_entry(msg)
..("ActMsgs:[bucket_count] SecQueue:[length(second_queue)]")
/datum/controller/subsystem/runechat/fire(resumed = FALSE)
// Store local references to datum vars as it is faster to access them this way
var/list/bucket_list = src.bucket_list
if (MC_TICK_CHECK)
return
// Check for when we need to loop the buckets, this occurs when
// the head_offset is approaching BUCKET_LEN ticks in the past
if (practical_offset > BUCKET_LEN)
head_offset += TICKS2DS(BUCKET_LEN)
practical_offset = 1
resumed = FALSE
// Store a reference to the 'working' chatmessage so that we can resume if the MC
// has us stop mid-way through processing
var/static/datum/chatmessage/cm
if (!resumed)
cm = null
// Iterate through each bucket starting from the practical offset
while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset - 1) * world.tick_lag) <= world.time)
var/datum/chatmessage/bucket_head = bucket_list[practical_offset]
if (!cm || !bucket_head || cm == bucket_head)
bucket_head = bucket_list[practical_offset]
cm = bucket_head
while (cm)
// If the chatmessage hasn't yet had its life ended then do that now
var/datum/chatmessage/next = cm.next
if (!cm.eol_complete)
cm.end_of_life()
else if (!QDELETED(cm)) // otherwise if we haven't deleted it yet, do so (this is after EOL completion)
qdel(cm)
if (MC_TICK_CHECK)
return
// Break once we've processed the entire bucket
cm = next
if (cm == bucket_head)
break
// Empty the bucket, check if anything in the secondary queue should be shifted to this bucket
bucket_list[practical_offset++] = null
var/i = 0
for (i in 1 to length(second_queue))
cm = second_queue[i]
if (cm.scheduled_destruction >= BUCKET_LIMIT)
i--
break
// Transfer the message into the bucket, performing necessary circular doubly-linked list operations
bucket_count++
var/bucket_pos = max(1, BUCKET_POS(cm.scheduled_destruction))
var/datum/timedevent/head = bucket_list[bucket_pos]
if (!head)
bucket_list[bucket_pos] = cm
cm.next = null
cm.prev = null
continue
if (!head.prev)
head.prev = head
cm.next = head
cm.prev = head.prev
cm.next.prev = cm
cm.prev.next = cm
if (i)
second_queue.Cut(1, i + 1)
cm = null
/datum/controller/subsystem/runechat/Recover()
bucket_list |= SSrunechat.bucket_list
second_queue |= SSrunechat.second_queue
/**
* Enters the runechat subsystem with this chatmessage, inserting it into the end-of-life queue
*
* This will also account for a chatmessage already being registered, and in which case
* the position will be updated to remove it from the previous location if necessary
*
* Arguments:
* * new_sched_destruction Optional, when provided is used to update an existing message with the new specified time
*/
/datum/chatmessage/proc/enter_subsystem(new_sched_destruction = 0)
// Get local references from subsystem as they are faster to access than the datum references
var/list/bucket_list = SSrunechat.bucket_list
var/list/second_queue = SSrunechat.second_queue
// When necessary, de-list the chatmessage from its previous position
if (new_sched_destruction)
if (scheduled_destruction >= BUCKET_LIMIT)
second_queue -= src
else
SSrunechat.bucket_count--
var/bucket_pos = BUCKET_POS(scheduled_destruction)
if (bucket_pos > 0)
var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
if (bucket_head == src)
bucket_list[bucket_pos] = next
if (prev != next)
prev.next = next
next.prev = prev
else
prev?.next = null
next?.prev = null
prev = next = null
scheduled_destruction = new_sched_destruction
// Ensure the scheduled destruction time is properly bound to avoid missing a scheduled event
scheduled_destruction = max(CEILING(scheduled_destruction, world.tick_lag), world.time + world.tick_lag)
// Handle insertion into the secondary queue if the required time is outside our tracked amounts
if (scheduled_destruction >= BUCKET_LIMIT)
BINARY_INSERT(src, SSrunechat.second_queue, datum/chatmessage, scheduled_destruction)
return
// Get bucket position and a local reference to the datum var, it's faster to access this way
var/bucket_pos = BUCKET_POS(scheduled_destruction)
// Get the bucket head for that bucket, increment the bucket count
var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
SSrunechat.bucket_count++
// If there is no existing head of this bucket, we can set this message to be that head
if (!bucket_head)
bucket_list[bucket_pos] = src
return
// Otherwise it's a simple insertion into the circularly doubly-linked list
if (!bucket_head.prev)
bucket_head.prev = bucket_head
next = bucket_head
prev = bucket_head.prev
next.prev = src
prev.next = src
/**
* Removes this chatmessage datum from the runechat subsystem
*/
/datum/chatmessage/proc/leave_subsystem()
// Attempt to find the bucket that contains this chat message
var/bucket_pos = BUCKET_POS(scheduled_destruction)
// Get local references to the subsystem's vars, faster than accessing on the datum
var/list/bucket_list = SSrunechat.bucket_list
var/list/second_queue = SSrunechat.second_queue
// Attempt to get the head of the bucket
var/datum/chatmessage/bucket_head
if (bucket_pos > 0)
bucket_head = bucket_list[bucket_pos]
// Decrement the number of messages in buckets if the message is
// the head of the bucket, or has a SD less than BUCKET_LIMIT implying it fits
// into an existing bucket, or is otherwise not present in the secondary queue
if(bucket_head == src)
bucket_list[bucket_pos] = next
SSrunechat.bucket_count--
else if(scheduled_destruction < BUCKET_LIMIT)
SSrunechat.bucket_count--
else
var/l = length(second_queue)
second_queue -= src
if(l == length(second_queue))
SSrunechat.bucket_count--
// Remove the message from the bucket, ensuring to maintain
// the integrity of the bucket's list if relevant
if(prev != next)
prev.next = next
next.prev = prev
else
prev?.next = null
next?.prev = null
prev = next = null
#undef BUCKET_LEN
#undef BUCKET_POS
#undef BUCKET_LIMIT
+272
View File
@@ -0,0 +1,272 @@
/// 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
/// 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 width of chat message in pixels
#define CHAT_MESSAGE_WIDTH 96
/// Max length of chat message in characters
#define CHAT_MESSAGE_MAX_LENGTH 110
/// Maximum precision of float before rounding errors occur (in this context)
#define CHAT_LAYER_Z_STEP 0.0001
/// The number of z-layer 'slices' usable by the chat message layering
#define CHAT_LAYER_MAX_Z (CHAT_LAYER_MAX - CHAT_LAYER) / CHAT_LAYER_Z_STEP
/// Macro from Lummox used to get height from a MeasureText proc
#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1))
/**
* # 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, 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
/// 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
/// 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
/**
* 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
* * italics - Should we use italics or not
* * lifespan - The lifespan of the message in deciseconds
*/
/datum/chatmessage/New(text, atom/target, mob/owner, italics, size, 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
INVOKE_ASYNC(src, .proc/generate_image, text, target, owner, lifespan, italics, size)
/datum/chatmessage/Destroy()
if (owned_by)
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
leave_subsystem()
return ..()
/**
* Calls qdel on the chatmessage when its parent is deleted, used to register qdel signal
*/
/datum/chatmessage/proc/on_parent_qdel()
qdel(src)
/**
* 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
* * radio_speech - Fancy shmancy radio icon represents that we use radio
* * lifespan - The lifespan of the message in deciseconds
* * italics - Just copy and paste, sir
*/
/datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, lifespan, italics, size)
// Register client who owns this message
owned_by = owner.client
RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, .proc/on_parent_qdel)
// Clip message
var/maxlen = CHAT_MESSAGE_MAX_LENGTH
var/datum/html/split_holder/s = split_html(text)
if (length_char(s.inner_text) > maxlen)
var/chattext = copytext_char(s.inner_text, 1, maxlen + 1) + "..."
text = jointext(s.opening, "") + chattext + jointext(s.closing, "")
// 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_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
var/output_color = sanitize_color(target.get_runechat_color()) // Get_runechat_color can be overriden on atoms to display a specific one (Example: Humans having their hair colour as runechat colour)
// Approximate text height
var/static/regex/html_metachars = new(@"&[A-Za-z]{1,7};", "g")
var/complete_text = "<span class='center maptext[size ? " [size]" : ""]' style='[italics ? "font-style: italic; " : ""]color: [output_color]'>[text]</span>"
var/mheight = WXH_TO_HEIGHT(owned_by.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 = 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
if (!m.eol_complete)
var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
m.enter_subsystem(world.time + remaining_time) // push updated time to runechat SS
// Reset z index if relevant
if (current_z_idx >= CHAT_LAYER_MAX_Z)
current_z_idx = 0
// Build message image
message = image(loc = message_loc, layer = CHAT_LAYER + CHAT_LAYER_Z_STEP * current_z_idx++)
message.plane = GAME_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
LAZYADDASSOC(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)
enter_subsystem()
/**
* 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)
eol_complete = scheduled_destruction + fadetime
animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL)
enter_subsystem(eol_complete) // re-enter the runechat SS with the EOL completion time to QDEL self
/**
* Creates a message overlay at a defined location for a given speaker
*
* Arguments:
* * speaker - The atom who is saying this message
* * raw_message - The text content of the message
* * italics - Vacuum and other things
* * size - Size of the message
*/
/mob/proc/create_chat_message(atom/movable/speaker, raw_message, italics=FALSE, size)
if(isobserver(src))
return
// Display visual above source
new /datum/chatmessage(raw_message, speaker, src, italics, size)
// 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 + station_name()), 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)]"
/**
* Ensures a colour is bright enough for the system
*
* This proc is used to brighten parts of a colour up if its too dark, and looks bad
*
* Arguments:
* * hex - Hex colour to be brightened up
*/
/datum/chatmessage/proc/sanitize_color(color)
var/list/HSL = rgb2hsl(hex2num(copytext(color,2,4)),hex2num(copytext(color,4,6)),hex2num(copytext(color,6,8)))
HSL[3] = max(HSL[3],0.4)
var/list/RGB = hsl2rgb(arglist(HSL))
return "#[num2hex(RGB[1],2)][num2hex(RGB[2],2)][num2hex(RGB[3],2)]"
/**
* Proc to allow atoms to set their own runechat colour
*
* This is a proc designed to be overridden in places if you want a specific atom to use a specific runechat colour
* Exampls include consoles using a colour based on their screen colour, and mobs using a colour based off of a customisation property
*
*/
/atom/proc/get_runechat_color()
return chat_color
+8
View File
@@ -48,6 +48,11 @@
var/list/atom_colours //used to store the different colors on an atom
//its inherent color, the colored paint applied on it, special color effect etc...
/// Last name used to calculate a color for the chatmessage overlays. Used for caching.
var/chat_color_name
/// Last color calculated for the the chatmessage overlays. Used for caching.
var/chat_color
/atom/New(loc, ...)
if(GLOB.use_preloader && (src.type == GLOB._preloader.target_path))//in case the instanciated atom is creating other atoms in New()
GLOB._preloader.load(src)
@@ -847,6 +852,9 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
if(M.client)
speech_bubble_hearers += M.client
if((M.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT) && M.can_hear())
M.create_chat_message(src, message)
if(length(speech_bubble_hearers))
var/image/I = image('icons/mob/talk.dmi', src, "[bubble_icon][say_test(message)]", FLY_LAYER)
I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
@@ -71,6 +71,10 @@
for(var/obj/O in oview(14, get_turf(src)))
O.hear_talk(user, message_to_multilingual("<span class='reallybig'>[message]</span>"))
for(var/mob/M in get_mobs_in_view(7, src))
if((M.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT) && M.can_hear())
M.create_chat_message(user, message, FALSE, "big")
/obj/item/megaphone/emag_act(user as mob)
if(!emagged)
to_chat(user, "<span class='warning'>You overload \the [src]'s voice synthesizer.</span>")
+3
View File
@@ -89,5 +89,8 @@
var/next_keysend_trip_reset = 0
var/keysend_tripped = FALSE
/// Messages currently seen by this client
var/list/seen_messages
// Last world.time that the player tried to request their resources.
var/last_ui_resource_send = 0
@@ -305,3 +305,11 @@
set desc = "Silence the current admin midi playing"
usr.stop_sound_channel(CHANNEL_ADMIN)
to_chat(src, "The current admin midi has been silenced")
/client/verb/toggle_runechat()
set name = "Enable/Disable Runechat"
set category = "Preferences"
set desc = "Toggle runechat messages"
prefs.toggles2 ^= PREFTOGGLE_2_RUNECHAT
to_chat(src, "You will [(prefs.toggles2 & PREFTOGGLE_2_RUNECHAT) ? "now see" : "no longer see"] floating chat messages.")
+18 -7
View File
@@ -3,7 +3,6 @@
/mob/proc/combine_message(var/list/message_pieces, var/verb, var/mob/speaker, always_stars = FALSE)
var/iteration_count = 0
var/msg = "" // This is to make sure that the pieces have actually added something
. = "[verb], \""
for(var/datum/multilingual_say_piece/SP in message_pieces)
iteration_count++
var/piece = SP.message
@@ -44,8 +43,10 @@
// There is literally no content left in this message, we need to shut this shit down
. = "" // hear_say will suppress it
else
. = trim(. + trim(msg))
. += "\""
if(verb)
. = "[verb], \"[trim(msg)]\""
else
. = trim(msg)
/mob/proc/hear_say(list/message_pieces, verb = "says", italics = 0, mob/speaker = null, sound/speech_sound, sound_vol, sound_frequency, use_voice = TRUE)
if(!client)
@@ -78,10 +79,11 @@
var/mob/living/carbon/human/H = speaker
speaker_name = H.GetVoice()
var/message = combine_message(message_pieces, verb, speaker)
var/message = combine_message(message_pieces, null, speaker)
if(message == "")
return
var/message_clean = message
if(italics)
message = "<i>[message]</i>"
@@ -101,13 +103,18 @@
else
to_chat(src, "<span class='name'>[speaker.name]</span> talks but you cannot hear [speaker.p_them()].")
else
to_chat(src, "<span class='game say'><span class='name'>[speaker_name]</span>[use_voice ? speaker.GetAltName() : ""] [track][message]</span>")
to_chat(src, "<span class='game say'><span class='name'>[speaker_name]</span>[speaker.GetAltName()] [track][verb], \"[message]\"</span>")
// Create map text message
if (client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT) // can_hear is checked up there on L99
create_chat_message(speaker, message_clean, italics)
if(speech_sound && (get_dist(speaker, src) <= world.view && src.z == speaker.z))
var/turf/source = speaker? get_turf(speaker) : get_turf(src)
playsound_local(source, speech_sound, sound_vol, 1, sound_frequency)
/mob/proc/hear_radio(list/message_pieces, verb = "says", part_a, part_b, mob/speaker = null, hard_to_hear = 0, vname = "", atom/follow_target)
/mob/proc/hear_radio(list/message_pieces, verb = "says", part_a, part_b, mob/speaker = null, hard_to_hear = 0, vname = "", atom/follow_target, radio_freq)
if(!client)
return
@@ -171,7 +178,7 @@
to_chat(src, heard)
/mob/proc/hear_holopad_talk(list/message_pieces, verb = "says", mob/speaker = null)
/mob/proc/hear_holopad_talk(list/message_pieces, verb = "says", mob/speaker = null, obj/effect/overlay/holo_pad_hologram/H)
if(sleeping || stat == UNCONSCIOUS)
hear_sleep(multilingual_to_message(message_pieces))
return
@@ -180,10 +187,14 @@
return
var/message = combine_message(message_pieces, verb, speaker)
var/message_unverbed = combine_message(message_pieces, null, speaker)
var/name = speaker.name
if(!say_understands(speaker))
name = speaker.voice_name
if((client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT) && can_hear())
create_chat_message(H, message_unverbed)
var/rendered = "<span class='game say'><span class='name'>[name]</span> [message]</span>"
to_chat(src, rendered)
@@ -1974,3 +1974,11 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
/mob/living/carbon/human/proc/get_perceived_trauma()
return min(health, maxHealth - getStaminaLoss())
/**
* Helper to get the mobs runechat colour span
*
* Basically just a quick redirect to the DNA handler that gets the species-specific colour handler
*/
/mob/living/carbon/human/get_runechat_color()
return dna.species.get_species_runechat_color(src)
@@ -860,3 +860,18 @@ It'll return null if the organ doesn't correspond, so include null checks when u
var/obj/item/organ/internal/ears/ears = H.get_int_organ(/obj/item/organ/internal/ears)
if(istype(ears) && !ears.deaf)
. = TRUE
/**
* Species-specific runechat colour handler
*
* Checks the species datum flags and returns the appropriate colour
* Can be overridden on subtypes to short-circuit these checks (Example: Grey colour is eye colour)
* Arguments:
* * H - The human who this DNA belongs to
*/
/datum/species/proc/get_species_runechat_color(mob/living/carbon/human/H)
if(bodyflags & HAS_SKIN_COLOR)
return H.skin_colour
else
var/obj/item/organ/external/head/HD = H.get_organ("head")
return HD.hair_colour
@@ -61,3 +61,7 @@
"eyes" = /obj/item/organ/internal/eyes/drask, //5 darksight.
"brain" = /obj/item/organ/internal/brain/drask
)
/datum/species/drask/get_species_runechat_color(mob/living/carbon/human/H)
var/obj/item/organ/internal/eyes/E = H.get_int_organ(/obj/item/organ/internal/eyes)
return E.eye_colour
@@ -88,3 +88,7 @@
H.adjustFireLoss(1)
return TRUE
return ..()
/datum/species/grey/get_species_runechat_color(mob/living/carbon/human/H)
var/obj/item/organ/internal/eyes/E = H.get_int_organ(/obj/item/organ/internal/eyes)
return E.eye_colour
@@ -40,3 +40,8 @@
"is cracking their exoskeleton!",
"is stabbing themselves with their mandibles!",
"is holding their breath!")
/datum/species/kidan/get_species_runechat_color(mob/living/carbon/human/H)
var/obj/item/organ/internal/eyes/E = H.get_int_organ(/obj/item/organ/internal/eyes)
return E.eye_colour
+3
View File
@@ -1268,6 +1268,9 @@ GLOBAL_LIST_INIT(ai_verbs_default, list(
var/name_used = M.GetVoice()
//This communication is imperfect because the holopad "filters" voices and is only designed to connect to the master only.
var/rendered = "<i><span class='game say'>Relayed Speech: <span class='name'>[name_used]</span> [message]</span></i>"
if(client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT)
var/message_clean = combine_message(message_pieces, null, M)
create_chat_message(M, message_clean)
show_message(rendered, 2)
/mob/living/silicon/ai/proc/malfhacked(obj/machinery/power/apc/apc)
+5 -1
View File
@@ -75,8 +75,12 @@
var/obj/machinery/hologram/holopad/T = current
if(istype(T) && T.masters[src])
var/obj/effect/overlay/holo_pad_hologram/H = T.masters[src]
if ((client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT) && can_hear())
var/message = combine_message(message_pieces, null, src)
create_chat_message(H, message)
for(var/mob/M in hearers(T.loc))//The location is the object, default distance.
M.hear_holopad_talk(message_pieces, verb, src)
M.hear_holopad_talk(message_pieces, verb, src, H)
to_chat(src, "<i><span class='game say'>Holopad transmitted, <span class='name'>[real_name]</span> [combine_message(message_pieces, verb, src)]</span></i>")
else
to_chat(src, "No holopad connected.")
+1
View File
@@ -7,3 +7,4 @@
area = /area/space
view = "15x15"
cache_lifespan = 0 //stops player uploaded stuff from being kept in the rsc past the current session
fps = 20 // If this isnt hard-defined, anything relying on this variable before world load will cry a lot
+12 -11
View File
@@ -1,16 +1,16 @@
macro "default"
menu "menu"
elem
elem
name = "&File"
command = ""
saved-params = "is-checked"
elem
elem
name = "&Quick screenshot\tF2"
command = ".screenshot auto"
category = "&File"
saved-params = "is-checked"
elem
elem
name = "&Save screenshot as...\tShift+F2"
command = ".screenshot"
category = "&File"
@@ -20,21 +20,21 @@ menu "menu"
command = ".reconnect"
category = "&File"
saved-params = "is-checked"
elem
elem
name = ""
command = ""
category = "&File"
saved-params = "is-checked"
elem
elem
name = "&Quit"
command = ".quit"
category = "&File"
saved-params = "is-checked"
elem
elem
name = "&Icons"
command = ""
saved-params = "is-checked"
elem
elem
name = "&Size"
command = ""
category = "&Icons"
@@ -82,7 +82,7 @@ menu "menu"
can-check = true
group = "size"
saved-params = "is-checked"
elem
elem
name = "&Scaling"
command = ""
category = "&Icons"
@@ -114,16 +114,16 @@ menu "menu"
category = "&Icons"
can-check = true
saved-params = "is-checked"
elem
elem
name = "&Help"
command = ""
saved-params = "is-checked"
elem
elem
name = "&Admin help\tF1"
command = "adminhelp"
category = "&Help"
saved-params = "is-checked"
elem
elem
name = "&Hotkeys"
command = "Hotkey-Help"
category = "&Help"
@@ -222,6 +222,7 @@ window "mapwindow"
text-color = none
is-default = true
saved-params = "icon-size"
style=".center { text-align: center; } .maptext { font-family: 'Small Fonts'; font-size: 7px; -dm-text-outline: 1px black; color: white; line-height: 1.1; } .small { font-size: 6px; } .big { font-size: 8px; } .reallybig { font-size: 8px; } .extremelybig { font-size: 8px; } .clown { color: #FF69Bf;} .tajaran {color: #803B56;} .skrell {color: #00CED1;} .solcom {color: #22228B;} .com_srus {color: #7c4848;} .zombie {color: #ff0000;} .soghun {color: #228B22;} .vox {color: #AA00AA;} .diona {color: #804000; font-weight: bold;} .trinary {color: #727272;} .kidan {color: #664205;} .slime {color: #0077AA;} .drask {color: #a3d4eb;} .vulpkanin {color: #B97A57;} .abductor {color: #800080; font-style: italic;} .his_grace { color: #15D512; } .hypnophrase { color: #0d0d0d; font-weight: bold; } .yell { font-weight: bold; }"
window "outputwindow"
elem "outputwindow"
+2
View File
@@ -240,6 +240,7 @@
#include "code\controllers\subsystem\overlays.dm"
#include "code\controllers\subsystem\parallax.dm"
#include "code\controllers\subsystem\radio.dm"
#include "code\controllers\subsystem\runechat.dm"
#include "code\controllers\subsystem\shuttles.dm"
#include "code\controllers\subsystem\sounds.dm"
#include "code\controllers\subsystem\spacedrift.dm"
@@ -266,6 +267,7 @@
#include "code\datums\beam.dm"
#include "code\datums\browser.dm"
#include "code\datums\callback.dm"
#include "code\datums\chatmessage.dm"
#include "code\datums\click_intercept.dm"
#include "code\datums\datacore.dm"
#include "code\datums\datum.dm"