mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-07-16 09:34:21 +01:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user